R/metric_graph.R

#' @title Metric graph
#' @description Class representing a general metric graph.
#' @details A graph object created from vertex and edge matrices, or from an
#' `sp::SpatialLines` object where each line is representing and edge. For more details,
#'  see the vignette:
#' \code{vignette("metric_graph", package = "MetricGraph")}
#' @return Object of \code{\link[R6]{R6Class}} for creating metric graphs.
#' @examples
#' edge1 <- rbind(c(0, 0), c(2, 0))
#' edge2 <- rbind(c(2, 0), c(1, 1))
#' edge3 <- rbind(c(1, 1), c(0, 0))
#' edges <- list(edge1, edge2, edge3)
#' graph <- metric_graph$new(edges)
#' graph$plot()
#'
#' @export
metric_graph <-  R6Class("metric_graph",
  public = list(
  #' @field V Matrix with positions in Euclidean space of the vertices of the
  #' graph.
  V = NULL,

  #' @field nV The number of vertices.
  nV = 0,

  #' @field E Matrix with the edges of the graph, where each row represents an
  #' edge, `E[i,1]` is the vertex at the start of the ith edge and `E[i,2]` is
  #' the vertex at the end of the edge.
  E = NULL,

  #' @field nE The number of edges.
  nE= 0,

  #' @field edge_lengths Vector with the lengths of the edges in the graph.
  edge_lengths = NULL,

  #' @field C Constraint matrix used to set Kirchhoff constraints.
  C = NULL,

  #' @field CoB Change-of-basis object used for Kirchhoff constraints.
  CoB = NULL,

  #' @field PtV Vector with the indices of the vertices which are observation
  #' locations.
  PtV  = NULL,

  #' @field mesh Mesh object used for plotting.
  mesh = NULL,

  #' @field edges The coordinates of the edges in the graph.
  edges = NULL,

  #' @field DirectionalWeightFunction_in Function for inwards weights in directional models
 DirectionalWeightFunction_in  =NULL,
  #' @field DirectionalWeightFunction_out Function for outwards weights in directional models
 DirectionalWeightFunction_out  = NULL,

  #' @field vertices The coordinates of the vertices in the graph, along with several attributes.
  vertices = NULL,


  #' @field geo_dist Geodesic distances between the vertices in the graph.
  geo_dist = NULL,

  #' @field res_dist Resistance distances between the observation locations.
  res_dist = NULL,

  #' @field Laplacian The weighted graph Laplacian of the vertices in the
  #' graph. The weights are given by the edge lengths.
  Laplacian = NULL,

  #' @field characteristics List with various characteristics of the graph.
  characteristics = NULL,

  #' @description Create a new `metric_graph` object.
  #' @param edges A list containing coordinates as `m x 2` matrices (that is, of `matrix` type) or m x 2 data frames (`data.frame` type) of sequence of points connected by straightlines. Alternatively, you can also prove an object of type `SSN`, `osmdata_sp`, `osmdata_sf`, `SpatialLinesDataFrame` or `SpatialLines` (from `sp` package) or `MULTILINESTRING` (from `sf` package).
  #' @param V n x 2 matrix with Euclidean coordinates of the n vertices. If non-NULL, no merges will be performed.
  #' @param E m x 2 matrix where each row represents one of the m edges. If non-NULL, no merges will be performed.
  #' @param vertex_unit The unit in which the vertices are specified. The options are 'degree' (the great circle distance in km), 'km', 'm' and 'miles'. The default is `NULL`, which means no unit. However, if you set `length_unit`, you need to set `vertex_unit`.
  #' @param length_unit The unit in which the lengths will be computed. The options are 'km', 'm' and 'miles'. The default, when longlat is `TRUE`, or an `sf` or `sp` objects are provided, is 'km'.
  #' @param edge_weights Either a number, a numerical vector with length given by the number of edges, providing the edge weights, or a `data.frame` with the number of rows being equal to the number of edges, where
  #' each row gives a vector of weights to its corresponding edge. Can be changed by using the `set_edge_weights()` method.
  #' @param kirchhoff_weights If non-null, the name (or number) of the column of `edge_weights` that contain the Kirchhoff weights. Must be equal to 1 (or `TRUE`) in case `edge_weights` is a single number and those are the Kirchhoff weights.
  #' @param directional_weights If non-null, the name (or number) of the column of `edge_weights` that contain the directional weights. The default is the first column of the edge weights.
  #' @param longlat There are three options: `NULL`, `TRUE` or `FALSE`. If `NULL` (the default option), the `edges` argument will be checked to see if there is a CRS or proj4string available, if so, `longlat` will be set to `TRUE`, otherwise, it will be set to `FALSE`. If `TRUE`, then it is assumed that the coordinates are given.
  #' in Longitude/Latitude and that distances should be computed in meters. If `TRUE` it takes precedence over
  #' `vertex_unit` and `length_unit`, and is equivalent to `vertex_unit = 'degree'` and `length_unit = 'm'`.
  #' @param include_obs If the object is of class `SSN`, should the observations be added? If `NULL` and the edges are of class `SSN`, the data will be automatically added. If `FALSE`, the data will not be added. Alternatively, one can set this argument to the numbers or names of the columns of the observations to be added as observations.
  #' @param add_obs_options List containing additional options to be passed to the `add_observations()` method when adding observations from `SSN` data?
  #' @param include_edge_weights If the object is of class `SSN`, `osmdata_sp`, `osmdata_sf`, `SpatialLinesDataFrame`, `MULTILINESTRING`, `LINESTRING`, `sfc_LINESTRING`, `sfc_MULTILINESTRING`, should the edge data (if any) be added as edge weights? If `NULL`, the edge data will be added as edge weights, if `FALSE` they will not be added. Alternatively, one can set this argument to the numbers or names of the columns of the edge data to be added as edge weights.
  #' @param crs Coordinate reference system to be used in case `longlat` is set to `TRUE` and `which_longlat` is `sf`. Object of class crs. The default choice, if the `edges` object does not have CRS nor proj4string, is `sf::st_crs(4326)`.
  #' @param proj4string Projection string of class CRS-class to be used in case `longlat` is set to `TRUE` and `which_longlat` is `sp`. The default choice, if the `edges` object does not have CRS nor proj4string, is `sp::CRS("+proj=longlat +datum=WGS84")`.
  #' @param which_longlat Compute the distance using which package? The options are `sp` and `sf`. The default is `sp`.
  #' @param project If `longlat` is `TRUE` should a projection be used to compute the distances to be used for the tolerances (see `tolerance` below)? The default is `FALSE`. When `TRUE`, the construction of the graph is faster.
  #' @param project_data If `longlat` is `TRUE` should the vertices be project to planar coordinates? The default is `FALSE`. When `TRUE`, the construction of the graph is faster.
  #' @param which_projection Which projection should be used in case `project` is `TRUE`? The options are `Robinson`, `Winkel tripel` or a proj4string. The default is `Winkel tripel`.
  #' @param manual_edge_lengths If non-NULL, a vector containing the edges lengths, and all the quantities related to edge lengths will be computed in terms of these. If merges are performed, it is likely that the merges will override the manual edge lengths. In such a case, to provide manual edge lengths, one should either set the `perform_merges` argument to `FALSE` or use the `set_manual_edge_lengths()` method.
  #' @param perform_merges There are three options, `NULL`, `TRUE` or `FALSE`. The default option is `NULL`. If `NULL`, it will be set to `FALSE` unless 'edges', 'V' and 'E' are `NULL`, in which case it will be set to `TRUE`. If FALSE, this will take priority over the other arguments, and no merges (except the optional `merge_close_vertices` below) will be performed. Note that the merge on the additional `merge_close_vertices` might still be performed, if it is set to `TRUE`.
  #' @param approx_edge_PtE Should the relative positions on the edges be approximated? The default is `TRUE`. If `FALSE`, the speed can be considerably slower, especially for large metric graphs.
  #' @param tolerance List that provides tolerances during the construction of the graph:
  #' - `vertex_vertex` Vertices that are closer than this number are merged (default = 1e-7).
  #' - `vertex_edge` If a vertex at the end of one edge is closer than this
  #' number to another edge, this vertex is connected to that edge
  #' (default = 1e-7). Previously `vertex_line`, which is now deprecated.
  #' - `edge_edge` If two edges at some point are closer than this number, a new
  #' vertex is added at that point and the two edges are connected (default = 0).
  #' - `vertex_line`, Deprecated. Use `vertex_edge` instead.
  #' - `line_line`, Deprecated. Use `edge_edge` instead.
  #'
  #' In case `longlat = TRUE`, the tolerances are given in `length_unit`.
  #' @param check_connected If `TRUE`, it is checked whether the graph is
  #' connected and a warning is given if this is not the case.
  #' @param remove_deg2 Set to `TRUE` to remove all vertices of degree 2 in the
  #' initialization. Default is `FALSE`.
  #' @param merge_close_vertices Should an additional step to merge close vertices be done? The options are `NULL` (the default), `TRUE` or `FALSE`. If `NULL`, it will be determined automatically. If `TRUE` this step will be performed even if `perfom_merges` is set to `FALSE`.
  #' @param factor_merge_close_vertices Which factor to be multiplied by tolerance `vertex_vertex` when merging close vertices at the additional step?
  #' @param remove_circles All circlular edges with a length smaller than this number
  #' are removed. If `TRUE`, the `vertex_vertex` tolerance will be used. If `FALSE`, no circles will be removed.
  #' @param auto_remove_point_edges Should edges of length zero, that is, edges that are actually points, be automatically removed? 
  #' @param verbose Print progress of graph creation. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @param lines `r lifecycle::badge("deprecated")` Use `edges` instead.
  #' @details A graph object can be initialized in two ways. The first method
  #' is to specify V and E. In this case, all edges are assumed to be straight
  #' lines. The second option is to specify the graph via the `lines` input.
  #' In this case, the vertices are set by the end points of the lines.
  #' Thus, if two lines are intersecting somewhere else, this will not be
  #' viewed as a vertex.
  #' @return A `metric_graph` object.
  initialize = function(edges = NULL,
                        V = NULL,
                        E = NULL,
                        vertex_unit = NULL,
                        length_unit = NULL,
                        edge_weights = NULL,
                        kirchhoff_weights = NULL,
                        directional_weights = NULL,
                        longlat = NULL,
                        crs = NULL,
                        proj4string = NULL,
                        which_longlat = "sp",
                        include_obs = NULL,
                        include_edge_weights = NULL,
                        project = FALSE,
                        project_data = FALSE,
                        which_projection = "Winkel tripel",
                        manual_edge_lengths = NULL,
                        perform_merges = NULL,
                        approx_edge_PtE = TRUE,
                        tolerance = list(vertex_vertex = 1e-3,
                                         vertex_edge = 1e-3,
                                         edge_edge = 0),
                        check_connected = TRUE,
                        remove_deg2 = FALSE,
                        merge_close_vertices = NULL,
                        factor_merge_close_vertices = 1,
                        remove_circles = FALSE,
                        auto_remove_point_edges = TRUE,                        
                        verbose = 1,
                        add_obs_options = list(return_removed = FALSE,
                                                verbose = verbose),
                        lines = deprecated()) {

      start_construction_time <- Sys.time()

      add_data_tmp <- FALSE

      private$project_data <- project_data

      if(is.null(merge_close_vertices)){
          merge_close_vertices <- TRUE
      }

      if(inherits(edges, "SSN")){
        if(is.null(include_obs) || include_obs[[1]] == TRUE){
          dataset_tmp <- edges$obs
          add_data_tmp <- TRUE
        } else if(is.numeric(include_obs) || is.character(include_obs)){
          dataset_tmp <- edges$obs[,include_obs]
          add_data_tmp <- TRUE
        } else if(!is.logical(include_obs[[1]])){
          stop("invalid option passed to include_obs.")
        }
        edges <- edges$edges        
        if(is.null(include_edge_weights) || include_edge_weights[[1]] == TRUE){
          edge_weights <- sf::st_drop_geometry(edges)
        } else if(is.numeric(include_edge_weights) || is.character(include_edge_weights)){
          edge_weights <- sf::st_drop_geometry(edges)          
          edge_weights <- edge_weights[,include_edge_weights]
        } else if(!is.logical(include_edge_weights[[1]])){
          stop("invalid option passed to include_edge_weights.")
        }
      }

      if(inherits(edges, c("osmdata_sp", "osmdata_sf"))){
        edges <- edges$osm_lines
        if(is.null(include_edge_weights) || include_edge_weights[[1]] == TRUE){
          if(inherits(edges, "osmdata_sp")){
            edge_weights <- edges@data
          } else{
            edge_weights <- sf::st_drop_geometry(edges)
          }
        } else if(is.numeric(include_edge_weights) || is.character(include_edge_weights)){
          if(inherits(edges, "osmdata_sp")){
            edge_weights <- edges@data
          } else{
            edge_weights <- sf::st_drop_geometry(edges)
          }          
          edge_weights <- edge_weights[,include_edge_weights]
        } else if(!is.logical(include_edge_weights[[1]])){
          stop("invalid option passed to include_edge_weights.")
        }        
      }

      if (inherits(edges,"SpatialLines") || inherits(edges,"SpatialLinesDataFrame")) {
        if(is.null(longlat) || longlat){
          if(!is.na(sp::proj4string(edges))){
            longlat <- TRUE
            if(is.null(proj4string) && is.null(crs)){
              proj4string <- sp::proj4string(edges)
              crs_tmp <- sf::st_crs(proj4string, parameters = TRUE)
              if(is.null(vertex_unit)){
                vertex_unit <- crs_tmp$units_gdal
                if(vertex_unit == "metre"){
                  vertex_unit <- "m"
                } else if(vertex_unit == "kilometre"){
                  vertex_unit <- "km"
                }
              }
            }
            if(is.null(length_unit)){
              length_unit <- "km"          
            }
          } else{
            longlat <- FALSE
          }
        }
        if(inherits(edges,"SpatialLinesDataFrame")){
          if(is.null(include_edge_weights) || include_edge_weights[[1]] == TRUE){
            edge_weights <- edges@data
        } else if(is.numeric(include_edge_weights) || is.character(include_edge_weights)){
            edge_weights <- edges@data      
            edge_weights <- edge_weights[,include_edge_weights]
        } else if(!is.logical(include_edge_weights[[1]])){
          stop("invalid option passed to include_edge_weights.")
        }        
        }
      } else if(inherits(edges, c("MULTILINESTRING", "LINESTRING", "sfc_LINESTRING", "sfc_MULTILINESTRING", "sf"))){
        if(is.null(longlat) || longlat){
          if(!is.na(sf::st_crs(edges))){
            longlat <- TRUE
            if(is.null(proj4string) && is.null(crs)){
              crs <- sf::st_crs(edges)
              crs_tmp <- sf::st_crs(edges, parameters = TRUE)
              if(is.null(vertex_unit)){
                vertex_unit <- crs_tmp$units_gdal
                if(vertex_unit == "metre"){
                  vertex_unit <- "m"
                } else if(vertex_unit == "kilometre"){
                  vertex_unit <- "km"
                }
              }
            }
            if(is.null(length_unit)){
              length_unit <- "km"              
            }
          } else{
            longlat <- FALSE
          }
        }

        if(inherits(edges, "data.frame")){
        if(is.null(include_edge_weights) || include_edge_weights[[1]] == TRUE){
            edge_weights <- sf::st_drop_geometry(edges)
        } else if(is.numeric(include_edge_weights) || is.character(include_edge_weights)){
            edge_weights <- sf::st_drop_geometry(edges)    
          edge_weights <- edge_weights[,include_edge_weights]
        } else if(!is.logical(include_edge_weights[[1]])){
          stop("invalid option passed to include_edge_weights.")
        }        

        }


      } else{
        if(is.null(longlat)){
          longlat <- FALSE
        }
      }

      if (lifecycle::is_present(lines)) {
         if (is.null(edges)) {
           lifecycle::deprecate_warn("1.2.0", "metric_graph$new(lines)", "metric_graph$new(edges)",
             details = c("`lines` was provided but not `edges`. Setting `edges <- lines`.")
           )
           edges <- lines
         } else {
           lifecycle::deprecate_warn("1.2.0", "metric_graph$new(lines)", "metric_graph$new(edges)",
             details = c("Both `edges` and `lines` were provided. Only `edges` will be considered.")
           )
         }
         lines <- NULL
       }

       if(is.null(edge_weights)){
          edge_weights <- 1
       }

       if(!is.null(kirchhoff_weights)){
        if(length(kirchhoff_weights)>1){
          warning("Only the first entry of 'kirchhoff_weights' was used.")
          kirchhoff_weights <- kirchhoff_weights[[1]]
        }
        if(!is.numeric(kirchhoff_weights)){
          if(!is.character(kirchhoff_weights)){
            stop("'kirchhoff_weights' must be either a number of a string.")
          }
          if(!(kirchhoff_weights%in%colnames(edge_weights))){
            stop(paste(kirchhoff_weights, "is not a column of 'edge_weights'!"))
          }
        } else{
          if(!is.data.frame(edge_weights)){
            if(kirchhoff_weights != 1){
              stop("Since 'edge_weights' is not a data.frame, 'kirchhoff_weights' must be either NULL or 1.")
            }
          } else{
            if(kirchhoff_weights %%1 != 0){
              stop("'kirchhoff_weights' must be an integer.")
            }
            if((kirchhoff_weights < 1) || (kirchhoff_weights > ncol(edge_weights))){
              stop("'kirchhoff_weights' must be a positive integer number smaller or equal to the number of columns of 'edge_weights'.")
            }
          }
        }
        private$kirchhoff_weights <- kirchhoff_weights
       } else{
        if(is.vector(edge_weights)){
          private$kirchhoff_weights <- 1
        } else{
          if(".weights" %in% colnames(edge_weights)){
            private$kirchhoff_weights <- ".weights"
          } else{
            edge_weights[, ".weights"] <- 1
            private$kirchhoff_weights <- ".weights"
          }
        }        
       }



      if(!is.null(directional_weights)){
        if(!is.numeric(directional_weights)){
          if(!is.character(directional_weights)){
            stop("'directional_weights' must be either a numerical vector of a string vector.")
          }
          if(!(directional_weights%in%colnames(edge_weights))){
            stop(paste(directional_weights, "is not a column of 'weights'!"))
          }
        } else{
          if(!is.data.frame(edge_weights)){
            if(directional_weights != 1){
              stop("Since 'weights' is not a data.frame, 'directional_weights' must be either NULL or 1.")
            }
          } else{
            if(directional_weights %%1 != 0){
              stop("'directional_weights' must be an integer.")
            }
            if((directional_weights < 1) || (directional_weights > ncol(edge_weights))){
              stop("'directional_weights' must be a positive integer number smaller or equal to the number of columns of 'weights'.")
            }
          }
        }
        private$directional_weights <- directional_weights
       } else{
        if(is.vector(edge_weights)){
          private$directional_weights <- 1
        } else{
          if(".weights" %in% colnames(edge_weights)){
            private$directional_weights <- ".weights"
          } else{
            edge_weights[, ".weights"] <- 1
            private$directional_weights <- ".weights"
          }
        }
       }


      if (is.null(tolerance$vertex_edge) && !is.null(tolerance$vertex_line)) {
           lifecycle::deprecate_warn("1.2.0", "metric_graph$new(tolerance = 'must contain either vertex_vertex, vertex_edge or edge_edge')",
             details = c("`tolerance$vertex_line` was provided but not `tolerance$vertex_edge`. Setting `tolerance$vertex_edge <- tolerance$vertex_line`.")
           )
           tolerance$vertex_edge <- tolerance$vertex_line
           tolerance$vertex_line <- NULL
         } else if(!is.null(tolerance$vertex_edge) && !is.null(tolerance$vertex_line)) {
           lifecycle::deprecate_warn("1.2.0","metric_graph$new(tolerance = 'must contain either vertex_vertex, vertex_edge or edge_edge')",
             details = c("Both `tolerance$vertex_edge` and `tolerance$vertex_line` were provided. Only `tolerance$vertex_edge` will be considered.")
           )
            tolerance$vertex_line <- NULL
         }

        if (is.null(tolerance$edge_edge) && !is.null(tolerance$line_line)) {
           lifecycle::deprecate_warn("1.2.0", "metric_graph$new(tolerance = 'must contain either vertex_vertex, vertex_edge or edge_edge')",
             details = c("`tolerance$line_line` was provided but not `tolerance$edge_edge`. Setting `tolerance$edge_edge <- tolerance$line_line`.")
           )
           tolerance$edge_edge <- tolerance$line_line
           tolerance$line_line <- NULL
         } else if(!is.null(tolerance$edge_edge) && !is.null(tolerance$line_line)) {
           lifecycle::deprecate_warn("1.2.0","metric_graph$new(tolerance = 'must contain either vertex_vertex, vertex_edge or edge_edge')",
             details = c("Both `tolerance$edge_edge` and `tolerance$line_line` were provided. Only `tolerance$edge_edge` will be considered.")
           )
          tolerance$line_line <- NULL
         }

      valid_units_vertex <- c("m", "km", "miles", "degree")
      valid_units_length <- c("m", "km", "miles")

      if(!(which_longlat %in% c("sp", "sf"))){
        stop("The options for 'which_longlat' are 'sp' and 'sf'!")
      }

      if(longlat){
        private$longlat <- TRUE
        private$which_longlat <- which_longlat
      }

      if(!is.null(proj4string)){
        if(!longlat){
          warning("proj4string was passed, so setting longlat to TRUE")
          longlat <- TRUE
          private$longlat <- TRUE
          private$which_longlat <- which_longlat
        }
        private$crs <- sf::st_crs(proj4string)
        private$proj4string <- proj4string
        crs <- private$crs
        private$transform <- !(sf::st_is_longlat(private$crs))
      }

      if(!is.null(crs)){
        if(!longlat){
          warning("crs was passed, so setting longlat to TRUE")
          longlat <- TRUE
          private$longlat <- TRUE
          private$which_longlat <- which_longlat
        }
        private$crs <- sf::st_crs(crs)
        private$proj4string <- sp::CRS(private$crs$proj4string)
        proj4string <- private$proj4string
        private$transform <- !(sf::st_is_longlat(private$crs))
      }

      if(longlat && (which_longlat == "sp") && is.null(proj4string)){
        proj4string <- sp::CRS("+proj=longlat +datum=WGS84")
        private$crs <- sf::st_crs(proj4string)
        private$proj4string <- proj4string
        private$transform <- !(sf::st_is_longlat(private$crs))
      }

      if(longlat && (which_longlat == "sf") && is.null(crs)){
        crs <- sf::st_crs(4326)
        private$crs <- crs
        private$transform <- !(sf::st_is_longlat(private$crs))
      }

    # private$longlat <- longlat

    if((is.null(vertex_unit) && !is.null(length_unit)) || (is.null(length_unit) && !is.null(vertex_unit))){
      stop("If one of 'vertex_unit' or 'length_unit' is NULL, and the edges are not sf nor sp objects, then the other must also be NULL.")
    }

    if(!is.null(vertex_unit)){
      vertex_unit <- vertex_unit[[1]]
      if(!is.character(vertex_unit)){
        stop("'vertex_unit' must be a string!")
      }
      if(!(vertex_unit %in% valid_units_vertex)){
        stop(paste("The possible options for 'vertex_unit' are ", toString(valid_units_vertex)))
      }
      private$vertex_unit <- vertex_unit
    }

    if(!is.null(length_unit)){
      length_unit <- length_unit[[1]]
      if(!is.character(length_unit)){
        stop("'length_unit' must be a string!")
      }
      if(length_unit == "degree"){
        length_unit <- "km"
      }
      if(!(length_unit %in% valid_units_length)){
        stop(paste("The possible options for 'length_unit' are ", toString(valid_units_length)))
      }
      private$length_unit <- length_unit
    }

    if(longlat){
      private$vertex_unit <- vertex_unit
      if(is.null(vertex_unit)){
        private$vertex_unit <- "degree"
      }
      if(!is.null(length_unit)){
        private$length_unit <- length_unit
      } else{
        private$length_unit <- "km"
      }
    } else if(!is.null(vertex_unit)){
        if(private$vertex_unit == "degree"){
          longlat <- TRUE
          private$longlat <- TRUE
        }
    }

    if(is.null(edges) && is.null(V) && is.null(E)) {
      edges <- logo_lines()
        if(is.null(perform_merges)){
          perform_merges <- TRUE
          remove_circles <- TRUE
        }      
    }

    if(!is.null(manual_edge_lengths)){
        if(is.null(perform_merges)){
          perform_merges <- FALSE
        }
      } else{
        if(is.null(perform_merges)){
          perform_merges <- FALSE
        }
      }

    factor_unit <- process_factor_unit(private$vertex_unit, private$length_unit)

    tolerance_default = list(vertex_vertex = 1e-7,
                             vertex_edge = 1e-7,
                              edge_edge = 0)



    for(i in 1:length(tolerance_default)){
      if(!(names(tolerance_default)[i] %in% names(tolerance))){
        tolerance[names(tolerance_default)[i]] <- tolerance_default[i]
      }
    }

    if(verbose > 0){
      message("Starting graph creation...")
      message(paste("LongLat is set to",longlat))
      if(longlat){
        message(paste("The unit for edge lengths is", private$length_unit))
        if(perform_merges){
          message(paste0("The current tolerances (in ",private$length_unit,") are:"))
          message(paste("\t Vertex-Vertex", tolerance$vertex_vertex))
          message(paste("\t Vertex-Edge", tolerance$vertex_edge))
          message(paste("\t Edge-Edge", tolerance$edge_edge))
        }
      } else if (perform_merges){
        message("The current tolerances are:")
        message(paste("\t Vertex-Vertex", tolerance$vertex_vertex))
        message(paste("\t Vertex-Edge", tolerance$vertex_edge))
        message(paste("\t Edge-Edge", tolerance$edge_edge))
      }
    }

    if(is.null(tolerance$buffer_edge_edge)){
      tolerance$buffer_edge_edge <- max(tolerance$edge_edge/2 - 1e-10,0)
    }
    max_tol <- max(c(tolerance$vertex_vertex,
                     tolerance$vertex_edge,
                     tolerance$edge_edge))

    private$tolerance <- tolerance

    PtE_tmp_edge_edge <- NULL
    PtE_tmp_edge_vertex <- NULL


    if(!(perform_merges %in% c(TRUE,FALSE))){
      stop("perform_merges should be either TRUE or FALSE.")
    }

    private$perform_merges <- perform_merges

    if(verbose > 0){
      message("Creating edges...")
    }

    if(!is.null(edges)){
      if(!is.null(V) || !is.null(E)){
        warning("object initialized from edges, then E and V are ignored")
      }
      if (inherits(edges,"SpatialLinesDataFrame")) {
        tmp_lines = sp::SpatialLines(edges@lines)
        self$edges <- lapply(1:length(tmp_lines), function(i){tmp_lines@lines[[i]]@Lines[[1]]@coords})
      } else if (inherits(edges,"SpatialLines")) {
        self$edges = lapply(1:length(edges), function(i){edges@lines[[i]]@Lines[[1]]@coords})
      } else if (inherits(edges, c("MULTILINESTRING", "LINESTRING", "sfc_LINESTRING", "sfc_MULTILINESTRING", "sf"))) {
        # Ensure 'edges' is an 'sf' object if it is not already
        if (!inherits(edges, "sf")) {
          # Convert to 'sfc' (simple feature geometry list column)
          edges <- sf::st_sfc(edges)
          edges <- sf::st_sf(geometry = edges)  # Wrap in an 'sf' data frame
        }

        # Define valid geometry types for filtering
        valid_types <- c("LINESTRING", "MULTILINESTRING")

        # Filter for valid geometry types in 'edges'
        valid_indices <- sf::st_geometry_type(edges) %in% valid_types
        valid_edges <- edges[valid_indices, , drop = FALSE]  # Filter only valid geometries

        # Extract coordinates for the valid edges
        coords_multilinestring <- sf::st_coordinates(sf::st_geometry(valid_edges))

        split_coords <- split(coords_multilinestring[, 1:2, drop = FALSE], coords_multilinestring[, "L1"])
        self$edges <- lapply(split_coords, function(coords) matrix(coords, ncol=2, byrow=FALSE))        
      } else if(is.list(edges)){
        self$edges <- check_lines_input(edges)
      } else {
        stop("edges should either be a list, or of class MULTILINESTRING, SpatialLines or SpatialLinesDataFrame")
      }

    } else {
      if(is.null(V) || is.null(E)){
        stop("You must supply edges or V and E")
      }
      if(ncol(V)!=2 || ncol(E)!=2){
        stop("V and E must have two columns!")
      }
      edges <- list()
      for(i in 1:dim(E)[1]) {
        edges[[i]] <- rbind(V[E[i,1], ], V[E[i,2], ])
      }
      self$edges <- edges
      self$E <- E
      self$V <- V
      private$perform_merges <- FALSE
    }

    self$edges <- lapply(self$edges, function(x) if(nrow(x) < 2) NULL else x)

    self$nE <- length(self$edges)

    if(verbose > 0){
      message("Setting edge weights...")
    }

    private$set_first_weights(weights = edge_weights)


    if(verbose > 0){
      message("Computing bounding box...")
    }
    private$compute_bounding_box()

    if(!is.null(manual_edge_lengths)){
      self$set_manual_edge_lengths(edge_lengths = manual_edge_lengths, unit = length_unit)
    }

    if(private$perform_merges){

    if(verbose > 0){
      message("Setup edges and merge close vertices")
    }      

      if(!is.null(manual_edge_lengths)){
        warning("Since 'perform_merges' is TRUE, the manual edge lengths will not be used. Either set 'perform_merges' to FALSE or use the 'set_manual_edge_lengths()' method on the graph after the graph construction.")
      }

      t <- system.time(
        private$line_to_vertex(tolerance = tolerance$vertex_vertex,
                           longlat = private$longlat, factor_unit, verbose=verbose,
                           private$crs, private$proj4string, which_longlat, private$length_unit, private$vertex_unit,
                           project, which_projection, project_data)
        )

      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
    
      self$compute_PtE_edges(approx = approx_edge_PtE, verbose=verbose)

      if(length(self$edges) > 1){

      if (tolerance$edge_edge > 0) {
      private$addinfo <- TRUE

      if(verbose > 0){
        message("Find edge-edge intersections")
      }

      t <- system.time(
        points_add <- private$find_edge_edge_points(tol = tolerance$edge_edge, verbose=verbose,
        crs=private$crs, proj4string = private$proj4string, longlat=private$longlat, fact = factor_unit, which_longlat = which_longlat)
        )

      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }

      PtE <- points_add$PtE

      PtE[,2] <- PtE[,2]/self$edge_lengths[PtE[,1]]

      filter_tol <- ((PtE[,2] > max_tol/self$edge_lengths[PtE[,1]]) &
                     (PtE[,2] < 1- max_tol/self$edge_lengths[PtE[,1]]))

      PtE <- PtE[filter_tol,]

      if(!is.null(PtE)){
        if(nrow(PtE) == 0){
          PtE <- NULL
        }
      }

      if(!is.null(PtE)){
        if(verbose == 2){
          message(sprintf("Add %d new vertices", nrow(PtE)))
        }

        PtE <- na.omit(PtE)

        t <- system.time(
        private$add_vertices(PtE, tolerance = tolerance$edge_edge, verbose = verbose)
        )

        if(verbose == 2){
          message(sprintf("time: %.3f s", t[["elapsed"]]))
        }
      }

      private$clear_initial_info()
      }

      if(tolerance$vertex_edge > 0){
        private$addinfo <- TRUE
        if(verbose > 0){
          message("Snap vertices to close edges")
        }

        t <- system.time(
          PtE_tmp <- private$coordinates_multiple_snaps(XY = self$V,
                                              tolerance = tolerance$vertex_edge, verbose = verbose,
          crs=private$crs, proj4string = private$proj4string, longlat=private$longlat, fact = factor_unit, which_longlat = which_longlat)
          )

        if(verbose == 2){
          message(sprintf("time: %.3f s", t[["elapsed"]]))
        }
        edge_length_filter <- self$edge_lengths[PtE_tmp[,1]]

        filter_tol <- ((PtE_tmp[,2] > max_tol/edge_length_filter) &
                       (PtE_tmp[,2] < 1- max_tol/edge_length_filter))

        PtE_tmp <- PtE_tmp[filter_tol,,drop = FALSE]
        PtE_tmp <- unique(PtE_tmp)
        PtE_tmp <- PtE_tmp[order(PtE_tmp[,1], PtE_tmp[,2]),,drop = FALSE]

        if(!is.null(PtE_tmp)){
          if(nrow(PtE_tmp) == 0){
            PtE_tmp <- NULL
          }
        }

        if(!is.null(PtE_tmp)){
          if(verbose == 2){
            message(sprintf("Add %d new vertices", nrow(PtE_tmp)))
          }

          PtE_tmp <- na.omit(PtE_tmp)

          t <- system.time(
            private$add_vertices(PtE_tmp, tolerance = tolerance$vertex_edge, verbose=verbose)
            )

          if(verbose == 2){
            message(sprintf("time: %.3f s", t[["elapsed"]]))
          }
        }
        private$clear_initial_info()
      }

      if(merge_close_vertices){
        private$merge_close_vertices(factor_merge_close_vertices * tolerance$vertex_vertex, factor_unit)
      }

      if(auto_remove_point_edges){
          private$remove_circles(1e-15, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
      }

      if(is.logical(remove_circles)){
        if(remove_circles){
          private$remove_circles(tolerance$vertex_vertex, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
        }
      } else {
        private$remove_circles(remove_circles, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
        remove_circles <- TRUE
      }


      if(merge_close_vertices || remove_circles){
        if(verbose == 2){
          message("Recomputing edge lengths")
        }
        t <- system.time({
          self$edge_lengths <- private$compute_lengths(private$longlat, private$length_unit, private$crs, private$proj4string, private$which_longlat, private$vertex_unit, project_data,private$transform)
        })
       if(verbose == 2){
      message(sprintf("time: %.3f s", t[["elapsed"]]))
         }
      }
      # End of cond of having more than 1 edge
      }

      # Cleaning the edges

      if(verbose == 2){
        message("Post-processing the edges")
      }


      t <- system.time(
            self$edges <- lapply(self$edges, function(edge){
              tmp_edge <- edge[1:(nrow(edge)-1),]
              tmp_edge <- unique(tmp_edge)
              tmp_edge <- rbind(tmp_edge, edge[nrow(edge),,drop=FALSE])
              if(nrow(tmp_edge)>2){
                tmp_edge <- tmp_edge[2:nrow(tmp_edge),]
                tmp_edge <- unique(tmp_edge)
                tmp_edge <- rbind(edge[1,,drop=FALSE], tmp_edge)
              }
              rownames(tmp_edge) <- NULL
              return(tmp_edge)
            }
              )
      )

      if(verbose == 2){
            message(sprintf("time: %.3f s", t[["elapsed"]]))
      }

      # Checking if there is some edge with infinite length
      if(any(!is.finite(self$edge_lengths))){
        warning("There is at least one edge of infinite length. Please, consider redefining the graph.")
      }

        # end of if do merges
    } else{

      if (verbose > 0) {
        message("Setting up edges")
      }

      # Extract the start and end vertices of all edges at once
      edges_vertices <- do.call(rbind, lapply(self$edges, function(edge) {
        edge[c(1, nrow(edge)), ]
      }))

      # Round the vertex coordinates and ensure uniqueness
      self$V <- unique(round(edges_vertices * 10^15) / 10^15)
      self$nV <- nrow(self$V)

      lvl <- matrix(0, nrow = length(self$edges), ncol = 2)

      # Collect all points and split them into start and end points
      all_points <- do.call(rbind, self$edges)
      n_points <- sapply(self$edges, nrow)

      # Split all points into start and end points
      start_indices <- cumsum(c(1, n_points[-length(n_points)]))
      end_indices <- cumsum(n_points)
      start_points <- all_points[start_indices, , drop = FALSE]
      end_points <- all_points[end_indices, , drop = FALSE]

      # Use RANN for efficient nearest-neighbor search
      nn_start <- nn2(self$V, start_points, k = 1)
      nn_end <- nn2(self$V, end_points, k = 1)

      # Extract the indices of the closest vertices
      lvl[, 1] <- nn_start$nn.idx
      lvl[, 2] <- nn_end$nn.idx

      self$E <- lvl
  
      if (merge_close_vertices) {
        if (verbose > 0) {
          message("Merging close vertices")
        }
        private$merge_close_vertices(factor_merge_close_vertices * tolerance$vertex_vertex, factor_unit)
      }
    }

    if(!private$perform_merges && is.null(manual_edge_lengths)){
        if(verbose == 2){
          message("Computing edge lengths")
        }
        t <- system.time({
          self$edge_lengths <- private$compute_lengths(private$longlat, private$length_unit, private$crs, private$proj4string, private$which_longlat, private$vertex_unit, project_data,private$transform)
        })
       if(verbose == 2){
      message(sprintf("time: %.3f s", t[["elapsed"]]))
         }
    }


      if(auto_remove_point_edges){
          private$remove_circles(1e-15, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
      }

    if(!private$perform_merges){
      if(is.logical(remove_circles)){
        if(remove_circles){
          private$remove_circles(tolerance$vertex_vertex, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
        }
      } else {
        private$remove_circles(remove_circles, verbose=verbose,longlat = private$longlat, unit=length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=which_longlat, vertex_unit=vertex_unit, project_data)
        remove_circles <- TRUE
      }
    }

    end_construction_time <- Sys.time()
    construction_time <- end_construction_time - start_construction_time

    if(verbose > 0){
      message(sprintf('Total construction time: %.2f %s', construction_time, units(construction_time)))
    }

    # Checking if graph is connected
    if (check_connected) {
      g <- make_graph(edges = c(t(self$E)), directed = FALSE)
      # components <- igraph::clusters(g, mode="weak")
      components <- igraph::components(g, mode="weak")
      nc <- components$no
      if(nc>1){
        message("The graph is disconnected. You can either use the function 'graph_components' to obtain the different connected components or set 'perform_merges' to 'TRUE' and adjust the 'tolerances' to create a single connected graph.")
        private$connected = FALSE
      }
    }
    # creating/updating reference edges
    private$ref_edges <- map_into_reference_edge(self, verbose=verbose)

    # Cloning the initial graph

    if(verbose > 0){
      message("Creating and updating vertices...")
    }

   private$create_update_vertices(verbose=verbose)

    if(verbose > 0){
      message("Storing the initial graph...")
    }
    
    self$compute_PtE_edges(approx = approx_edge_PtE, verbose=verbose)

    private$initial_graph <- self$clone()

    # Cloning again to add the initial graph to the initial graph
    private$initial_graph <- self$clone()
    
    self$set_edge_weights(weights = private$edge_weights, kirchhoff_weights = private$kirchhoff_weights, directional_weights = private$directional_weights, verbose=verbose)
   
    self$setDirectionalWeightFunction()

    if (remove_deg2) {
      if (verbose > 0) {
        message("Remove degree 2 vertices")
      }
      t <- system.time(
        self$prune_vertices(verbose = verbose)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
    }
    # Adding IDs to edges and setting up their class

    # for(i in 1:length(self$edges)){
    #   attr(self$edges[[i]], "id") <- i
    #   class(self$edges[[i]]) <- "metric_graph_edge"
    # }

    if(add_data_tmp){
      if(verbose > 0){
        message("Adding observations...")
      }
      add_obs_options[["data"]] <- dataset_tmp
      do.call(self$add_observations, add_obs_options)
    }

    # Checking if there is some edge with zero length
    if(any(self$edge_lengths == 0)){
        warning("There is at least one edge of length zero. Please, consider redefining the graph.")
    }

  },

  #' @description Sets the edge weights
  #' @param tolerance Tolerance at which circles with length less than this will be removed.
#' @param verbose Print progress of graph creation. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return No return value. Called for its side effects.

  remove_small_circles = function(tolerance, verbose = 1){
    private$remove_circles(tolerance, verbose=verbose,longlat = private$longlat, unit=private$length_unit, crs=private$crs, proj4string=private$proj4string, which_longlat=private$which_longlat, vertex_unit=private$vertex_unit, project_data = private$project_data)
    private$create_update_vertices(verbose=verbose)
  },

  #' @description Exports the edges of the MetricGraph object as an `sf` or `sp`.
  #' @param format The format for the exported object. The options are `sf` (default), `sp` and `list`.
  #' @return 
#' For `format == "sf"`, the function returns an `sf` object of `LINESTRING` geometries, where the associated data frame includes edge weights. 
#' 
#' For `format == "sp"`, the function returns a `SpatialLinesDataFrame` where the data frame includes edge weights. 

  get_edges = function(format = c("sf", "sp", "list")){
    format <- format[[1]]
    if(!(format%in%c("sf", "sp", "list"))){
      stop("'format' must be one of the following: 'sf', 'sp' or 'list'.")
    }
      if(format == "sf"){
        edges_geometries <- lapply(self$edges, sf::st_linestring) 
      
        if(is.vector(private$edge_weights)){
          ew_tmp <- data.frame(.weights = private$edge_weights)
        } else{
          ew_tmp <- as.data.frame(private$edge_weights)
        }

        ew_tmp[[".edge_lengths"]] <- self$edge_lengths

        edges_sf <- sf::st_sf(ew_tmp, geometry = sf::st_sfc(edges_geometries), crs = if(!is.null(private$crs)) private$crs else sf::NA_crs_)
        return(edges_sf)
      } else if(format == "sp"){
             edges_list <- lapply(1:length(self$edges), function(i) {
        sp::Line(coords = matrix(self$edges[[i]], nrow = dim(self$edges[[i]])[1], ncol = dim(self$edges[[i]])[2]))
      })
      sp_edges <- sp::SpatialLines(
          lapply(1:length(edges_list), function(i) sp::Lines(list(edges_list[[i]]), ID = as.character(i))),
          proj4string = if (!is.null(private$crs)) private$proj4string else sp::CRS(NA_character_)
      )
      if(is.vector(private$edge_weights)){
        ew_tmp <- data.frame(.weights = private$edge_weights)
      } else{
        ew_tmp <- as.data.frame(private$edge_weights)
      }
      ew_tmp[[".ID"]] <- as.character(1:length(sp_edges))
      ew_tmp[[".edge_lengths"]] <- self$edge_lengths
      edges_sldf <- sp::SpatialLinesDataFrame(sp_edges, data = ew_tmp, match.ID = ".ID")
      return(edges_sldf)
      } else{
        return(self$edges)
      }
  },

  #' @description Bounding box of the metric graph
  #' @param format If the metric graph has a coordinate reference system, the format for the exported object. The options are `sf` (default), `sp` and `matrix`.
  #' @return A bounding box of the metric graph

  get_bounding_box = function(format = "sf"){
    bounding_box <- private$bounding_box
      if (private$longlat) {
      if (format == "sf") {
        bounding_box <- sf::st_bbox(
          c(xmin = bounding_box$min_x, 
            ymin = bounding_box$min_y,
            xmax = bounding_box$max_x, 
            ymax = bounding_box$max_y),
          crs = private$crs 
        )
      } else {
        sp_bbox_matrix <- matrix(
          c(bounding_box$min_x, bounding_box$max_x, 
            bounding_box$min_y, bounding_box$max_y), 
          ncol = 2, 
          dimnames = list(c("x", "y"), c("min", "max"))
        )

        bounding_box <- sp::SpatialPolygons(
          list(sp::Polygons(list(sp::Polygon(cbind(
            c(sp_bbox_matrix[1, "min"], sp_bbox_matrix[1, "max"], sp_bbox_matrix[1, "max"], sp_bbox_matrix[1, "min"], sp_bbox_matrix[1, "min"]),
            c(sp_bbox_matrix[2, "min"], sp_bbox_matrix[2, "min"], sp_bbox_matrix[2, "max"], sp_bbox_matrix[2, "max"], sp_bbox_matrix[2, "min"])
          ))), ID = "1")), 
          proj4string = private$proj4string  
        )
      }
    }
    return(bounding_box)
  },

  #' @description Exports the vertices of the MetricGraph object as an `sf`, `sp` or as a matrix.
  #' @param format The format for the exported object. The options are `sf` (default), `sp` and `matrix`.
  #' @return 
#' For `which_format == "sf"`, the function returns an `sf` object of `POINT` geometries. 
#' 
#' For `which_format == "sp"`, the function returns a `SpatialPointsDataFrame` object.

  get_vertices = function(format = c("sf", "sp", "list")){
    format <- format[[1]]
    if(!(format%in%c("sf", "sp", "list"))){
      stop("'format' must be one of the following: 'sf', 'sp' or 'list'.")
    }
      vertices_df <- do.call(rbind, lapply(self$vertices, function(v) {
        data.frame(
          X = v[1],
          Y = v[2],
          degree = attr(v, "degree"),
          indegree = attr(v, "indegree"),
          outdegree = attr(v, "outdegree"),
          problematic = attr(v, "problematic"),
          id = attr(v, "id"),
          longlat = attr(v, "longlat"),
          stringsAsFactors = FALSE
          )}))    
    if(format == "sf"){
      vertices_df <- sf::st_as_sf(vertices_df, coords = c("X", "Y"))
      if(!is.null(private$crs)){
        sf::st_crs(vertices_df) <- private$crs
      } else{
        sf::st_crs(vertices_df) <- sf::NA_crs_
      }
    } else if(format == "sp"){
      sp::coordinates(vertices_df) <- ~ X + Y
      if(!is.null(private$proj4string)){
        sp::proj4string(vertices_df) <- private$proj4string
      }
    } 
    return(vertices_df)
  }, 

  #' @description Exports the MetricGraph object as an `sf` or `sp` object.
  #' @param format The format for the exported object. The options are `sf` (default) and `sp`.
  #' @return Returns a list with three elements: `edges`, `vertices`, and `data`.
  #' 
  #' For `format == "sf"`, `edges` is an `sf` object of `LINESTRING` geometries with edge weights, and `vertices` and `data` are `sf` objects with `POINT` geometries.
  #' 
  #' For `format == "sp"`, `edges` is a `SpatialLinesDataFrame` with edge weights, and `vertices` and `data` are `SpatialPointsDataFrame`.

  export = function(format = "sf"){
    edges <- self$get_edges(format = format)
    vertices <- self$get_vertices(format = format)
    if(!is.null(private$data)){
      data = self$get_data(format = format, drop_all_na=FALSE, drop_na = FALSE, group = NULL)
    } else{
      data = NULL
    }
    exported_metric_graph = list(edges = edges, vertices = vertices, data = data)
    # if(format == "sf"){
    #   edges_geometries <- lapply(self$edges, sf::st_linestring) 
      
    #   if(is.vector(private$edge_weights)){
    #     ew_tmp <- data.frame(.weights = private$edge_weights)
    #   } else{
    #     ew_tmp <- as.data.frame(private$edge_weights)
    #   }

    #   edges_sf <- sf::st_sf(ew_tmp, geometry = sf::st_sfc(edges_geometries), crs = if(!is.null(private$crs)) private$crs else NULL)

    #   if(!is.null(private$data)){
    #     data_tmp <- as.data.frame(private$data)
    #     data_geometries <- lapply(1:nrow(data_tmp), function(i) sf::st_point(as.numeric(data_tmp[i, c('.coord_x', '.coord_y')])))
    #     data_sf <- sf::st_sf(data_tmp, geometry = sf::st_sfc(data_geometries), crs = if(!is.null(private$crs)) private$crs else NULL)
    #     class(data_sf) <- c("metric_graph_data", class(data_sf))
    #   } else{
    #     data_sf <- NULL
    #   }
    #   exported_metric_graph <- list(edges = edges_sf, data = data_sf)
    # } else if(format == "sp"){
    #   edges_list <- lapply(1:length(self$edges), function(i) {
    #     sp::Line(coords = matrix(self$edges[[i]], nrow = dim(self$edges[[i]])[1], ncol = dim(self$edges[[i]])[2]))
    #   })
    #   sp_edges <- sp::SpatialLines(
    #       lapply(1:length(edges_list), function(i) sp::Lines(list(edges_list[[i]]), ID = as.character(i))),
    #       proj4string = if (!is.null(private$crs)) private$proj4string else NULL
    #   )
    #   if(is.vector(private$edge_weights)){
    #     ew_tmp <- data.frame(.weights = private$edge_weights)
    #   } else{
    #     ew_tmp <- as.data.frame(private$edge_weights)
    #   }
    #   ew_tmp[[".ID"]] <- as.character(1:length(sp_edges))
    #   edges_sldf <- sp::SpatialLinesDataFrame(sp_edges, data = ew_tmp, match.ID = ".ID")
    #   data_sp <- as.data.frame(private$data)
    #   sp::coordinates(data_sp) <- ~ .coord_x + .coord_y
    #   exported_metric_graph <- list(edges = edges_sldf, data = data_sp)
    # } else{
    #   stop(paste(format,"is not currently not a valid format to be exported."))
    # }
    return(exported_metric_graph)
  },

  #' @description Return the metric graph as a `leaflet::leaflet()` object to be built upon.
  #' @param width	the width of the map
  #' @param height the height of the map
  #' @param padding	the padding of the map
  #' @param options	the map options
  #' @param elementId	Use an explicit element ID for the widget (rather than an automatically generated one).
  #' @param sizingPolicy	htmlwidgets sizing policy object. Defaults to `leafletSizingPolicy()`.
  
  leaflet = function(width = NULL, height = NULL, padding = 0, options = leafletOptions(), elementId = NULL,
  sizingPolicy = leafletSizingPolicy(padding = padding)){
    edges_sf <- self$get_edges(format = "sf")
    return(leaflet::leaflet(data = edges_sf, width = width, height = height, padding = padding, options = options, elementId = elementId, sizingPolicy = sizingPolicy))
  },

  #' @description Returns a `mapview::mapview()` object of the metric graph
  #' @param ... Additional arguments to be passed to `mapview::mapview()`. The `x` argument of mapview, containing the metric graph is already passed internally.
  
  mapview = function(...){
    edges_sf <- self$get_edges(format = "sf")
    return(mapview::mapview(x = edges_sf, ...))
  },

  #' @description Sets the edge weights
  #' @param weights Either a number, a numerical vector with length given by the number of edges, providing the edge weights, or a `data.frame` with the number of rows being equal to the number of edges, where
  #' each row gives a vector of weights to its corresponding edge.
  #' @param kirchhoff_weights If non-null, the name (or number) of the column of `weights` that contain the Kirchhoff weights. Must be equal to 1 (or `TRUE`) in case `weights` is a single number and those are the Kirchhoff weights.
  #' @param directional_weights If non-null, the name (or number) of the column of `weights` that contain the directional weights.
  #' @param verbose There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return No return value. Called for its side effects.

  set_edge_weights = function(weights = NULL, kirchhoff_weights = NULL,
                            directional_weights = NULL, verbose = 0) {
  # Validate input types early
    if(!is.vector(weights) && !is.data.frame(weights) && !is.null(weights)){
      stop("'weights' must be either a vector or a data.frame!")
    }

    if(verbose == 2){
      message("Setting edge weights...")
    }

    if(!is.null(weights)){
        if(is.vector(weights)){
          if ( (length(weights) != 1) && (length(weights) != self$nE)){
            stop(paste0("The length of 'weights' must be either 1 or ", self$nE))
          }
          if(length(weights)==1){
            private$edge_weights <- rep(weights, self$nE)
          } else{
            private$edge_weights <- weights
          }
        } else{
          if(nrow(weights) != self$nE){
            stop("The number of rows of weights must be equal to the number of edges!")
          }
          private$edge_weights <- weights
          if(!(".weights" %in% colnames(private$edge_weights))){
            private$edge_weights[, ".weights"] <- 1
          }
        }
    } else{
      weights <- private$edge_weights
    }

    if(!is.null(kirchhoff_weights)){
        if(length(kirchhoff_weights)>1){
          warning("Only the first entry of 'kirchhoff_weights' was used.")
          kirchhoff_weights <- kirchhoff_weights[[1]]
        }
        if(!is.numeric(kirchhoff_weights)){
          if(!is.character(kirchhoff_weights)){
            stop("'kirchhoff_weights' must be either a number of a string.")
          }
          if(!(kirchhoff_weights%in%colnames(weights))){
            stop(paste(kirchhoff_weights, "is not a column of 'weights'!"))
          }
        } else{
          if(!is.data.frame(weights)){
            if(kirchhoff_weights != 1){
              stop("Since 'weights' is not a data.frame, 'kirchhoff_weights' must be either NULL or 1.")
            }
          } else{
            if(kirchhoff_weights %%1 != 0){
              stop("'kirchhoff_weights' must be an integer.")
            }
            if((kirchhoff_weights < 1) || (kirchhoff_weights > ncol(weights))){
              stop("'kirchhoff_weights' must be a positive integer number smaller or equal to the number of columns of 'weights'.")
            }
          }
        }
        private$kirchhoff_weights <- kirchhoff_weights
       } else{
        if(is.vector(weights)){
          private$kirchhoff_weights <- 1
        } else{
          private$kirchhoff_weights <- ".weights"
        }        
       }

      if(!is.null(directional_weights)){
        if(!is.numeric(directional_weights)){
          if(!is.character(directional_weights)){
            stop("'directional_weights' must be either a numerical vector of a string vector.")
          }
          if(!(directional_weights%in%colnames(weights))){
            stop(paste(directional_weights, "is not a column of 'weights'!"))
          }
        } else{
          if(!is.data.frame(weights)){
            if(directional_weights != 1){
              stop("Since 'weights' is not a data.frame, 'directional_weights' must be either NULL or 1.")
            }
          } else{
            if(directional_weights %%1 != 0){
              stop("'directional_weights' must be an integer.")
            }
            if((directional_weights < 1) || (directional_weights > ncol(weights))){
              stop("'directional_weights' must be a positive integer number smaller or equal to the number of columns of 'weights'.")
            }
          }
        }
        private$directional_weights <- directional_weights
       } else{
        if(is.vector(weights)){
          private$directional_weights <- 1
        } else{
          private$directional_weights <- ".weights"
        }
       }

  # Get edge lengths and preallocate attributes
  edge_lengths <- self$get_edge_lengths()
  edges_PtE <- lapply(self$edges, function(edge) attr(edge, "PtE"))
  nE <- self$nE

  # Use `lapply` with indexing for varying attributes
  self$edges <- lapply(seq_along(self$edges), function(i) {
    edge <- self$edges[[i]]
    if(is.data.frame(private$edge_weights)){
      attr(edge, "weight") <- private$edge_weights[i, , drop=FALSE]
    } else{
      attr(edge, "weight") <- private$edge_weights[i]
    }
    attr(edge, "length") <- edge_lengths[i]
    attr(edge, "id") <- i
    attr(edge, "PtE") <- edges_PtE[[i]]
    attr(edge, "longlat") <- private$longlat
    attr(edge, "crs") <- private$crs$input
    attr(edge, "kirchhoff_weight") <- private$kirchhoff_weights
    attr(edge, "directional_weights") <- private$directional_weights

    # Set the class for each edge
    class(edge) <- "metric_graph_edge"
    edge
  })

  # Set the class of `self$edges` to `metric_graph_edges`
  class(self$edges) <- "metric_graph_edges"
  },


  #' @description Gets the edge weights
  #' @param data.frame If the edge weights are given as vectors, should the result be returned as a data.frame?
 #' @param format Which format should the data be returned? The options are `tibble` for `tidyr::tibble`, `sf` for `POINT`, `sp` for `SpatialPointsDataFrame` and `list` for the internal list format.
 #' @param tibble `r lifecycle::badge("deprecated")` Use `format` instead.
  #' @return A vector or `data.frame` containing the edge weights.

  get_edge_weights = function(data.frame = FALSE, format = c("tibble", "sf", "sp", "list"), tibble = deprecated()){

  if (lifecycle::is_present(tibble)) {
      lifecycle::deprecate_warn("1.3.0.9000", "get_edge_weights(tibble)", "get_edge_weights(format)",
        details = c("The argument `tibble` was deprecated in favor of the argument `format`.")
      )
    if(tibble){
      format <- "tibble"
    }
  }   

    format <- format[[1]]

    format <- tolower(format)

    if(!(format %in% c("tibble", "sf", "sp", "list"))){
      stop("The possible formats are 'tibble', 'sf', 'sp' and 'list'.")
    }  
    
    tmp <- private$edge_weights
    row.names(tmp) <- NULL
    if(!is.data.frame(tmp) && data.frame){
      tmp <- data.frame(.weights = tmp)
    }
    if(format == "tibble"){
      if(!is.data.frame(tmp)){
        tmp <- data.frame(.weights = tmp)
      }
      tmp <- dplyr::as_tibble(tmp)
    } else{
      return(self$get_edges(format = format))
    }
    class(tmp) <- c("metric_graph_weights", class(tmp))
    return(tmp)
  },

  #' @description Gets vertices with incompatible directions
  #' @return A vector containing the vertices with incompatible directions.

  get_vertices_incomp_dir = function(){
    if(is.null(self$vertices)){
      start.deg <- end.deg <- rep(0,self$nV)
      for(i in 1:self$nV) {
        start.deg[i] <- sum(self$E[,1]==i)
        end.deg[i] <- sum(self$E[,2]==i)
      }

      degrees <- self$get_degrees()

      # Finding problematic vertices, that is, vertices with incompatible directions
      # They will not be pruned.
      problematic <- (degrees > 1) & (start.deg == 0 | end.deg == 0)
      return(which(problematic))
    } else{
      problematic <- sapply(self$vertices, function(vert){attr(vert, "problematic")})
      return(which(problematic))
    }

  },

  #' @description Prints a summary of various informations of the graph
  #' @param messages Should message explaining how to build the results be given for missing quantities?
  #' @param compute_characteristics Should the characteristics of the graph be computed? If `NULL` it will be determined based on the size of the graph.
  #' @param check_euclidean Check if the graph has Euclidean edges? If `NULL` it will be determined based on the size of the graph.
  #' @param check_distance_consistency Check the distance consistency assumption? If `NULL` it will be determined based on the size of the graph.
  #' @return No return value. Called for its side effects.

  summary = function(messages = FALSE, compute_characteristics = NULL, check_euclidean = NULL, check_distance_consistency = NULL){

    if(self$nV > 10000){
      if(is.null(compute_characteristics)){
        compute_characteristics <- FALSE
      }
      if(is.null(check_euclidean)){
        check_euclidean <- FALSE
      }
      if(is.null(check_distance_consistency)){
        check_distance_consistency <- FALSE
      }
    } else{
      if(is.null(compute_characteristics)){
        compute_characteristics <- TRUE
      }
      if(is.null(check_euclidean)){
        check_euclidean <- TRUE
      }
      if(is.null(check_distance_consistency)){
        check_distance_consistency <- TRUE
      }
    }

    if(compute_characteristics){
      self$compute_characteristics()
    }
    if(check_distance_consistency){
      self$check_distance_consistency()
    }
    if(check_euclidean){
      self$check_euclidean()
    }
    cat("A metric graph object with:\n\n")
    cat("Vertices:\n")
    cat("\t Total:", self$nV,"\n")
    degrees <- self$get_degrees()
    cat("\t")
    degrees_u <- sort(unique(degrees))
    for(i in 1:length(degrees_u)){
      if((i>1) && (i%%5 == 1)){
        cat("\n\t")
      }
      cat(paste0(" Degree ", degrees_u[i],": ",sum(degrees == degrees_u[i]), "; "))
    }
    cat("\n")

    cat("\t With incompatible directions: ", length(self$get_vertices_incomp_dir()), "\n\n")
    cat("Edges: \n")
    cat("\t Total:", self$nE,"\n")
    cat("\t Lengths: \n")
    cat("\t\t Min:", min(self$get_edge_lengths()), " ; Max:", max(self$get_edge_lengths()), " ; Total:", sum(self$get_edge_lengths()), "\n")
    cat("\t Weights: \n")
    if(is.vector(private$edge_weights)){
      cat("\t\t Min:", min(private$edge_weights), " ; Max:", max(private$edge_weights), "\n")
    } else{
      if(!is.null(colnames(private$edge_weights))){
        cat("\t\t Columns:", colnames(private$edge_weights),"\n")
      } else{
        cat("\t\t Number of columns:", ncol(private$edge_weights), "\n")
      }
    }
    cat("\t That are circles: ", sum(self$E[,1] == self$E[,2]), "\n\n")
    cat("Graph units: \n")
    cat("\t Vertices unit: ", ifelse(is.null(private$vertex_unit), "None", private$vertex_unit), " ; Lengths unit: ", ifelse(is.null(private$length_unit), "None", private$length_unit), "\n\n")
    cat("Longitude and Latitude coordinates: ", private$longlat)
    if(private$longlat){
      cat("\n\t Which spatial package: ", private$which_longlat, "\n")
      cat("\t CRS: ", private$crs$input)
    }
    cat("\n\n")
    if(!is.null(self$characteristics)) {
      cat("Some characteristics of the graph:\n")
      if(self$characteristics$connected){
        cat("\t Connected: TRUE\n")
      } else {
        cat("\t Connected: FALSE\n")
      }
      if(self$characteristics$has_loops){
        cat("\t Has loops: TRUE\n")
      } else {
        cat("\t Has loops: FALSE\n")
      }
      if(self$characteristics$has_multiple_edges){
        cat("\t Has multiple edges: TRUE\n")
      } else {
        cat("\t Has multiple edges: FALSE\n")
      }
      if(self$characteristics$is_tree){
        cat("\t Is a tree: TRUE\n")
      } else {
        cat("\t Is a tree: FALSE\n")
      }
      if(!is.null(self$characteristics$distance_consistency)){
        if(self$characteristics$distance_consistency){
          cat("\t Distance consistent: TRUE\n")
        } else {
          cat("\t Distance consistent: FALSE\n")
        }
      } else{
        cat("\t Distance consistent: unknown\n")
        if(messages){
          message("To check if the graph satisfies the distance consistency, run the `check_distance_consistency()` method.")
        }
      }
      if(!is.null(self$characteristics$euclidean)){
        if(self$characteristics$euclidean){
          cat("\t Has Euclidean edges: TRUE\n")
        } else {
          cat("\t Has Euclidean edges: FALSE\n")
        }
      } else{
        cat("\t Has Euclidean edges: unknown\n")
        if(messages){
          message("To check if the graph has Euclidean edges, run the `check_euclidean()` method.")
        }
      }
    } else{
      cat("Some characteristics of the graph: Not computed.\n")
      if(messages){
        message("To compute the characteristics, run the `compute_characteristics()` method.")
      }
    }
    cat("\n")
    cat("Computed quantities inside the graph: \n")
    cat("\t Laplacian: ", !is.null(self$Laplacian), " ; Geodesic distances: ", !is.null(self$geo_dist), "\n")
    if(is.null(self$Laplacian)){
      if(messages){
        message("To compute the Laplacian, run the 'compute_laplacian()' method.")
      }
    }
    if(is.null(self$geo_dist)){
      if(messages){
        message("To compute the geodesic distances, run the 'compute_geodist()' method.")
      }
    }
    cat("\t Resistance distances: ", !is.null(self$res_dist), " ; Finite element matrices: ", !is.null(self$mesh$C), "\n")
    if(is.null(self$res_dist)){
      if(messages){
        message("To compute the resistance distances, run the 'compute_resdist()' method.")
      }
    }
    if(is.null(self$mesh$C)){
      if(messages){
        message("To compute the finite element matrices, run the 'compute_fem()' method.")
      }
    }
    cat("\n")
    if(is.null(self$mesh)){
      cat("Mesh: The graph has no mesh! \n")
      if(messages){
        message("To build the mesh, run the 'build_mesh()' method.")
      }
    } else{
      cat("Mesh: \n")
      cat("\t Max h_e: ", max(self$mesh$h_e), " ; Min n_e: ", min(self$mesh$n_e), "\n")
    }
    cat("\n")
    if(is.null(private$data)){
      cat("Data: The graph has no data!\n")
      if(messages){
        message("To add observations, use the 'add_observations()' method.")
      }
    } else{
      cat("Data: \n")
      col_names_valid <- grepl("^[^.]+$", names(private$data))
      cat("\t Columns: ", names(private$data)[col_names_valid], "\n")
      cat("\t Groups: ", ifelse(is.null(private$group_col), "None", private$group_col), "\n")
    }
    cat("\n")
    cat("Tolerances: \n")
    cat("\t vertex-vertex: ", private$tolerance$vertex_vertex, "\n")
    cat("\t vertex-edge: ", private$tolerance$vertex_edge, "\n")
    cat("\t edge-edge: ", private$tolerance$edge_edge, "\n")
  },


  #' @description Prints various characteristics of the graph
  #' @return No return value. Called for its side effects.

  print = function() {
    cat("A metric graph with ", self$nV, " vertices and ", self$nE, " edges.\n\n")
    cat("Vertices:\n")
    degrees <- self$get_degrees()
    cat("\t")
    degrees_u <- sort(unique(degrees))
    for(i in 1:length(degrees_u)){
      if((i>1) && (i%%5 == 1)){
        cat("\n\t")
      }
      cat(paste0(" Degree ", degrees_u[i],": ",sum(degrees == degrees_u[i]), "; "))
    }
    cat("\n")
    cat("\t With incompatible directions: ", length(self$get_vertices_incomp_dir()), "\n\n")
    cat("Edges: \n")
    cat("\t Lengths: \n")
    cat("\t\t Min:", min(self$get_edge_lengths()), " ; Max:", max(self$get_edge_lengths()), " ; Total:", sum(self$get_edge_lengths()), "\n")
    cat("\t Weights: \n")
    if(is.vector(private$edge_weights)){
      cat("\t\t Min:", min(private$edge_weights), " ; Max:", max(private$edge_weights), "\n")
    } else{
      if(!is.null(colnames(private$edge_weights))){
        cat("\t\t Columns:", colnames(private$edge_weights),"\n")
      } else{
        cat("\t\t Number of columns:", ncol(private$edge_weights), "\n")
      }
    }
    cat("\t That are circles: ", sum(self$E[,1] == self$E[,2]), "\n\n")
    cat("Graph units: \n")
    cat("\t Vertices unit: ", ifelse(is.null(private$vertex_unit), "None", private$vertex_unit), " ; Lengths unit: ", ifelse(is.null(private$length_unit), "None", private$length_unit), "\n\n")
    cat("Longitude and Latitude coordinates: ", private$longlat)
    if(private$longlat){
      cat("\n\t Which spatial package: ", private$which_longlat, "\n")
      cat("\t CRS: ", private$crs$input)
    }
    cat("\n\n")

    if(!is.null(self$characteristics)) {
      cat("Some characteristics of the graph:\n")
      if(self$characteristics$connected){
        cat("  Connected: TRUE\n")
      } else {
        cat("  Connected: FALSE\n")
      }
      if(self$characteristics$has_loops){
        cat("  Has loops: TRUE\n")
      } else {
        cat("  Has loops: FALSE\n")
      }
      if(self$characteristics$has_multiple_edges){
        cat("  Has multiple edges: TRUE\n")
      } else {
        cat("  Has multiple edges: FALSE\n")
      }
      if(self$characteristics$is_tree){
        cat("  Is a tree: TRUE\n")
      } else {
        cat("  Is a tree: FALSE\n")
      }
      if(!is.null(self$characteristics$distance_consistency)){
        if(self$characteristics$distance_consistency){
          cat("  Distance consistent: TRUE\n")
        } else {
          cat("  Distance consistent: FALSE\n")
        }
      } else{
        cat("  Distance consistent: unknown\n")
        message("To check if the graph satisfies the distance consistency, run the `check_distance_consistency()` method.")
      }
      if(!is.null(self$characteristics$euclidean)){
        if(self$characteristics$euclidean){
          cat("  Has Euclidean edges: TRUE\n")
        } else {
          cat("  Has Euclidean edges: FALSE\n")
        }
      } else{
        cat("  Has Euclidean edges: unknown\n")
        message("To check if the graph has Euclidean edges, run the `check_euclidean()` method.")
      }
    }
    invisible(self)
  },
  #' @description Computes various characteristics of the graph
  #' @param check_euclidean Also check if the graph has Euclidean edges? This essentially means that the distance consistency check will also be perfomed. If the graph does not have Euclidean edges due to another reason rather than the distance consistency, then it will already be indicated that the graph does not have Euclidean edges.
  #' @return No return value. Called for its side effects. The computed characteristics
  #' are stored in the `characteristics` element of the `metric_graph` object.
  compute_characteristics = function(check_euclidean = FALSE) {
    if(is.null(self$characteristics)){
      self$characteristics <- list()
    }

    #check for loops
    if(is.null(self$characteristics$has_loops)){
      if(sum(self$E[,1]==self$E[,2])>0) {
        self$characteristics$has_loops <- TRUE
      } else {
        self$characteristics$has_loops <- FALSE
      }
    }

    self$characteristics$connected <- private$connected

    #check for multiple edges
    if(is.null(self$characteristics$has_multiple_edges)){
      self$characteristics$has_multiple_edges <- FALSE
      k <- 1
      while(k < self$nV && self$characteristics$has_multiple_edges == FALSE) {
        ind <- which(self$E[,1]==k | self$E[,2]==k) #edges starting or ending in k
        if(length(ind) > length(unique(rowSums(self$E[ind,,drop=FALSE])))) {
          self$characteristics$has_multiple_edges <- TRUE
        } else {
          k <- k + 1
        }
      }
    }


    #check for tree structure
    if(!self$characteristics$has_loops && !self$characteristics$has_multiple_edges){
      self$characteristics$is_tree <- self$is_tree()
    } else {
      self$characteristics$is_tree <- FALSE
    }

    if(!self$characteristics$connected || self$characteristics$has_loops || self$characteristics$has_multiple_edges){
      self$characteristics$euclidean <- FALSE
    } else if(self$characteristics$is_tree){
      self$characteristics$euclidean <- TRUE
    }

  },

  #' @description Check if the graph has Euclidean edges.
  #' @return Returns `TRUE` if the graph has Euclidean edges, or `FALSE` otherwise.
  #' The result is stored in the `characteristics` element of the `metric_graph` object.
  #' The result is displayed when the graph is printed.

  check_euclidean = function(){
    self$compute_characteristics()
    if(!is.null(self$characteristics$euclidean)){
      return(invisible(NULL))
    }

    if(is.null(self$characteristics$distance_consistency)){
      self$check_distance_consistency()
      if(self$characteristics$distance_consistency){
        self$characteristics$euclidean <- TRUE
      } else{
        self$characteristics$euclidean <- FALSE
      }
    } else{
      if(self$characteristics$distance_consistency){
        self$characteristics$euclidean <- TRUE
      } else{
        self$characteristics$euclidean <- FALSE
      }
    }
  },

  #' @description Checks distance consistency of the graph.
  #' @return No return value.
  #' The result is stored in the `characteristics` element of the `metric_graph` object.
  #' The result is displayed when the graph is printed.

  check_distance_consistency = function(){
    self$compute_characteristics()
    if(is.null(self$geo_dist)){
      self$geo_dist <- list()
    }

    if(is.null(self$geo_dist[[".vertices"]])){
      g <- make_graph(edges = c(t(self$E)), directed = FALSE)
      E(g)$weight <- self$edge_lengths
      self$geo_dist[[".vertices"]] <- distances(g)
    }

    geo_dist_edges <- self$geo_dist[[".vertices"]][self$E]
    if(any(abs(geo_dist_edges - self$edge_lengths) > 1e-8)){
      self$characteristics$distance_consistency <- FALSE
    } else{
      self$characteristics$distance_consistency <- TRUE
    }
  },

  #' @description Computes shortest path distances between the vertices in the
  #' graph
  #' @param full Should the geodesic distances be computed for all
  #' the available locations? If `FALSE`, it will be computed
  #' separately for the locations of each group.
  #' @param obs Should the geodesic distances be computed at the observation
  #' locations?
  #' @param group Vector or list containing which groups to compute the distance
  #' for. If `NULL`, it will be computed for all groups.
  #' @param verbose Print progress of the computation of the geodesic distances. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return No return value. Called for its side effects. The computed geodesic
  #' distances are stored in the `geo_dist` element of the `metric_graph` object.
  compute_geodist = function(full = FALSE, obs = TRUE, group = NULL, verbose = 0) {
    if(is.null(self$geo_dist)){
      self$geo_dist <- list()
    }

    if(is.null(private$data)){
      obs <- FALSE
    }
    if(!obs){
      if(verbose == 2){
        message("Creating auxiliary graph...")
      }
      t <- system.time({
        g <- make_graph(edges = c(t(self$E)), directed = FALSE)
        E(g)$weight <- self$edge_lengths
      })
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
      if(verbose > 0){
        message("Computing geodesic distances...")
      }
      t <- system.time(
        self$geo_dist[[".vertices"]] <- distances(g)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
    } else if(full){
      PtE_full <- self$get_PtE()
      self$geo_dist[[".complete"]] <- self$compute_geodist_PtE(PtE = PtE_full,
                                                              normalized = TRUE, verbose = verbose)
    } else{
      if(is.null(group)){
          group <- unique(private$data[[".group"]])
      }
      for(grp in group){
          data_grp <- select_group(private$data, grp)
          idx_notna <- idx_not_all_NA(data_grp)
          PtE_group <- cbind(data_grp[[".edge_number"]][idx_notna],
                     data_grp[[".distance_on_edge"]][idx_notna])
          self$geo_dist[[grp]] <- self$compute_geodist_PtE(PtE = PtE_group,
                                                              normalized = TRUE, verbose = verbose)
      }
    }
  },
  #' @description Computes shortest path distances between the vertices in the
  #' graph.
  #' @param PtE Points to compute the metric for.
  #' @param normalized are the locations in PtE in normalized distance?
  #' @param include_vertices Should the original vertices be included in the
  #' distance matrix?
  #' @param verbose Print progress of the computation of the geodesic distances. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return A matrix containing the geodesic distances.
  compute_geodist_PtE = function(PtE,
                                 normalized = TRUE,
                                 include_vertices = TRUE, verbose = 0){
      if(verbose == 2){
        message("Processing the graph locations...")
      }
      t <- system.time({
        graph.temp <- self$clone()
        graph.temp$clear_observations()
        df_temp <- data.frame(y = rep(0, dim(PtE)[1]),
                            edge_number = PtE[,1],
                            distance_on_edge = PtE[,2])
        if(sum(duplicated(df_temp))>0){
          warning("Duplicated locations were found when computing geodist. The returned values are given for unique locations.")
          df_temp <- unique(df_temp)
        }

        df_temp <- standardize_df_positions(df_temp, self)

        graph.temp$build_mesh(h = 10000)

        df_temp2 <- data.frame(y = 0, edge_number = graph.temp$mesh$VtE[1:nrow(self$V),1],
                                  distance_on_edge = graph.temp$mesh$VtE[1:nrow(self$V),2])

        df_temp2 <- standardize_df_positions(df_temp2, self)

        df_temp$included <- TRUE
        temp_merge <- merge(df_temp, df_temp2, all = TRUE)

        df_temp$included <- NULL

        df_temp2 <- temp_merge[is.na(temp_merge["included"]),]

        nV_new <- sum(is.na(temp_merge["included"]))

        df_temp2$included <- NULL

        df_temp <- rbind(df_temp2, df_temp)

        df_temp[["__dummy"]] <- 1:nrow(df_temp)

        graph.temp$add_observations(data = df_temp,
                                     normalized = normalized,
                                     verbose=0,
                  suppress_warnings = TRUE)

      })

      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }

      if(verbose == 2){
        message("Turning observations of the auxiliary graph to vertices...")
      }
      t <- system.time(
        graph.temp$observation_to_vertex(mesh_warning = FALSE)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }
      if(verbose == 2){
        message("Creating auxiliary graph...")
      }
      t <- system.time({
        g <- make_graph(edges = c(t(graph.temp$E)), directed = FALSE)
        E(g)$weight <- graph.temp$edge_lengths
      })
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }
      if(verbose>0){
        message("Computing geodesic distances...")
      }
      t <- system.time(
        geodist_temp <- distances(g)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }

      if(length(graph.temp$PtV)[1]!=nrow(geodist_temp)){
        un_PtV <- unique(graph.temp$PtV)
        un_coords <- !duplicated(graph.temp$PtV)

        geodist_temp <- geodist_temp[un_PtV, un_PtV]

        tmp_vec <- graph.temp$.__enclos_env__$private$data[["__dummy"]][un_coords]

        un_ord <- order(tmp_vec)

        tmp_vec[un_ord] <- 1:length(tmp_vec)
        #Ordering back in the input order
        geodist_temp[tmp_vec,tmp_vec] <- geodist_temp
      } else{
          geodist_temp <- geodist_temp[graph.temp$PtV, graph.temp$PtV]
          #Ordering back in the input order
          geodist_temp[graph.temp$.__enclos_env__$private$data[["__dummy"]],graph.temp$.__enclos_env__$private$data[["__dummy"]]] <- geodist_temp
      }

      if(!include_vertices){
        geodist_temp <- geodist_temp[(nV_new+1):nrow(geodist_temp), (nV_new+1):nrow(geodist_temp)]
      }

      attr(geodist_temp, "unit") <- private$length_unit

      return(geodist_temp)
  },

  #' @description Computes shortest path distances between the vertices in the
  #' mesh.
  #' @return No return value. Called for its side effects. The geodesic distances
  #' on the mesh are stored in `mesh$geo_dist` in the `metric_graph` object.
  compute_geodist_mesh = function() {
    g <- make_graph(edges = c(t(self$mesh$E)), directed = FALSE)
    E(g)$weight <- self$mesh$h_e
    self$mesh$geo_dist <- distances(g)
  },

  #' @description Computes the resistance distance between the observation
  #' locations.
  #' @param full Should the resistance distances be computed for all
  #' the available locations. If `FALSE`, it will be computed
  #' separately for the locations of each group.
  #' @param obs Should the resistance distances be computed at the observation
  #' locations?
  #' @param group Vector or list containing which groups to compute the distance
  #' for. If `NULL`, it will be computed for all groups.
  #' @param check_euclidean Check if the graph used to compute the resistance distance has Euclidean edges? The graph used to compute the resistance distance has the observation locations as vertices.
  #' @param include_vertices Should the vertices of the graph be also included in the resulting matrix when using `FULL=TRUE`?
  #' @param verbose Print progress of the computation of the resistance distances. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return No return value. Called for its side effects. The geodesic distances
  #' are stored in the `res_dist` element of the `metric_graph` object.
  compute_resdist = function(full = FALSE, obs = TRUE, group = NULL,
                                 check_euclidean = FALSE, include_vertices = FALSE, verbose = 0) {
    self$res_dist <- list()
    if(is.null(private$data)){
      obs <- FALSE
    }
    if(!obs){
      graph.temp <- self$clone()
      graph.temp$build_mesh(h=1000)

      PtE <- graph.temp$mesh$VtE[1:nrow(self$V),]
      rm(graph.temp)
      self$res_dist[[".vertices"]] <- self$compute_resdist_PtE(PtE,
                                                                normalized=TRUE,
                                                                       check_euclidean = check_euclidean, verbose = verbose)
    } else if(full){
      PtE <- self$get_PtE()
      if(!include_vertices){
        self$res_dist[[".complete"]] <- self$compute_resdist_PtE(PtE,
                                                                normalized=TRUE, include_vertices = FALSE,
                                                                       check_euclidean = check_euclidean, verbose = verbose)
      } else{
        self$res_dist[[".complete"]] <- self$compute_resdist_PtE(PtE,
                                                                normalized=TRUE, include_vertices = TRUE,
                                                                       check_euclidean = check_euclidean, verbose = verbose)
      }

    } else{
      if(is.null(group)){
          group <- unique(private$data[[".group"]])
      }
      for(grp in group){
        data_grp <- select_group(private$data, grp)
        idx_notna <- idx_not_all_NA(data_grp)
        if(sum(idx_notna) == 0){
          stop("There are no non-NA observations.")
        }
        PtE <- cbind(data_grp[[".edge_number"]][idx_notna],
                     data_grp[[".distance_on_edge"]][idx_notna])
        self$res_dist[[as.character(grp)]] <- self$compute_resdist_PtE(PtE,
                                                                       normalized=TRUE,
                                                                       check_euclidean = check_euclidean, verbose = verbose)
      }
    }
  },

  #' @description Computes the resistance distance between the observation
  #' locations.
  #' @param PtE Points to compute the metric for.
  #' @param normalized Are the locations in PtE in normalized distance?
  #' @param include_vertices Should the original vertices be included in the
  #' Laplacian matrix?
  #' @param check_euclidean Check if the graph used to compute the resistance distance has Euclidean edges? The graph used to compute the resistance distance has the observation locations as vertices.
  #' @param verbose Print progress of the computation of the resistance distances. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return A matrix containing the resistance distances.
  compute_resdist_PtE = function(PtE,
                                 normalized = TRUE,
                                 include_vertices = FALSE,
                                 check_euclidean = FALSE, verbose = 0) {
      if(verbose == 2){
        message("Processing the graph locations...")
      }
      t <- system.time({
        graph.temp <- self$clone()
        graph.temp$clear_observations()
        df_temp <- data.frame(y = rep(0, dim(PtE)[1]),
                            edge_number = PtE[,1],
                            distance_on_edge = PtE[,2])
        if(sum(duplicated(df_temp))>0){
          warning("Duplicated locations were found when computing geodist. The returned values are given for unique locations.")
          df_temp <- unique(df_temp)
        }

        graph.temp$build_mesh(h = 1000)

        df_temp2 <- data.frame(y = 0,
                             edge_number = graph.temp$mesh$VtE[1:nrow(self$V), 1],
                             distance_on_edge = graph.temp$mesh$VtE[1:nrow(self$V), 2])

        df_temp$included <- TRUE
        temp_merge <- merge(df_temp, df_temp2, all = TRUE)

        df_temp$included <- NULL

        df_temp2 <- temp_merge[is.na(temp_merge["included"]),]

        nV_new <- sum(is.na(temp_merge["included"]))

        df_temp2$included <- NULL

        df_temp <- rbind(df_temp2, df_temp)

        df_temp[["__dummy"]] <- 1:nrow(df_temp)

        graph.temp$add_observations(data = df_temp,
                                     normalized = normalized, verbose = 0,
                  suppress_warnings = TRUE)
      })
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
      if(verbose == 2){
        message("Turning observations of the auxiliary graph to vertices...")
      }
      t <- system.time(
        graph.temp$observation_to_vertex(mesh_warning=FALSE)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }
      if(verbose > 0){
        message("Computing auxiliary geodesic distances...")
      }
      t <- system.time(
        graph.temp$compute_geodist(full=TRUE,verbose = verbose)
      )
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }

        if(check_euclidean){
          graph.temp$check_euclidean()
          is_euclidean <- graph.temp$characteristics$euclidean
        }

      if(verbose > 0){
        message("Computing the resistance distances...")
      }
      t <- system.time({
        geodist_temp <- graph.temp$geo_dist[[".complete"]]
        geodist_temp[graph.temp$PtV, graph.temp$PtV] <- geodist_temp

        L <- Matrix(0, graph.temp$nV, graph.temp$nV)

        for (i in 1:graph.temp$nE) {
          tmp <- -1 / geodist_temp[graph.temp$E[i, 1],
                                                         graph.temp$E[i, 2]]
          L[graph.temp$E[i, 2], graph.temp$E[i, 1]] <- tmp
          L[graph.temp$E[i, 1], graph.temp$E[i, 2]] <- tmp
        }
        for(i in 1:graph.temp$nV){
          L[i, i] <- -sum(L[i, -i])
        }
        L[1, 1] <- L[1, 1] + 1

        Li <- solve(L)
        R <- -2*Li + t(diag(Li)) %x% rep(1, graph.temp$nV) +
          t(rep(1, graph.temp$nV)) %x% diag(Li)

        R <- R[graph.temp$PtV, graph.temp$PtV]
        R[graph.temp$.__enclos_env__$private$data[["__dummy"]],graph.temp$.__enclos_env__$private$data[["__dummy"]]] <- R

        if(!include_vertices){
          R <- R[(nV_new+1):nrow(R), (nV_new+1):nrow(R)]
        }
      })

      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }

      if(check_euclidean){
        attr(R, "euclidean") <- is_euclidean
      }

      return(R)
  },

  #' @description Returns the degrees of the vertices in the metric graph.
  #' @param which If "degree", returns the degree of the vertex. If "indegree", returns the indegree,
  #' and if "outdegree", it returns the outdegree.
  #' @return A vector containing the degrees of the vertices.
  get_degrees = function(which = "degree"){
    which <- which[[1]]
    if(!(which %in% c("degree", "indegree", "outdegree"))){
      stop("'which' must be either 'degree', 'indegree' or 'outdegree'!")
    }
    if(which == "degree"){
      degrees <- sapply(self$vertices, function(vert){attr(vert, "degree")})
    } else if(which == "indegree"){
       degrees <- sapply(self$vertices, function(vert){attr(vert, "indegree")})
    } else{
        degrees <- sapply(self$vertices, function(vert){attr(vert, "outdegree")})
    }
    return(degrees)
  },

  #' @description Computes the relative positions of the coordinates of the edges and save it as an attribute to each edge. This improves the quality of plots obtained by the `plot_function()` method, however it might be costly to compute.
  #' @param approx Should the computation of the relative positions be approximate? Default is `TRUE`. If `FALSE`, the speed can be considerably slower, especially for large metric graphs.
  #' @param verbose Level of verbosity, 0, 1 or 2. The default is 0.
  #' @return No return value, called for its side effects.
  compute_PtE_edges = function(approx = TRUE, verbose = 0){
    
    if(verbose > 0){
      message("Computing the relative positions of the edges...")
      if(verbose == 2){
        bar_edges_pte <- msg_progress_bar(length(self$edges))
      }
    }
    
    if(approx){
      edges_PtE <- lapply(self$edges, function(edge){
      if(verbose == 2){
        bar_edges_pte$increment()
      }
      private$approx_coordinates(edge = edge)
      })
    } else{
      edges_PtE <- lapply(self$edges, function(edge){
      if(verbose == 2){
        bar_edges_pte$increment()
      }
      private$exact_PtE_coordinates(edge = edge)
      })
    }
    self$edges <- lapply(1:length(self$edges), function(j){
      edge <- self$edges[[j]]
      attr(edge, "PtE") <- edges_PtE[[j]]
      return(edge)
    })
    class(self$edges) <- "metric_graph_edges"
    return(invisible(NULL))
  },

  #' @description Computes the resistance metric between the vertices in the
  #' mesh.
  #' @return No return value. Called for its side effects. The geodesic distances
  #' on the mesh are stored in the `mesh$res_dist` element in the `metric_graph`
  #' object.
  compute_resdist_mesh = function() {
    if (is.null(self$mesh)) {
      stop("no mesh provided")
    }
    if(is.null(self$mesh$geo_dist)){
      self$compute_geodist_mesh()
    }
    L <- Matrix(0, dim(self$mesh$V)[1], dim(self$mesh$V)[1])
    for (i in 1:dim(self$mesh$E)[1]) {
      tmp <- -1 / self$mesh$geo_dist[self$mesh$E[i, 1], self$mesh$E[i, 2]]
      L[self$mesh$E[i, 2], self$mesh$E[i, 1]] <- tmp
      L[self$mesh$E[i, 1], self$mesh$E[i, 2]] <- tmp
    }
    for (i in 1:dim(self$mesh$V)[1]) {
      L[i, i] <- -sum(L[i, -i])
    }
    L[1, 1] <- L[1, 1] + 1

    Li <- solve(L)
    self$mesh$res_dist <- -2*Li + t(diag(Li)) %x% rep(1,dim(self$mesh$V)[1]) +
      t(rep(1,dim(self$mesh$V)[1])) %x% diag(Li)
  },


  #' @description Computes the weigthed graph Laplacian for the graph.
  #' @param full Should the resistance distances be computed for all
  #' the available locations. If `FALSE`, it will be computed
  #' separately for the locations of each group.
  #' @param obs Should the resistance distances be computed at the observation
  #' locations? It will only compute for locations in which there is at least one observations that is not NA.
  #' @param group Vector or list containing which groups to compute the
  #' Laplacian for. If `NULL`, it will be computed for all groups.
  #' @param verbose Print progress of the computation of the Laplacian. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @return No reutrn value. Called for its side effects. The Laplacian is stored
  #' in the `Laplacian` element in the `metric_graph` object.
  compute_laplacian = function(full = FALSE, obs = TRUE, group = NULL, verbose = 0) {
    self$Laplacian <- list()
    if(is.null(private$data)){
      obs <- FALSE
    }
    if(!obs){
      graph.temp <- self$clone()
      graph.temp$build_mesh(h=1000)

      PtE <- graph.temp$mesh$VtE[1:nrow(self$V),]
      rm(graph.temp)
      self$Laplacian[[".vertices"]] <- private$compute_laplacian_PtE(PtE,
                                                            normalized = TRUE, verbose = verbose)
    } else if(full){
      PtE <- self$get_PtE()
      self$Laplacian[[".complete"]] <- private$compute_laplacian_PtE(PtE,
                                                            normalized = TRUE, verbose = verbose)
    } else{
      if(is.null(group)){
          group <- unique(private$data[[".group"]])
      }
      for(grp in group){
          data_grp <- select_group(private$data, grp)
          idx_notna <- idx_not_all_NA(data_grp)
          PtE <- cbind(data_grp[[".edge_number"]][idx_notna],
                       data_grp[[".distance_on_edge"]][idx_notna])
          if(nrow(PtE) == 0){
            stop("All the observations are NA.")
          }
          self$Laplacian[[grp]] <- private$compute_laplacian_PtE(PtE,
                                                              normalized = TRUE, verbose = verbose)
      }
    }
  },

  #' @description Removes vertices of degree 2 from the metric graph.
  #' @return No return value. Called for its side effects.
  #' @param check_weights If `TRUE` will only prune edges with different weights.
  #' @param check_circles If `TRUE` will not prune a vertex such that the resulting edge is a circle.
 #' @param verbose Print progress of pruning. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @details
    #' Vertices of degree 2 are removed as long as the corresponding edges that
    #' would be merged are compatible in terms of direction.
    #'
  prune_vertices = function(check_weights = TRUE, check_circles = TRUE, verbose = FALSE){
    t <- system.time({
    degrees <- private$degrees$degrees

    # Finding problematic vertices, that is, vertices with incompatible directions
    # They will not be pruned.

    problematic <- sapply(self$vertices, function(vert){attr(vert,"problematic")})

    if((verbose > 0) && (sum(problematic) > 0)){
      message(paste(sum(problematic), "vertices were not pruned due to incompatible directions."))
    }

    if (check_weights) {
      # Identify the vertices with degree 2 that are not problematic
      idx_tmp <- which(degrees == 2 & !problematic)
      problematic_weights <- rep(FALSE, self$nV)

      if (verbose == 2) {
        message("Checking weight compatibility")
      }

      # Vectorized operations
      start_deg <- match(idx_tmp, self$E[, 1], nomatch = 0)
      end_deg <- match(idx_tmp, self$E[, 2], nomatch = 0)

      # Combine indices to get the edges related to each vertex
      edges_tmp <- cbind(start_deg, end_deg)
      valid_edges <- rowSums(edges_tmp == 0) == 0  # Exclude invalid matches

      if (any(valid_edges)) {
        # Only check weights for valid edges
        edges_tmp <- edges_tmp[valid_edges, , drop = FALSE]
        idx_tmp <- idx_tmp[valid_edges]

        # Check weight compatibility in a vectorized way
        if (is.vector(private$edge_weights)) {
          # Create temporary copies with NA replaced by a unique placeholder
          edge_weights_copy <- private$edge_weights
          edge_weights_copy[is.na(edge_weights_copy)] <- ".dummy_na_val"

          # Compare the edges normally, treating NA == NA as TRUE via the placeholder
          cnd_tmp <- edge_weights_copy[edges_tmp[, 1]] != edge_weights_copy[edges_tmp[, 2]]
        } else {
          # For matrices, replace NA with a unique placeholder in a temporary copy
          edge_weights_copy <- private$edge_weights
          edge_weights_copy[is.na(edge_weights_copy)] <- ".dummy_na_val"

          # Perform row-wise comparison, each row must have all columns matching
          cnd_tmp <- rowSums(
            edge_weights_copy[edges_tmp[, 1], , drop = FALSE] != edge_weights_copy[edges_tmp[, 2], , drop = FALSE]
          ) == ncol(edge_weights_copy)
        }

        # Update problematic_weights vector based on the results
        problematic_weights[idx_tmp] <- cnd_tmp
      }

      # Update problematic vertices
      problematic <- (problematic | problematic_weights)

      if ((verbose > 0) && (sum(problematic_weights) > 0)) {
        message(paste(sum(problematic_weights), "vertices were not pruned due to incompatible weights. Turn 'check_weights' to FALSE to prune these vertices."))
      }
    }

    res <- list(degrees = degrees, problematic = problematic)

    res[["problematic_circles"]] <- rep(FALSE, length(degrees))

    if(verbose > 0){
      to.prune <- sum(res$degrees==2 & !res$problematic)
      k <- 1
      message(sprintf("removing %d vertices", to.prune))
      if(to.prune > 0) {
        # pb = txtProgressBar(min = 1, max = to.prune, initial = 1, style = 3)
        bar_prune <- msg_progress_bar(to.prune)
      }

    }

   while(sum(res$degrees==2 & !res$problematic & !res$problematic_circles)>0) {
     if((verbose == 2) && to.prune > 0){
      #  setTxtProgressBar(pb,k)
      bar_prune$increment()
       #message(sprintf("removing vertex %d of %d.", k, to.prune))
       k <- k + 1
     }
     res <- private$remove.first.deg2(res, check_circles = check_circles)
   }
    if(verbose == 2){
      if(!is.null(res$circles_avoided)){
        message(paste(sum(res$problematic_circles),"vertices were not pruned in order to avoid creating circles. Turn 'check_circles' to FALSE to prune these vertices."))
      }
    }
   })
    if(verbose  == 2){
          message(sprintf("time: %.3f s", t[["elapsed"]]))
    }
    # if(verbose && to.prune > 0){
    #   close(pb)
    # }

   t <- system.time({
      private$create_update_vertices(verbose=verbose)
      # creating/updating reference edges
      private$ref_edges <- map_into_reference_edge(self, verbose=verbose)
      if(verbose==2){
                message("Updating attributes of the edges")
                bar_update_attr_edges <- msg_progress_bar(length(self$edges))
      }
      # for(i in 1:length(self$edges)){
      #    attr(self$edges[[i]], "id") <- i
      #    attr(self$edges[[i]], "longlat") <- private$longlat
      #    attr(self$edges[[i]], "crs") <- private$crs$input
      #    attr(self$edges[[i]], "length") <- self$edge_lengths[i]
      #    class(self$edges[[i]]) <- "metric_graph_edge"
      #   if(!is.null(private$length_unit)){
      #     units(attr(self$edges[[i]], "length")) <- private$length_unit
      #   }
      #   if(is.vector(private$edge_weights)){
      #     attr(self$edges[[i]], "weight") <- private$edge_weights[i]
      #   } else{
      #     attr(self$edges[[i]], "weight") <- private$edge_weights[i,]
      #   }
      #   attr(self$edges[[i]], "kirchhoff_weight") <- private$kirchhoff_weights
      #   if(verbose == 2){
      #     bar_update_attr_edges$increment()
      #   }        
      # }

    edge_lengths_ <- self$get_edge_lengths()

    self$edges <- lapply(1:self$nE, function(i){
      edge <- self$edges[[i]]
      if(is.vector(private$edge_weights)){
        attr(edge,"weight") <- private$edge_weights[i]
      } else{
        attr(edge,"weight") <- private$edge_weights[i, ,drop=FALSE]
      }
      attr(edge, "longlat") <- private$longlat
      attr(edge, "crs") <- private$crs$input
      attr(edge, "length") <- edge_lengths_[i]
      attr(edge, "id") <- i
      attr(edge, "kirchhoff_weight") <- private$kirchhoff_weights
      attr(edge, "directional_weights") <- private$directional_weights
      class(edge) <- "metric_graph_edge"
        if(verbose == 2){
          bar_update_attr_edges$increment()
        }      
      return(edge)
    })



   })
   if(verbose == 2){
            message(sprintf("time: %.3f s", t[["elapsed"]]))
   }


   if(!is.null(private$data)){
    if(verbose > 0){
      message("Updating data locations.")
    }
      t <- system.time({
      x_coord <- private$data[[".coord_x"]]
      y_coord <- private$data[[".coord_y"]]
      new_PtE <- self$coordinates(XY = cbind(x_coord, y_coord))
      group_vec <- private$data[[".group"]]
      private$data[[".edge_number"]] <- new_PtE[,1]
      private$data[[".distance_on_edge"]] <- new_PtE[,2]
      order_idx <- order(group_vec, new_PtE[,1], new_PtE[,2])
      old_group_variable <- attr(private$data, "group_variable")
      private$data <- lapply(private$data, function(dat){dat[order_idx]})
      attr(private$data, "group_variable") <- old_group_variable
      })
      if(verbose == 2){
            message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
   }

   if(!is.null(self$mesh)){
    max_h <- max(self$mesh$h_e)
    self$mesh <- NULL
    self$build_mesh(h = max_h)
   }

   self$C <- NULL
   self$CoB <- NULL

   private$pruned <- TRUE
   private$degrees <- NULL
   if(private$prune_warning){
    warning("At least two edges with different weights were merged due to pruning. Only one of the weights has been assigned to the merged edge. Please, review carefully.")
    private$prune_warning <- FALSE
   }
  },


  #' @description Gets the groups from the data.
  #' @param edge_lengths edge lengths to be set to the metric graph edges.
  #' @param unit set or override the edge lengths unit.
  #' @return does not return anything. Called for its side effects.

    set_manual_edge_lengths = function(edge_lengths, unit = NULL){
      if(is.null(edge_lengths)){
        warning("edge_lengths is NULL, edge lengths were not set.")
        return(invisible(NULL))
      }
      if(length(edge_lengths)!=length(self$edges)){
        stop("edge_lengths must have length equal to the number of edges.")
      }
      private$manual_edge_lengths <- TRUE
      self$edge_lengths <- edge_lengths
      private$length_unit <- unit
      return(invisible(NULL))
  },

  #' @description Gets the groups from the data.
  #' @param get_cols Should the names of the columns that created the group variable be returned?
  #' @return A vector containing the available groups in the internal data.

  get_groups = function(get_cols = FALSE){
    if(is.null(private$data)){
      warning("There is no data!")
      return(invisible(NULL))
    }
    if(get_cols){
      return(private$group_col)
    }
    return(unique(private$data[[".group"]]))
  },

  #' @description Gets PtE from the data.
  #' @param group For which group, should the PtE be returned? `NULL` means that all PtEs available will be returned.
  #' @param include_group Should the group be included as a column? If `TRUE`, the PtEs for each group will be concatenated, otherwise a single matrix containing the unique PtEs will be returned.
  #' @return A matrix with two columns, where the first column contains the edge
  #' number and the second column contains the distance on edge of the
  #' observation locations.
  get_PtE = function() {
    if(is.null(private$data)){
      warning("There is no data!")
      return(invisible(NULL))
    }
    group <- private$data[[".group"]]
    group <- which(group == group[1])
    PtE <- cbind(private$data[[".edge_number"]][group],
                 private$data[[".distance_on_edge"]][group])
    return(PtE)
  },

  #' @description Gets the edge lengths with the corresponding unit.
  #' @param unit If non-NULL, changes from `length_unit` from the graph construction to `unit`.
  #' @return a vector with the length unit (if the graph was constructed with a length unit).

  get_edge_lengths = function(unit = NULL){
    el <- self$edge_lengths
    units(el) <- private$length_unit
    if(!is.null(unit)){
      units(el) <- unit
    }
    names(el) <- NULL
    return(el)
  },

  #' @description Gets the spatial locations from the data.
  #' @return A `data.frame` object with observation locations. If `longlat = TRUE`, the column names are lon and lat, otherwise the column names are x and y.
  get_locations = function(){
     if(is.null(private$data)){
      warning("There is no data!")
      return(invisible(NULL))
    }
    group <- private$data[[".group"]]
    group <- which(group == group[1])
    Spoints <- data.frame(x = private$data[[".coord_x"]][group], y = private$data[[".coord_y"]][group])
    if(private$longlat){
      colnames(Spoints) <- c("lon", "lat")
    }
    return(Spoints)
  },

  #' @description Adds observation locations as vertices in the graph.
  #' @param share_weights Should the same weight be shared among the split edges? If `FALSE`, the weights will be removed, and a common weight given by 1 will be given.
  #' @param mesh_warning Display a warning if the graph structure change and the metric graph has a mesh object.
  #' @param verbose Print progress of the steps when adding observations. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @param tolerance `r lifecycle::badge("deprecated")`. Not used anymore
  #' @return No return value. Called for its side effects.

  observation_to_vertex = function(mesh_warning = TRUE, verbose = 0, tolerance = deprecated()) {
    if (lifecycle::is_present(tolerance)) {
        lifecycle::deprecate_warn("1.3.0.9000", "observation_to_vertex(tolerance)")
    }
    
    # Initialize temp data
    if (is.null(private$data)) {
        stop("There is no data!")
    }

    private$temp_PtE <- self$get_PtE()
    n <- nrow(private$temp_PtE)
    self$PtV <- rep(NA, n)
    private$temp_PtE <- cbind(private$temp_PtE, seq_len(n))

    # Identify start and end vertices based on the tolerance
    is_start_vertex <- abs(private$temp_PtE[, 2]) < 1e-15
    is_end_vertex <- private$temp_PtE[, 2] > 1 - 1e-15

    # Assign known vertices directly
    self$PtV[is_start_vertex] <- self$E[private$temp_PtE[is_start_vertex, 1], 1]
    self$PtV[is_end_vertex] <- self$E[private$temp_PtE[is_end_vertex, 1], 2]

    # Get remaining indices that need to be split
    remaining_indices <- which(!(is_start_vertex | is_end_vertex))

    # Group PtE by edges
    edge_groups <- split(private$temp_PtE[remaining_indices, c(2, 3)], private$temp_PtE[remaining_indices, 1])
    edge_groups <- lapply(edge_groups, function(coords) matrix(coords, ncol = 2, byrow = FALSE))   

    # Progress bar setup
    if (verbose == 2) {
        bar_otv <- msg_progress_bar(length(edge_groups))
    }

    # Loop over each edge in `edge_groups`
    for (Ei in names(edge_groups)) {
        
        # Extract `t_values` and `indices`
        t_values <- edge_groups[[Ei]][, 1]  # positions on edges
        indices <- edge_groups[[Ei]][, 2]   # original row number
        
        # Progress bar increment (if verbose mode is enabled)
        if (verbose == 2) {
            bar_otv$increment()
        }
        
        # Perform `private$split_edge`
        new_vertices <- private$split_edge(as.numeric(Ei), t_values,  indices = indices)
        
        # Assign new vertices
        self$PtV[indices] <- new_vertices
    }

    # Remove NA values from PtV
    self$PtV <- self$PtV[!is.na(self$PtV)]

    # Update temp_PtE for the known vertices

    # Find the positions in `self$E[,1]` where they match `self$PtV[is_start]` and `self$PtV[is_end]`
    start_positions <- match(self$PtV[is_start_vertex], self$E[, 1])
    end_positions <- match(self$PtV[is_end_vertex], self$E[, 2])

    # Use vectorized assignment for `private$temp_PtE` values
    private$temp_PtE[is_start_vertex, 1] <- start_positions
    private$temp_PtE[is_end_vertex, 1] <- end_positions

    # Assign 1 for `is_start_vertex` rows in the second column and 0 for `is_end_vertex` rows
    private$temp_PtE[is_start_vertex, 2] <- 0
    private$temp_PtE[is_end_vertex, 2] <- 1

    # Replicate edge numbers and distances for the number of groups
    n_group <- length(unique(private$data[[".group"]]))
    private$data[[".edge_number"]] <- rep(private$temp_PtE[, 1], times = n_group)
    private$data[[".distance_on_edge"]] <- rep(private$temp_PtE[, 2], times = n_group)

    # Reorder the data based on group and edge information
    tmp_df <- data.frame(
        PtE1 = private$data[[".edge_number"]],
        PtE2 = private$data[[".distance_on_edge"]],
        group = private$data[[".group"]]
    )
    index_order <- order(tmp_df$group, tmp_df$PtE1, tmp_df$PtE2)
    old_group_variable <- attr(private$data, "group_variable")
    
    private$data <- lapply(private$data, function(dat) dat[index_order])
    attr(private$data, "group_variable") <- old_group_variable

    # Reorder PtV according to index_order
    self$PtV <- self$PtV[index_order[1:length(self$PtV)]]

    # Reset temporary data
    private$temp_PtE <- NULL

    # Invalidate cached distances and recompute if necessary
    self$geo_dist <- NULL
    self$res_dist <- NULL
    if (!is.null(self$CoB)) self$buildC(2)

    if (!is.null(self$mesh)) {
        self$mesh <- NULL
        if (mesh_warning) {
            warning("Removing the existing mesh due to the change in the graph structure, please create a new mesh if needed.")
        }
    }

    # Update vertices and reference edges
    private$ref_edges <- map_into_reference_edge(self, verbose = verbose)
    private$data <- standardize_df_positions(private$data, self, edge_number = ".edge_number", distance_on_edge = ".distance_on_edge")
    private$create_update_vertices(verbose = verbose)
    self$set_edge_weights(
        weights = private$edge_weights,
        kirchhoff_weights = private$kirchhoff_weights,
        directional_weights = private$directional_weights,
        verbose = verbose
    )
  },
  
  #' @description Turns edge weights into data on the metric graph
  #' @param loc A `matrix` or `data.frame` with two columns containing the locations to generate the data from the edge weights. If `data_coords` is 'spatial', the first column must be the x-coordinate of the data, and the second column must be the y-coordinate. If `data_coords` is 'PtE', the first column must be the edge number and the second column must be the distance on edge.
  #' @param data_loc Should the data be generated to the data locations? In this case, the `loc` argument will be ignored. Observe that the metric graph must have data for one to use this option. CAUTION: To add edgeweight to data to both the data locations and mesh locations, please, add at the data locations first, then to mesh locations.
  #' @param mesh Should the data be generated to the mesh locations? In this case, the `loc` argument will be ignored. Observe that the metric graph must have a mesh built for one to use this option. CAUTION: To add edgeweight to data to both the data locations and mesh locations, please, add at the data locations first, then to mesh locations.
  #' @param weight_col Which columns of the edge weights should be turned into data? If `NULL`, all columns will be turned into data.
  #' @param add Should the data generated be added to the metric graph internal data?
  #' @param data_coords To be used only if `mesh` is `FALSE`. It decides which
  #' coordinate system to use. If `PtE`, the user must provide `edge_number` and
  #' `distance_on_edge`, otherwise if `spatial`, the user must provide
  #' `coord_x` and `coord_y`.
  #' @param normalized if TRUE, then the distances in `distance_on_edge` are
  #' assumed to be normalized to (0,1). Default FALSE.
  #' @param tibble Should the data be returned in a `tibble` format?
  #' @param format If `return` is `TRUE`, the format of the output: "tibble", "sf", or "sp". Default is "tibble". 
  #' @param verbose Print progress of the steps when adding observations. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @param suppress_warnings Suppress warnings related to duplicated observations?
  #' @param return Should the data be returned? If `return_removed` is `TRUE`, only the removed locations will be return (if there is any).
  edgeweight_to_data = function(loc = NULL, mesh = FALSE,
                data_loc = FALSE,
                weight_col = NULL, add = TRUE,
                data_coords = c("PtE", "spatial"),
                normalized = FALSE,
                tibble = FALSE,
                format = c("tibble", "sf", "sp", "list"),
                verbose = 1,
                suppress_warnings = FALSE,
                return = FALSE){

              if(is.null(loc) && !mesh && !data_loc){
                stop("Either 'loc' must be provided, 'mesh' must be TRUE or 'data_loc' must be TRUE.")
              }
              if(mesh){
                if(is.null(self$mesh)){
                  stop("There is no mesh! Build a mesh using the build_mesh() method.")
                }
                loc <- self$mesh$VtE
                normalized <- TRUE
                data_coords <- "PtE"
              }

              if(data_loc){
                if(is.null(private$data)){
                  stop("The graph has no data!")
                }
                loc <- self$get_PtE()
                normalized <- TRUE
                data_coords <- "PtE"
              }

              data_coords <- data_coords[[1]]

              if(tolower(data_coords) == "pte"){
                df_ew <- data.frame(.edge_number = loc[,1],
                .distance_on_edge = loc[,2])
                if(normalized){
                  if(max(loc[,2])>1){
                    stop("distance_on_edge of normalized locations cannot be greater than 1!")
                    }
                  }
              } else if(tolower(data_coords) == "spatial"){
                loc_tmp <- self$coordinates(XY = loc)
                nr_tmp <- nrow(loc_tmp)
                loc_tmp <- unique(loc_tmp)
                if(nr_tmp != nrow(loc_tmp)){
                  if(!suppress_warnings){
                    warning("Some locations were projected to the same point of the metric graph and were not considered.")
                  }
                }
                df_ew <- data.frame(.edge_number = loc_tmp[,1],
                .distance_on_edge = loc_tmp[,2])
                normalized <- TRUE
              } else{
                stop("'data_coords' must be either 'PtE' or 'spatial'!")
              }

              if(!is.null(private$data[[".group"]])){
                df_ew <- cbind(df_ew, .group = rep(unique(private$data[[".group"]]),
                            each = nrow(df_ew)))
              }

              ew <- private$get_edge_weights_internal()

              if(is.vector(ew)){
                ew <- data.frame(.weight = ew)
              }

              if(!is.null(weight_col)){
                ew <- ew[, weight_col, drop=FALSE]
              }

              ew[[".edge_number"]] <- 1:nrow(ew)

              ew[[".edge_lengths"]] <- self$edge_lengths

              df_ew <- merge(df_ew, ew, by = ".edge_number")

              if(!normalized){
                if(max(df_ew[[".distance_on_edge"]]/df_ew[[".edge_lengths"]]) > 1){
                  stop("There is at least one distance on edge that is greater than the corresponding edge length!")
                }
              }

              df_ew[[".edge_lengths"]] <- NULL

              if(add){
                self$add_observations(data = df_ew,
                                      edge_number = ".edge_number",
                                      distance_on_edge = ".distance_on_edge",
                                      data_coords = "PtE",
                                      group = ".group",
                                      tibble = tibble,
                                      normalized = normalized,
                                      verbose = verbose)
              }

              if(return){
                  return(self$process_data(data = df_ew,
                                      edge_number = ".edge_number",
                                      distance_on_edge = ".distance_on_edge",
                                      data_coords = "PtE",
                                      group = ".group",
                                      normalized = normalized,
                                      format = format,
                                      verbose = verbose))
              } else{
                return(invisible(NULL))
              }
  },

  #' @description Returns a list or a matrix with the mesh locations.
  #' @param bru Should an 'inlabru'-friendly list be returned?
  #' @param loc If `bru` is set to `TRUE`, the column names of the location variables.
  #' The default name is `c('.edge_number', '.distance_on_edge')`.
  #' @param normalized If TRUE, then the distances in `distance_on_edge` are
  #' assumed to be normalized to (0,1). Default TRUE.
  #' @param loc_name The name of the location variables. Not needed for `rSPDE` models.
  #'
  #' @return A list or a matrix containing the mesh locations.
  get_mesh_locations = function(bru = FALSE, loc = c(".edge_number", ".distance_on_edge"), loc_name = NULL, normalized = TRUE) {
    if(is.null(self$mesh)){
      warning("There is no mesh!")
      return(invisible(NULL))
    }

    if(!bru){
      return(self$mesh$VtE)
    } else{
      if(is.null(loc)){
        stop("If bru is TRUE, then the loc argument must be provided!")
      }
      tmp_VtE <- self$mesh$VtE
      if(!normalized){
        tmp_VtE[,2] <- tmp_VtE[,2] * self$edge_lengths[tmp_VtE[, 1]]
      }
      data_list <- list()
      data_list[[loc[1]]] <- tmp_VtE[,1]
      data_list[[loc[2]]] <- tmp_VtE[,2]
      data_list <- as.data.frame(data_list)
      if(!is.null(loc_name)){
        ret_list <- list()
        ret_list[[loc_name]] <- data_list
        return(ret_list)
      }
      return(data_list)
    }
  },

  #' @description Clear all observations from the `metric_graph` object.
  #' @return No return value. Called for its side effects.
  clear_observations = function() {
    private$data <- NULL
    self$geo_dist <- NULL
    self$res_dist <- NULL
    self$PtV <- NULL
  },


  #' @description Process data to the metric graph data format.
  #' @param data A `data.frame` or named list containing the observations. In
  #' case of groups, the data.frames for the groups should be stacked vertically,
  #' with a column indicating the index of the group. If `data` is not `NULL`,
  #' it takes priority over any eventual data in `Spoints`.
  #' @param edge_number Column (or entry on the list) of the `data` that
  #' contains the edge numbers. If not supplied, the column with name
  #' "edge_number" will be chosen. Will not be used if `Spoints` is not `NULL`.
  #' @param distance_on_edge Column (or entry on the list) of the `data` that
  #' contains the edge numbers. If not supplied, the column with name
  #' "distance_on_edge" will be chosen.  Will not be used if `Spoints` is not
  #' `NULL`.
  #' @param coord_x Column (or entry on the list) of the `data` that contains
  #' the x coordinate. If not supplied, the column with name "coord_x" will be
  #' chosen. Will not be used if `Spoints` is not `NULL` or if `data_coords` is
  #' `PtE`.
  #' @param coord_y Column (or entry on the list) of the `data` that contains
  #' the y coordinate. If not supplied, the column with name "coord_x" will be
  #' chosen. Will not be used if `Spoints` is not `NULL` or if `data_coords` is
  #' `PtE`.
  #' @param data_coords It decides which
  #' coordinate system to use. If `PtE`, the user must provide `edge_number` and
  #' `distance_on_edge`, otherwise if `spatial`, the user must provide
  #' `coord_x` and `coord_y`. The option `euclidean` is `r lifecycle::badge("deprecated")`. Use `spatial` instead.
  #' @param group Vector. If the data is grouped (for example measured at different time
  #' points), this argument specifies the columns (or entries on the list) in
  #' which the group variables are stored. It will be stored as a single column `.group` with the combined entries.
  #' @param group_sep separator character for creating the new group variable when grouping two or more variables.
  #' @param normalized if TRUE, then the distances in `distance_on_edge` are
  #' assumed to be normalized to (0,1). Default FALSE.
  #' @param format Which format should the data be returned? The options are `tibble` for `tidyr::tibble`, `sf` for `POINT`, `sp` for `SpatialPointsDataFrame` and `list` for the internal list format.
  #' @param duplicated_strategy Which strategy to handle observations on the same location on the metric graph (that is, if there are two or more observations projected at the same location).
  #' The options are 'closest' and 'jitter'. If 'closest', only the closest observation will be used. If 'jitter', a small perturbation will be performed on the projected observation location. The default is 'closest'.
  #' @param include_distance_to_graph When `data_coord` is 'spatial', should the distance of the observations to the graph be included as a column?
  #' @param only_return_removed Should the removed data (if it exists) when using 'closest' `duplicated_strategy` be returned instead of the processed data?
  #' @param tolerance Parameter to control a warning when adding observations.
  #' If the distance of some location and the closest point on the graph is
  #' greater than the tolerance, the function will display a warning.
  #' This helps detecting mistakes on the input locations when adding new data.
  #' @param verbose If `TRUE`, report steps and times.
  #' @param suppress_warnings Suppress warnings related to duplicated observations?
  #' @param Spoints `r lifecycle::badge("deprecated")` Use `data` instead.
  #' @param tibble  `r lifecycle::badge("deprecated")` Use `format` instead.
  #' @return No return value. Called for its side effects. The observations are
  #' stored in the `data` element of the `metric_graph` object.

  process_data = function(    data = NULL,
                              edge_number = "edge_number",
                              distance_on_edge = "distance_on_edge",
                              coord_x = "coord_x",
                              coord_y = "coord_y",
                              data_coords = c("PtE", "spatial"),
                              group = NULL,
                              group_sep = ".",
                              normalized = FALSE,
                              format = c("tibble", "sf", "sp", "list"),
                              duplicated_strategy = "closest",
                              include_distance_to_graph = TRUE,
                              only_return_removed = FALSE,
                              tolerance = max(self$edge_lengths)/2,
                              verbose = FALSE,
                              suppress_warnings = FALSE,
                              Spoints = lifecycle::deprecated(),
                              tibble = lifecycle::deprecated()) {

  if (lifecycle::is_present(tibble)) {
      lifecycle::deprecate_warn("1.3.0.9000", "get_edge_weights(tibble)", "get_edge_weights(format)",
        details = c("The argument `tibble` was deprecated in favor of the argument `format`.")
      )
    if(tibble){
      format <- "tibble"
    }
  }       

    format <- format[[1]]

    format <- tolower(format)

    if(!(format %in% c("tibble", "sf", "sp", "list"))){
      stop("The possible formats are 'tibble', 'sf', 'sp' and 'list'.")
    }

        data_coords <- data_coords[[1]]
        if(!is.null(group)){
          group <- unique(group)
        }

    if(length(tolerance)>1){
      tolerance <- tolerance[[1]]
      warning("'tolerance' had more than one element, only the first one will be used.")
    }

    if(lifecycle::is_present(Spoints)){
           lifecycle::deprecate_warn("1.2.9000", "add_observations(Spoints)", "add_observations(data)",
             details = c("`Spoints` is deprecated, use `data` instead.")
           )
           data <- Spoints
    }

    removed_data <- NULL
    strc_data <- FALSE

    if(inherits(data, "sf")){
      if(!inherits(data, "data.frame")){
        stop("No data was found in 'data'")
      }
      data_coords <- "spatial"
      coord_x = ".coord_x"
      coord_y = ".coord_y"

      if(!is.null(private$crs)){
        if(!is.na((sf::st_crs(data)))){
          data <- sf::st_transform(data, crs = private$crs)
        }
      }
      coord_tmp <- sf::st_coordinates(data,geometry)
      data <- sf::st_drop_geometry(data)
      data[[".coord_x"]] <- coord_tmp[,1]
      data[[".coord_y"]] <- coord_tmp[,2]
      strc_data <- TRUE
    }

    if("SpatialPointsDataFrame"%in%is(data)){
      data_coords <- "spatial"
      coord_x = ".coord_x"
      coord_y = ".coord_y"
      if(!is.null(private$proj4string)){
        if(!is.na(sp::proj4string(data))){
          data <- sp::spTransform(data,sp::CRS(private$proj4string))
        }
      }
      coord_tmp <- data@coords
      data <- data@data
      data[[".coord_x"]] <- coord_tmp[,1]
      data[[".coord_y"]] <- coord_tmp[,2]
      strc_data <- TRUE
    }

    # Store factor columns and their levels
    factor_columns <- lapply(names(data), function(col) {
      if (is.factor(data[[col]])) {
        list(column = col, levels = levels(data[[col]]))
      } else {
        NULL
      }
    })
    
    # Filter out non-factor columns
    factor_columns <- Filter(Negate(is.null), factor_columns)
    
    factor_info <- do.call(rbind, lapply(factor_columns, function(x) {
      data.frame(Column = x$column, Levels = paste(x$levels, collapse = ", "))
    }))    

    if(inherits(data, "metric_graph_data")){
      if(!any(c(".edge_number", ".distance_on_edge", ".group", ".coord_x", ".coord_y") %in% names(data))){
        warning("The data is of class 'metric_graph_data', but it is not a proper 'metric_graph_data' object. The data will be added as a regular data.")
        class(data) <- setdiff(class(data), "metric_graph_data")
      } else{
        data_coords <- "PtE"
        edge_number <- ".edge_number"
        distance_on_edge <- ".distance_on_edge"
        group <- ".group"
        normalized <- TRUE
      }
    }

        if(is.null(data)){
            stop("No data provided!")
        }

        if(!is.null(data)){
          if(!is.list(data) && !is.data.frame(data)){
            stop("'data' must be either a list or a data.frame!")
          }
        }

        data <- as.list(data)


        if(!strc_data){
        if(data_coords == "PtE"){
          if(any( !(c(edge_number, distance_on_edge) %in% names(data)))){
            stop(paste("The data does not contain either the column", edge_number,"or the column",distance_on_edge))
          }
        } else{
          if(any( !(c(coord_x, coord_y) %in% names(data)))){
            stop(paste("The data does not contain either the column", coord_x,"or the column",coord_y))
          }
        }
        }

        if(!is.null(group)){
          if(!all(group%in%names(data))){
            stop("There were group variables that are not columns of 'data'!")
          }
          data_group_tmp <- lapply(group, function(lab){data[[lab]]})
          ord_tmp <- do.call(order, data_group_tmp)
          rm(data_group_tmp)
          data <- lapply(data, function(dat){dat[ord_tmp]})
          rm(ord_tmp)
          data[[".dummy_var"]] <- as.character(data[[group[1]]])
          if(length(group)>1){
            for(j in 2:length(group)){
              data[[".dummy_var"]] <- sapply(1:length(data[[".dummy_var"]]), function(i){paste0(data[[".dummy_var"]][i], group_sep,data[[group[j]]][i])})
            }
          }
          data[[".group"]] <- data[[".dummy_var"]]
          data[[".dummy_var"]] <- NULL
        }

        ## convert everything to PtE
        if(verbose > 0){
          if(data_coords == "spatial"){
          message("Converting data to PtE")
          if(private$longlat){
            message("This step may take long. If this step is taking too long consider pruning the vertices to possibly obtain some speed up.")
          }
          }
        }

        ## Check data for repeated observations
        if(data_coords == "spatial"){
          if(is.null(group)){
            data_tmp <- cbind(data[[coord_x]], data[[coord_y]])
          } else{
            data_tmp <- cbind(data[[coord_x]], data[[coord_y]], data[[".group"]])
          }
        } else{
          if(is.null(group)){
            data_tmp <- cbind(data[[edge_number]], data[[distance_on_edge]])
          } else{
            data_tmp <- cbind(data[[edge_number]], data[[distance_on_edge]], data[[".group"]])
          }
        }

        if(nrow(unique(data_tmp)) != nrow(data_tmp)){
          if(!suppress_warnings){
            warning("There is at least one 'column' of the data with repeated (possibly different) values at the same location for the same group variable. Only one of these values will be used. Consider using the group variable to differentiate between these values or provide different names for such variables.")
          }
        }


        t <- system.time({
            if(data_coords == "PtE"){
                PtE <- cbind(data[[edge_number]], data[[distance_on_edge]])
                if(!normalized){
                  PtE[, 2] <- PtE[,2] / self$edge_lengths[PtE[, 1]]
                }
              } else if(data_coords == "spatial"){
                point_coords <- cbind(data[[coord_x]], data[[coord_y]])
                PtE <- self$coordinates(XY = point_coords)


                if(tolower(duplicated_strategy) == "closest"){
                  norm_XY <- NULL
                  fact <- process_factor_unit(private$vertex_unit, private$length_unit)
                  PtE_new <- NULL
                  far_points <- NULL
                  dup_points <- NULL
                  closest_points <- NULL
                  grp_dat <- data[[".group"]]
                  if(length(grp_dat) == 0){
                    grp_dat <- rep(1, nrow(PtE))
                  }

                  for(grp in unique(grp_dat)){
                      idx_grp <- which(grp_dat == grp)
                      PtE_grp <- PtE[idx_grp,, drop=FALSE]
                      XY_new_grp <- self$coordinates(PtE = PtE_grp, normalized = TRUE)
                      point_coords_grp <- point_coords[idx_grp,, drop=FALSE]
                    # norm_XY <- max(sqrt(rowSums( (point_coords-XY_new)^2 )))
                      norm_XY_grp <- compute_aux_distances(lines = point_coords_grp, points = XY_new_grp, crs = private$crs, longlat = private$longlat, proj4string = private$proj4string, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform)
                    # norm_XY <- max(norm_XY)
                    # if(norm_XY > tolerance){
                    #   warning("There was at least one point whose location is far from the graph,
                    #     please consider checking the input.")
                    #   }
                      far_points_grp <- (norm_XY_grp > tolerance)
                      PtE_grp <- PtE_grp[!far_points_grp,,drop=FALSE]
                      XY_new_grp <- XY_new_grp[!far_points_grp,,drop=FALSE]
                      point_coords_grp <- point_coords_grp[!far_points_grp,,drop=FALSE]
                      dup_points_grp <- duplicated(XY_new_grp) | duplicated(XY_new_grp, fromLast=TRUE)
                      norm_XY_grp <- norm_XY_grp[!far_points_grp]
                      if(any(dup_points_grp)){
                        old_new_coords <- cbind(point_coords_grp[dup_points_grp,], XY_new_grp[dup_points_grp,], norm_XY_grp[dup_points_grp], which(dup_points_grp))
                        old_new_coords <- as.data.frame(old_new_coords)
                        colnames(old_new_coords) <- c("coordx", "coordy", "pcoordx", "pcoordy", "dist", "idx")
                        old_new_coords <- dplyr::as_tibble(old_new_coords)
                        old_new_coords <- old_new_coords %>% dplyr::group_by(pcoordx, pcoordy) %>% dplyr::mutate(min_dist = min(dist)) %>% dplyr::mutate(min_idx = dist == min_dist, min_idx = get_only_first(min_idx)) %>% dplyr::ungroup()
                        min_dist_idx <- old_new_coords[["idx"]][!old_new_coords[["min_idx"]]]
                        closest_points_grp <- rep(FALSE, length(dup_points_grp))
                        closest_points_grp[min_dist_idx] <- TRUE
                        norm_XY_grp <- norm_XY_grp[!closest_points_grp]
                        PtE_grp <- PtE_grp[!closest_points_grp,,drop=FALSE]
                        closest_points <- c(closest_points, closest_points_grp)
                      } else{
                        closest_points_grp <- rep(FALSE, length(norm_XY_grp))
                        closest_points <- c(closest_points, closest_points_grp)
                      }

                      dup_points <- c(dup_points, dup_points_grp)
                      far_points <- c(far_points, far_points_grp)
                      PtE_new <- rbind(PtE_new, PtE_grp)
                      norm_XY <- c(norm_XY, norm_XY_grp)
                    }
                    PtE <- PtE_new

                    if(sum(dup_points)>0){
                      if(!suppress_warnings){
                        warning("There were points projected at the same location. Only the closest point was kept. To keep all the observations change 'duplicated_strategy'   to 'jitter'.")
                      }
                    }

                    data <- lapply(data, function(dat){dat[!far_points]})
                    if(!is.null(closest_points)){
                      removed_data <-  lapply(data, function(dat){dat[closest_points]})
                      data <- lapply(data, function(dat){dat[!closest_points]})
                    }
                    if(any(far_points)){
                      if(!suppress_warnings){
                          warning(paste("There were points that were farther than the tolerance. These points were removed. If you want them projected into the graph, please increase the tolerance. The total number of points removed due do being far is",sum(far_points)))
                      }
                    }
                    if(include_distance_to_graph){
                      data[[".distance_to_graph"]] <- norm_XY
                    }


                } else if(tolower(duplicated_strategy) == "jitter"){
                    norm_XY <- NULL
                    fact <- process_factor_unit(private$vertex_unit, private$length_unit)
                    PtE_new <- NULL
                    far_points <- NULL

                    grp_dat <- data[[".group"]]
                    if(length(grp_dat) == 0){
                      grp_dat <- rep(1, nrow(PtE))
                    }

                    for(grp in unique(grp_dat)){
                      idx_grp <- which(grp_dat == grp)
                      PtE_grp <- PtE[idx_grp,, drop=FALSE]
                      dup_points_grp <- duplicated(PtE_grp)
                      while(sum(dup_points_grp)>0){
                        cond_0 <- PtE_grp[dup_points_grp,2] == 0
                        cond_1 <- PtE_grp[dup_points_grp,2] == 1
                        idx_pte0 <- which(cond_0)
                        idx_pte1 <-  which(cond_1)
                        idx_other_pte <- which(!cond_0 & !cond_1)
                        d_points <- which(dup_points_grp)
                        if(length(idx_pte0)>0){
                          PtE_grp[d_points[idx_pte0],2] <- PtE_grp[d_points[idx_pte0],2] + 1e-2 * runif(length(idx_pte0))
                        }
                        if(length(idx_pte1)>0){
                          PtE_grp[d_points[idx_pte1],2] <- PtE_grp[d_points[idx_pte1],2] - 1e-2 * runif(length(idx_pte1))
                        }
                        if(length(idx_other_pte)>0){
                          delta_dif <- min(1e-2, 1-max(PtE_grp[d_points[idx_other_pte],2]), min(PtE_grp[d_points[idx_other_pte],2]))
                          PtE_grp[d_points[idx_other_pte],2] <- PtE_grp[d_points[idx_other_pte],2] + delta_dif * runif(length(idx_other_pte))
                        }
                        dup_points_grp <- duplicated(PtE_grp)
                      }
                      XY_new_grp <- self$coordinates(PtE = PtE_grp, normalized = TRUE)
                      point_coords_grp <- point_coords[idx_grp,, drop=FALSE]
                      norm_XY_grp <- compute_aux_distances(lines = point_coords_grp, points = XY_new_grp, crs = private$crs, longlat = private$longlat, proj4string = private$proj4string, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform)
                      far_points_grp <- (norm_XY_grp > tolerance)
                      PtE_grp <- PtE_grp[!far_points_grp,,drop=FALSE]
                      norm_XY_grp <- norm_XY_grp[!far_points_grp]
                      far_points <- c(far_points, far_points_grp)
                      PtE_new <- rbind(PtE_new, PtE_grp)
                      norm_XY <- c(norm_XY, norm_XY_grp)
                    }
                    PtE <- PtE_new
                    data <- lapply(data, function(dat){dat[!far_points]})
                    if(any(far_points)){
                      if(!suppress_warnings){
                              warning(paste("There were points that were farther than the tolerance. These points were removed. If you want them projected into the graph, please increase the tolerance. The total number of points removed due do being far is",sum(far_points)))
                      }
                    }
                    # PtE <- PtE[!far_points,,drop=FALSE]
                    if(include_distance_to_graph){
                      data[[".distance_to_graph"]] <- norm_XY
                    }
            } else{
                  stop(paste(duplicated_strategy, "is not a valid duplicated strategy!"))
                }

                rm(far_points)
                rm(norm_XY)

            } else{
                stop("The options for 'data_coords' are 'PtE' and 'spatial'.")
            }
        })

    if(only_return_removed){
      if(!is.null(removed_data)){
        return(as.data.frame(removed_data))
      }
    }

      if(verbose == 2) {
      message(sprintf("time: %.3f s", t[["elapsed"]]))

      message("Processing data")
    }

    t <- system.time({
     if(!is.null(group)){
       group_vector <- data[[".group"]]
     } else{
       group <- ".group"
       group_vector <- NULL
     }

    lapply(data, function(dat){if(nrow(matrix(PtE, ncol=2)) != length(dat)){
        stop(paste(dat,"has a different number of elements than the number of
                   coordinates!"))
       }})


    group_vals <- unique(group_vector)


    # n_group <- length(unique(group_vector))
    n_group <- length(group_vals)
    n_group <- ifelse(n_group == 0, 1, n_group)

    data[[edge_number]] <- NULL
    data[[distance_on_edge]] <- NULL
    data[[coord_x]] <- NULL
    data[[coord_y]] <- NULL
    data[[".group"]] <- NULL
    data[[".coord_x"]] <- NULL
    data[[".coord_y"]] <- NULL


    length_data <- unique(unlist(lapply(data, length)))

    if(length(length_data)>1){
      stop("There was a problem when processing the data. The number of observations and observation locations (after projecting on the metric graph) are not matching.")
    }

    # Process the data (find all the different coordinates
    # across the different replicates, and also merge the new data to the old data)
    data <- process_data_add_obs(PtE, new_data = data, old_data = NULL,
                                        group_vector, suppress_warnings = suppress_warnings)

    data <- standardize_df_positions(data, self)
    ## convert to Spoints and add
    group_1 <- data[[".group"]]
    group_1 <- which(group_1 == group_1[1])
    PtE <- cbind(data[[".edge_number"]][group_1],
                 data[[".distance_on_edge"]][group_1])
    spatial_points <- self$coordinates(PtE = PtE, normalized = TRUE)
    data[[".coord_x"]] <- rep(spatial_points[,1], times = n_group)
    data[[".coord_y"]] <- rep(spatial_points[,2], times = n_group)

    # Assigning back the columns that are factors with their respective levels:
    for (col_info in factor_columns) {
      column_name <- col_info$column
      levels_specified <- col_info$levels

      # Convert to factor with specified levels
      private$data[[column_name]] <- factor(private$data[[column_name]], levels = levels_specified)
    }    

    if(format == "tibble"){
      data <- tidyr::as_tibble(data)
    }

    if(format == "sf"){
      data <- as.data.frame(data)
      data_geometries <- lapply(1:nrow(data), function(i) sf::st_point(as.numeric(data[i, c('.coord_x', '.coord_y')])))
      data <- sf::st_sf(data, geometry = sf::st_sfc(data_geometries), crs = if(!is.null(private$crs)) private$crs else NULL)      
    } 

    class(data) <- c("metric_graph_data", class(data))
        if(!is.null(group)){
      attr(data, "group_variables") <- group
    } else{
      attr(data, "group_variables") <- ".none"
    }
    })

    if(format == "sp"){
      data <- as.data.frame(data)
      sp::coordinates(data) <- ~ .coord_x + .coord_y
    }

          if(verbose == 2) {
      message(sprintf("time: %.3f s", t[["elapsed"]]))
          }
          return(data)

                              },

  #' @description Add observations to the metric graph.
  #' @param data A `data.frame` or named list containing the observations. In
  #' case of groups, the data.frames for the groups should be stacked vertically,
  #' with a column indicating the index of the group. `data` can also be an `sf` object, a
  #' `SpatialPointsDataFrame` object or an `SSN` object.
  #' in which case `data_coords` will automatically be spatial, and there is no need to specify the `coord_x` or `coord_y` arguments.
  #' @param edge_number Column (or entry on the list) of the `data` that
  #' contains the edge numbers. If not supplied, the column with name
  #' "edge_number" will be chosen. Will not be used if `Spoints` is not `NULL`.
  #' @param distance_on_edge Column (or entry on the list) of the `data` that
  #' contains the edge numbers. If not supplied, the column with name
  #' "distance_on_edge" will be chosen.  Will not be used if `Spoints` is not
  #' `NULL`.
  #' @param coord_x Column (or entry on the list) of the `data` that contains
  #' the x coordinate. If not supplied, the column with name "coord_x" will be
  #' chosen. Will not be used if `Spoints` is not `NULL` or if `data_coords` is
  #' `PtE`.
  #' @param coord_y Column (or entry on the list) of the `data` that contains
  #' the y coordinate. If not supplied, the column with name "coord_x" will be
  #' chosen. Will not be used if `Spoints` is not `NULL` or if `data_coords` is
  #' `PtE`.
  #' @param data_coords It decides which
  #' coordinate system to use. If `PtE`, the user must provide `edge_number` and
  #' `distance_on_edge`, otherwise if `spatial`, the user must provide
  #' `coord_x` and `coord_y`. The option `euclidean` is `r lifecycle::badge("deprecated")`. Use `spatial` instead.
  #' @param group Vector. If the data is grouped (for example measured at different time
  #' points), this argument specifies the columns (or entries on the list) in
  #' which the group variables are stored. It will be stored as a single column `.group` with the combined entries.
  #' @param group_sep separator character for creating the new group variable when grouping two or more variables.
  #' @param normalized if TRUE, then the distances in `distance_on_edge` are
  #' assumed to be normalized to (0,1). Default FALSE.
  #' @param clear_obs Should the existing observations be removed before adding the data?
  #' @param tibble Should the data be returned as a `tidyr::tibble`?
  #' @param duplicated_strategy Which strategy to handle observations on the same location on the metric graph (that is, if there are two or more observations projected at the same location).
  #' The options are 'closest' and 'jitter'. If 'closest', only the closest observation will be used. If 'jitter', a small perturbation will be performed on the projected observation location. The default is 'closest'.
  #' @param include_distance_to_graph When `data_coord` is 'spatial', should the distance of the observations to the graph be included as a column?
  #' @param tolerance Parameter to control a warning when adding observations.
  #' If the distance of some location and the closest point on the graph is
  #' greater than the tolerance, the function will display a warning.
  #' This helps detecting mistakes on the input locations when adding new data.
  #' @param tolerance_merge tolerance (in edge_length units) for merging points that are very close and are on a common edge. By default, this tolerance is zero, meaning no merges will be performed.
  #' @param merge_strategy The strategies to handle observations that are within the tolerance. The options are `remove`, `merge`, `average`. The default is `merge`, in which one of the observations will be chosen, and the remaining will be used to try to fill all columns with non-NA values. The second strategy is `remove`, meaning that if two observations are within the tolerance one of them will be removed. Finally, `average` will take the average over the close observations for numerical variables, and will choose one non-NA for non-numerical variables.
  #' @param return_removed Should the removed data (if it exists) due to being projected to the same place when using 'closest' `duplicated_strategy`, or due to some merge strategy, be returned?
  #' @param verbose Print progress of the steps when adding observations. There are 3 levels of verbose, level 0, 1 and 2. In level 0, no messages are printed. In level 1, only messages regarding important steps are printed. Finally, in level 2, messages detailing all the steps are printed. The default is 1.
  #' @param suppress_warnings Suppress warnings related to duplicated observations?
  #' @param Spoints `r lifecycle::badge("deprecated")` Use `data` instead.
  #' @return No return value. Called for its side effects. The observations are
  #' stored in the `data` element of the `metric_graph` object.
  add_observations = function(data = NULL,
                              edge_number = "edge_number",
                              distance_on_edge = "distance_on_edge",
                              coord_x = "coord_x",
                              coord_y = "coord_y",
                              data_coords = c("PtE", "spatial"),
                              group = NULL,
                              group_sep = ".",
                              normalized = FALSE,
                              clear_obs = FALSE,
                              tibble = FALSE,
                              tolerance = max(self$edge_lengths)/2,
                              duplicated_strategy = "closest",
                              include_distance_to_graph = TRUE,
                              return_removed = TRUE,
                              tolerance_merge = 0,
                              merge_strategy = "merge",
                              verbose = 1,
                              suppress_warnings = FALSE,
                              Spoints = lifecycle::deprecated()) {

    merge_strategy <- match.arg(merge_strategy, c("remove", "merge", "average"))
    duplicated_strategy <- match.arg(duplicated_strategy, c("closest", "jitter"))
    data_coords <- match.arg(data_coords, c("PtE", "spatial"))

    if(clear_obs){
      df_temp <- data
      self$clear_observations()
      data <- df_temp
    }

    if(inherits(data, "SSN")){
      data <- data$obs
    }

    if(lifecycle::is_present(Spoints)){
           lifecycle::deprecate_warn("1.2.9000", "add_observations(Spoints)", "add_observations(data)",
             details = c("`Spoints` is deprecated, use `data` instead.")
           )
           data <- Spoints
    }

    removed_data <- NULL
    far_data <- NULL
    strc_data <- FALSE

    if(inherits(data, "sf")){
      if(!inherits(data, "data.frame")){
        stop("No data was found in 'data'")
      }
      data_coords <- "spatial"
      coord_x = ".coord_x"
      coord_y = ".coord_y"

      if(!is.null(private$crs)){
        if(!is.na((sf::st_crs(data)))){
          data <- sf::st_transform(data, crs = private$crs)
        }
      }
      coord_tmp <- sf::st_coordinates(data,geometry)
      data <- sf::st_drop_geometry(data)
      data[[".coord_x"]] <- coord_tmp[,1]
      data[[".coord_y"]] <- coord_tmp[,2]
      strc_data <- TRUE
    }

    if("SpatialPointsDataFrame"%in%is(data)){
      data_coords <- "spatial"
      coord_x = ".coord_x"
      coord_y = ".coord_y"
      if(!is.null(private$proj4string)){
        if(!is.na(sp::proj4string(data))){
          data <- sp::spTransform(data,sp::CRS(private$proj4string))
        }
      }
      coord_tmp <- data@coords
      data <- data@data
      data[[".coord_x"]] <- coord_tmp[,1]
      data[[".coord_y"]] <- coord_tmp[,2]
      strc_data <- TRUE
    }

    # Store factor columns and their levels
    factor_columns <- lapply(names(data), function(col) {
      if (is.factor(data[[col]])) {
        list(column = col, levels = levels(data[[col]]))
      } else {
        NULL
      }
    })
    
    # Filter out non-factor columns
    factor_columns <- Filter(Negate(is.null), factor_columns)
    
    factor_info <- do.call(rbind, lapply(factor_columns, function(x) {
      data.frame(Column = x$column, Levels = paste(x$levels, collapse = ", "))
    }))


    if(length(tolerance)>1){
      tolerance <- tolerance[[1]]
      warning("'tolerance' had more than one element, only the first one will be used.")
    }

    if(verbose>0){
      message("Adding observations...")
      if(data_coords == "PtE"){
        if(normalized){
          message("Assuming the observations are normalized by the length of the edge.")
        } else{
          message("Assuming the observations are NOT normalized by the length of the edge.")
        }
      }
      if(private$longlat){
        message(paste("The unit for edge lengths is", private$length_unit))
        message(paste0("The current tolerance for removing distant observations is (in ",private$length_unit,"): ", tolerance))
      }
    }

    if(!is.null(group)){
      group <- unique(group)
    }

    if(inherits(data, "metric_graph_data")){
      if(!any(c(".edge_number", ".distance_on_edge", ".group", ".coord_x", ".coord_y") %in% names(data))){
        warning("The data is of class 'metric_graph_data', but it is not a proper 'metric_graph_data' object. The data will be added as a regular data.")
        class(data) <- setdiff(class(data), "metric_graph_data")
      } else{
        data_coords <- "PtE"
        edge_number <- ".edge_number"
        distance_on_edge <- ".distance_on_edge"
        group <- ".group"
        normalized <- TRUE
      }
    }

        data_coords <- data_coords[[1]]
        if(data_coords == "euclidean"){
          lifecycle::deprecate_warn("1.2.0", "add_observations(data_coords = 'must be either PtE or spatial')")
          data_coords <- "spatial"
        }
        if(is.null(data)){
            stop("No data provided!")
        }

        if(!is.null(data)){
          if(!is.list(data) && !is.data.frame(data)){
            stop("'data' must be either a list or a data.frame!")
          }
        }


        data <- as.list(data)

        if(!strc_data){
        if(data_coords == "PtE"){
          if(any( !(c(edge_number, distance_on_edge) %in% names(data)))){
            stop(paste("The data does not contain either the column", edge_number,"or the column",distance_on_edge))
          }
        } else{
          if(any( !(c(coord_x, coord_y) %in% names(data)))){
            stop(paste("The data does not contain either the column", coord_x,"or the column",coord_y))
          }
        }
        }

        if(!is.null(group)){
          if(!all(group%in%names(data))){
            stop("There were group variables that are not columns of 'data'!")
          }
          data_group_tmp <- lapply(group, function(lab){data[[lab]]})
          ord_tmp <- do.call(order, data_group_tmp)
          rm(data_group_tmp)
          data <- lapply(data, function(dat){dat[ord_tmp]})
          rm(ord_tmp)
          data[[".dummy_var"]] <- as.character(data[[group[1]]])
          if(length(group)>1){
            for(j in 2:length(group)){
              data[[".dummy_var"]] <- sapply(1:length(data[[".dummy_var"]]), function(i){paste0(data[[".dummy_var"]][i], group_sep,data[[group[j]]][i])})
            }
          }
          data[[".group"]] <- data[[".dummy_var"]]
          data[[".dummy_var"]] <- NULL
        }


        ## convert everything to PtE
        if(verbose > 0){
          if(data_coords == "spatial"){
          message("Converting data to PtE")
          if(private$longlat){
            message("This step may take long. If this step is taking too long consider pruning the vertices to possibly obtain some speed up.")
          }
          }
        }



        ## Check data for repeated observations
        if(data_coords == "spatial"){
          if(is.null(group)){
            data_tmp <- cbind(data[[coord_x]], data[[coord_y]])
          } else{
            data_tmp <- cbind(data[[coord_x]], data[[coord_y]], data[[".group"]])
          }
        } else{
          if(is.null(group)){
            data_tmp <- cbind(data[[edge_number]], data[[distance_on_edge]])
          } else{
            data_tmp <- cbind(data[[edge_number]], data[[distance_on_edge]], data[[".group"]])
          }
        }

        if(nrow(unique(data_tmp)) != nrow(data_tmp)){
          if(!suppress_warnings){
            warning("There is at least one 'column' of the data with repeated (possibly different) values at the same location for the same group variable. Only one of these values will be used. Consider using the group variable to differentiate between these values or provide different names for such variables.")
          }
        }

        t <- system.time({
            if(data_coords == "PtE"){
                PtE <- cbind(data[[edge_number]], data[[distance_on_edge]])
                if(!normalized){
                  PtE[, 2] <- PtE[,2] / self$edge_lengths[PtE[, 1]]
                  if(any(PtE[,2] > 1)){
                   stop("There were invalid distances on edges. If your data is normalized, please set the 'normalized' argument to TRUE.") 
                  }
                }
              } else if(data_coords == "spatial"){
                point_coords <- cbind(data[[coord_x]], data[[coord_y]])
                PtE <- self$coordinates(XY = point_coords)

                if(tolower(duplicated_strategy) == "closest"){
                  norm_XY <- NULL
                  fact <- process_factor_unit(private$vertex_unit, private$length_unit)
                  PtE_new <- NULL
                  far_points <- NULL
                  dup_points <- NULL
                  closest_points <- NULL
                  grp_dat <- data[[".group"]]
                  if(length(grp_dat) == 0){
                    grp_dat <- rep(1, nrow(PtE))
                  }

                  for(grp in unique(grp_dat)){
                      idx_grp <- which(grp_dat == grp)
                      PtE_grp <- PtE[idx_grp,, drop=FALSE]
                      XY_new_grp <- self$coordinates(PtE = PtE_grp, normalized = TRUE)
                      point_coords_grp <- point_coords[idx_grp,, drop=FALSE]
                    # norm_XY <- max(sqrt(rowSums( (point_coords-XY_new)^2 )))
                      norm_XY_grp <- compute_aux_distances(lines = point_coords_grp, points = XY_new_grp, crs = private$crs, longlat = private$longlat, proj4string = private$proj4string, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform)
                    # norm_XY <- max(norm_XY)
                    # if(norm_XY > tolerance){
                    #   warning("There was at least one point whose location is far from the graph,
                    #     please consider checking the input.")
                    #   }
                      far_points_grp <- (norm_XY_grp > tolerance)
                      PtE_grp <- PtE_grp[!far_points_grp,,drop=FALSE]
                      XY_new_grp <- XY_new_grp[!far_points_grp,,drop=FALSE]
                      point_coords_grp <- point_coords_grp[!far_points_grp,,drop=FALSE]
                      dup_points_grp <- duplicated(XY_new_grp) | duplicated(XY_new_grp, fromLast=TRUE)
                      norm_XY_grp <- norm_XY_grp[!far_points_grp]
                      if(any(dup_points_grp)){
                        old_new_coords <- cbind(point_coords_grp[dup_points_grp,], XY_new_grp[dup_points_grp,], norm_XY_grp[dup_points_grp], which(dup_points_grp))
                        old_new_coords <- as.data.frame(old_new_coords)
                        colnames(old_new_coords) <- c("coordx", "coordy", "pcoordx", "pcoordy", "dist", "idx")
                        old_new_coords <- dplyr::as_tibble(old_new_coords)
                        old_new_coords <- old_new_coords %>% dplyr::group_by(pcoordx, pcoordy) %>% dplyr::mutate(min_dist = min(dist)) %>% dplyr::mutate(min_idx = dist == min_dist, min_idx = get_only_first(min_idx)) %>% dplyr::ungroup()
                        min_dist_idx <- old_new_coords[["idx"]][!old_new_coords[["min_idx"]]]
                        closest_points_grp <- rep(FALSE, length(dup_points_grp))
                        closest_points_grp[min_dist_idx] <- TRUE
                        norm_XY_grp <- norm_XY_grp[!closest_points_grp]
                        PtE_grp <- PtE_grp[!closest_points_grp,,drop=FALSE]
                        closest_points <- c(closest_points, closest_points_grp)
                      } else{
                        closest_points_grp <- rep(FALSE, length(norm_XY_grp))
                        closest_points <- c(closest_points, closest_points_grp)
                      }

                      dup_points <- c(dup_points, dup_points_grp)
                      far_points <- c(far_points, far_points_grp)
                      PtE_new <- rbind(PtE_new, PtE_grp)
                      norm_XY <- c(norm_XY, norm_XY_grp)
                    }
                    PtE <- PtE_new

                    if(sum(dup_points)>0){
                        if(!suppress_warnings){
                          warning("There were points projected at the same location. Only the closest point was kept. To keep all the observations change 'duplicated_strategy' to 'jitter'.")
                        }
                    }
                    if(any(far_points)){
                        if(!suppress_warnings){
                          warning(paste("There were points that were farther than the tolerance. These points were removed. If you want them projected into the graph, please increase the tolerance. The total number of points removed due do being far is",sum(far_points)))
                        }
                        far_data <- lapply(data, function(dat){dat[far_points]})
                    }                    
                    data <- lapply(data, function(dat){dat[!far_points]})
                    if(!is.null(closest_points)){
                      removed_data <-  lapply(data, function(dat){dat[closest_points]})
                      data <- lapply(data, function(dat){dat[!closest_points]})
                    }                    
                    if(include_distance_to_graph){
                      data[[".distance_to_graph"]] <- norm_XY
                    }


                } else if(tolower(duplicated_strategy) == "jitter"){
                    norm_XY <- NULL
                    fact <- process_factor_unit(private$vertex_unit, private$length_unit)
                    PtE_new <- NULL
                    far_points <- NULL

                    grp_dat <- data[[".group"]]
                    if(length(grp_dat) == 0){
                      grp_dat <- rep(1, nrow(PtE))
                    }

                    for(grp in unique(grp_dat)){
                      idx_grp <- which(grp_dat == grp)
                      PtE_grp <- PtE[idx_grp,, drop=FALSE]
                      dup_points_grp <- duplicated(PtE_grp)
                      while(sum(dup_points_grp)>0){
                        cond_0 <- PtE_grp[dup_points_grp,2] == 0
                        cond_1 <- PtE_grp[dup_points_grp,2] == 1
                        idx_pte0 <- which(cond_0)
                        idx_pte1 <-  which(cond_1)
                        idx_other_pte <- which(!cond_0 & !cond_1)
                        d_points <- which(dup_points_grp)
                        if(length(idx_pte0)>0){
                          PtE_grp[d_points[idx_pte0],2] <- PtE_grp[d_points[idx_pte0],2] + 1e-2 * runif(length(idx_pte0))
                        }
                        if(length(idx_pte1)>0){
                          PtE_grp[d_points[idx_pte1],2] <- PtE_grp[d_points[idx_pte1],2] - 1e-2 * runif(length(idx_pte1))
                        }
                        if(length(idx_other_pte)>0){
                          delta_dif <- min(1e-2, 1-max(PtE_grp[d_points[idx_other_pte],2]), min(PtE_grp[d_points[idx_other_pte],2]))
                          PtE_grp[d_points[idx_other_pte],2] <- PtE_grp[d_points[idx_other_pte],2] + delta_dif * runif(length(idx_other_pte))
                        }
                        dup_points_grp <- duplicated(PtE_grp)
                      }
                      XY_new_grp <- self$coordinates(PtE = PtE_grp, normalized = TRUE)
                      point_coords_grp <- point_coords[idx_grp,, drop=FALSE]
                      norm_XY_grp <- compute_aux_distances(lines = point_coords_grp, points = XY_new_grp, crs = private$crs, longlat = private$longlat, proj4string = private$proj4string, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform)
                      far_points_grp <- (norm_XY_grp > tolerance)
                      PtE_grp <- PtE_grp[!far_points_grp,,drop=FALSE]
                      norm_XY_grp <- norm_XY_grp[!far_points_grp]
                      far_points <- c(far_points, far_points_grp)
                      PtE_new <- rbind(PtE_new, PtE_grp)
                      norm_XY <- c(norm_XY, norm_XY_grp)
                    }
                    PtE <- PtE_new
                    if(any(far_points)){
                      if(!suppress_warnings){
                            warning(paste("There were points that were farther than the tolerance. These points were removed. If you want them projected into the graph, please increase the tolerance. The total number of points removed due do being far is",sum(far_points)))
                      }
                      far_data <- lapply(data, function(dat){dat[far_points]})
                    }
                    data <- lapply(data, function(dat){dat[!far_points]})
                    # PtE <- PtE[!far_points,,drop=FALSE]
                    if(include_distance_to_graph){
                      data[[".distance_to_graph"]] <- norm_XY
                    }
                } else{
                  stop(paste(duplicated_strategy, "is not a valid duplicated strategy!"))
                }

                rm(far_points)
                rm(norm_XY)

            } else{
                stop("The options for 'data_coords' are 'PtE' and 'spatial'.")
            }
        })

      if(verbose == 2) {
      message(sprintf("time: %.3f s", t[["elapsed"]]))

      message("Processing data")
    }


    t <- system.time({
     if(!is.null(group)){
       group_vector <- data[[".group"]]
     } else{
       group_vector <- NULL
     }

    lapply(data, function(dat){if(nrow(matrix(PtE, ncol=2)) != length(dat)){
        stop(paste(dat,"has a different number of elements than the number of
                   coordinates!"))
       }})

    if(!is.null(private$data[[".group"]])){
      group_vals <- unique(private$data[[".group"]])
      group_vals <- unique(union(group_vals, group_vector))
    } else{
      group_vals <- unique(group_vector)
    }

    # n_group <- length(unique(group_vector))
    n_group <- length(group_vals)
    n_group <- ifelse(n_group == 0, 1, n_group)

    data[[edge_number]] <- NULL
    data[[distance_on_edge]] <- NULL
    data[[coord_x]] <- NULL
    data[[coord_y]] <- NULL
    data[[".group"]] <- NULL
    private$data[[".coord_x"]] <- NULL
    private$data[[".coord_y"]] <- NULL

    ## Filtering data that are very close and are on a common edge
    if(length(tolerance_merge)>1){
      warning("tolerance_merge is not of length 1. Only the first element will be used.")
      tolerance_merge <- tolerance_merge[[1]]
    }
    
    if(is.null(tolerance_merge)){
      warning("tolerance_merge is NULL, so it was set to 0.")
      tolerance_merge <- 0
    }

    if(tolerance_merge < 0){
      stop("tolerance_merge cannot be negative.")
    }

    removed_merge <- NULL

    if(tolerance_merge > 0){
        # data, group_vector and PtE
        if(!is.null(group_vector)){
          ord_idx <- order(PtE[,1], PtE[,2], group_vector)
          group_vector <- group_vector[ord_idx]
          PtE <- PtE[ord_idx,,drop=FALSE]      
        } else{
          ord_idx <- order(PtE[,1], PtE[,2])
          PtE <- PtE[ord_idx,,drop=FALSE]
        }

        data <- lapply(data, function(dat){dat[ord_idx]})

        aux_length <- self$edge_lengths[PtE[,1]] * PtE[,2]

        merge_idx <- get_idx_within_merge_tolerance(PtE, group_vector, aux_length, tolerance_merge)

        removed_merge_idx <- setdiff(1:length(ord_idx), merge_idx)
        
        removed_merge <- lapply(data, function(dat){dat[removed_merge_idx]})
        removed_merge[[".edge_number"]] <- PtE[removed_merge_idx, 1]
        removed_merge[[".distance_on_edge"]] <- PtE[removed_merge_idx, 2]

        data <- lapply(data, function(dat){dat[merge_idx]})
        group_vector <- group_vector[merge_idx]
        PtE <- PtE[merge_idx,,drop=FALSE]

        if(merge_strategy %in% c("average", "merge")){
          merge_idx_map <- setNames(seq_along(merge_idx), as.character(merge_idx))
          ref_idx_merges <- find_merged_indices_for_unselected(merge_idx, length(ord_idx))
          data <- apply_merge_strategy(data, removed_merge, merge_idx_map, ref_idx_merges, merge_strategy)
        }
    }

    # Process the data (find all the different coordinates
    # across the different replicates, and also merge the new data to the old data)
    length_data <- unique(unlist(lapply(data, length)))

    if(length(length_data)>1){
      stop("There was a problem when processing the data. The number of observations and observation locations (after projecting on the metric graph) are not matching.")
    }

    private$data <- process_data_add_obs(PtE, new_data = data, private$data,
                                        group_vector, suppress_warnings = suppress_warnings)

    private$data <- standardize_df_positions(private$data, self, edge_number = ".edge_number", distance_on_edge = ".distance_on_edge")

    ## convert to Spoints and add
    PtE <- self$get_PtE()

    spatial_points <- self$coordinates(PtE = PtE, normalized = TRUE)

    private$data[[".coord_x"]] <- rep(spatial_points[,1], times = n_group)
    private$data[[".coord_y"]] <- rep(spatial_points[,2], times = n_group)
    if(tibble){
      private$data <- tidyr::as_tibble(private$data)
    }
    private$group_col <- group
    # distance_graph_tmp <- private$data[[".distance_to_graph"]]

    # Assigning back the columns that are factors with their respective levels:
    for (col_info in factor_columns) {
      column_name <- col_info$column
      levels_specified <- col_info$levels

      # Convert to factor with specified levels
      private$data[[column_name]] <- factor(private$data[[column_name]], levels = levels_specified)
    }

    class(private$data) <- c("metric_graph_data", class(private$data))
    if(!is.null(group)){
      attr(private$data, "group_variables") <- group
    } else{
      attr(private$data, "group_variables") <- ".none"
    }
    # attr(private$data, "distance_to_graph") <- distance_graph_tmp
    })
          if(verbose == 2) {
      message(sprintf("time: %.3f s", t[["elapsed"]]))
          }

    if(return_removed){
      ret_list <- list()
      if(!is.null(removed_data)){
        ret_list[["removed"]] <- as.data.frame(removed_data)
      }
      if(!is.null(far_data)){
        ret_list[["far_data"]] <- as.data.frame(far_data)
      }
      if(!is.null(removed_merge)){
        ret_list[["removed_merge"]] <- as.data.frame(removed_merge)
      }
      if(length(ret_list) > 0){
        return(ret_list)
      } else {
        return(invisible(NULL))
      }
    }
  },

#' @description Use `dplyr::mutate` function on the internal edge weights object.
#' @param ... Arguments to be passed to `dplyr::mutate()`.
#' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
#' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @details A wrapper to use `dplyr::mutate()` on the internal edge weights object and return the result in the requested format.
#' @return A `tidyr::tibble`, `sf` or `sp` object containing the resulting data list after the mutate.
mutate_weights = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if (!inherits(private$edge_weights, "tbl_df")) {
        edge_weights_res <- tidyr::as_tibble(private$edge_weights)
    } else {
        edge_weights_res <- private$edge_weights
    }

    if (.drop_all_na) {
        is_tbl <- inherits(edge_weights_res, "tbl_df")
        idx_temp <- idx_not_all_NA(edge_weights_res)
        edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        if (is_tbl) {
            edge_weights_res <- tidyr::as_tibble(edge_weights_res)
        }
    }

    if (.drop_na) {
        if (!inherits(edge_weights_res, "tbl_df")) {
            idx_temp <- idx_not_any_NA(edge_weights_res)
            edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        } else {
            edge_weights_res <- tidyr::drop_na(edge_weights_res)
        }
    }

    edge_weights_res <- dplyr::mutate(.data = edge_weights_res, ...)

    if (!inherits(edge_weights_res, "metric_graph_weights")) {
        class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    }

    return(private$format_weights(edge_weights_res, format))
},


#' @description Use `dplyr::select` function on the internal edge weights object.
#' @param ... Arguments to be passed to `dplyr::select()`.
#' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
#' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @details A wrapper to use `dplyr::select()` on the internal edge weights object and return the result in the requested format.
#' @return A `tidyr::tibble`, `sf` or `sp` object containing the resulting data list after the select.
select_weights = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if (!inherits(private$edge_weights, "tbl_df")) {
        edge_weights_res <- tidyr::as_tibble(private$edge_weights)
    } else {
        edge_weights_res <- private$edge_weights
    }

    edge_weights_res <- dplyr::select(.data = edge_weights_res, ...)

    if (.drop_all_na) {
        is_tbl <- inherits(edge_weights_res, "tbl_df")
        idx_temp <- idx_not_all_NA(edge_weights_res)
        edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        if (is_tbl) {
            edge_weights_res <- tidyr::as_tibble(edge_weights_res)
        }
    }

    if (.drop_na) {
        if (!inherits(edge_weights_res, "tbl_df")) {
            idx_temp <- idx_not_any_NA(edge_weights_res)
            edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        } else {
            edge_weights_res <- tidyr::drop_na(edge_weights_res)
        }
    }

    if (!inherits(edge_weights_res, "metric_graph_weights")) {
        class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    }

    return(private$format_weights(edge_weights_res, format))
},


#' @description Use `dplyr::filter` function on the internal edge weights object.
#' @param ... Arguments to be passed to `dplyr::filter()`.
#' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
#' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @details A wrapper to use `dplyr::filter()` on the internal edge weights object and return the result in the requested format.
#' @return A `tidyr::tibble`, `sf` or `sp` object containing the resulting data list after the filter.
filter_weights = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if (!inherits(private$edge_weights, "tbl_df")) {
        edge_weights_res <- tidyr::as_tibble(private$edge_weights)
    } else {
        edge_weights_res <- private$edge_weights
    }

    if (.drop_all_na) {
        is_tbl <- inherits(edge_weights_res, "tbl_df")
        idx_temp <- idx_not_all_NA(edge_weights_res)
        edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        if (is_tbl) {
            edge_weights_res <- tidyr::as_tibble(edge_weights_res)
        }
    }

    if (.drop_na) {
        if (!inherits(edge_weights_res, "tbl_df")) {
            idx_temp <- idx_not_any_NA(edge_weights_res)
            edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        } else {
            edge_weights_res <- tidyr::drop_na(edge_weights_res)
        }
    }

    edge_weights_res <- dplyr::filter(.data = edge_weights_res, ...)

    if (!inherits(edge_weights_res, "metric_graph_weights")) {
        class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    }

    return(private$format_weights(edge_weights_res, format))
},


#' @description Use `dplyr::summarise` function on the internal edge weights object grouped by the edge numbers.
#' @param ... Arguments to be passed to `dplyr::summarise()`.
#' @param .groups A vector of strings containing the names of the columns to be grouped, when computing the summaries. The default is `NULL`.
#' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
#' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @details A wrapper to use `dplyr::summarise()` on the internal edge weights object and return the result in the requested format.
#' @return A `tidyr::tibble`, `sf` or `sp` object containing the resulting data list after the summarise.
summarise_weights = function(..., .groups = NULL, .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if (!inherits(private$edge_weights, "tbl_df")) {
        edge_weights_res <- tidyr::as_tibble(private$edge_weights)
    } else {
        edge_weights_res <- private$edge_weights
    }

    if (.drop_all_na) {
        is_tbl <- inherits(edge_weights_res, "tbl_df")
        idx_temp <- idx_not_all_NA(edge_weights_res)
        edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        if (is_tbl) {
            edge_weights_res <- tidyr::as_tibble(edge_weights_res)
        }
    }

    if (.drop_na) {
        if (!inherits(edge_weights_res, "tbl_df")) {
            idx_temp <- idx_not_any_NA(edge_weights_res)
            edge_weights_res <- lapply(edge_weights_res, function(dat) { dat[idx_temp] })
        } else {
            edge_weights_res <- tidyr::drop_na(edge_weights_res)
        }
    }

    edge_weights_res <- dplyr::group_by_at(.tbl = edge_weights_res, .vars = .groups)
    edge_weights_res <- dplyr::summarise(.data = edge_weights_res, ...)
    edge_weights_res <- dplyr::ungroup(edge_weights_res)

    if (!inherits(edge_weights_res, "metric_graph_weights")) {
        class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    }

    return(private$format_weights(edge_weights_res, format))
},

#' @description Use `tidyr::drop_na()` function on the internal edge weights object.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @param ... Arguments to be passed to `tidyr::drop_na()`.
#' @details A wrapper to use `tidyr::drop_na()` within the internal edge weights object.
#' @return A `tidyr::tibble`, `sf`, or `sp` object containing the resulting data list after the drop_na.
drop_na_weights = function(...,format = "tibble") {
    if (!inherits(private$edge_weights, "tbl_df")) {
        edge_weights_res <- tidyr::as_tibble(private$edge_weights)
    } else {
        edge_weights_res <- private$edge_weights
    }

    edge_weights_res <- tidyr::drop_na(data = edge_weights_res, ...)

    if (!inherits(edge_weights_res, "metric_graph_weights")) {
        class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    }

    return(private$format_weights(edge_weights_res, format))
},


 #' @description Use `dplyr::mutate` function on the internal metric graph data object.
#' @param ... Arguments to be passed to `dplyr::mutate()`.
#' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
#' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
#' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
#' @details A wrapper to use `dplyr::mutate()` within the internal metric graph data object and return the result in the requested format.
#' @return A `tidyr::tibble`, `sf`, or `sp` object containing the resulting data list after the mutate.
mutate = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if (!inherits(private$data, "tbl_df")) {
        data_res <- tidyr::as_tibble(private$data)
    } else {
        data_res <- private$data
    }

    if (.drop_all_na) {
        is_tbl <- inherits(data_res, "tbl_df")
        idx_temp <- idx_not_all_NA(data_res)
        data_res <- lapply(data_res, function(dat) { dat[idx_temp] })
        if (is_tbl) {
            data_res <- tidyr::as_tibble(data_res)
        }
    }

    if (.drop_na) {
        if (!inherits(data_res, "tbl_df")) {
            idx_temp <- idx_not_any_NA(data_res)
            data_res <- lapply(data_res, function(dat) { dat[idx_temp] })
        } else {
            data_res <- tidyr::drop_na(data_res)
        }
    }

    data_res <- dplyr::mutate(.data = data_res, ...)

    if (!inherits(data_res, "metric_graph_data")) {
        class(data_res) <- c("metric_graph_data", class(data_res))
    }

    return(private$format_data(data_res, format))
},

  #' @description Use `tidyr::drop_na()` function on the internal metric graph data object.
  #' @param ... Arguments to be passed to `tidyr::drop_na()`.
  #' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble".
  #' @details A wrapper to use `dplyr::drop_na()` within the internal metric graph data object.
  #' @return A `tidyr::tibble` object containing the resulting data list after the drop_na.
  drop_na = function(..., format = "tibble") {
    if(!inherits(private$data, "tbl_df")){
      data_res <- tidyr::as_tibble(private$data)
    } else{
      data_res <- private$data
    }

    data_res <- tidyr::drop_na(data = data_res, ...)

    if(!inherits(data_res, "metric_graph_data")){
      class(data_res) <- c("metric_graph_data", class(data_res))
    }

    return(private$format_data(data_res, format))
  },



  #' @description Use `dplyr::select` function on the internal metric graph data object.
  #' @param ... Arguments to be passed to `dplyr::select()`.
  #' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
  #' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
  #' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble". 
  #' @details A wrapper to use `dplyr::select()` within the internal metric graph data object. Observe that it is a bit different from directly using `dplyr::select()` since it does not allow to remove the internal positions that are needed for the metric_graph methods to work.
  #' @return A `tidyr::tibble` object containing the resulting data list after the selection.
  select = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if(!inherits(private$data, "tbl_df")){
      data_res <- tidyr::as_tibble(private$data)
    } else{
      data_res <- private$data
    }
    data_res <- dplyr::select(.data = data_res, ...)
    data_res[[".group"]] <- private$data[[".group"]]
    data_res[[".edge_number"]] <- private$data[[".edge_number"]]
    data_res[[".distance_on_edge"]] <- private$data[[".distance_on_edge"]]
    data_res[[".coord_x"]] <- private$data[[".coord_x"]]
    data_res[[".coord_y"]] <- private$data[[".coord_y"]]


    if(.drop_all_na){
      is_tbl <- inherits(data_res, "tbl_df")
        idx_temp <- idx_not_all_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
        if(is_tbl){
          data_res <- tidyr::as_tibble(data_res)
        }
    }

    if(.drop_na){
      if(!inherits(data_res, "tbl_df")){
        idx_temp <- idx_not_any_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
      } else{
        data_res <- tidyr::drop_na(data_res)
      }
    }

    if(!inherits(data_res, "metric_graph_data")){
      class(data_res) <- c("metric_graph_data", class(data_res))
    }
    return(private$format_data(data_res, format))
  },

    #' @description Use `dplyr::filter` function on the internal metric graph data object.
  #' @param ... Arguments to be passed to `dplyr::filter()`.
   #' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
 #' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
  #' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble". 
  #' @details A wrapper to use `dplyr::filter()` within the internal metric graph data object.
  #' @return A `tidyr::tibble` object containing the resulting data list after the filter.
  filter = function(..., .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if(!inherits(private$data, "tbl_df")){
      data_res <- tidyr::as_tibble(private$data)
    } else{
      data_res <- private$data
    }


    if(.drop_all_na){
      is_tbl <- inherits(data_res, "tbl_df")
        idx_temp <- idx_not_all_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
        if(is_tbl){
          data_res <- tidyr::as_tibble(data_res)
        }
    }

    if(.drop_na){
      if(!inherits(data_res, "tbl_df")){
        idx_temp <- idx_not_any_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
      } else{
        data_res <- tidyr::drop_na(data_res)
      }
    }

    data_res <- dplyr::filter(.data = data_res, ...)
    data_res <- dplyr::arrange(.data = data_res, `.group`, `.edge_number`, `.distance_on_edge`)

    if(!inherits(data_res, "metric_graph_data")){
      class(data_res) <- c("metric_graph_data", class(data_res))
    }

    return(private$format_data(data_res, format))
  },


  #' @description Use `dplyr::summarise` function on the internal metric graph data object grouped by the spatial locations and the internal group variable.
  #' @param ... Arguments to be passed to `dplyr::summarise()`.
  #' @param .include_graph_groups Should the internal graph groups be included in the grouping variables? The default is `FALSE`. This means that, when summarising, the data will be grouped by the internal group variable together with the spatial locations.
  #' @param .groups A vector of strings containing the names of the columns to be additionally grouped, when computing the summaries. The default is `NULL`.
   #' @param .drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
 #' @param .drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
   #' @param format The format of the output: "tibble", "sf", or "sp". Default is "tibble". 
  #' @details A wrapper to use `dplyr::summarise()` within the internal metric graph data object grouped by manually inserted groups (optional), the internal group variable (optional) and the spatial locations. Observe that if the integral group variable was not used as a grouping variable for the summarise, a new column, called `.group`, will be added, with the same value 1 for all rows.
  #' @return A `tidyr::tibble` object containing the resulting data list after the summarise.
  summarise = function(..., .include_graph_groups = FALSE, .groups = NULL, .drop_na = FALSE, .drop_all_na = TRUE, format = "tibble") {
    if(!inherits(private$data, "tbl_df")){
      data_res <- tidyr::as_tibble(private$data)
    } else{
      data_res <- private$data
    }


    if(.drop_all_na){
      is_tbl <- inherits(data_res, "tbl_df")
        idx_temp <- idx_not_all_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
        if(is_tbl){
          data_res <- tidyr::as_tibble(data_res)
        }
    }

    if(.drop_na){
      if(!inherits(data_res, "tbl_df")){
        idx_temp <- idx_not_any_NA(data_res)
        data_res <- lapply(data_res, function(dat){dat[idx_temp]})
      } else{
        data_res <- tidyr::drop_na(data_res)
      }
    }


    group_vars <- c(".edge_number", ".distance_on_edge", ".coord_x", ".coord_y")
    if(.include_graph_groups){
      group_vars <- c(".group", group_vars)
    }
    group_vars <- c(.groups, group_vars)
    data_res <- dplyr::group_by_at(.tbl = data_res, .vars = group_vars)
    data_res <- dplyr::summarise(.data = data_res, ...)
    data_res <- dplyr::ungroup(data_res)
    if(is.null(data_res[[".group"]])){
      data_res[[".group"]] <- 1
    }

    data_res <- dplyr::arrange(.data = data_res, `.group`, `.edge_number`, `.distance_on_edge`)

    if(!inherits(data_res, "metric_graph_data")){
      class(data_res) <- c("metric_graph_data", class(data_res))
    }

    return(private$format_data(data_res, format))

  },

 #' @description Return the internal data with the option to filter by groups.
 #' @param group A vector contaning which groups should be returned? The default is `NULL`, which gives the result for the all groups.
 #' @param format Which format should the data be returned? The options are `tibble` for `tidyr::tibble`, `sf` for `POINT`, `sp` for `SpatialPointsDataFrame` and `list` for the internal list format.
 #' @param drop_na Should the rows with at least one NA for one of the columns be removed? DEFAULT is `FALSE`.
 #' @param drop_all_na Should the rows with all variables being NA be removed? DEFAULT is `TRUE`.
 #' @param tibble `r lifecycle::badge("deprecated")` Use `format` instead.

  get_data = function(group = NULL, format = c("tibble", "sf", "sp", "list"), drop_na = FALSE, drop_all_na = TRUE, tibble = deprecated()){
    if(is.null(private$data)){
      stop("The graph does not contain data.")
    }

  if (lifecycle::is_present(tibble)) {
      lifecycle::deprecate_warn("1.3.0.9000", "get_edge_weights(tibble)", "get_edge_weights(format)",
        details = c("The argument `tibble` was deprecated in favor of the argument `format`.")
      )
    if(tibble){
      format <- "tibble"
    }
  }       

    format <- format[[1]]

    format <- tolower(format)

    if(!(format %in% c("tibble", "sf", "sp", "list"))){
      stop("The possible formats are 'tibble', 'sf', 'sp' and 'list'.")
    }


    if(!is.null(group)){
      total_groups <- self$get_groups()
      if(!(all(group %in% total_groups))){
        if(all(group%%1 == 0)){
          group <- total_groups[group]
        } else{
          stop("At least one entry of 'group' is a not an existing group or an existing index.")
        }
      }

      data_temp <- select_group(private$data, group)
    } else{
      data_temp <- private$data
    }


    if(format == "tibble"){
      data_temp <- tidyr::as_tibble(data_temp)
    }

    if(drop_all_na){
      is_tbl <- inherits(data_temp, "tbl_df")
        idx_temp <- idx_not_all_NA(data_temp)
        data_temp <- lapply(data_temp, function(dat){dat[idx_temp]})
        if(is_tbl){
          data_temp <- tidyr::as_tibble(data_temp)
        }
    }

    if(drop_na){
      if(!inherits(data_temp, "tbl_df")){
        idx_temp <- idx_not_any_NA(data_temp)
        data_temp <- lapply(data_temp, function(dat){dat[idx_temp]})
      } else{
        data_temp <- tidyr::drop_na(data_temp)
      }
    }

    if(format == "sf"){
      data_temp <- as.data.frame(data_temp)
      data_geometries <- lapply(1:nrow(data_temp), function(i) sf::st_point(as.numeric(data_temp[i, c('.coord_x', '.coord_y')])))
      data_temp <- sf::st_sf(data_temp, geometry = sf::st_sfc(data_geometries), crs = if(!is.null(private$crs)) private$crs else NULL)      
    } 

    if(!inherits(data_temp, "metric_graph_data")){
      class(data_temp) <- c("metric_graph_data", class(data_temp))
    }

    if(format == "sp"){
      data_temp <- as.data.frame(data_temp)
      sp::coordinates(data_temp) <- ~ .coord_x + .coord_y
    }

    return(data_temp)
  },
 #' @description Define the columns to be used for creating the directional vertex
 #' weights. Also possible to supply user defined functions for input and output
 #' to create ones own weights.
 #' @param f_in functions for the input vertex (default `w/sum(w)`) uses the columns of name_column
 #' @param f_out functions for the output vertex (deafult `rep(-1,length(w))`) uses the columns of name_column
 #' @details For more details see paper (that does not exists yet).
 #' @return No return value.
 setDirectionalWeightFunction = function(f_in = NULL,
                                         f_out = NULL){


  if(!is.null(self$C)){
    warning('The constraint matrix has been deleted')
  }
   self$C     = NULL
   self$CoB   = NULL
   self$CoB$T = NULL

   if(!is.null(f_in)){
     self$DirectionalWeightFunction_in = f_in
   }else{
     self$DirectionalWeightFunction_in  = function(w){ w/sum(w)}
   }
   if(!is.null(f_out)){
     self$DirectionalWeightFunction_out = f_out
   }else{
     self$DirectionalWeightFunction_out = function(w){ rep(-1,length(w))}
   }
  },

 #' @description Build directional ODE constraint matrix from edges.
 #' @param alpha how many derivatives the processes has
 #' @param weight weighting for each vertex used in the constraint (E x 2)
 #' @details Currently not implemented for circles (edges that start and end
 #' in the same vertex)
 #' @return No return value. Called for its side effects.
  buildDirectionalConstraints = function(alpha = 1){
    if(alpha %% 1 != 0){
      stop("alpha should be an integer")
    }

    weight <- self$get_edge_weights()
    weight <- as.vector(weight[[private$directional_weights]])
    V_indegree = self$get_degrees("indegree")
    V_outdegree = self$get_degrees("outdegree")
    # index_outdegree <- V_outdegree > 0 & V_indegree >0
    # index_in0      <- V_indegree == 0
    # nC = (sum(V_outdegree[index_outdegree] *(1 + V_indegree[index_outdegree])) + sum(V_outdegree[index_in0]-1)) * alpha
    # i_  =  rep(0, nC)
    # j_  =  rep(0, nC)
    # x_  =  rep(0, nC)
    # Vs <- which(index_outdegree)
    # count_constraint <- 0
    # count <- 0
    # for (v in Vs) {
    #   out_edges   <- which(self$E[, 1] %in% v)
    #   in_edges    <- which(self$E[, 2] %in% v)
    #   #for each out edge
    #   n_in <- length(in_edges)
    #   for(i in 1:length(out_edges)){
    #     for(der in 1:alpha){
    #       i_[count + 1:(n_in+1)] <- count_constraint + 1
    #       j_[count + 1:(n_in+1)] <- c(2 * alpha * (out_edges[i]-1) + der,
    #                                   2 * alpha * (in_edges-1)  + alpha + der)


    #       x_[count + 1:(n_in+1)] <- c(as.matrix(self$DirectionalWeightFunction_out(weight[out_edges[i]])),
    #                                   as.matrix(self$DirectionalWeightFunction_in(weight[in_edges])))

    #       count <- count + (n_in+1)
    #       count_constraint <- count_constraint + 1
    #     }
    #   }
    # }
    # Vs0 <- which(index_in0)
    # for (v in Vs0) {
    #   out_edges   <- which(self$E[, 1] %in% v)
    #   #for each out edge
    #   if(length(out_edges)>1){
    #     for(i in 2:length(out_edges)){
    #       for(der in 1:alpha){
    #         i_[count + 1:2] <- count_constraint + 1
    #         j_[count + 1:2] <- c(2 * alpha * (out_edges[i]-1) + der,
    #                              2 * alpha * (out_edges[i-1]-1)   + der)

    #         x_[count + 1:2] <- c(1,
    #                              -1)
    #         count <- count + 2
    #         count_constraint <- count_constraint + 1
    #       }
    #     }
    #   }
    # }
    # C <- Matrix::sparseMatrix(i = i_[1:count],
    #                           j = j_[1:count],
    #                           x = x_[1:count],
    #                           dims = c(count_constraint, 2*alpha*self$nE))
    # self$C = C
    temp_E <- apply(self$E,2,as.integer)
    self$C <-construct_directional_constraint_matrix(temp_E, as.integer(self$nV), as.integer(self$nE), as.integer(alpha),
    as.integer(V_indegree), as.integer(V_outdegree), weight, self$DirectionalWeightFunction_out, self$DirectionalWeightFunction_in)

    self$CoB <- c_basis2(self$C)
    self$CoB$T <- t(self$CoB$T)
    self$CoB$alpha <- 1
  },


  #' @description Build Kirchoff constraint matrix from edges.
  #' @param alpha the type of constraint (currently only supports 2)
  #' @param edge_constraint if TRUE, add constraints on vertices of degree 1
  #' @details Currently not implemented for circles (edges that start and end
  #' in the same vertex)
  #' @return No return value. Called for its side effects.
  buildC = function(alpha = 2, edge_constraint = FALSE) {

    if(alpha==2){
      temp_E <- apply(self$E,2,as.integer)

      self$C <- construct_constraint_matrix(temp_E, as.integer(self$nV), as.integer(edge_constraint))
      self$CoB <- c_basis2(self$C)
      self$CoB$T <- t(self$CoB$T)
      self$CoB$alpha <- 2
    }else{
      error("only alpha=2 implemented")
    }
  },

  #' @description Builds mesh object for graph.
  #' @param h Maximum distance between mesh nodes (should be provided if n is
  #' not provided).
  #' @param n Maximum number of nodes per edge (should be provided if h is not
  #' provided).
  #' @param  continuous If `TRUE` (default), the mesh contains only one node per vertex.
  #' If `FALSE`, each vertex v is split into deg(v) disconnected nodes to allow
  #' for the creation of discontinuities at the vertices.
  #' @param continuous.outs If `continuous = FALSE` and `continuous.outs = TRUE`, continuity is
  #' assumed for the outgoing edges from each vertex.
  #' @param continuous.deg2 If `TRUE`, continuity is assumed at degree 2 vertices.
  #' @details The mesh is a list with the objects:
  #' - `PtE` The mesh locations excluding the original vertices;
  #' - `V` The verties of the mesh;
  #' - `E` The edges of the mesh;
  #' - `n_e` The number of vertices in the mesh per original edge in the graph;
  #' - `h_e` The mesh width per edge in the graph;
  #' - `ind` The indices of the vertices in the mesh;
  #' - `VtE` All mesh locations including the original vertices.
  #' @return No return value. Called for its side effects. The mesh is stored in
  #' the `mesh` element of the `metric_graph` object.

build_mesh = function(h = NULL, n = NULL, continuous = TRUE,
                         continuous.outs = FALSE, continuous.deg2 = FALSE) {
    if (is.null(h) && is.null(n)) {
      stop("You should specify either h or n!")
    }

    if (!is.null(h)) {
      if (length(h) > 1 || !is.numeric(h)) stop("h should be a single number")
      if (h <= 0) stop("h must be positive!")
    }

    if (!is.null(n)) {
      if (length(n) > 1 || !is.numeric(n)) stop("n should be a single number")
      if (n <= 0) stop("n must be positive!")
      if (n %% 1 != 0) {
        warning("A noninteger n was given, we are rounding it to an integer.")
        n <- round(n)
      }
    }

    mesh <- list(PtE = NULL, V = NULL, E = NULL, n_e = integer(length(self$edges)), h_e = NULL, ind = NULL, VtE = NULL)
    mesh$n_e <- integer(length(self$edges))
    attr(mesh, "continuous") <- continuous

    edge_lengths <- self$edge_lengths
    n_edges <- length(self$edges)

    if (continuous) {
      mesh$V <- self$V
      mesh$ind <- seq_len(self$nV)

      if (is.null(n)) {
        mesh$n_e <- ceiling(edge_lengths / h) + 1 - 2
      } else {
        mesh$n_e <- rep(n, n_edges)
      }

      # Call the Rcpp function
      mesh_data <- generate_mesh(n_edges, edge_lengths, mesh$n_e, self$E, mesh$ind, continuous)

      mesh$PtE <- cbind(mesh_data$PtE_edge, mesh_data$PtE_pos)
      mesh$h_e <- mesh_data$h_e
      mesh$E <- cbind(mesh_data$E_start, mesh_data$E_end)

      mesh$VtE <- rbind(self$VtEfirst(), mesh$PtE)
      if (!is.null(mesh$PtE) && nrow(mesh$PtE) > 0) {
        mesh$V <- rbind(mesh$V, self$coordinates(PtE = mesh$PtE))
      }
      self$mesh <- mesh
    } else {
      mesh$ind <- 0

      if (is.null(n)) {
        mesh$n_e <- ceiling(edge_lengths / h) + 1
      } else {
        mesh$n_e <- rep(n + 2, n_edges)
      }

      # Call the Rcpp function for the non-continuous case
      mesh_data <- generate_mesh(n_edges, edge_lengths, mesh$n_e, self$E, mesh$ind, continuous = FALSE)

      mesh$PtE <- cbind(mesh_data$PtE_edge, mesh_data$PtE_pos)
      mesh$h_e <- mesh_data$h_e
      mesh$E <- cbind(mesh_data$E_start, mesh_data$E_end)

      mesh$VtE <- mesh$PtE
      if(!is.null(mesh$PtE) && nrow(mesh$PtE) > 0) {
        mesh$V <- self$coordinates(PtE = mesh$PtE)
      }
      self$mesh <- mesh
      if (continuous.outs) private$mesh_merge_outs()
      private$move_V_first()
      if (continuous.deg2) private$mesh_merge_deg2()
    }
  }, 

  #' @description Build mass and stiffness matrices for given mesh object.
  #' @details The function builds: The matrix `C` which is the mass matrix with
  #' elements \eqn{C_{ij} = <\phi_i, \phi_j>}, the matrix `G` which is the stiffness
  #' matrix with elements \eqn{G_{ij} = <d\phi_i, d\phi_j>}, the matrix `B` with
  #' elements \eqn{B_{ij} = <d\phi_i, \phi_j>}, the matrix `D` with elements
  #' \eqn{D_{ij} = \sum_{v\in V}\phi_i(v)\phi_j(v)}, and the vector with weights
  #' \eqn{<\phi_i, 1>}.
  #' @param petrov Compute Petrov-Galerkin matrices? (default `FALSE`). These
  #' are defined as \eqn{Cpet_{ij} = <\phi_i, \psi_j>} and \eqn{Gpet_{ij} = <d\phi_i, \psi_j>},
  #' where \eqn{\psi_{i}} are piecewise constant basis functions on the edges of
  #' the mesh.
  #' @return No return value. Called for its side effects. The finite element
  #' matrices `C`, `G` and `B` are stored in the `mesh` element in the
  #' `metric_graph` object. If `petrov=TRUE`, the corresponding Petrov-Galerkin
  #' matrices are stored in `Cpet` and `Gpet`.
  compute_fem = function(petrov = FALSE) {
    if (is.null(self$mesh)) {
      stop("no mesh provided")
    }
    nV <- dim(self$mesh$V)[1]
    fem_temp <- assemble_fem(E = self$mesh$E, h_e = self$mesh$h_e, nV = nV, petrov = petrov)
    self$mesh$C <- fem_temp$C
    self$mesh$G <- fem_temp$G
    self$mesh$B <- fem_temp$B
    self$mesh$D <- Diagonal(dim(self$mesh$C)[1],
                            c(rep(1, self$nV), rep(0, dim(self$mesh$C)[1] - self$nV)))
    #set weighted Krichhoff matrix
    self$mesh$K <- Diagonal(dim(self$mesh$C)[1],
                            c(rep(0, self$nV), rep(0, dim(self$mesh$C)[1] - self$nV)))

    # if(!all(private$get_edge_weights_internal()==1)){
    if(!is.null(private$kirchhoff_weights)){
      for(i in 1:self$nV) {
        if(attr(self$vertices[[i]],"degree") > 1) {
          edges.i <- which(rowSums(self$E==i)>0)
          edges.mesh <- which(rowSums(self$mesh$E==i)>0)
          w <- rep(0,length(edges.mesh))
          h <- rep(0,length(edges.mesh))
          for(j in 1:length(edges.mesh)) {
            V.e <- self$mesh$E[edges.mesh[j],which(self$mesh$E[edges.mesh[j],]!=i)]
            if(length(V.e) == 0){
              V.e <- self$mesh$E[edges.mesh[j],1]
            }
            E.e <- self$mesh$VtE[V.e,1] #the edge the mesh node is on
            # w[j] <- attr(self$edges[[E.e]],"weight")
            kw <- attr(self$edges[[E.e]], "kirchhoff_weight")
            w_tmp <- attr(self$edges[[E.e]],"weight")
            w[j] <- w_tmp[[kw]]
            h[j] <- self$mesh$h_e[edges.mesh[j]]
          }
          for(j in 2:attr(self$vertices[[i]],"degree")){
            self$mesh$K[i,i] <- self$mesh$K[i,i] +  (w[j]/w[1] - 1)/h[j]
          }
        }
      }
    }
    # }

    if(petrov) {
      self$mesh$Cpet <- fem_temp$Cpet
      self$mesh$Gpet <- fem_temp$Gpet
      private$set_petrov_matrices()
    }

    self$mesh$weights <- rowSums(self$mesh$C)
  },

  #' @description Deprecated - Computes observation matrix for mesh.
  #'
  #'  `r lifecycle::badge("deprecated")` in favour of `metric_graph$fem_basis()`.
  #' @param PtE Locations given as (edge number in graph, normalized location on
  #' edge)
  #' @details For n locations and a mesh with m nodes, `A` is an n x m matrix with
  #' elements \eqn{A_{ij} = \phi_j(s_i)}{A_{ij} = \phi_j(s_i)}.
  #' @return The observation matrix.
  #'

  mesh_A = function(PtE){
    lifecycle::deprecate_warn("1.2.0", "metric_graph$mesh_A()", "metric_graph$fem_basis()")
    self$fem_basis(PtE)
  },

  #' @description Computes observation matrix for mesh.
  #' @param PtE Locations given as (edge number in graph, normalized location on
  #' edge)
  #' @details For n locations and a mesh with m nodes, `A` is an n x m matrix with
  #' elements \eqn{A_{ij} = \phi_j(s_i)}{A_{ij} = \phi_j(s_i)}.
  #' @return The observation matrix.
  fem_basis = function(PtE) {
    if(ncol(PtE)!= 2){
      stop("PtE must have two columns!")
    }

    if (min(PtE[,2]) < 0) {
      stop("PtE[, 2] has negative values")
    }
    if ((max(PtE[,2]) > 1)) {
      stop("For normalized distances, the values in PtE[, 2] should not be
             larger than 1")
    }
    if (is.null(self$mesh)) {
      stop("no mesh given")
    }

    x <- private$PtE_to_mesh(PtE)
    n <- dim(x)[1]

    A <- sparseMatrix(i = c(1:n, 1:n),
                      j = c(self$mesh$E[x[, 1], 1], self$mesh$E[x[, 1], 2]),
                      x = c(1 - x[, 2], x[, 2]),
                      dims = c(n, dim(self$mesh$V)[1]))
    return(A)
  },

  #' @description Find one edge corresponding to each vertex.
  #' @return A nV x 2 matrix the first element of the `i`th row is the edge
  #' number corresponding to the `i`th vertex and the second value is 0
  #' if the vertex is at the start of the edge and 1 if the vertex
  #' is at the end of the edge.
  VtEfirst = function() {
    n.V <- dim(self$V)[1]
    VtE <- matrix(0, n.V, 2)

    for (i in 1:n.V) {
      Ei <- which(self$E[, 1] == i)[1]
      pos <- 0
      if (is.na(Ei) == 1) {
        pos <- 1
        Ei <- which(self$E[, 2] == i)[1]
      }
      VtE[i,] <- c(Ei, pos)
    }
    return(VtE)
  },

  #' @description Plots the metric graph.
  #' @param data Which column of the data to plot? If `NULL`, no data will be
  #' plotted.
  #' @param newdata A dataset of class `metric_graph_data`, obtained by any `get_data()`, `mutate()`, `filter()`, `summarise()`, `drop_na()` methods of metric graphs, see the vignette on data manipulation for more details.
  #' @param group If there are groups, which group to plot? If `group` is a
  #' number and `newdata` is `NULL`, it will be the index of the group as stored internally and if `newdata` is provided, it will be the index of the group stored in `newdata`. If `group`
  #' is a character, then the group will be chosen by its name.
  #' @param type The type of plot to be returned. The options are `ggplot` (the default), that uses `ggplot2`; `plotly` that uses `plot_ly` for 3D plots, which requires the `plotly` package, and `mapview` that uses the `mapview` function, to build interactive plots, which requires the `mapview` package.
  #' @param interactive Only works for 2d plots. If `TRUE`, an interactive plot will be displayed. Unfortunately, `interactive` is not compatible with `edge_weight` if `add_new_scale_weights` is TRUE.
  #' @param vertex_size Size of the vertices.
  #' @param vertex_color Color of vertices.
  #' @param edge_width Line width for edges. If `edge_width_weight` is not `NULL`, this determines the maximum edge width.
  #' @param edge_color Color of edges.
  #' @param data_size Size of markers for data.
  #' @param support_width For 3D plot, width of support lines.
  #' @param support_color For 3D plot, color of support lines.
  #' @param mesh Plot the mesh locations?
  #' @param X Additional values to plot.
  #' @param X_loc Locations of the additional values in the format
  #' (edge, normalized distance on edge).
  #' @param p Existing objects obtained from 'ggplot2' or 'plotly' to add the graph to
  #' @param degree Show the degrees of the vertices?
  #' @param direction Show the direction of the edges? For `type == "mapview"` the arrows are not shown, only the color of the vertices indicating whether they are problematic or not.
  #' @param arrow_size The size of the arrows if direction is TRUE.
  #' @param edge_weight Which column from edge weights to determine the colors of the edges? If `NULL` edge weights are not plotted. To plot the edge weights when the metric graph `edge_weights` is a vector instead of a `data.frame`, simply set to 1.
  #' `edge_weight` is only available for 2d plots. For 3d plots with edge weights, please use the `plot_function()` method.
    #' @param edge_width_weight Which column from edge weights to determine the edges widths? If `NULL` edge width will be determined from `edge_width`. Currently it is not supported for `type = "mapview"`.
    #' @param scale_color_main Color scale for the data to be plotted.
    #' @param scale_color_weights Color scale for the edge weights. Will only be used if `add_new_scale_weights` is TRUE.
    #' @param scale_color_main_discrete Color scale for the data to be plotted, for discrete data.
    #' @param scale_color_weights_discrete Color scale for discrete edge weights. Will only be used if `add_new_scale_weights` is TRUE.
    #' @param scale_color_degree Color scale for the degrees.
    #' @param add_new_scale_weights Should a new color scale for the edge weights be created?
    #' @param scale_color_mapview Color scale to be applied for data when `type = "mapview"`.
    #' @param scale_color_weights_mapview Color scale to be applied for edge weights when `type = "mapview"`.
    #' @param scale_color_weights_discrete_mapview Color scale to be applied for degrees when `type = "mapview"`. If `NULL` `RColorBrewer::brewer.pal(n = n_weights, "Set1")` will be used where `n_weights` is the number of different degrees.
    #' @param scale_color_degree_mapview Color scale to be applied for degrees when `type = "mapview"`. If `NULL` `RColorBrewer::brewer.pal(n = n_degrees, "Set1")` will be used where `n_degrees` is the number of different degrees.
    #' @param plotly  `r lifecycle::badge("deprecated")` Use `type` instead.
##  # ' @param mutate A string containing the commands to be passed to `dplyr::mutate` function in order to obtain new variables as functions of the existing variables.
##  # ' @param filter A string containing the commands to be passed to `dplyr::filter` function in order to obtain new filtered data frame.
##  # ' @param summarise A string containing the commands to be passed to `dplyr::summarise` function in order to obtain new  data frame containing the summarised variable.
##  # ' @param summarise_group_by A vector of strings containing the names of the columns to be additionally grouped, when computing the summaries. The default is `NULL`.
##  # ' @param summarise_by_graph_group Should the internal graph groups be included in the grouping variables? The default is `FALSE`. This means that, when summarising, the data will be grouped by the internal group variable together with the spatial locations.
  #' @param ... Additional arguments to pass to `ggplot()` or `plot_ly()`
  #' @return A `plot_ly` (if `type = "plotly"`) or `ggplot` object.
  plot = function(data = NULL,
                  newdata = NULL,
                  group = 1,
                  type = c("ggplot", "plotly", "mapview"),
                  interactive = FALSE,
                  vertex_size = 3,
                  vertex_color = 'black',
                  edge_width = 0.3,
                  edge_color = 'black',
                  data_size = 1,
                  support_width = 0.5,
                  support_color = "gray",
                  mesh = FALSE,
                  X = NULL,
                  X_loc = NULL,
                  p = NULL,
                  degree = FALSE,
                  direction = FALSE,
                  arrow_size = ggplot2::unit(0.25, "inches"),
                  edge_weight = NULL,
                  edge_width_weight = NULL,
                  scale_color_main = ggplot2::scale_color_viridis_c(option = "D"),
                  scale_color_weights = ggplot2::scale_color_viridis_c(option = "C"),
                  scale_color_degree = ggplot2::scale_color_viridis_d(option = "D"),
                  scale_color_weights_discrete = ggplot2::scale_color_viridis_d(option = "C"),
                  scale_color_main_discrete = ggplot2::scale_color_viridis_d(option = "C"),
                  add_new_scale_weights = TRUE,
                  scale_color_mapview = viridis::viridis(100, option = "D"),
                  scale_color_weights_mapview = viridis::viridis(100, option = "C"),
                  scale_color_weights_discrete_mapview = NULL,
                  scale_color_degree_mapview = NULL,
                  plotly = deprecated(),
                  ...) {

 if (lifecycle::is_present(plotly)) {
      lifecycle::deprecate_warn("1.3.0.9000", "plot(plotly)", "plot(type)",
        details = c("The argument `plotly` was deprecated in favor of the argument `type`.")
      )
    if(plotly){
      type <- "plotly"
    }
  }                 

    if(!is.null(data) && is.null(private$data) && is.null(newdata)) {
      stop("The graph does not contain data.")
    }
    if(is.numeric(group) && !is.null(data) && is.null(newdata)) {
      unique_group <- unique(private$data[[".group"]])
      group <- unique_group[group]
    } else if(!is.null(newdata) && is.numeric(group)){
      unique_group <- unique(newdata[[".group"]])
      group <- unique_group[group]
    }
    if(!is.null(newdata)){
      if(!inherits(newdata, "metric_graph_data")){
        stop("'newdata' must be of class 'metric_graph_data'!")
      }
    }

    type <- type[[1]]

    if(!(type %in% c("ggplot", "plotly", "mapview"))){
      stop("type must be one of the following: 'ggplot', 'plotly' or 'mapview'.")
    }

    if(type == "ggplot") {
      p <- private$plot_2d(line_width = edge_width,
                           marker_size = vertex_size,
                           vertex_color = vertex_color,
                           edge_color = edge_color,
                           data = data,
                           newdata = newdata,
                           data_size = data_size,
                           group = group,
                           mesh = mesh,
                           X = X,
                           X_loc = X_loc,
                           p = p,
                           degree = degree,
                           direction = direction,
                           edge_weight = edge_weight,
                           edge_width_weight = edge_width_weight,
                           scale_color_main = scale_color_main,
                           scale_color_weights = scale_color_weights,
                           scale_color_degree = scale_color_degree,
                           add_new_scale_weights = add_new_scale_weights,
                           arrow_size = arrow_size,
                           scale_color_main_discrete = scale_color_main_discrete,
                           scale_color_weights_discrete = scale_color_weights_discrete,                           
                           ...)
      if(!is.null(private$vertex_unit)){
        if(private$vertex_unit == "degree" && !private$transform){
          p <- p + labs(x = "Longitude",  y = "Latitude")
        } else{
          p <- p + labs(x = paste0("x (in ",private$vertex_unit, ")"),  y = paste0("y (in ",private$vertex_unit, ")"))
        }
      }
    } else if(type == "plotly") {
      requireNamespace("plotly")
      p <- private$plot_3d(line_width = edge_width,
                           marker_size = vertex_size,
                           vertex_color = vertex_color,
                           edge_color = edge_color,
                           data = data,
                           newdata = newdata,
                           data_size = data_size,
                           group = group,
                           mesh = mesh,
                           X = X,
                           X_loc = X_loc,
                           support_color = support_color,
                           support_width = support_width,
                           p = p,
                           edge_width_weight = edge_width_weight,
                           ...)
      p <-  plotly::layout(p, scene = list(xaxis = list(autorange = "reversed")))                             
      if(!is.null(private$vertex_unit)){
        if(private$vertex_unit == "degree" && !private$transform){
          p <- plotly::layout(p, scene = list(xaxis = list(title = "Longitude"), yaxis = list(title = "Latitude")))
        } else{
          p <- plotly::layout(p, scene = list(xaxis = list(title = paste0("x (in ",private$vertex_unit, ")")), yaxis = list(title = paste0("y (in ",private$vertex_unit, ")"))))
        }
      }
    } else if(type == "mapview"){
requireNamespace("mapview")
edge_width <- 2.5 * edge_width
edges_sf <- self$get_edges(format = "sf")
if(is.null(newdata) && !is.null(data)){
  data_sf <- self$get_data(format = "sf")
  idx_grp <- (data_sf[[".group"]] == group)
  data_sf <- data_sf[idx_grp, , drop=FALSE]
  class(data_sf) <- setdiff(class(data_sf), "metric_graph_data")
} else if (!is.null(data)){
  data_sf <- as.data.frame(newdata)
  data_geometries <- lapply(1:nrow(data_sf), function(i) sf::st_point(as.numeric(data_sf[i, c('.coord_x', '.coord_y')])))
  data_sf <- sf::st_sf(data_sf, geometry = sf::st_sfc(data_geometries), crs = if(!is.null(private$crs)) private$crs else NULL)     
  idx_grp <- (data_sf[[".group"]] == group)
  data_sf <- data_sf[idx_grp, , drop=FALSE]   
  class(data_sf) <- setdiff(class(data_sf), "metric_graph_data")        
}

mapview_output <- p

if(is.null(p)){
  if (!is.null(edge_weight)) {
    if (!(edge_weight %in% colnames(edges_sf))) {
      stop(paste(edge_weight, "is not a valid column in edges_sf"))
    }

    if(is.character(edges_sf[[edge_weight]]) || is.factor(edges_sf[[edge_weight]])){
      if(is.null(scale_color_degree_mapview)){
        scale_color_weights_discrete_mapview <- RColorBrewer::brewer.pal(n = length(levels(edges_sf[[edge_weight]])), "Set1")
      }         
      scale_weights <- scale_color_weights_discrete_mapview
    } else{
      scale_weights <- scale_color_weights_mapview
    }
    mapview_output <- mapview::mapview(
      x = edges_sf,
      zcol = edge_weight,  
      color = scale_weights,  
      lwd = edge_width,  
      layer.name = "Edges",  
      col.regions = scale_weights,  
      ...
    )
  } else {
    mapview_output <- mapview::mapview(
      x = edges_sf,
      lwd = edge_width,  
      color = edge_color, 
      layer.name = "Edges",
      ...
    )
  }
} else{
    if (!is.null(edge_weight)) {
    if (!(edge_weight %in% colnames(edges_sf))) {
      stop(paste(edge_weight, "is not a valid column in edges_sf"))
    }
    if(is.character(edges_sf[[edge_weight]]) || is.factor(edges_sf[[edge_weight]])){
      if(is.null(scale_color_degree_mapview)){
        scale_color_weights_discrete_mapview <- RColorBrewer::brewer.pal(n = length(levels(edges_sf[[edge_weight]])), "Set1")
      }      
      scale_weights <- scale_color_weights_discrete_mapview
    } else{
      scale_weights <- scale_color_weights_mapview
    }    
    mapview_output <- mapview_output + mapview::mapview(
      x = edges_sf,
      zcol = edge_weight,  
      color = scale_weights,  
      lwd = edge_width,  
      layer.name = "Edges",  
      col.regions = scale_weights,  
      ...
    )
  } else {
    mapview_output <- mapview_output + mapview::mapview(
      x = edges_sf,
      lwd = edge_width,  
      color = edge_color, 
      layer.name = "Edges",
      ...
    )
  }
}



if (degree) {
  vertices_sf <- self$get_vertices(format = "sf")
  vertices_sf$degree <- as.factor(vertices_sf$degree)
  if(is.null(scale_color_degree_mapview)){
    scale_color_degree_mapview <- RColorBrewer::brewer.pal(n = length(levels(vertices_sf$degree)), "Set1")
  }

  mapview_output <- mapview_output + mapview::mapview(
    x = vertices_sf,
    zcol = "degree",
    cex = vertex_size,
    col.regions = scale_color_degree_mapview,
    color = scale_color_degree_mapview,
    layer.name = "Vertices (Degree)",
    ...
  )
} else if (direction) {
  vertices_sf <- self$get_vertices(format = "sf")
  problematic_vertices <- vertices_sf[vertices_sf$problematic == TRUE, ]
  non_problematic_vertices <- vertices_sf[vertices_sf$problematic == FALSE, ]

  mapview_output <- mapview_output + mapview::mapview(
    x = problematic_vertices,
    cex = vertex_size,
    col.regions = "red",
    color = "red",
    layer.name = "Problematic Vertices",
    ...
  )

  mapview_output <- mapview_output + mapview::mapview(
    x = non_problematic_vertices,
    cex = vertex_size,
    col.regions = "green",
    color = "green",
    layer.name = "Non-problematic Vertices",
    ...
  )
} else if(vertex_size > 0){
  vertices_sf <- self$get_vertices(format = "sf")
  mapview_output <- mapview_output + mapview::mapview(
    x = vertices_sf,
    cex = vertex_size,
    col.regions = vertex_color,
    color = vertex_color,
    layer.name = "Vertices",
    ...
  )
}

if (!is.null(data)) {
  if (!(data %in% names(data_sf))) {
    stop(paste(data, "is not an existing column name in the dataset."))
  }
  data_size <- 2 * data_size

  mapview_output <- mapview_output + mapview::mapview(
    x = data_sf,
    zcol = data,
    cex = data_size,
    col.regions = scale_color_mapview,
    color = scale_color_mapview,
    layer.name = data,
    ...
  )
}

if(vertex_size > 0){
  if (mesh) {
    if (is.null(self$mesh)) {
      stop("The metric graph does not contain a mesh.")
    }

    mesh_df <- as.data.frame(self$mesh$V)
    colnames(mesh_df) <- c("X", "Y")
    mesh_sf <- sf::st_as_sf(mesh_df, coords = c("X", "Y"))

    if (!is.null(private$crs)) {
      sf::st_crs(mesh_sf) <- private$crs
    } else {
      sf::st_crs(mesh_sf) <- sf::NA_crs_
    }

    mapview_output <- mapview_output + mapview::mapview(
      x = mesh_sf,
      cex = vertex_size * 0.5,
      col.regions = "gray",
      color = "black",
      layer.name = "Mesh",
      ...
    )
  } else{
    warning("The mesh was not shown since vertex_size is zero.")
  }
} 

if (!is.null(X)) {
  if (is.null(X_loc)) {
    stop("X supplied but not X_loc")
  }
  if (length(X) != nrow(X_loc)) {
    stop("The number of observations does not match the number of locations!")
  }
  
  points_xy <- self$coordinates(PtE = X_loc)
  x_loc_sf <- sf::st_as_sf(data.frame(x = points_xy[, 1], y = points_xy[, 2], val = as.vector(X)),
                           coords = c("x", "y"))

  mapview_output <- mapview_output + mapview::mapview(
    x = x_loc_sf,
    zcol = "val",
    cex = data_size,
    col.regions = scale_color_mapview,
    layer.name = "X Points",
    ...
  )
}

return(mapview_output)

    }
    if(interactive && (type == "ggplot")){
      print(plotly::ggplotly(p))
      return(invisible(p))
    }
    return(p)
  },

  #' @description Plots the connections in the graph
  #' @return No return value. Called for its side effects.
  plot_connections = function(){
        g <- make_graph(edges = c(t(self$E)), directed = FALSE)
        plot(g)
  },

  #' @description Checks if the graph is a tree (without considering directions)
  #' @return TRUE if the graph is a tree and FALSE otherwise.
  is_tree = function(){
        g <- make_graph(edges = c(t(self$E)), directed = FALSE)
        return(igraph::is_tree(g, mode = "all"))
  },

  #' @description Plots continuous function on the graph.
  #' @param data Which column of the data to plot? If `NULL`, no data will be
  #' plotted.
  #' @param newdata A dataset of class `metric_graph_data`, obtained by any `get_data()`, `mutate()`, `filter()`, `summarise()`, `drop_na()` methods of metric graphs, see the vignette on data manipulation for more details.
  #' @param group If there are groups, which group to plot? If `group` is a
  #' number, it will be the index of the group as stored internally. If `group`
  #' is a character, then the group will be chosen by its name.
  #' @param X A vector with values for the function
  #' evaluated at the mesh in the graph
  #' @param type The type of plot to be returned. The options are `ggplot` (the default), that uses `ggplot2`; `plotly` that uses `plot_ly` for 3D plots, which requires the `plotly` package, and `mapview` that uses the `mapview` function, to build interactive plots, which requires the `mapview` package.
  #' @param continuous Should continuity be assumed when the plot uses `newdata`?
  #' @param interpolate_plot Should the values to be plotted be interpolated?
  #' @param vertex_size Size of the vertices.
  #' @param vertex_color Color of vertices.
  #' @param edge_width Width for edges.
  #' @param edge_weight Which column from edge weights to plot? If `NULL` edge weights are not plotted. To plot the edge weights when the metric graph `edge_weights` is a vector instead of a `data.frame`, simply set to 1.
  #' @param edge_color For 3D plot, color of edges.
  #' @param line_width For 3D plot, line width of the function curve.
  #' @param line_color Color of the function curve.
  #' @param scale_color Color scale to be used for data and weights.
  #' @param scale_color_mapview Color scale to be applied for data when `type = "mapview"`.
  #' @param support_width For 3D plot, width of support lines.
  #' @param support_color For 3D plot, color of support lines.
  #' @param mapview_caption Caption for the function if `type = "mapview"`.
  #' @param p Previous plot to which the new plot should be added.
  #' @param plotly  `r lifecycle::badge("deprecated")` Use `type` instead.
  #' @param improve_plot  `r lifecycle::badge("deprecated")` Use `interpolate` instead. There is no need to use it to improve the edges.
  #' @param ... Additional arguments for `ggplot()` or `plot_ly()`
  #' @return Either a `ggplot` (if `plotly = FALSE`) or a `plot_ly` object.
  plot_function = function(data = NULL,
                           newdata = NULL,
                           group = 1,
                           X = NULL,
                           type = c("ggplot", "plotly", "mapview"),
                           continuous = TRUE,
                           interpolate_plot = TRUE,
                           edge_weight = NULL,
                           vertex_size = 5,
                           vertex_color = "black",
                           edge_width = 1,
                           edge_color = 'black',
                           line_width = NULL,
                           line_color = 'rgb(0,0,200)',
                           scale_color = ggplot2::scale_color_viridis_c(option = "D"),
                           scale_color_mapview = viridis::viridis(100, option = "D"),
                           support_width = 0.5,
                           support_color = "gray",
                           mapview_caption = "Function",
                           p = NULL,
                           plotly = deprecated(),
                           improve_plot = deprecated(),
                           ...){
    if (is.null(line_width)) {
      line_width = edge_width
    }


     if (lifecycle::is_present(plotly)) {
      lifecycle::deprecate_warn("1.3.0.9000", "plot(plotly)", "plot(type)",
        details = c("The argument `plotly` was deprecated in favor of the argument `type`.")
      )
    if(plotly){
      type <- "plotly"
    }
  }                 

    type <- type[[1]]

    if(!(type %in% c("ggplot", "plotly", "mapview"))){
      stop("type must be one of the following: 'ggplot', 'plotly' or 'mapview'.")
    }

    mesh <- FALSE

    if(!is.null(edge_weight)){
      edge_weight <- edge_weight[[1]]
      continuous <- FALSE
      newdata <- do.call(rbind, self$edges)
      newdata <- self$edgeweight_to_data(loc = newdata, weight_col = edge_weight,
                                        data_coords = "spatial",
                                        add = FALSE,
                                        return = TRUE,
                                        verbose = 0,
                                        suppress_warnings = TRUE)
      data <- edge_weight
    }

    if(is.null(data) && is.null(X) && is.null(edge_weight)){
      stop("You should provide either 'data', 'X' or 'edge_weight'.")
    }

    if(!is.null(data) && !is.null(X)){
      warning("Both 'data' and 'X' were provided. Only 'data' will be considered.")
      X <- NULL
    }

    if(!is.character(data) && (is.vector(data) || !is.null(dim(data)))){
      X <- data
    }

    if(!is.null(X)){
      mesh <- TRUE
      if(!is.vector(X) && is.null(dim(X))){
        stop("'X' should be a vector, or a row-matrix or a column-matrix!")
      }
      if(!is.null(dim(X)) && min(dim(X)) > 1){
        stop("If 'X' is a matrix, it needs to be either a row matrix or a column matrix!")
      }
      X <- as.vector(X)
    }

    if (mesh) {
      if (is.null(self$mesh)) {
        stop("X is a vector but no mesh provided")
      }

      if (is.null(self$mesh$PtE)) {
        PtE_dim <- 0
      } else {
        PtE_dim <- dim(self$mesh$PtE)[1]
      }

      if (length(X) == PtE_dim && attr(self$mesh, "continuous")) {
        X <- c(rep(NA, dim(self$V)[1]), X)
      }


      if (length(X) != dim(unique(self$mesh$V))[1] && length(X) != dim(self$mesh$V)[1]) {
        stop(paste0("X does not have the correct size (the possible sizes are ",
                    PtE_dim,", ", dim(self$mesh$V)[1], " and ",
                    dim(unique(self$mesh$V))[1],")"))
      }

      if(dim(unique(self$mesh$V))[1] != dim(self$mesh$V)[1]){
        if(length(X) == dim(unique(self$mesh$V))[1]){
          X_temp <-  rep(NA, dim(self$mesh$V)[1])
          X_temp[which(!duplicated(self$mesh$V))] <- X
          X <- X_temp
        }
      }

      if(attr(self$mesh, "continuous")){
        n.v <- dim(self$V)[1]
        XV <- X[1:n.v]
      } 
    } else{
      if(is.null(newdata)){
        X <- self$get_data(group = group)
        X <- X[,c(".edge_number", ".distance_on_edge", data)]
      } else{
        if(!inherits(newdata, "metric_graph_data")){
          stop("'newdata' must be of class 'metric_graph_data'!")
        }
        X <- newdata[,c(".edge_number", ".distance_on_edge", data)]
      }
    }

    PtE_edges <- lapply(self$edges, function(edge){attr(edge, "PtE")})

    x.loc <- y.loc <- z.loc <- i.loc <- NULL
    kk = 1
    for (i in 1:self$nE) {
      Vs <- self$E[i, 1]
      Ve <- self$E[i, 2]
      if (mesh) {
        ind <- self$mesh$PtE[, 1] == i

        if (sum(ind)==0) {
          if(attr(self$mesh,"continuous")){
            vals <- rbind(c(0, XV[Vs]),
                        c(1, XV[Ve]))
          } else{
            vals <- NULL
          }

        } else {
          if(attr(self$mesh,"continuous")) {
            vals <- rbind(c(0, XV[Vs]),
                          cbind(self$mesh$PtE[ind, 2], X[n.v + which(ind)]),
                          c(1, XV[Ve]))
          } else {
              vals <- cbind(self$mesh$PtE[ind, 2], X[which(ind)])
          }


        }

        if(interpolate_plot){
          PtE_tmp <- PtE_edges[[i]]
          PtE_tmp <- setdiff(PtE_tmp, vals[,1])
          if(length(PtE_tmp)>0){
                PtE_tmp <- cbind(PtE_tmp, NA)
                vals <- rbind(vals,PtE_tmp)
          }

          # if(nrow(vals)>0){
          #   ord_idx <- order(vals[,1])
          #   vals <- vals[ord_idx,]
          #   if(vals[1,1] > 0){
          #     vals <- rbind(c(0,NA), vals)
          #   }
          #   if(vals[nrow(vals),1] < 1){
          #     vals <- rbind(vals, c(1,NA))
          #   }
          #   max_val <- max(vals[,2], na.rm=TRUE)
          #   min_val <- min(vals[,2], na.rm=TRUE)
          #   vals[,2] <- na.const(pmax(pmin(object = zoo::na.approx(object = vals[,2],
          #                                         x = vals[,1],
          #                                             na.rm=FALSE, ties = "mean"),
          #                                      max_val), min_val))
          #   vals <- vals[(vals[,1] >= 0) & (vals[,1]<=1),]
          # }
          if(nrow(vals) > 0) {
            # Sort by first column
            ord_idx <- order(vals[,1])
            vals <- vals[ord_idx,]

            # Add boundary points if needed
            if(vals[1,1] > 0) {
              vals <- rbind(c(0,NA), vals)
            }
            if(vals[nrow(vals),1] < 1) {
              vals <- rbind(vals, c(1,NA))
            }

            # Only proceed with interpolation if there are any non-NA values
            if(any(!is.na(vals[,2]))) {
              max_val <- max(vals[,2], na.rm=TRUE)
              min_val <- min(vals[,2], na.rm=TRUE)

              # Perform interpolation
              interpolated <- try(
                zoo::na.approx(object = vals[,2],
                               x = vals[,1],
                               na.rm=FALSE, 
                               ties = "mean"),
                silent = TRUE
              )

              if(!inherits(interpolated, "try-error")) {
                vals[,2] <- na.const(pmax(pmin(interpolated, max_val), min_val))
              }
            }
            # Keep only values in [0,1] range
            vals <- vals[(vals[,1] >= 0) & (vals[,1] <= 1),]
          }
        }


      } else {
        X <- as.data.frame(X)
        vals <- X[X[, 1]==i, 2:3, drop = FALSE]

    if(continuous){
      if(nrow(vals)>0){

        if(!interpolate_plot){
            if (max(vals[, 1]) < 1) {
              # Check if we can add end value from another edge
              start_Ei <- which(self$E[, 1] == Ve)  # Edges that start at Ve
              end_Ei <- which(self$E[, 2] == Ve)    # Edges that end at Ve

              min.val <- NA
              max.val <- NA

              # Process edges starting at Ve
              if (length(start_Ei) > 0) {
                valid_X_start <- X[X[, 1] %in% start_Ei, , drop = FALSE]
                ind_start <- which(valid_X_start[, 2] == 0)
                if (length(ind_start) > 0) {
                  ind_min <- which.min(valid_X_start[ind_start, 3])
                  min.val <- valid_X_start[ind_start, 3][ind_min]
                }
              }

              # Process edges ending at Ve
              if (length(end_Ei) > 0) {
                valid_X_end <- X[X[, 1] %in% end_Ei, , drop = FALSE]
                ind_end <- which(valid_X_end[, 2] == 1)
                if (length(ind_end) > 0) {
                  ind_max <- which.max(valid_X_end[ind_end, 3])
                  max.val <- valid_X_end[ind_end, 3][ind_max]
                }
              }

              # Add the closest value to the current vertex
              if (!is.na(min.val) && !is.na(max.val)) {
                if (1 - max.val < min.val) {
                  vals <- rbind(vals, c(1, max.val))
                } else {
                  vals <- rbind(vals, c(1, min.val))
                }
              } else if (!is.na(min.val)) {
                vals <- rbind(vals, c(1, min.val))
              } else if (!is.na(max.val)) {
                vals <- rbind(vals, c(1, max.val))
              }
          } 

          if   (min(vals[, 1]) > 0) {
              # Check if we can add start value from another edge
              start_Ei <- which(self$E[, 1] == Vs)  # Edges that start at Vs
              end_Ei <- which(self$E[, 2] == Vs)    # Edges that end at Vs

              min.val <- NA
              max.val <- NA

              # Process edges starting at Vs
              if (length(start_Ei) > 0) {
                valid_X_start <- X[X[, 1] %in% start_Ei, , drop = FALSE]
                ind_start <- which(valid_X_start[, 2] == 0)
                if (length(ind_start) > 0) {
                  ind_min <- which.min(valid_X_start[ind_start, 3])
                  min.val <- valid_X_start[ind_start, 3][ind_min]
                }
              }

              # Process edges ending at Vs
              if (length(end_Ei) > 0) {
                valid_X_end <- X[X[, 1] %in% end_Ei, , drop = FALSE]
                ind_end <- which(valid_X_end[, 2] == 1)
                if (length(ind_end) > 0) {
                  ind_max <- which.max(valid_X_end[ind_end, 3])
                  max.val <- valid_X_end[ind_end, 3][ind_max]
                }
              }

              # Add the closest value to the current vertex
              if (!is.na(min.val) && !is.na(max.val)) {
                if (1 - max.val < min.val) {
                  vals <- rbind(c(0, max.val), vals)
                } else {
                  vals <- rbind(c(0, min.val), vals)
                }
              } else if (!is.na(min.val)) {
                vals <- rbind(c(0, min.val), vals)
              } else if (!is.na(max.val)) {
                vals <- rbind(c(0, max.val), vals)
              } else if (nrow(vals) > 0) {
                # Add a placeholder value based on the closest existing value in vals
                idx_tmp <- which.min(vals[, 1])
                vals <- rbind(c(0, vals[idx_tmp, 2]), vals)
              }
          } 
        } else {        
          PtE_tmp <- PtE_edges[[i]]

          # Check for edges ending at the starting point of the current edge
          if (any(self$E[, 2] == self$E[i, 1])) {
            edge_new <- which(self$E[, 2] == self$E[i, 1])[1]
            idx_new <- which(X[, 1] == edge_new)
            new_val <- X[idx_new, 2:3, drop = FALSE]
            if (nrow(new_val) > 0) {
              sub_fact <- ifelse(any(vals[, 1] == 0), 1 + 1e-6, max(new_val[, 1]))
              new_val[, 1] <- new_val[, 1] - sub_fact
              vals <- rbind(vals, new_val)
            }
          } else if (any(self$E[-i, 1] == self$E[i, 1])) {
            edge_new <- which(self$E[-i, 1] == self$E[i, 1])[1]
            idx_new <- which(X[, 1] == edge_new)
            new_val <- X[idx_new, 2:3, drop = FALSE]
            if (nrow(new_val) > 0) {
              sum_fact <- ifelse(any(vals[, 1] == 1), -1e-6, min(new_val[, 1]))
              new_val[, 1] <- -new_val[, 1] + sum_fact
              vals <- rbind(vals, new_val)
            }
          }

          # Check for edges starting at the ending point of the current edge
          if (any(self$E[, 1] == self$E[i, 2])) {
            edge_new <- which(self$E[, 1] == self$E[i, 2])[1]
            idx_new <- which(X[, 1] == edge_new)
            new_val <- X[idx_new, 2:3, drop = FALSE]
            if (nrow(new_val) > 0) {
              sum_fact <- ifelse(any(vals[, 1] == 1), 1 + 1e-6, 1 - min(new_val[, 1]))
              new_val[, 1] <- new_val[, 1] + sum_fact
              vals <- rbind(vals, new_val)
            }
          } else if (any(self$E[, 2] == self$E[i, 2])) {
            edge_new <- which(self$E[, 2] == self$E[i, 2])[1]
            idx_new <- which(X[, 1] == edge_new)
            new_val <- X[idx_new, 2:3, drop = FALSE]
            if (nrow(new_val) > 0) {
              sub_fact <- ifelse(any(vals[, 1] == 0), 1 + 1e-6, -max(new_val[, 1]) - 1)
              new_val[, 1] <- -new_val[, 1] - sub_fact
              vals <- rbind(vals, new_val)
            }
          }

          # Add remaining values from PtE_tmp not in vals
          PtE_tmp <- setdiff(PtE_tmp, vals[, 1])
          if (length(PtE_tmp) > 0) {
            PtE_tmp <- cbind(PtE_tmp, NA)
            PtE_tmp <- as.data.frame(PtE_tmp)
            colnames(PtE_tmp) <- c(".distance_on_edge", data)
            vals <- rbind(vals, PtE_tmp)
          }

          # Sort values by the first column
          ord_idx <- order(vals[, 1])
          vals <- vals[ord_idx, ]

          # Interpolate missing values within bounds
          # max_val <- max(vals[, 2], na.rm = TRUE)
          # min_val <- min(vals[, 2], na.rm = TRUE)
          # vals[, 2] <- na.const(
          #   pmax(
          #     pmin(
          #       zoo::na.approx(object = vals[, 2], x = vals[, 1], na.rm = FALSE, ties = "mean"),
          #       max_val
          #     ),
          #     min_val
          #   )
          # )

          # Only proceed if there are any non-NA values
          if(any(!is.na(vals[,2]))) {
            max_val <- max(vals[,2], na.rm = TRUE)
            min_val <- min(vals[,2], na.rm = TRUE)

            # Try interpolation with error handling
            interpolated <- try(
              zoo::na.approx(
                object = vals[,2], 
                x = vals[,1], 
                na.rm = FALSE, 
                ties = "mean"
              ),
              silent = TRUE
            )

            if(!inherits(interpolated, "try-error")) {
              vals[,2] <- na.const(
                pmax(
                  pmin(
                    interpolated,
                    max_val
                  ),
                  min_val
                )
              )
            }
          } # If all values are NA, vals[,2] remains unchanged


          # Filter values to lie within [0, 1]
          vals <- vals[(vals[, 1] >= 0) & (vals[, 1] <= 1), ]

          # Add a starting value if vals is non-empty and no value at 0
          if (nrow(vals) > 0 && !any(vals[, 1] == 0)) {
            vals <- rbind(c(0, vals[1, 2, drop = TRUE]), vals)
          }            
        }
      } else{
        if(interpolate_plot){
          vals <- NULL
              Ei <- self$E[, 1] == Ve #edges that start in Ve
              Ei <- which(Ei)
              if (sum(Ei) > 0) {
                ind <- which(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE] == 0)
                if(sum(ind)>0){
                  ind <- which.min(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                  min.val <- X[X[,1,drop=TRUE] %in% Ei, 3,drop=TRUE][ind]
                } else {
                ind <- NULL
                ind.val <- which.min(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                min.val <- X[X[,1,drop=TRUE] %in% Ei, 3,drop=TRUE][ind.val]
              }} else{
                ind <- NULL
                ind.val <- integer(0)
              }
              if (length(ind) > 0) {
                vals <- rbind(vals, c(1, min.val))
                  # if(length(min.val)>0){
                  #   vals <- rbind(vals, c(1, min.val[[1]]))
                  # } else{
                  #   vals <- rbind(vals, c(1, X[ind, 3,drop=TRUE]))
                  # }
                  vals <- rbind(vals, c(1, X[ind, 3,drop=TRUE]))
              }
              else {
                Ei <- self$E[, 2] == Ve #edges that end in Ve
                Ei <- which(Ei)
                if (sum(Ei)  > 0) {
                  ind <- which(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE] == 1)
                  if(sum(ind)>0){
                    ind <- which.max(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                    max.val <- X[X[,1,drop=TRUE] %in% Ei, 3,drop=TRUE][ind]
                  } else {
                  ind.val.max <- which.max(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                  max.val <- X[X[,1,drop=TRUE] %in% Ei, 3,drop=TRUE][ind.val.max]
                  if(length(ind.val) == 0){
                    ind <- ind.val.max
                  } else if (length(ind.val.max) == 0){
                    ind <- ind.val
                  } else{
                    ind <- ifelse(1-max.val < min.val, ind.val.max, ind.val)
                  }
                } } else{
                  if(length(ind.val)>0){
                    ind <- ind.val
                  } else{
                    ind <- NULL
                  }
                }
                if (length(ind) > 0){
                  # if(length(max.val)>0){
                  #   vals <- rbind(vals, c(1, max.val[[1]]))
                  # } else{
                  #   vals <- rbind(vals, c(1, X[ind, 3, drop=TRUE]))
                  # }
                  vals <- rbind(vals, c(1, X[ind, 3, drop=TRUE]))
                }
              }

              Ei <- self$E[, 1] == Vs #edges that start in Vs
              Ei <- which(Ei)
              if (sum(Ei) > 0) {
                  ind <- which(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE] == 0)
                if(sum(ind)>0){
                    ind <- ind[1]
                  } else {
                ind <- NULL
                ind.val <- which.min(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                min.val <- X[ind.val, 2,drop=TRUE]
              }} else{
                ind <- NULL
                ind.val <- integer(0)
              }
              if (length(ind) > 0) {
                vals <- rbind(c(0, X[ind, 3, drop=TRUE]), vals)
              } else {
                Ei <- self$E[, 2] == Vs #edges that end in Vs
                Ei <- which(Ei)
                if (sum(Ei) > 0) {
                  ind <- which(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE] == 1)
                  if(sum(ind)>0){
                    ind <- ind[1]
                  } else {
                  ind.val.max <- which.max(X[X[,1,drop=TRUE] %in% Ei, 2,drop=TRUE])
                  max.val <- X[ind.val.max, 2,drop=TRUE]
                  if(length(ind.val) == 0){
                    ind <- ind.val.max
                  } else if (length(ind.val.max) == 0){
                    ind <- ind.val
                  } else{
                    ind <- ifelse(1-max.val < min.val, ind.val.max, ind.val)
                  }
                } } else{
                  if(length(ind.val)>0){
                    ind <- ind.val
                  } else{
                    ind <- NULL
                  }
                }
                if (length(ind) > 0) {
                  vals <- rbind(c(0, X[ind, 3, drop=TRUE]), vals)
                } else{
                  vals <- rbind(c(0, vals[1, 2, drop=TRUE]), vals)
                }
              }
              if(ncol(vals)<2){
                vals <- NULL
              }
        }
      }
      } else if(interpolate_plot){

          PtE_tmp <- PtE_edges[[i]]
          PtE_tmp <- setdiff(PtE_tmp, vals[,1])
          if(length(PtE_tmp)>0){
                PtE_tmp <- cbind(PtE_tmp, NA)
                PtE_tmp <- as.data.frame(PtE_tmp)
                colnames(PtE_tmp) <- c(".distance_on_edge", data)
                vals <- rbind(vals,PtE_tmp)
          }
            # if(nrow(vals)>0){
            #   ord_idx <- order(vals[,1])
            #   vals <- vals[ord_idx,]
            #   if(vals[1,1] > 0){
            #     vals <- rbind(c(0,NA), vals)
            #   }
            #   if(vals[nrow(vals),1] < 1){
            #     vals <- rbind(vals, c(1,NA))
            #   }
            #   max_val <- max(vals[,2], na.rm=TRUE)
            #   min_val <- min(vals[,2], na.rm=TRUE)
            #   vals[,2] <- na.const(pmax(pmin(object = zoo::na.approx(object = vals[,2],
            #                                         x = vals[,1],
            #                                             na.rm=FALSE, ties = "mean"),
            #                                      max_val), min_val))
            #   vals <- vals[(vals[,1] >= 0) & (vals[,1]<=1),]
            # }
            
            if(nrow(vals) > 0) {
              # Sort by first column
              ord_idx <- order(vals[,1])
              vals <- vals[ord_idx,]

              # Add boundary points if needed
              if(vals[1,1] > 0) {
                vals <- rbind(c(0,NA), vals)
              }
              if(vals[nrow(vals),1] < 1) {
                vals <- rbind(vals, c(1,NA))
              }

              # Only proceed with interpolation if there are any non-NA values
              if(any(!is.na(vals[,2]))) {
                max_val <- max(vals[,2], na.rm=TRUE)
                min_val <- min(vals[,2], na.rm=TRUE)

                # Perform interpolation
                interpolated <- try(
                  zoo::na.approx(object = vals[,2],
                                 x = vals[,1],
                                 na.rm=FALSE, 
                                 ties = "mean"),
                  silent = TRUE
                )

                if(!inherits(interpolated, "try-error")) {
                  vals[,2] <- na.const(pmax(pmin(interpolated, max_val), min_val))
                }
              }
              # Keep only values in [0,1] range
              vals <- vals[(vals[,1] >= 0) & (vals[,1] <= 1),]
            }

      }
      }

        data.to.plot <- vals
        if(!is.null(data.to.plot)){
          data.to.plot.order <- data.to.plot[order(vals[, 1,drop=TRUE]), ,
                                           drop = FALSE]


          coords <- interpolate2(self$edges[[i]],
                                 pos = data.to.plot.order[, 1, drop = TRUE],
                                 normalized = TRUE)

          x.loc <- c(x.loc, coords[, 1])
          y.loc <- c(y.loc, coords[, 2])
          z.loc <- c(z.loc, data.to.plot.order[, 2, drop=TRUE])
          i.loc <- c(i.loc, rep(kk, length(coords[, 1])))
          kk = kk+1
        }



    }

    data <- data.frame(x = x.loc, y = y.loc, i = i.loc, z = z.loc)

    if(type == "plotly"){
      requireNamespace("plotly")
      if(is.null(p)){
        p <- self$plot(type = "plotly",
                       vertex_color = vertex_color,
                       vertex_size = vertex_size,
                       edge_width = edge_width,
                       edge_color = edge_color)
      }
      p <- plotly::add_trace(p, data = data, x = ~y, y = ~x, z = ~z,
                           mode = "lines", type = "scatter3d",
                           line = list(width = line_width,
                                       color = line_color),
                           split = ~i, showlegend = FALSE, ...)
      if(support_width > 0) {
        data.mesh <- data.frame(x = c(x.loc, x.loc), y = c(y.loc, y.loc),
                                z = c(rep(0, length(z.loc)), z.loc),
                                i = rep(1:length(z.loc), 2))
        p <- plotly::add_trace(p, data = data.mesh, x = ~y, y = ~x, z = ~z,
                             mode = "lines", type = "scatter3d",
                             line = list(width = support_width,
                                         color = support_color),
                             split = ~i, showlegend = FALSE)
      }

      p <-  plotly::layout(p, scene = list(xaxis = list(autorange = "reversed")))

      if(!is.null(private$vertex_unit)){
        if(private$vertex_unit == "degree" && !private$transform){
          p <- plotly::layout(p, scene = list(xaxis = list(title = "Longitude"), yaxis = list(title = "Latitude")))
        } else{
          p <- plotly::layout(p, scene = list(xaxis = list(title = paste0("x (in ",private$vertex_unit, ")")), yaxis = list(title = paste0("y (in ",private$vertex_unit, ")"))))
        }
      }

    } else if (type == "ggplot"){
      if(is.null(p)) {
          p <- ggplot(data = data) +
          geom_path( mapping = aes(x = x, y = y,
                                     group = i,
                                     colour = z), linewidth = line_width) + labs(colour = "") + scale_color # + scale_color_viridis() +
      } else {
        p <- p + geom_path(data = data, mapping =
                           aes(x = x, y = y,
                               group = i, colour = z),
                           linewidth = line_width) + labs(colour = "") + scale_color # + scale_color_viridis()
      }
          p <- self$plot(edge_width = 0, vertex_size = vertex_size,
                     vertex_color = vertex_color, p = p)
      if(!is.null(private$vertex_unit)){
        if(private$vertex_unit == "degree" && !private$transform){
          p <- p + labs(x = "Longitude",  y = "Latitude")
        } else{
          p <- p + labs(x = paste0("x (in ",private$vertex_unit, ")"),  y = paste0("y (in ",private$vertex_unit, ")"))
        }
      }
    } else if(type == "mapview"){
    requireNamespace("mapview")
    data <- na.omit(data[, c("x", "y", "z", "i")])
    data_split <- split(data, data$i)
    
    linestrings <- do.call(c, lapply(data_split, function(group_data) {
      if (nrow(group_data) < 2) {
          return(NULL)  
      }
      lapply(1:(nrow(group_data) - 1), function(j) {
        sf::st_linestring(as.matrix(group_data[j:(j + 1), c("x", "y")]))
      })
    }))
    
    data_segment <- do.call(rbind, lapply(data_split, function(group_data) {
      data.frame(
        z = group_data$z[-nrow(group_data)], 
        i = group_data$i[-nrow(group_data)]  
      )
    }))
    
    data_sf <- sf::st_as_sf(
      data_segment,
      geometry = sf::st_sfc(linestrings),
      crs = if (!is.null(private$crs)) private$crs else sf::NA_crs_
    )

    data_sf <- sf::st_as_sf(
      data_segment, 
      geometry = sf::st_sfc(linestrings),
      crs = if (!is.null(private$crs)) private$crs else sf::NA_crs_
    )

    edges_sf <- self$get_edges(format = "sf")

    if(is.null(p)){
      mapview_output <- mapview::mapview(
        x = edges_sf,
        lwd = edge_width,  
        color = edge_color, 
        layer.name = "Edges",
        ...
      )
    } else{
      mapview_output <- mapview::mapview(
        x = edges_sf,
        lwd = 1.75 * edge_width,  
        color = edge_color, 
        layer.name = "Edges",
        ...
      )
    }
    edge_width <- 2.5 * edge_width
    mapview_output <- mapview_output + mapview::mapview(
        x = data_sf,
        zcol = "z",
        color = scale_color_mapview, 
        lwd = edge_width, 
        cex = vertex_size, 
        col.regions = scale_color_mapview, 
        layer.name = mapview_caption
      )


    return(mapview_output)
    }
    return(p)
  },

  #' @description Plots a movie of a continuous function evolving on the graph.
  #' @param X A m x T matrix where the ith column represents the function at the
  #' ith time, evaluated at the mesh locations.
  #' @param type Type of plot. Either `"plotly"` or `"ggplot"`.
  #' @param vertex_size Size of the vertices.
  #' @param vertex_color Color of vertices.
  #' @param edge_width Width for edges.
  #' @param edge_color For 3D plot, color of edges.
  #' @param line_width For 3D plot, line width of the function curve.
  #' @param line_color Color of the function curve.
  #' @param ... Additional arguments for ggplot or plot_ly.
  #' @return Either a `ggplot` (if `plotly=FALSE`) or a `plot_ly` object.
  plot_movie = function(X,
                        type = "plotly",
                        vertex_size = 5,
                        vertex_color = "black",
                        edge_width = 1,
                        edge_color = 'black',
                        line_width = NULL,
                        line_color = 'rgb(0,0,200)',
                           ...){
    if (is.null(line_width)) {
      line_width = edge_width
    }

    if (type == "plotly") {
      requireNamespace("plotly")
      plotly <- TRUE
    } else if (type == "ggplot") {
      requireNamespace("ggplot2")
      plotly <- FALSE
    }

    if (is.null(self$mesh)) {
      stop("X is a vector but no mesh provided")
    }

    if (is.null(self$mesh$PtE)) {
      PtE_dim <- 0
    } else {
      PtE_dim <- dim(self$mesh$PtE)[1]
    }

    if (length(X) == PtE_dim) {
      X <- c(rep(NA, dim(self$V)[1]), X)
    }

    if (dim(X)[1] != dim(unique(self$mesh$V))[1]) {
      stop(paste0("X does not have the correct size (the possible sizes are ", PtE_dim, " and ", dim(unique(self$mesh$V))[1],")"))
    }

    n.v <- dim(self$V)[1]
    XV <- X[1:n.v,]

    x.loc <- y.loc <- z.loc <- i.loc <- f.loc <-  NULL
    kk = 1
    frames <- dim(X)[2]
    for (i in 1:self$nE) {
      Vs <- self$E[i, 1]
      Ve <- self$E[i, 2]

      ind <- self$mesh$PtE[, 1] == i

      if (sum(ind)==0) {
        vals <- rbind(c(0, XV[Vs,]),
                      c(1, XV[Ve,]))

      } else {
        vals <- rbind(c(0, XV[Vs,]),
                      cbind(self$mesh$PtE[ind, 2], X[n.v + which(ind),]),
                      c(1, XV[Ve,]))

      }

        data.to.plot <- vals
        data.to.plot.order <- data.to.plot[order(data.to.plot[, 1]), ,
                                           drop = FALSE]


        coords <- interpolate2(self$edges[[i]],
                               pos = data.to.plot.order[, 1, drop = TRUE],
                               normalized = TRUE)
        x.loc <- c(x.loc, rep(coords[, 1], frames))
        y.loc <- c(y.loc, rep(coords[, 2], frames))
        z.loc <- c(z.loc, c(data.to.plot.order[, 2:(frames+1)]))
        i.loc <- c(i.loc, rep(rep(kk, length(coords[, 1])), frames))
        f.loc <- c(f.loc, rep(1:frames, each = length(coords[, 1])))
        kk = kk+1
    }
    data <- data.frame(x = x.loc, y = y.loc, z = z.loc, i = i.loc, f = f.loc)

    if(plotly){
      requireNamespace("plotly")
      x <- y <- ei <- NULL
      for (i in 1:self$nE) {
        xi <- self$edges[[i]][, 1]
        yi <- self$edges[[i]][, 2]
        ii <- rep(i,length(xi))
        x <- c(x, xi)
        y <- c(y, yi)
        ei <- c(ei, ii)
      }
      frames <- dim(X)[2]
      data.graph <- data.frame(x = rep(x, frames),
                               y = rep(y, frames),
                               z = rep(rep(0,length(x)), frames),
                               i = rep(ei, frames),
                               f = rep(1:frames, each = length(x)))

      p <- plotly::plot_ly(data=data.graph, x = ~y, y = ~x, z = ~z,frame = ~f)
      p <- plotly::add_trace(p, data = data.graph, x = ~y, y = ~x, z = ~z,
                             frame = ~f,
                             mode = "lines", type = "scatter3d",
                             line = list(width = line_width,
                                         color = edge_color),
                             split = ~i, showlegend = FALSE)

      p <- plotly::add_trace(p, data = data, x = ~y, y = ~x, z = ~z,
                             frame = ~f,
                             mode = "lines", type = "scatter3d",
                             line = list(width = line_width,
                                         color = line_color),
                             split = ~i, showlegend = FALSE, ...)

      if(!is.null(private$vertex_unit)){
        if(private$vertex_unit == "degree" && !private$transform){
          p <- plotly::layout(p, scene = list(xaxis = list(title = "Longitude"), yaxis = list(title = "Latitude")))
        } else{
          p <- plotly::layout(p, scene = list(xaxis = list(title = paste0("x (in ",private$vertex_unit, ")")), yaxis = list(title = paste0("y (in ",private$vertex_unit, ")"))))
        }
      }

    } else {
      stop("not implemented")
    }
    return(p)
  },

  #' @description Add observations on mesh to the object.
  #' @param data A `data.frame` or named list containing the observations.
  #' In case of groups, the data.frames for the groups should be stacked vertically,
  #' with a column indicating the index of the group. If `data_frame` is not
  #' `NULL`, it takes priority over any eventual data in `Spoints`.
  #' @param group If the data_frame contains groups, one must provide the column
  #' in which the group indices are stored.
  #' @return No return value. Called for its side effects. The observations are
  #' stored in the `data` element in the `metric_graph` object.
  add_mesh_observations = function(data = NULL, group = NULL) {

    if(is.null(self$mesh)){
      stop("You should have a mesh!")
    }
    PtE_mesh <- self$mesh$PtE
    data[[".edge_number"]] <- PtE_mesh[,1]
    data[[".distance_on_edge"]] <- PtE_mesh[,2]
    self$add_observations(data = data, group = group,
                          edge_number = ".edge_number",
                          distance_on_edge = ".distance_on_edge",
                          normalized = TRUE)
  },

  #' @description Returns a copy of the initial metric graph.
  #' @return A `metric_graph` object.
  get_initial_graph = function() {
    tmp_graph <- private$initial_graph$clone()
    if(private$pruned){
      tmp_graph$prune_vertices()
   }
    return(tmp_graph)
  },

  #' @description Convert between locations on the graph and Euclidean
  #' coordinates.
  #' @param PtE Matrix with locations on the graph (edge number and normalized
  #' position on the edge).
  #' @param XY Matrix with locations in Euclidean space
  #' @param normalized If `TRUE`, it is assumed that the positions in `PtE` are
  #' normalized to (0,1), and the object returned if `XY` is specified contains
  #' normalized locations.
  #' @return If `PtE` is specified, then a matrix with Euclidean coordinates of
  #' the locations is returned. If `XY` is provided, then a matrix with the
  #' closest locations on the graph is returned.
  coordinates = function(PtE = NULL, XY = NULL, normalized = TRUE) {
    if(is.null(PtE) && is.null(XY)) {
      stop("PtE or XY must be provided")
    } else if(!is.null(PtE) && !is.null(XY)) {
      stop("Either PtE or XY must be provided, not both")
    }
    x <- y <- NULL
    if (!is.null(PtE)) {
      if(is.vector(PtE)) {
        if(length(PtE) != 2)
          stop("PtE is a vector but does not have length 2")

        PtE <- matrix(PtE,1,2)
      }
      if(ncol(PtE)!= 2){
        stop("PtE must have two columns!")
      }

      if (min(PtE[,2]) < 0) {
        stop("PtE[, 2] has negative values")
      }
      if ((max(PtE[,2]) > 1) && normalized) {
        stop("For normalized distances, the values in PtE[, 2] should not be
             larger than 1")
      }
      if(max(PtE[,2] - self$edge_lengths[PtE[, 1]]) > 0 && !normalized) {
        stop("PtE[, 2] contains values which are larger than the edge lengths")
      }

      if(!normalized) {
        PtE <- cbind(PtE[, 1], PtE[, 2] / self$edge_lengths[PtE[, 1]])
      }

      Points <- matrix(NA, nrow=nrow(PtE), ncol=ncol(PtE))


      for (i in 1:dim(PtE)[1]) {
        Points[i,] <- interpolate2(self$edges[[PtE[i, 1]]] ,
                                   pos = PtE[i, 2], normalized = TRUE)
      }
      return(Points)
    } else {
      SP <- snapPointsToLines(XY, self$edges, longlat = private$longlat, crs = private$crs)
      # coords.old <- XY
      # colnames(coords.old) <- paste(colnames(coords.old), '_old', sep="")
      XY = t(SP[["coords"]])
      PtE <- cbind(match(SP[["df"]][["nearest_line_index"]], 1:length(self$edges)), 0)

      for (ind in unique(PtE[, 1])) {
        index.p <- PtE[, 1] == ind
        PtE[index.p,2]=projectVecLine2(self$edges[[ind]], XY[index.p, , drop=FALSE],
                                       normalized=TRUE)

      }

      if (!normalized) {
        PtE <- cbind(PtE[, 1], PtE[, 2] * self$edge_lengths[PtE[, 1]])
      }
      return(PtE)
    }
  }
  ),

  private = list(
  #function for creating Vertex and Edges from self$edges
  line_to_vertex = function(tolerance = 0, longlat = FALSE, fact, verbose, crs, proj4string, which_longlat, length_unit, vertex_unit, project, which_projection, project_data) {



    if(project_data && longlat){
     if(verbose == 2) {
      message("Projecting edges")
      bar_edges_proj <- msg_progress_bar(length(self$edges))
    }
      private$vertex_unit <- length_unit
      if(which_longlat == "sf"){
        if(which_projection == "Robinson"){
          str_proj <- "+proj=robin +datum=WGS84 +no_defs +over"
        } else if (which_projection == "Winkel tripel") {
          str_proj <- "+proj=wintri +datum=WGS84 +no_defs +over"
        } else{
          str_proj <- which_projection
        }
        fact <- process_factor_unit("m", length_unit)
        for(i in 1:length(self$edges)){
          if(verbose == 2) {
            bar_edges_proj$increment()
          }
          sf_points <- sf::st_as_sf(as.data.frame(self$edges[[i]]), coords = 1:2, crs = crs)
          sf_points_eucl <- sf::st_transform(sf_points, crs=sf::st_crs(str_proj))
          self$edges[[i]] <- sf::st_coordinates(sf_points_eucl) * fact
        }
      } else{
        if(which_projection == "Robinson"){
          str_proj <- "+proj=robin +datum=WGS84 +no_defs +over"
        } else if (which_projection == "Winkel tripel") {
          str_proj <- "+proj=wintri +datum=WGS84 +no_defs +over"
        } else{
          str_proj <- which_projection
        }
        fact <- process_factor_unit("km", length_unit)
        for(i in 1:length(self$edges)){
          if(verbose == 2) {
            bar_edges_proj$increment()
          }
          sp_points <- sp::SpatialPoints(coords = self$edges[[i]], proj4string = proj4string)
          sp_points_eucl <- sp::spTransform(sp_points,CRSobj=sp::CRS(str_proj))
          self$edges[[i]] <- sp::coordinates(sp_points_eucl) * fact
        }
      }
      private$longlat <- FALSE
      private$crs <- NULL
      private$proj4string <- NULL
    }

    if(verbose == 2) {
      message("Part 1/2")
      bar_line_vertex <- msg_progress_bar(length(self$edges))
    }


    lines <- matrix(nrow = 2*length(self$edges), ncol = 3)
    for(i in 1:length(self$edges)){
      if(verbose == 2) {
        bar_line_vertex$increment()
      }
      points <- self$edges[[i]]
      n <- dim(points)[1]
      lines[2*i-1,] <- c(i, points[1,])
      lines[2*i, ] <- c(i, points[n,])
    }

    #save all vertices that are more than tolerance distance apart
    if(verbose == 2) {
      message("Computing auxiliary distances")
    }

    if(!project_data && longlat){
    if(!project || !longlat){
        fact <- process_factor_unit(vertex_unit, length_unit)
          dists <- compute_aux_distances(lines = lines[,2:3,drop=FALSE], crs = crs, longlat = longlat, proj4string = proj4string, fact = fact, which_longlat = which_longlat, length_unit = private$length_unit, transform = private$transform)
    } else if (which_longlat == "sf"){
        if(which_projection == "Robinson"){
          str_proj <- "+proj=robin +datum=WGS84 +no_defs +over"
        } else if (which_projection == "Winkel tripel") {
          str_proj <- "+proj=wintri +datum=WGS84 +no_defs +over"
        } else{
          str_proj <- which_projection
        }
        sf_points <- sf::st_as_sf(as.data.frame(lines), coords = 2:3, crs = crs)
        sf_points_eucl <- sf::st_transform(sf_points, crs=sf::st_crs(str_proj))
        fact <- process_factor_unit("m", length_unit)
        dists <- dist(sf::st_coordinates(sf_points_eucl)) * fact
    } else{
        if(which_projection == "Robinson"){
          str_proj <- "+proj=robin +datum=WGS84 +no_defs +over"
        } else if (which_projection == "Winkel tripel") {
          str_proj <- "+proj=wintri +datum=WGS84 +no_defs +over"
        } else{
          str_proj <- which_projection
        }
        sp_points <- sp::SpatialPoints(coords = lines[,2:3], proj4string = proj4string)
        sp_points_eucl <- sp::spTransform(sp_points,CRSobj=sp::CRS(str_proj))
        fact <- process_factor_unit("m", length_unit)
        dists <- dist(sp_points_eucl@coords) * fact
    }
    } else{
      fact <- process_factor_unit(vertex_unit, length_unit)
      dists <- dist(lines[,2:3,drop=FALSE]) * fact
    }


    if(verbose == 2) {
      message("Done!")
    }


    if(!inherits(dists,"dist")){
        idx_keep <- sapply(1:nrow(lines), function(i){ifelse(i==1,TRUE,all(dists[i, 1:(i-1)] > tolerance))})
      vertex <- lines[idx_keep,, drop=FALSE]
    } else{
        idx_keep <- sapply(1:nrow(lines), function(i){ifelse(i==1,TRUE,all(dists[ nrow(lines)*(1:(i-1)-1) - (1:(i-1))*(1:(i-1) -1)/2 + i -1:(i-1)] > tolerance))})
      vertex <- lines[idx_keep,, drop=FALSE]
    }
      # if(inherits(dists,"dist")) dists <- dist2mat(dists,256)
      # idx_keep <- sapply(1:nrow(lines), function(i){ifelse(i==1,TRUE,all(dists[i, 1:(i-1)] > tolerance))})
      # vertex <- lines[idx_keep,]

    if(verbose == 2) {
      message("Part 2/2")
      bar_line_vertex <- msg_progress_bar(max(lines[, 1]))
    }

    lvl <- matrix(0, nrow = max(lines[,1]), 4)
    k=1
    lines_keep_id <- NULL

    for (i in 1:max(lines[, 1])) {

      if(verbose == 2) {
        bar_line_vertex$increment()
      }

      which.line <- sort(which(lines[, 1] == i))
      line <- lines[which.line, , drop=FALSE]

      #index of vertex corresponding to the start of the line
      ind1 <- which.min((vertex[, 2] - line[1, 2])^2 +
                          (vertex[, 3] - line[1, 3])^2)
      #index of vertex corresponding to the end of the line
      ind2 <- which.min((vertex[, 2] - line[2, 2])^2 +
                          (vertex[, 3] - line[2, 3])^2)
      if(length(ind1)>0){
        self$edges[[i]][1,] <- vertex[ind1, 2:3, drop=FALSE]
        i.e <- dim(self$edges[[i]])[1]
      } else{
        i.e <- 1
      }
      if(length(ind2)>0){
        self$edges[[i]][i.e,] <- vertex[ind2, 2:3, drop=FALSE]
      }
      ll <- compute_line_lengths(self$edges[[i]], longlat = longlat, unit = length_unit, crs = crs, proj4string, which_longlat, vertex_unit, project_data, private$transform)
      if(ll > tolerance) {
        lvl[k,] <- c(i, ind1, ind2, ll)
        k=k+1
        lines_keep_id <- c(lines_keep_id, i)
      }
    }

    lvl <- lvl[1:(k-1),,drop = FALSE]
    self$edges <- self$edges[lines_keep_id]

    if(is.vector(private$edge_weights)){
        private$edge_weights <- private$edge_weights[lines_keep_id]
    } else{
        private$edge_weights <- private$edge_weights[lines_keep_id,,drop=FALSE]
    }

    self$V <- vertex[, 2:3, drop = FALSE]
    self$E <- lvl[, 2:3, drop = FALSE]
    self$edge_lengths <- lvl[,4]
    # units(self$edge_lengths) <- length_unit
    self$nV <- dim(self$V)[1]
    self$nE <- dim(self$E)[1]
  },

  #Compute PtE for mesh given PtE for graph
  PtE_to_mesh = function(PtE){
    Vertexes <- self$VtEfirst()
    VtE <- rbind(Vertexes, self$mesh$PtE)
    PtE_update <- matrix(0, dim(PtE)[1], 2)
    for (i in 1:dim(PtE)[1]) {
      ei <- PtE[i, 1] #extract edge

      #extract distances of all mesh nodes on edge including end points
      ind.nodes <- which(self$mesh$PtE[,1] == ei)
      dist.nodes <- self$mesh$PtE[ind.nodes,2]
      if(length(ind.nodes)>0) {
        ind <- c(self$E[ei,1], ind.nodes + self$nV, self$E[ei,2])
        dists <- c(0,dist.nodes,1)
      } else {
        ind <- c(self$E[ei,1], self$E[ei,2])
        dists <- c(0,1)
      }
      
      ind2 <- sort(sort(abs(dists - PtE[i, 2]), index.return = TRUE)$ix[1:2])
      v1 <- ind[ind2[1]] #vertex "before" the point
      v2 <- ind[ind2[2]] #vertex "after" the point
      d1 <- dists[ind2[1]] #distance on the edge of the point before
      d2 <- dists[ind2[2]] #distance on the edge of the point after

      #find the "edge" in the mesh on which the point is
      if(v1 != v2){
        # if they are different, only consider edges with different vertices as endpoints
        valid_ind <- (rowSums(self$mesh$E == v1) > 0) & (rowSums(self$mesh$E == v2) > 0)
        e <- which(valid_ind & (rowSums(self$mesh$E == v1) + rowSums(self$mesh$E == v2) == 2))
      } else{
        e <- which(rowSums((self$mesh$E == v1)) == 2)
      }
      # Handle the case of multiple edges
      # In the case the edge lengths are different, we can identify
      # In the case they are equal, we cannot identify, but it does not matter, as there is no difference then.
      e_bkp <- e
      if(length(e)>1){
       ind <- which(self$edge_lengths[ei] == self$mesh$h_e[e])
       e <- e[ind]
       e <- e[1]
      }
      # a loop might have been split into smaller parts, in this case, we simply get the first
      if(self$edge_lengths[ei] == sum(self$mesh$h_e[e_bkp])){
        e <- e_bkp[which(self$mesh$E[e_bkp,1] == v1)]
        e <- e[1]
      }
      for(ei in e){
        if (self$mesh$E[ei, 1] == v1) { #edge starts in the vertex before
          d <- (PtE[i, 2] - d1)/(d2 - d1)
        } else {
          d <- 1- (PtE[i, 2] - d1)/(d2 - d1)
        }
          PtE_update[i, ] <- c(e, d)
        }
    } 
    return(PtE_update)
  },

  ## Adding column_y argument which tells which column of y to get

  plot_2d = function(line_width = 0.1,
                     marker_size = 1,
                     vertex_color = 'black',
                     edge_color = 'black',
                     data,
                     newdata,
                     data_size = 1,
                     group = 1,
                     mesh = FALSE,
                     X = NULL,
                     X_loc = NULL,
                     p = NULL,
                     degree = FALSE,
                     direction = FALSE,
                     edge_weight = NULL,
                     edge_width_weight = NULL,
                     scale_color_main = ggplot2::scale_color_viridis_c(option = "D"),
                     scale_color_weights = ggplot2::scale_color_viridis_c(option = "A"),
                     scale_color_degree = ggplot2::scale_color_viridis_d(option = "D"),
                     add_new_scale_weights = TRUE,
                     arrow_size,
                     scale_color_main_discrete,
                     scale_color_weights_discrete,
                     ...){
    xyl <- c()

    nc <- do.call(rbind,lapply(self$edges, function(x) dim(x)[1]))
    xyl <- cbind(do.call(rbind,self$edges), rep(1:length(nc), times = nc))
    df_plot <- data.frame(x = xyl[, 1], y = xyl[, 2], grp = xyl[, 3])

    if(!is.null(edge_weight)){
      edge_weight <- edge_weight[[1]]
      e_weights <- private$get_edge_weights_internal(data.frame = TRUE)
      e_weights <- e_weights[,edge_weight, drop = FALSE]
      colnames(e_weights) <- "weights"
      e_weights["grp"] <- 1:self$nE
      df_plot <- merge(df_plot, e_weights)
    } else{
      df_plot[["weights"]] <- rep(edge_color, nrow(df_plot))
    }
    if(!is.null(edge_width_weight)){
      edge_width_weight <- edge_width_weight[[1]]
      e_weights <- private$get_edge_weights_internal(data.frame = TRUE)
      e_weights <- e_weights[,edge_width_weight, drop = FALSE]
      e_weights[,1] <- e_weights[,1] * line_width / max(e_weights[,1])
      e_weights[,1] <- e_weights[,1]
      colnames(e_weights) <- "widths"
      e_weights["grp"] <- 1:self$nE
      df_plot <- merge(df_plot, e_weights)
    } else{
      df_plot[["widths"]] <- rep(line_width, nrow(df_plot))
    }

    if(is.null(p)){
      if(!is.null(edge_weight)){
        if(is.factor(df_plot[["weights"]]) || is.character(df_plot[["weights"]])){
          scale_weights <- scale_color_weights_discrete
        } else{
          scale_weights <- scale_color_weights
        }
        p <- ggplot() + geom_path(data = df_plot,
                                  mapping = aes(x = x, y = y, group = grp,
                                  colour = weights, linewidth = widths),
                                  ...) + ggplot2::scale_linewidth_identity() + scale_weights + labs(colour = edge_weight)
          if(add_new_scale_weights){
            p <- p + new_scale_color()
          }
      } else{
        p <- ggplot() + geom_path(data = df_plot,
                                  mapping = aes(x = x, y = y, group = grp, linewidth = widths),
                                  color = edge_color,
                                  # linewidth = line_width,
                                  ...) + ggplot2::scale_linewidth_identity()
      }
    } else {
      if(!is.null(edge_weight)){
        if(is.factor(df_plot[["weights"]]) || is.character(df_plot[["weights"]])){
          scale_weights <- scale_color_weights_discrete
        } else{
          scale_weights <- scale_color_weights
        }        
        p <- p + geom_path(data = df_plot,
                           mapping = aes(x = x, y = y, group = grp, colour = weights, linewidth =widths),
                           ...) + ggplot2::scale_linewidth_identity() + scale_weights + labs(colour = edge_weight)
          if(add_new_scale_weights){
            p <- p + new_scale_color()
          }
      } else{
        p <- p + geom_path(data = df_plot,
                           mapping = aes(x = x, y = y, group = grp,  linewidth = widths), color = edge_color, ...) + ggplot2::scale_linewidth_identity()
      }
    }

    if(direction) {
      mid.l <- self$coordinates(PtE = cbind(1:self$nE, rep(0.49,self$nE)))
      mid.u <- self$coordinates(PtE = cbind(1:self$nE, rep(0.5,self$nE)))
      p <- p + geom_path(data = data.frame(x = c(mid.l[,1], mid.u[,1]),
                                           y = c(mid.l[, 2], mid.u[,2]),
                                           edge = c(1:self$nE,1:self$nE)),
                          mapping = aes(x = x, y = y, group = edge),
                         arrow = ggplot2::arrow(length=arrow_size),
                          linewidth = marker_size/2, ...)
    }

    if (marker_size > 0) {
      if(degree) {
        x <- self$V[,1]
        y <- self$V[,2]
        degrees <- self$get_degrees()
        p <- p + geom_point(data = data.frame(x = self$V[, 1],
                                              y = self$V[, 2],
                                              degree = degrees),
                            mapping = aes(x, y, colour = factor(degree)),
                            size= marker_size, ...) + scale_color_degree # +
    # scale_color_viridis(discrete = TRUE, guide_legend(title = ""))
      } else if (direction) {
        degrees <- self$get_degrees()
        start.deg <- end.deg <- rep(0,self$nV)
        for(i in 1:self$nV) {
          start.deg[i] <- sum(self$E[,1]==i)
          end.deg[i] <- sum(self$E[,2]==i)
        }
        problematic <- (degrees > 1) & (start.deg == 0 | end.deg == 0)
        p <- p + geom_point(data = data.frame(x = self$V[problematic, 1],
                                              y = self$V[problematic, 2]),
                            mapping = aes(x, y),
                            colour = "red",
                            size= marker_size, ...)
        p <- p + geom_point(data = data.frame(x = self$V[!problematic, 1],
                                              y = self$V[!problematic, 2]),
                            mapping = aes(x, y),
                            colour = "green",
                            size= marker_size, ...)

      } else {
        p <- p + geom_point(data = data.frame(x = self$V[, 1],
                                              y = self$V[, 2]),
                            mapping = aes(x, y),
                            colour = vertex_color,
                            size= marker_size, ...)
      }
    }
    if (!is.null(data)) {
      x <- y <- NULL

      if(!is.null(newdata)){
        data_group <- select_group(newdata, group)
      } else{
        data_group <- select_group(private$data, group)
      }

      if(!(data%in%names(data_group))){
        stop(paste(data,"is not an existing column name of the dataset."))
      }

      y_plot <-data_group[[data]]

      x <- data_group[[".coord_x"]]
      y <- data_group[[".coord_y"]]

      if(is.factor(as.vector(y_plot[!is.na(as.vector(y_plot))])) || is.character(as.vector(y_plot[!is.na(as.vector(y_plot))]))){
        scale_main <- scale_color_main_discrete
      } else{
        scale_main <- scale_color_main
      }

      p <- p + geom_point(data = data.frame(x = x[!is.na(as.vector(y_plot))],
                                            y = y[!is.na(as.vector(y_plot))],
                                            val = as.vector(y_plot[!is.na(as.vector(y_plot))])),
                          mapping = aes(x, y, color = val),
                          size = data_size, ...) + labs(color = data) + scale_main #+
        # scale_colour_gradientn(colours = viridis(100), guide_legend(title = ""))

    }
    if (mesh) {
      p <- p + geom_point(data=data.frame(x = self$mesh$V[, 1],
                                          y = self$mesh$V[, 2]),
                          mapping = aes(x, y), size = marker_size * 0.5,
                          pch = 21,
                          colour = "black",
                          fill = "gray")
    }
    if(!is.null(X)){
      if(is.null(X_loc)){
        stop("X supplied but not X_loc")
      }
      x <- y <- NULL
      if(length(X) != nrow(X_loc)){
        stop("The number of observations does not match the number of observations!")
      }
      points_xy <- self$coordinates(PtE = X_loc)
      x <- points_xy[,1]
      y <- points_xy[,2]
      if(is.factor(as.vector(X)) || is.character(as.vector(X))){
        scale_main <- scale_color_main_discrete
      } else{
        scale_main <- scale_color_main
      }
      p <- p + geom_point(data = data.frame(x = x, y = y,
                                            val = as.vector(X)),
                          mapping = aes(x, y, color = val),
                          size = data_size) + labs(colour = "") + scale_main #+
        # scale_color_viridis()
    }
    p <- p + coord_fixed()
    return(p)
  },

  ## Adding column_y argument which tells which column of y to get

  plot_3d = function(line_width = 1,
                     marker_size = 1,
                     vertex_color = 'rgb(0,0,0)',
                     edge_color = 'rgb(0,0,0)',
                     data,
                     newdata,
                     data_size = 1,
                     group = 1,
                     mesh = FALSE,
                     p = NULL,
                     support_width = 0.5,
                    support_color = "gray",
                    edge_width_weight = NULL,
                     ...){
      x <- y <- ei <- NULL
      for (i in 1:self$nE) {
        xi <- self$edges[[i]][, 1]
        yi <- self$edges[[i]][, 2]
        ii <- rep(i,length(xi))
        x <- c(x, xi)
        y <- c(y, yi)
        ei <- c(ei, ii)
      }
      data.plot <- data.frame(x = x, y = y, z = rep(0,length(x)), i = ei)

    if(!is.null(edge_width_weight)){
        edge_width_weight <- edge_width_weight[[1]]
        e_weights <- private$get_edge_weights_internal(data.frame = TRUE)
        e_weights <- e_weights[,edge_width_weight, drop = FALSE]
        e_weights[,1] <- e_weights[,1] * line_width / max(e_weights[,1])
        e_weights[,1] <- e_weights[,1]
        colnames(e_weights) <- "widths"
        e_weights["i"] <- 1:self$nE
        data.plot <- merge(data.plot, e_weights)
    } else{
      data.plot[["widths"]] <- rep(line_width, nrow(data.plot))
    }

    if(is.null(p)) {
      p <- plotly::plot_ly(data=data.plot, x = ~y, y = ~x, z = ~z,...)
      p <- plotly::add_trace(p, data = data.plot, x = ~y, y = ~x, z = ~z,
                           mode = "lines", type = "scatter3d",
                           line = list(width = ~widths,
                                       color = edge_color),
                           split = ~i, showlegend = FALSE)
    } else {
      p <- plotly::add_trace(p, data = data.plot, x = ~y, y = ~x, z = ~z,
                           mode = "lines", type = "scatter3d",
                           line = list(width = ~widths,
                                       color = edge_color),
                           split = ~i, showlegend = FALSE)
    }


    if(marker_size > 0) {
      data.plot2 <- data.frame(x = self$V[, 1], y = self$V[, 2],
                               z = rep(0, self$nV))
      p <- plotly::add_trace(p, data = data.plot2, x = ~y, y = ~x, z = ~z,
                           type = "scatter3d", mode = "markers",
                           marker = list(size = marker_size,
                                         color = vertex_color))
    }

    if (!is.null(data)) {
      x <- y <- NULL
      if(!is.null(newdata)){
        data_group <- select_group(newdata, group)
      } else{
        data_group <- select_group(private$data, group)
      }
      y_plot <- data_group[[data]]

      x <- data_group[[".coord_x"]]
      y <- data_group[[".coord_y"]]

      data.plot <- data.frame(x = x[!is.na(as.vector(y_plot))],
                              y = y[!is.na(as.vector(y_plot))],
                              z = rep(0,length(x[!is.na(as.vector(y_plot))])),
                              val = as.vector(y_plot[!is.na(as.vector(y_plot))]),
                              i =rep(1:length(y_plot), 2))

      p <- plotly::add_trace(p, data = data.plot, x = ~y, y = ~x, z = ~val,
                           type = "scatter3d", mode = "markers",
                           marker = list(size = marker_size,
                                         color = ~val,
                                         colorbar=list(title='', len = 0.5),
                                         colorscale='Viridis'),
                           showlegend=FALSE)
      if(support_width > 0) {
          data.support <- data.frame(x = c(data.plot$x, data.plot$x), y = c(data.plot$y, data.plot$y),
                                z = c(rep(0, length(data.plot$z)), data.plot$val),
                                i = rep(1:length(data.plot$val), 2))
        p <- plotly::add_trace(p, data = data.support, x = ~y, y = ~x, z = ~z,
                             mode = "lines", type = "scatter3d",
                             line = list(width = support_width,
                                         color = support_color),
                             split = ~i, showlegend = FALSE)
      }
    }
    if (mesh) {
      data.plot <- data.frame(x = self$mesh$V[, 1],
                              y = self$mesh$V[, 2],
                              z = rep(0, dim(self$mesh$V)[1]))
      p <- plotly::add_trace(p, data = data.plot, x = ~y, y = ~x, z = ~z,
                           type = "scatter3d", mode = "markers",
                           marker = list(size = marker_size/2,
                                         color = 'rgb(100,100,100)'),
                           showlegend = FALSE)
    }
    return(p)
  },

  ## Coordinates function to return all the lines intersecting within a tolerance

    coordinates_multiple_snaps = function(XY, tolerance, verbose = verbose, crs, proj4string, longlat, fact, which_longlat) {

      coords_line <- c()
      coords_tmp <- c()

      if(!is.null(XY)){
        class(XY) <- setdiff(class(XY), "metric_graph_edge")
      }


      if(!private$longlat){
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}))
          points_sf <- sf::st_as_sf(as.data.frame(XY), coords = 1:2)
      crs <- NULL
    } else if (private$which_longlat == "sf"){
      points_sf <- sf::st_as_sf(as.data.frame(XY), coords = 1:2, crs = private$crs)
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}), crs = private$crs)
    } else{
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}), crs = sf::st_crs(private$proj4string))
      crs <- sf::st_crs(private$proj4string)
      points_sf <- sf::st_as_sf(as.data.frame(XY), coords = 1:2, crs = private$crs)
    }

      if(verbose == 2) {
        message("Computing auxiliary distances")
      }

      within_dist <- t(as.matrix(sf::st_is_within_distance(points_sf, lines_sf, dist = tolerance)))

      if(verbose == 2) {
        message("Done!")
      }

      if(verbose == 2) {
        message("Snapping vertices")
        bar_multiple_snaps <- msg_progress_bar(length(self$edges))
      }

      for(i in 1:length(self$edges)){
        if(verbose == 2) {
          bar_multiple_snaps$increment()
        }
        select_points <- matrix(XY[within_dist[i,],], ncol=2)
        if(nrow(select_points) > 0){
          SP <- snapPointsToLines(select_points, self$edges[i], longlat, crs, i)
          idx_tol <- (SP[["df"]][["snap_dist"]] <= tolerance)
          coords_line <- c(coords_line, SP[["df"]][["nearest_line_index"]][idx_tol])
          coords_tmp <- rbind(coords_tmp, (t(SP[["coords"]]))[idx_tol,])
        }
      }

      XY <- coords_tmp

      PtE = cbind(match(coords_line, 1:length(self$edges)), 0)

      for (ind in unique(PtE[, 1])) {
        if(verbose == 2) {
          bar_multiple_snaps$increment()
        }
        index.p <- PtE[, 1] == ind

        PtE[index.p,2] <- projectVecLine2(self$edges[[ind]], XY[index.p,,drop=FALSE],
                                       normalized=TRUE)
      }
      return(PtE)
      },

  #utility function to merge close vertices
  merge_close_vertices = function(tolerance, fact) {
    if(tolerance > 0) {
      dists <- compute_aux_distances(lines = self$V, crs = private$crs, longlat = private$longlat, proj4string = private$proj4string, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform)
      v.merge <- NULL
      k <- 0

      if(self$nV>1){
      for (i in 2:self$nV) {
            if(!inherits(dists,"dist")){
                i.min <- which.min(dists[i, 1:(i-1)])
                cond_tmp <- (dists[i, i.min] < tolerance)
            } else{
                i.min <- which.min(dists[ nrow(self$V)*(1:(i-1)-1) - (1:(i-1))*(1:(i-1) -1)/2 + i -1:(i-1)])
                cond_tmp <- (dists[ nrow(self$V)*(i.min-1) - (i.min)*(i.min -1)/2 + i -i.min] < tolerance)
            }

        # i.min <- which.min(dists[i, 1:(i-1)])
        if (cond_tmp) {
          k <- k + 1
          v.merge <- rbind(v.merge, sort(c(i, i.min)))
        }
      }
      }
      if(k>0){
        for( j in 1:k) {
          v.keep <- v.merge[1,1]
          v.rem <- v.merge[1,2]
          if(j < k) {
            v.merge <- v.merge[-1,,drop = FALSE]
            v.merge[v.merge == v.rem] <- v.keep
            v.merge[v.merge > v.rem] <- v.merge[v.merge > v.rem] - 1
          }

          idx_E1 <- which(self$E[,1] == v.rem)
          idx_E2 <- which(self$E[,2] == v.rem)

          for(k1 in idx_E1){
            self$edges[[k1]][1,] <- self$V[v.keep,]
          }
          for(k2 in idx_E2){
            self$edges[[k2]][nrow(self$edges[[k2]]),] <- self$V[v.keep,]
          }

          self$V <- self$V[-v.rem,]
          self$nV <- self$nV - 1
          self$E[self$E == v.rem] <- v.keep
          self$E[self$E > v.rem] <- self$E[self$E > v.rem] - 1
        }
      }
    }
  },

  # utility function to remove small circles
  remove_circles = function(threshold, verbose,longlat, unit, crs, proj4string, which_longlat, vertex_unit, project_data) {
    if(verbose == 2) {
      message("Removing small circles")
    }

    if(is.null(private$degrees)){
      degrees <- private$compute_degrees(verbose=verbose)$degrees
    } else{
      degrees <- private$degrees$degrees
    }

    if(threshold > 0) {
      loop.ind <- which(self$E[,1] == self$E[,2])
      if(length(loop.ind)>0) {
        loop.size <- self$edge_lengths[loop.ind]
        ind <- loop.ind[loop.size < threshold]
        if(length(ind)>0) {
          v.loop <- self$E[ind,1]
          v.degrees <- degrees[v.loop]

          ind.rem <- v.loop[v.degrees == 2]
          ind.keep <- v.loop[v.degrees > 2]

          if(length(ind.rem)>0) {
            self$V <- self$V[-ind.rem,]
            self$nV <- self$nV - length(ind.rem)
            i.sort <- sort(ind.rem, decreasing = TRUE)
            for(i in i.sort) {
              self$E[self$E >= i] <- self$E[self$E >= i] - 1
            }
          }

          self$edges <- self$edges[-ind]
          if(!is.null(self$edge_lengths)){
            self$edge_lengths <- self$edge_lengths[-ind]
          }
          self$E <- self$E[-ind,]
          self$nE <- self$nE - length(ind)
          if(is.vector(private$edge_weights)){
            private$edge_weights <- private$edge_weights[-ind]
          } else{
            private$edge_weights <- private$edge_weights[-ind,,drop=FALSE]
          }

        }
      }
    }
  },

  # utility function to merge lines connected by degree 2 vertices
  merge.all.deg2 = function() {
   while(sum(private$compute_degrees()$degrees==2)>0) {
     private$remove.first.deg2()
   }
  },


  remove.first.deg2 = function(res, check_circles) {
    if(!check_circles){
      ind <- which(res$degrees==2 & !res$problematic)
    } else{
      ind <- which(res$degrees==2 & !res$problematic & !res$problematic_circles)
    }
    res.out <- res
    if(length(ind)>0) {
      ind <- ind[1]
      e1 <- which(self$E[,2]==ind)
      e2 <- which(self$E[,1]==ind)

      if(!check_circles || (self$E[e1,1] != self$E[e2,2])){
          order_edges <- order(c(e1,e2))
          e_rem <- c(e1,e2)[order_edges]

          # Finding the right order, so the edges can be merged.

          which_line_starts <- which(order_edges == 1)

          v1 <- setdiff(self$E[e_rem[1],],ind)
          v2 <- setdiff(self$E[e_rem[2],],ind)

          if(v1 > ind) {
            v1 <- v1-1
          }
          if(v2 > ind) {
            v2 <- v2 - 1
          }

          # Making sure it is not a single circle

          if( e_rem[1] != e_rem[2]){
          
             coords <- self$edges[[e_rem[1]]] #line from v1 to v.rem
             tmp <- self$edges[[e_rem[2]]] #line from v.rem to v2
             PtEedge1 <- attr(coords, "PtE")
             PtEedge2 <- attr(tmp, "PtE")

            if(which_line_starts == 1){
               coords <- rbind(coords, tmp[-1,,drop=FALSE])
               E_new <- matrix(c(v1,v2),1,2)
               PtEedge1 <- PtEedge1 * self$edge_lengths[e_rem[1]]
               PtEedge2 <- self$edge_lengths[e_rem[1]] + PtEedge2[-1] * self$edge_lengths[e_rem[2]]
               attr(coords, "PtE") <- c(PtEedge1,PtEedge2)/(self$edge_lengths[e_rem[1]] + self$edge_lengths[e_rem[2]])
            } else{
               coords <- rbind(tmp,coords[-1,,drop=FALSE])
               E_new <- matrix(c(v2,v1),1,2)
               PtEedge2 <- PtEedge2 * self$edge_lengths[e_rem[2]]               
               PtEedge1 <- self$edge_lengths[e_rem[2]] + PtEedge1[-1] * self$edge_lengths[e_rem[1]]
               attr(coords, "PtE") <- c(PtEedge2,PtEedge1)/(self$edge_lengths[e_rem[1]] + self$edge_lengths[e_rem[2]])
            }

          # Updating the merged graph

          res.out$degrees <- res$degrees[-ind]
          res.out$problematic <- res$problematic[-ind]
          res.out$problematic_circles <- res.out$problematic_circles[-ind]

          #update vertices
          self$V <- self$V[-ind,]
          self$vertices[[ind]] <- NULL
          self$nV <- self$nV - 1
          # for(i in ind:length(self$vertices)){
          #   attr(self$vertices[[i]], "id") <- attr(self$vertices[[i]], "id") - 1
          # }

          #update edges
          self$E[self$E >= ind] <- self$E[self$E >= ind] - 1
          self$E <- self$E[-e_rem[2],,drop=FALSE]
          self$E[e_rem[1],] <- E_new
          self$edges[[e_rem[1]]] <- coords
          self$edges <- self$edges[-e_rem[2]]
          # for(i in e_rem[2]:length(self$edges)){
          #     attr(self$edges[[i]], "id") <- attr(self$edges[[i]], "id") - 1
          # }
          # attr(self$edges[[e_rem[1]]], "id") <- e_rem[1]

          self$edge_lengths[e_rem[1]] <- self$edge_lengths[e_rem[1]] + self$edge_lengths[e_rem[2]]
          self$edge_lengths <- self$edge_lengths[-e_rem[2]]

          self$nE <- self$nE - 1

          if(is.vector(private$edge_weights)){
            if(private$edge_weights[e_rem[2]] != private$edge_weights[e_rem[1]]){
                private$prune_warning <- TRUE
            }
            private$edge_weights <- private$edge_weights[-e_rem[2]]
          } else{
            if(any(private$edge_weights[e_rem[2],,drop=FALSE] != private$edge_weights[e_rem[1],,drop=FALSE])){
                private$prune_warning <- TRUE
            }
            private$edge_weights <- private$edge_weights[-e_rem[2],,drop=FALSE]
          }
          }

      } else{
          res.out$problematic_circles[ind] <- TRUE
          res.out$circles_avoided <- TRUE 
      }
    }
    class(self$edges) <- "metric_graph_edges"
    return(res.out)
  },

  # Compute lengths

  compute_lengths = function(longlat, unit, crs, proj4string, which_longlat, vertex_unit, project_data, transform){
          ll <- sapply(self$edges,
          function(edge){compute_line_lengths(edge, longlat = longlat, unit = unit, crs = crs, proj4string, which_longlat, vertex_unit, project_data, transform)})
          return(ll)
  },

  approx_coordinates = function(edge){
    # Calculate the Euclidean distances between consecutive rows (vertices)
    dists <- sqrt(rowSums((edge[-1, ,drop=FALSE] - edge[-nrow(edge), ,drop=FALSE])^2))

    # Get cumulative sum to obtain positions along the edge
    cum_dists <- c(0, cumsum(dists))  # Start from 0 for the first vertex

    # Normalize the cumulative distances to get relative positions between 0 and 1
    rel_positions <- cum_dists / cum_dists[length(cum_dists)]

    return(rel_positions)
  },

  exact_PtE_coordinates = function(edge) {
  # Get distances using compute_aux_distances between consecutive rows
  lines <- edge[-1, , drop = FALSE]      # All rows except the first
  points <- edge[-nrow(edge), , drop = FALSE] # All rows except the last
  
  # Compute distances between each consecutive point
  dists <- compute_aux_distances(lines = lines, crs = private$crs, longlat = private$longlat, 
                                 proj4string = private$proj4string, points = points, 
                                 fact = private$fact, which_longlat = private$which_longlat, 
                                 length_unit = private$length_unit, transform = private$transform)
  
  # Calculate the relative positions using cumulative sums
  total_length <- sum(dists)
  relative_positions <- c(0, cumsum(dists) / total_length)
  
  return(relative_positions)
},

 ## @description Get the observation/prediction matrix A
 ## @param group A vector. If `NULL`, the A matrix for the first group will be
 ## returned. One can use all groups by simply setting the `group` variable
 ## to `.all`. Otherwise, the A matrix for the groups in the vector will be
 ## returned.
 ## @param obs_to_vert Should the observations be turned into vertices?
 ## @param include_NA Should the locations for which all observations are NA be
 ## included?
 ## @return The observation or prediction matrix.
  A = function(group = NULL,
               obs_to_vert = FALSE,
               drop_na = FALSE,
               drop_all_na = FALSE){

    if(is.null(self$PtV) && !obs_to_vert){
        stop("The A matrix was not computed. If you want to compute rerun this
             method with 'obs_to_vertex=TRUE', in which the observations will be
             turned to vertices and the A matrix will then be computed")
    } else if(is.null(self$PtV)){
      self$observation_to_vertex(mesh_warning=FALSE)
    }

    if(is.null(group)){
      group <- unique(private$data[[".group"]])
      group <- group[1]
    } else if (group[1] == ".all"){
      group <- unique(private$data[[".group"]])
    }
    n_group <- length(unique(group))

    if(!drop_na && !drop_all_na){
      A <- Matrix::Diagonal(self$nV)[self$PtV, ]
      return(Matrix::kronecker(Diagonal(n_group),A))
    } else {
        data_group <- select_group(private$data, group[1])
        if(drop_na){
          idx_notna <- idx_not_any_NA(data_group)
        } else if(drop_all_na){
          idx_notna <- idx_not_all_NA(data_group)
        }
        # nV_tmp <- sum(idx_notna)
        # A <- Matrix::Diagonal(nV_tmp)[self$PtV[idx_notna], ]
        A <- Matrix::Diagonal(self$nV)[self$PtV[idx_notna], ]
        if(n_group > 1){
          for (i in 2:length(group)) {
            data_group <- select_group(private$data, group[i])
            if(drop_na){
              idx_notna <- idx_not_any_NA(data_group)
            } else if(drop_all_na){
              idx_notna <- idx_not_all_NA(data_group)
            }
            # nV_tmp <- sum(idx_notna)
            A <- Matrix::bdiag(A, Matrix::Diagonal(self$nV)[self$PtV[idx_notna], ])
          }
        }
        return(A)
      }
  },


  #'  Gets the edge weights
  #'  data.frame If the edge weights are given as vectors, should the result be returned as a data.frame?
  #'  A vector or `data.frame` containing the edge weights.

  get_edge_weights_internal = function(data.frame = FALSE){
    tmp <- private$edge_weights
    row.names(tmp) <- NULL
    if(!is.data.frame(tmp) && data.frame){
      tmp <- data.frame(.weights = tmp)
    }
    return(tmp)
  },

format_data = function(data_res, format) {

    if(!(format%in%c("tibble", "sf", "sp"))){
      stop("The possible formats are 'tibble', 'sf', 'sp'.")
    }

    if (format == "sf") {
        # Ensure the coordinates exist in the data
        if (!all(c(".coord_x", ".coord_y") %in% colnames(data_res))) {
            stop("Cannot convert to sf: .coord_x and .coord_y columns are missing.")
        }
        
        # Create sf object from coordinates
        data_res_sf <- sf::st_as_sf(data_res, coords = c(".coord_x", ".coord_y"), crs = sf::NA_crs_)
        class(data_res_sf) <- c("metric_graph_data", class(data_res_sf))
        return(data_res_sf)

    } else if (format == "sp") {
        # Ensure the coordinates exist in the data
        if (!all(c(".coord_x", ".coord_y") %in% colnames(data_res))) {
            stop("Cannot convert to sp: .coord_x and .coord_y columns are missing.")
        }

        # Create SpatialPointsDataFrame from coordinates
        coords <- cbind(data_res$.coord_x, data_res$.coord_y)
        sp_data <- sp::SpatialPointsDataFrame(
            coords = coords,
            data = data_res,
            proj4string = sp::CRS(NA_character_)
        )
        return(sp_data)

    } else {
        data_res <- tidyr::as_tibble(data_res)
        class(data_res) <- c("metric_graph_data", class(data_res))
        return(data_res)
    }
},


  format_weights = function(edge_weights_res, format) {

    if(!(format%in%c("tibble", "sf", "sp"))){
      stop("The possible formats are 'tibble', 'sf', 'sp'.")
    }

  if (format == "sf") {
    edges_geometries <- lapply(self$edges, sf::st_linestring)

    if (is.vector(private$edge_weights)) {
      ew_tmp <- data.frame(.weights = private$edge_weights)
    } else {
      ew_tmp <- as.data.frame(private$edge_weights)
    }
    ew_tmp[[".edge_lengths"]] <- self$edge_lengths

    edges_sf <- sf::st_sf(ew_tmp, geometry = sf::st_sfc(edges_geometries), crs = if (!is.null(private$crs)) private$crs else sf::NA_crs_)
    class(edges_sf) <- c("metric_graph_weights", class(edges_sf))
    return(edges_sf)

  } else if (format == "sp") {
    edges_list <- lapply(1:length(self$edges), function(i) {
      sp::Line(coords = matrix(self$edges[[i]], nrow = dim(self$edges[[i]])[1], ncol = dim(self$edges[[i]])[2]))
    })
    sp_edges <- sp::SpatialLines(
      lapply(1:length(edges_list), function(i) sp::Lines(list(edges_list[[i]]), ID = as.character(i))),
      proj4string = if (!is.null(private$crs)) private$proj4string else sp::CRS(NA_character_)
    )
    
    if (is.vector(private$edge_weights)) {
      ew_tmp <- data.frame(.weights = private$edge_weights)
    } else {
      ew_tmp <- as.data.frame(private$edge_weights)
    }
    ew_tmp[[".ID"]] <- as.character(1:length(sp_edges))
    ew_tmp[[".edge_lengths"]] <- self$edge_lengths
    edges_sldf <- sp::SpatialLinesDataFrame(sp_edges, data = ew_tmp, match.ID = ".ID")
    return(edges_sldf)

  } else {
    edge_weights_res <- tidyr::as_tibble(edge_weights_res)
    class(edge_weights_res) <- c("metric_graph_weights", class(edge_weights_res))
    return(edge_weights_res)
  }
},

  #' data List containing data on the metric graph.

  data = NULL,

  # Initial edges added

  initial_edges_added = NULL,

  # Initial graph

  initial_graph = NULL,

  # pruned

  pruned = FALSE,

  # should the information be saved when splitting edges?

  addinfo = FALSE,

  # vertex unit

  vertex_unit = NULL,

  # Length unit

  length_unit = NULL,

  # longlat

  longlat = FALSE,

  # crs

  crs = NULL,

  # proj4string

  proj4string = NULL,

  # which_longlat

  which_longlat = NULL,

  # connected

  connected = TRUE,

  # group columns

  group_col = NULL,

  # Kichhoff weights

  kirchhoff_weights = NULL,

  # manual edge lengths

  manual_edge_lengths = FALSE,

  # perform merges?

  perform_merges = NULL,

  #grouping variables when adding data

  group_variables = NULL,

  project_data = NULL,

  degrees = NULL,

  # Warning if edges with different weights have been merged

  prune_warning = FALSE,

  clear_initial_info = function(){
    private$addinfo = FALSE
    private$initial_edges_added = NULL
  },

  # Observe that by construction of get_PtE, all PtEs are sorted.
  split_edge = function(Ei, t_values, indices = NULL) {

    # Calculate distances and determine if new vertices need to be added
    edge <- self$edges[[Ei]]
    edge_length <- self$edge_lengths[Ei]
    PtE_edge <- attr(edge, "PtE")

    # Data frame creation and filling NA values
    val_results <- interpolate2(edge, pos = t_values, normalized = TRUE, get_idx = TRUE)
    idx_positions <- val_results[["idx"]]
    val_lines <- val_results[["coords"]]

    # Initialize new_vertices
    new_vertices <- numeric(length(t_values))

    # Loop through each value in t_values, updating self$V conditionally
    for (i in seq_along(t_values)) { 
        val_line <- matrix(val_lines[i, , drop = FALSE], nrow = 1)
        newV <- self$nV + 1
        self$V <- rbind(self$V, val_line)
        self$nV <- self$nV + 1
        new_vertices[i] <- newV
    }

    # Edge updates
    edge_updates <- self$nE + seq_len(length(t_values))
    
    if (!is.null(private$data) && !is.null(indices)) {
      private$temp_PtE[indices, 1] <- edge_updates
      private$temp_PtE[indices, 2] <- (private$temp_PtE[indices, 2] - t_values) / (1 - t_values)
    }
    edge_updates <- c(Ei, edge_updates)

    coords_list1 <- edge[1:idx_positions[1], , drop = FALSE]
    coords_list1 <- rbind(coords_list1, val_lines[1, , drop = FALSE])
    tmp_vec <- c(PtE_edge[1:idx_positions[1]], t_values[1])

    pos_edge_diff <- tmp_vec - tmp_vec[1]
    norm_factor <- tmp_vec[length(tmp_vec)] - tmp_vec[1]
    tmp_PtE <- pos_edge_diff / norm_factor

    # Add tmp_PtE as an attribute
    attr(coords_list1, "PtE") <- tmp_PtE

    n_t_values <- length(t_values)

    coords_list2 <- vector("list", n_t_values)

    for (i in seq_along(t_values)) {
      val_line_start <- matrix(val_lines[i, , drop = FALSE], nrow = 1)

      if (i < n_t_values) {
        val_line_end <- matrix(val_lines[i + 1, , drop = FALSE], nrow = 1)
        if (idx_positions[i] != idx_positions[i + 1]) {
          coords_list2[[i]] <- rbind(
            val_line_start,
            edge[(idx_positions[i] + 1):idx_positions[i + 1], , drop = FALSE],
            val_line_end
          )
          tmp_vec <- c(t_values[i], PtE_edge[(idx_positions[i] + 1):idx_positions[i + 1]], t_values[i + 1])
        } else {
          coords_list2[[i]] <- rbind(
            val_line_start,
            val_line_end
          )
          tmp_vec <- c(t_values[i], t_values[i + 1])
        }
      } else {
        coords_list2[[i]] <- rbind(
          val_line_start,
          edge[(idx_positions[i] + 1):nrow(edge), , drop = FALSE]
        )
        tmp_vec <- c(t_values[i], PtE_edge[(idx_positions[i] + 1):nrow(edge)])
      }

      pos_edge_diff <- tmp_vec - tmp_vec[1]
      norm_factor <- tmp_vec[length(tmp_vec)] - tmp_vec[1]
      tmp_PtE <- pos_edge_diff / norm_factor

      # Add tmp_PtE as an attribute
      attr(coords_list2[[i]], "PtE") <- tmp_PtE
    }

    # Construct aux_matrix for self$E
    if (length(t_values) == 1) {
      aux_matrix <- matrix(
        c(self$E[Ei, 1], new_vertices[1], new_vertices[1], self$E[Ei, 2]),
        nrow = 2, byrow = TRUE
      )
    } else {
      aux_matrix <- rbind(
        c(self$E[Ei, 1], new_vertices[1]),
        cbind(new_vertices[-length(new_vertices)], new_vertices[-1]),
        c(new_vertices[length(new_vertices)], self$E[Ei, 2])
      )
    }

    # Update self$E, self$edges, and related attributes
    self$E[Ei, ] <- aux_matrix[1, ]
    self$E <- rbind(self$E, aux_matrix[-1, ])
    self$edges[[Ei]] <- coords_list1
    self$edges <- c(self$edges, coords_list2)
    self$nE <- self$nE + length(t_values)

    # Update edge lengths and weights
    segment_lengths <- c(t_values[1], diff(t_values), 1 - t_values[length(t_values)]) * self$edge_lengths[Ei]
    self$edge_lengths <- c(self$edge_lengths, segment_lengths[-1])
    self$edge_lengths[Ei] <- segment_lengths[1]

    if (is.vector(private$edge_weights)) {
      private$edge_weights <- c(private$edge_weights, rep(private$edge_weights[Ei], length(t_values)))
    } else {
      private$edge_weights <- rbind(private$edge_weights, do.call(rbind, replicate(length(t_values), private$edge_weights[Ei, , drop = FALSE], simplify = FALSE)))
    }

    return(new_vertices)
  },

  compute_laplacian_PtE = function(PtE, normalized = TRUE, verbose = verbose) {
      if(verbose == 2){
        message("Processing the graph locations...")
      }
    t <- system.time({
      graph.temp <- self$clone()
      graph.temp$clear_observations()
      df_temp <- data.frame(y = rep(0, dim(PtE)[1]),
                         edge_number = PtE[,1],
                         distance_on_edge = PtE[,2])
      if(sum(duplicated(df_temp))>0){
        warning("Duplicated locations were found when computing the laplacian. The returned values are given for unique locations.")
        df_temp <- unique(df_temp)
      }

      graph.temp$build_mesh(h = 1000)

      df_temp2 <- data.frame(y = 0, edge_number = graph.temp$mesh$VtE[1:nrow(self$V),1],
                                  distance_on_edge = graph.temp$mesh$VtE[1:nrow(self$V),2])
      df_temp$included <- TRUE
      temp_merge <- merge(df_temp, df_temp2, all = TRUE)

      df_temp$included <- NULL
      df_temp2 <- temp_merge[is.na(temp_merge["included"]),]
      df_temp2$included <- NULL
      df_temp <- rbind(df_temp2, df_temp)

      nV_new <- sum(is.na(temp_merge["included"]))

      df_temp[["__dummy"]] <- 1:nrow(df_temp)

      graph.temp$add_observations(data = df_temp, normalized = normalized, verbose = 0,
                  suppress_warnings = TRUE)
    })
      if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
      }
      if(verbose == 2){
        message("Turning observations of the auxiliary graph to vertices...")
      }
    t <- system.time(
      graph.temp$observation_to_vertex(mesh_warning = FALSE)
    )
    if(verbose > 0){
      message("Computing the Laplacian...")
    }
    t <- system.time({
      Wmat <- Matrix(0,graph.temp$nV,graph.temp$nV)

      for (i in 1:graph.temp$nE) {
        Wmat[graph.temp$E[i, 1], graph.temp$E[i, 2]] <- 1 / graph.temp$edge_lengths[i]
        Wmat[graph.temp$E[i, 2], graph.temp$E[i, 1]] <- 1 / graph.temp$edge_lengths[i]
      }
      Laplacian <- Matrix::Diagonal(graph.temp$nV,
                                  as.vector(Matrix::rowSums(Wmat))) - Wmat

      # Reordering from vertices to points
      Laplacian <- Laplacian[graph.temp$PtV, graph.temp$PtV]
    # Order back to the input order
      Laplacian[graph.temp$.__enclos_env__$private$data[["__dummy"]], graph.temp$.__enclos_env__$private$data[["__dummy"]]] <- Laplacian
    })
    if(verbose == 2){
        message(sprintf("time: %.3f s", t[["elapsed"]]))
       }

    attr(Laplacian, "nV_idx") <- nV_new

    return(Laplacian)
  },

  find_edge_edge_points = function(tol,verbose, crs, proj4string, longlat, fact, which_longlat) {

    if(!private$longlat){
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}))
      crs <- NULL
    } else if (private$which_longlat == "sf"){
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}), crs = private$crs)
    } else{
      lines_sf <- sf::st_sfc(lapply(self$edges, function(i){sf::st_linestring(i)}), crs = sf::st_crs(private$proj4string))
      crs <- sf::st_crs(proj4string)
    }

  dists <- t(as.matrix(sf::st_is_within_distance(lines_sf, dist = tol)))
  points_add <- NULL
  points_add_PtE <- NULL

  if(verbose == 2) {
    bar_line_line <- msg_progress_bar(length(self$edges)-1)
  }
  for(i in 1:(length(self$edges)-1)) {
    if(verbose == 2) {
      bar_line_line$increment()
    }
    #lines within tol of line i
    inds <- i+which(as.vector(dists[i, (i+1):length(self$edges)]))
    if(length(inds)>0) {
      for(j in inds) {
        #first check if there are intersections

        intersect_tmp <- intersection3(lines_sf[i], lines_sf[j])

        if( any(c("GEOMETRYCOLLECTION","GEOMETRY") %in% sf::st_geometry_type(intersect_tmp)) || (length(sf::st_geometry_type(intersect_tmp)) > 1)){
          intersect_tmp1 <- sf::st_collection_extract(intersect_tmp, type = "LINESTRING")
          intersect_tmp2 <- sf::st_collection_extract(intersect_tmp, type = "POINT")
          if(nrow(sf::st_coordinates(intersect_tmp1))>0){
            intersect_tmp <- intersect_tmp1
          } else{
            intersect_tmp <- intersect_tmp2
          }
        }

        p_cur <- NULL
        # if(!is.null(intersect_tmp)) {
        if(nrow(sf::st_coordinates(intersect_tmp))>0){
          # if("SpatialPoints"%in%is(intersect_tmp)){
          if( ("POINT"%in%sf::st_geometry_type(intersect_tmp))||("MULTIPOINT"%in%sf::st_geometry_type(intersect_tmp))){
            # coord_tmp <- coordinates(intersect_tmp)
            coord_tmp <- sf::st_coordinates(intersect_tmp)
            coord_tmp <- coord_tmp[,1:2]
          # } else if ("SpatialLines"%in%is(intersect_tmp)){
          } else if ( ("LINESTRING"%in%sf::st_geometry_type(intersect_tmp)) || ("MULTILINESTRING"%in%sf::st_geometry_type(intersect_tmp))){
            intersect_tmp <- sf::st_coordinates(intersect_tmp)
            intersect_tmp <- intersect_tmp[,1:2]
            # coord_tmp <-gInterpolate(intersect_tmp, d=0.5, normalized = TRUE)
            coord_tmp <- interpolate2(intersect_tmp, pos=0.5, normalized = TRUE)
            coord_tmp <- matrix(coordinates(coord_tmp),1,2)
          }

            # for(k in 1:length(intersect_tmp)) {
            coord_tmp <- matrix(coord_tmp, ncol=2)

            for(k in 1:nrow(coord_tmp)){
              p <- matrix(coord_tmp[k,],1,2)
              #add points if they are not close to V or previous points
              if(!is.matrix(self$V)){
                self$V <- matrix(self$V,ncol=2)
              }
              if(min(compute_aux_distances(lines = self$V, crs=private$crs, longlat=private$longlat, proj4string = private$proj4string, points = p, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform))>tol) {
                p_cur <- rbind(p_cur,p)
                p2 <- snapPointsToLines(p,self$edges[i], longlat, crs)
                p2 <- t(p2[["coords"]])
                # p2 <- cbind(p2["X",], p2["Y",])

                points_add <- rbind(points_add, p, p2)
                points_add_PtE <- rbind(points_add_PtE,
                                        c(i,projectVecLine2(self$edges[[i]],
                                                     p)),
                                        c(j,projectVecLine2(self$edges[[j]],p)))
              }
            }
        }
        #now check if there are intersections with buffer

        lines_tmp_sf <- lines_sf[i]
        lines2_tmp_sf <- lines_sf[j]
        tmp_line <- sf::st_buffer(lines_tmp_sf, dist = tol)

        intersect_tmp <- intersection3(tmp_line, lines2_tmp_sf)

        if( "GEOMETRYCOLLECTION" %in% sf::st_geometry_type(intersect_tmp)){
          intersect_tmp <- sf::st_collection_extract(intersect_tmp, type = "LINESTRING")
        }
        # if(!is.null(intersect_tmp)) {
        if(nrow(sf::st_coordinates(intersect_tmp))>0){
            for(k in 1:length(intersect_tmp)) {
              if ( ("LINESTRING"%in%sf::st_geometry_type(intersect_tmp)) || ("MULTILINESTRING"%in%sf::st_geometry_type(intersect_tmp))){
              # coord_tmp <-gInterpolate(intersect_tmp[k], d=0.5, normalized = TRUE)
              coord_int <- sf::st_coordinates(intersect_tmp[k])
              coord_int <- coord_int[,c("X","Y")]
              coord_tmp <- interpolate2(coord_int, pos=0.5, normalized = TRUE)
              p <- matrix(coord_tmp,1,2)
            } else {
              coord_int <- sf::st_coordinates(intersect_tmp[k])
              p <- matrix(coord_int[,c("X","Y")],1,2)
              # p <- matrix(sf::st_coordinates(intersect_tmp[k]),1,2)
            }

            #add points if they are not close to V or previous points
            if(min(compute_aux_distances(lines = self$V, crs=private$crs, longlat=private$longlat, proj4string = private$proj4string, points = p, fact = fact, which_longlat = private$which_longlat, length_unit = private$length_unit, transform = private$transform))>tol) {
              # if(is.null(p_cur) || gDistance(SpatialPoints(p_cur), intersect_tmp[k])>tol) {
                if(!private$longlat && !is.null(p_cur)){
                  dist_tmp <- sf::st_distance(sf::st_as_sf(as.data.frame(p_cur), coords = 1:2), intersect_tmp[k])
                } else if (!is.null(p_cur)) {
                  intersect_tmp_sfc <- sf::st_sfc(intersect_tmp[k], crs = private$crs)
                  dist_tmp <- sf::st_distance(sf::st_as_sf(as.data.frame(p_cur), coords = 1:2, crs = private$crs), intersect_tmp_sfc)
                  units(dist_tmp) <- private$length_unit
                  units(dist_tmp) <- NULL
                }
              if(is.null(p_cur) || min(dist_tmp) >tol) {
                p2 <- snapPointsToLines(p,self$edges[i], private$longlat, private$crs)
                p2 <- t(p2[["coords"]])
                points_add <- rbind(points_add, p, p2)
                points_add_PtE <- rbind(points_add_PtE,
                                        c(i,projectVecLine2(self$edges[[i]],p)),
                                        c(j,projectVecLine2(self$edges[[j]], p)))

              }
            }
          }
        }
      }
    }
  }
  # ret_tmp <- list(points = points_add, PtE = points_add_PtE)
  return(list(points = points_add, PtE = points_add_PtE))
},

add_vertices = function(PtE, tolerance = 1e-10, verbose) {
  e.u <- unique(PtE[,1])
  if(verbose == 2) {
    bar_eu <- msg_progress_bar(length(e.u))
  }
  for (i in 1:length(e.u)) {
    if(verbose == 2) {
      bar_eu$increment()
    }
    dists <- sort(PtE[which(PtE[,1]==e.u[i]),2])
    if(length(dists) > 0){
    if(dists[1] > tolerance){
      private$split_edge(e.u[i], dists[1])
    }
    }
    if(length(dists)>1) {
      dists_up <- dists
      for(j in 2:length(dists)){
        dists_up[j] <- (dists[j] - dists[j-1])/(1 - dists[j-1])
        if(dists_up[j] > tolerance){
          private$split_edge(self$nE, dists_up[j])
        } 
      }
    }
  }
  return(self)
},

 # Function to compute the degrees of the vertices,

  compute_degrees = function(verbose = 0, add = FALSE) {
    if (verbose == 2) {
      message("Computing degrees...")
    }

    # Vectorized computation of in-degrees and out-degrees
    degrees_out <- tabulate(self$E[, 1], nbins = self$nV)
    degrees_in <- tabulate(self$E[, 2], nbins = self$nV)

    # Compute total degrees
    degrees <- degrees_in + degrees_out

    if (add) {
      private$degrees <- list(degrees = degrees, indegrees = degrees_in, outdegrees = degrees_out)
    }

    return(list(degrees = degrees, indegrees = degrees_in, outdegrees = degrees_out))
  },

  # Reference edges for the vertices

  ref_edges = NULL,

  #  Creates/updates the vertices element of the metric graph list

   create_update_vertices = function(verbose = 0) {
    degrees <- private$compute_degrees(verbose = verbose, add = TRUE)

    # Check if the number of rows in self$V matches the length of degrees$degrees
    n_vertices <- nrow(self$V)
    if (length(degrees$degrees) != n_vertices ||
        length(degrees$indegrees) != n_vertices ||
        length(degrees$outdegrees) != n_vertices) {
      stop("Mismatch in the number of vertices and degree information. Please check your graph data.")
    }

    if (verbose == 2) {
      if (is.null(self$vertices)) {
        message("Creating vertices object")
      } else {
        message("Updating vertices object")
      }
      bar_update_attr_edges <- msg_progress_bar(n_vertices)
    }

    # Set column names for self$V
    colnames(self$V) <- c("X", "Y")

    # Compute problematic vertices
    problematic <- (degrees$degrees > 1) & ((degrees$indegrees == 0) | (degrees$outdegrees == 0))
    ids <- seq_len(n_vertices)

    # Prepare longlat (which should always be present)
    longlat <- rep(private$longlat, n_vertices)

    if (is.null(private$crs$input)) {
      # Case when crs is NULL
      vertices_df <- data.frame(
        X = self$V[, 1],
        Y = self$V[, 2],
        degree = degrees$degrees,
        indegree = degrees$indegrees,
        outdegree = degrees$outdegrees,
        problematic = problematic,
        longlat = longlat,
        id = ids
      )

      # Convert each row to a "metric_graph_vertex" object without crs
      self$vertices <- lapply(1:n_vertices, function(i) {
        vert <- as.numeric(vertices_df[i, c("X", "Y")])  # Convert to numeric vector
        attr(vert, "degree") <- vertices_df$degree[i]
        attr(vert, "indegree") <- vertices_df$indegree[i]
        attr(vert, "outdegree") <- vertices_df$outdegree[i]
        attr(vert, "problematic") <- vertices_df$problematic[i]
        attr(vert, "longlat") <- vertices_df$longlat[i]
        attr(vert, "id") <- vertices_df$id[i]
        class(vert) <- "metric_graph_vertex"
        if (verbose == 2) {
          bar_update_attr_edges$increment()
        }
        return(vert)
      })
    } else {
      # Case when crs is not NULL
      crs <- rep(private$crs$input, n_vertices)
      vertices_df <- data.frame(
        X = self$V[, 1],
        Y = self$V[, 2],
        degree = degrees$degrees,
        indegree = degrees$indegrees,
        outdegree = degrees$outdegrees,
        problematic = problematic,
        longlat = longlat,
        crs = crs,
        id = ids
      )

      # Convert each row to a "metric_graph_vertex" object with crs
      self$vertices <- lapply(1:n_vertices, function(i) {
        vert <- as.numeric(vertices_df[i, c("X", "Y")])  # Convert to numeric vector
        attr(vert, "degree") <- vertices_df$degree[i]
        attr(vert, "indegree") <- vertices_df$indegree[i]
        attr(vert, "outdegree") <- vertices_df$outdegree[i]
        attr(vert, "problematic") <- vertices_df$problematic[i]
        attr(vert, "longlat") <- vertices_df$longlat[i]
        attr(vert, "crs") <- vertices_df$crs[i]
        attr(vert, "id") <- vertices_df$id[i]
        class(vert) <- "metric_graph_vertex"
        if (verbose == 2) {
          bar_update_attr_edges$increment()
        }
        return(vert)
      })
    }

    class(self$vertices) <- "metric_graph_vertices"
  },

  # Merge degree 2 vertices in mesh
  mesh_merge_deg2 = function() {
    outs <- rep(0,self$nV)
    ins <- rep(0, self$nV)
    for(i in 1:self$nV) {
      outs[i] <- sum(self$E[,1] == i)
      ins[i] <- sum(self$E[,2] == i)
    }
    ind <- which(outs == 1 & ins == 1)
    if(length(ind)>0) {
      for(i in 1:length(ind)) {
        V.keep <- self$mesh$V[ind[i],]
        V.rem <- which(self$mesh$V[,1] == V.keep[1] & self$mesh$V[,2] == V.keep[2])[2]
        self$mesh$PtE <- self$mesh$PtE[-V.rem,]
        self$mesh$V <- self$mesh$V[-V.rem,]
        self$mesh$E[self$mesh$E == V.rem] <- ind[i]
        self$mesh$E[self$mesh$E>V.rem] <- self$mesh$E[self$mesh$E>V.rem] - 1
      }
    }
  },

  mesh_merge_outs = function() {
    outs <- rep(0,self$nV)
    for(i in 1:self$nV) {
      outs[i] <- sum(self$E[,1] == i)
    }
    ind <- which(outs > 1)
    while(length(ind)>0) {
      #find edges going out
      e.ind <- which(self$E[,1]==ind[1])
      V.keep <- which(self$mesh$PtE[,1]==e.ind[1])[1]
      V.rem <- which(self$mesh$PtE[,1]==e.ind[2])[1]
      self$mesh$PtE <- self$mesh$PtE[-V.rem,]
      self$mesh$V <- self$mesh$V[-V.rem,]
      self$mesh$E[self$mesh$E == V.rem] <- V.keep
      self$mesh$E[self$mesh$E>V.rem] <- self$mesh$E[self$mesh$E>V.rem] - 1
      outs[ind[1]] <- outs[ind[1]] - 1
      ind <- which(outs > 1)
    }
    #now merge ins with outs if there is only one in
    ins <- rep(0,self$nV)
    outs <- rep(0,self$nV)
    for(i in 1:self$nV) {
      ins[i] <- sum(self$E[,2] == i)
      outs[i] <- sum(self$E[,1] == i)
    }
    ind <- which(ins == 1 & outs > 1)
    while(length(ind)>0) {
      #merge in vertex with out
      e.ind.in <- which(self$E[,2]==ind[1])
      e.ind <- which(self$E[,1]==ind[1])
      V.keep <- which(self$mesh$PtE[,1]==e.ind[1])[1]
      V.rem <- which(self$mesh$PtE[,1]==e.ind.in)
      V.rem <- V.rem[length(V.rem)]
      self$mesh$PtE <- self$mesh$PtE[-V.rem,]
      self$mesh$V <- self$mesh$V[-V.rem,]
      self$mesh$E[self$mesh$E == V.rem] <- V.keep
      self$mesh$E[self$mesh$E>V.rem] <- self$mesh$E[self$mesh$E>V.rem] - 1
      ins[ind[1]] <- ins[ind[1]] - 1
      ind <- which(ins == 1 & outs > 1)
    }
  },

  #find one mesh node corresponding to each vertex and move it first
  move_V_first = function() {
    nv <- dim(self$mesh$V)[1]
    for(i in 1:self$nV) {
      ind <- which(self$mesh$V[,1] == self$V[i,1] & self$mesh$V[,2] == self$V[i,2])[1]
      if(!is.na(ind) && length(ind)>0) {
        if(ind > i && i < nv) {
          if (i == 1) {
            reo <- c(ind, setdiff(i:nv,ind))
          } else {
            reo <- c(1:(i-1), ind, setdiff(i:nv,ind))
          }
          self$mesh$V <- self$mesh$V[reo,]
          self$mesh$PtE <- self$mesh$PtE[reo,]
          self$mesh$VtE <- self$mesh$VtE[reo,]
          Etmp <- self$mesh$E
          ind1 <- Etmp == ind
          ind2 <- Etmp >= i & Etmp < ind
          self$mesh$E[ind1] = i
          self$mesh$E[ind2] = self$mesh$E[ind2] + 1
        }
      }
    }
  },

  # find vertices and connections for Petrov-Galerkin matrices
  find_mesh_bc = function() {
    if(attr(self$mesh,"continuous")) {
      stop("mesh discontinuous")
    }
    starts <- which(self$mesh$PtE[,2]==0)
    edges <- self$mesh$PtE[starts,1]

    ind.keep <- rep(TRUE,length(starts))
    for(i in 1:length(starts)) {
      vert <- self$E[edges[i],1]
      if(length(which(self$E[,2] == vert)) == 1) {
        ind.keep[i] <- FALSE
      }
    }
    starts <- starts[ind.keep]
    edges <- edges[ind.keep]
    connections <-list()
    for(i in 1:length(starts)) {
      vert <- self$E[edges[i],1] #the vertex of the start point
      n.in <- sum(self$E[,2] == vert)
      if(n.in == 0) {
        connections[[i]] <- 0 #true starting value
      } else {
        #find edges ending in the vertex
        edge.in <- which(self$E[,2] == vert)
        #find edges starting in the vertex
        edge.out <- which(self$E[,1] == vert)

        mesh.in <- NULL
        for(j in 1:length(edge.in)){
          tmp <- which(self$mesh$PtE[,1] == edge.in[j]) #mesh nodes on the edge
          mesh.in <- c(mesh.in, tmp[length(tmp)])
        }
        connections[[i]] <- mesh.in
      }
    }
    return(connections)
  },

  # Function to add vertex conditions to Petrov-Galerkin matrices
  set_petrov_matrices = function() {
    if(attr(self$mesh,"continuous")) {
      V <- self$mesh$V[1:self$nV,]
      deg1 <- which(self$get_degrees() == 1)
      starts <- deg1[deg1 %in% self$E[,1]]

      if(length(starts)>1){

        G <- rbind(sparseMatrix(i = 1:length(starts), j = starts,
                                x = rep(0, length(starts)),
                                dims = c(length(starts), dim(self$mesh$Cpet)[1])),
                   t(self$mesh$Gpet[,-starts[-1]]))
      } else {
        C <- rbind(sparseMatrix(i = 1, j = starts, x = 1,
                                dims = c(1,dim(self$mesh$Cpet)[1])),
                   t(self$mesh$Cpet))
        G <- rbind(sparseMatrix(i = 1, j = starts, x= 0,
                                dims = c(1, dim(self$mesh$Cpet)[1])),
                   t(self$mesh$Gpet))
      }
    } else {
      bc <- private$find_mesh_bc()
      starts <- which(self$mesh$PtE[,2]==0)
      edges <- self$mesh$PtE[starts,1]

      ind.keep <- rep(TRUE,length(starts))
      for(i in 1:length(starts)) {
        vert <- self$E[edges[i],1]
        if(length(which(self$E[,2] == vert)) == 1) {
          ind.keep[i] <- FALSE
        }
      }
      starts <- starts[ind.keep]
      edges <- edges[ind.keep]
      C <- t(self$mesh$Cpet)
      G <- t(self$mesh$Gpet)

      for(i in length(bc):1) {
        G <- rbind(sparseMatrix(i = 1, j = 1, x = 0,
                                dims = c(1, dim(self$mesh$Cpet)[1])), G)
        if(bc[[i]][1] == 0) {
          C <- rbind(sparseMatrix(i = 1,j = starts[i], x = 1,
                                  dims = c(1, dim(self$mesh$Cpet)[1])), C)
        } else {
          C <- rbind(sparseMatrix(i = rep(1,length(bc[[i]])+1),
                                  j = c(starts[i], bc[[i]]),
                                  x = c(1, rep(-1/length(bc[[i]]), length(bc[[i]]))),
                                  dims = c(1, dim(self$mesh$Cpet)[1])), C)
        }

      }
    }
    self$mesh$Cpet = C
    self$mesh$Gpet = G
    self$mesh$n.bc = length(starts)
    self$mesh$h0 = which(unlist(lapply(bc,length))>1)
  },

  # Bounding box

  bounding_box = NULL,

  compute_bounding_box = function(){
    all_coords <- do.call(rbind, self$edges) 

    # Calculate the bounding box
    min_x <- min(all_coords[, 1])
    max_x <- max(all_coords[, 1])
    min_y <- min(all_coords[, 2])
    max_y <- max(all_coords[, 2])

    private$bounding_box <- list(min_x = min_x, max_x = max_x, min_y = min_y, max_y = max_y)
  },

  # Temp PtE

  temp_PtE = NULL,

  # # longlat
  # longlat = NULL,

  # tolerance

  tolerance = NULL,

  # transform to long lat?

  transform = FALSE,

  # edge_weights

  edge_weights = NULL,

  #
  directional_weights = NULL,

    set_first_weights = function(weights = rep(1, self$nE)){
      if(is.null(weights)){
        weights <- rep(1, self$nE)
      }
    if(!is.vector(weights) && !is.data.frame(weights)){
      stop("'weights' must be either a vector or a data.frame!")
    }
    
    if(is.vector(weights)){
      if ( (length(weights) != 1) && (length(weights) != self$nE)){
        stop(paste0("The length of 'weights' must be either 1 or ", self$nE))
      }
      if(length(weights)==1){
        private$edge_weights <- rep(weights, self$nE)
      } else{
        private$edge_weights <- weights
      }
    } else{
      if(nrow(weights) != self$nE){
        stop("The number of rows of weights must be equal to the number of edges!")
      }
      private$edge_weights <- weights
    }

  }

))

#' @title Connected components of metric graph
#' @description Class representing connected components of a metric graph.
#' @details A list of `metric_graph` objects (representing the different
#' connected components in the full graph) created from vertex and edge matrices,
#' or from an sp::SpatialLines object where each line is representing and edge.
#' For more details, see the vignette:
#' \code{vignette("metric_graph", package = "MetricGraph")}
#' @return Object of \code{\link[R6]{R6Class}} for creating metric graph components.
#' @examples
#' library(sp)
#' edge1 <- rbind(c(0, 0), c(1, 0))
#' edge2 <- rbind(c(1, 0), c(2, 0))
#' edge3 <- rbind(c(1, 1), c(2, 1))
#' edges <- list(edge1, edge2, edge3)
#'
#' graphs <- graph_components$new(edges)
#' graphs$plot()
#' @export
graph_components <-  R6::R6Class("graph_components",
   public = list(
   #' @field graphs List of the graphs representing the connected components.
   graphs = NULL,

   #' @field n The number of graphs.
   n = 0,

   #' @field sizes Number of vertices for each of the graphs.
   sizes = NULL,

   #' @field lengths Total edge lengths for each of the graphs.
   lengths = NULL,

   #' Create metric graphs for connected components
   #'
  #' @param edges A list containing coordinates as `m x 2` matrices (that is, of `matrix` type) or m x 2 data frames (`data.frame` type) of sequence of points connected by straightlines. Alternatively, you can also prove an object of type `SpatialLinesDataFrame` or `SpatialLines` (from `sp` package) or `MULTILINESTRING` (from `sf` package).
   #' @param V n x 2 matrix with Euclidean coordinates of the n vertices.
   #' @param E m x 2 matrix where each row represents an edge.
  #' @param vertex_unit The unit in which the vertices are specified. The options are 'degree' (the great circle distance in km), 'km', 'm' and 'miles'. The default is `NULL`, which means no unit. However, if you set `length_unit`, you need to set `vertex_unit`.
  #' @param length_unit The unit in which the lengths will be computed. The options are 'km', 'm' and 'miles'. The default is `vertex_unit`. Observe that if `vertex_unit` is `NULL`, `length_unit` can only be `NULL`.
  #' If `vertex_unit` is 'degree', then the default value for `length_unit` is 'km'.
  #' @param longlat If TRUE, then it is assumed that the coordinates are given.
  #' in Longitude/Latitude and that distances should be computed in meters. It takes precedence over
  #' `vertex_unit` and `length_unit`, and is equivalent to `vertex_unit = 'degree'` and `length_unit = 'm'`.
   #' @param tolerance Vertices that are closer than this number are merged when
   #' constructing the graph (default = 1e-10). If `longlat = TRUE`, the
   #' tolerance is given in km.
   #' @param by_length Sort the components by total edge length? If `FALSE`,
   #' the components are sorted by the number of vertices.
    #' @param edge_weights Either a number, a numerical vector with length given by the number of edges, providing the edge weights, or a `data.frame` with the number of rows being equal to the number of edges, where
   #' @param ... Additional arguments used when specifying the graphs
   #' @param lines `r lifecycle::badge("deprecated")` Use `edges` instead.
   #' @return A `graph_components` object.
   initialize = function(edges = NULL,
                         V = NULL,
                         E = NULL,
                         by_length = TRUE,
                         edge_weights = NULL,
                         ...,
                         lines = deprecated()) {

      if (lifecycle::is_present(lines)) {
         if (is.null(edges)) {
           lifecycle::deprecate_warn("1.2.0", "graph_components$new(lines)", "graph_components$new(edges)",
             details = c("`lines` was provided but not `edges`. Setting `edges <- lines`.")
           )
           edges <- lines
         } else {
           lifecycle::deprecate_warn("1.2.0", "graph_components$new(lines)", "graph_components$new(edges)",
             details = c("Both `edges` and `lines` were provided. Only `edges` will be considered.")
           )
         }
         lines <- NULL
       }

      dots_args <- list(...)
      dots_list <- as.list(dots_args)

      if (is.null(dots_args$verbose)) {
        verbose <- 1 
      } else {
        verbose <- dots_args$verbose
      }

      if(!is.null(dots_list[["project_data"]])){
        warning("The argument project_data is not compatible with graph_components. Setting project_data to FALSE.")
        dots_list[["project_data"]] <- FALSE
        dots_list[["edges"]] <- edges
        dots_list[["V"]] <- V
        dots_list[["E"]] <- E
        dots_list[["check_connected"]] <- FALSE
        dots_list[["edge_weights"]] <- edge_weights
        graph <- do.call(metric_graph$new, dots_list)
      } else{
            graph <- metric_graph$new(edges = edges, V = V, E = E,
                               check_connected = FALSE, edge_weights = edge_weights,...)
      }

    
     # Making a combinatorial graph to extract the components

     if(verbose > 0){
      message("Extracting components...")
     }

     g <- make_graph(edges = c(t(graph$E)), directed = FALSE)

    if(!is.null(edge_weights)){
      edge_weights <- graph$get_edge_weights(data.frame=TRUE)
    }

     igraph::E(g)$weight <- graph$edge_lengths
    #  components <- igraph::clusters(g, mode="weak")
    components <- igraph::components(g, mode="weak")

    self$n <- components$no

    dots_list[["longlat"]] <- graph$.__enclos_env__$private$longlat
    dots_list[["crs"]] <- graph$.__enclos_env__$private$crs
    dots_list[["proj4string"]] <- graph$.__enclos_env__$private$proj4string
    dots_list[["which_longlat"]] <- graph$.__enclos_env__$private$which_longlat
    dots_list[["check_connected"]] <- FALSE

    if(is.null(edge_weights)){
      edge_weights <- graph$.__enclos_env__$private$edge_weights
    }

    data_tmp <- graph$.__enclos_env__$private$data

    if(verbose > 0){
      message("Constructing graphs...")
    }

     if(self$n > 1) {
       self$graphs <- vector(mode = "list", length = self$n)
       for(k in 1:self$n) {
         if(verbose > 0){
           message(paste("Processing component", k))
         }
         vert_ids <- igraph::V(g)[components$membership == k]
         edge_rem <- NULL
        if(verbose == 2){
           message("Detecting the edges of the component...")
         }
         # Vectorized operation to identify edges to remove
        edge_rem <- which(!(graph$E[, 1] %in% vert_ids) & !(graph$E[, 2] %in% vert_ids))
         if(verbose == 2){
           message("Processing the edges to keep...")
         }
         edge_keep <- setdiff(1:graph$nE, edge_rem)
         ind_keep <- rep(0,graph$nE)
         ind_keep[edge_keep] <- 1
         if(is.null(edge_weights)){
          ew_tmp <- NULL
         } else{
          if(verbose == 2){
            message("Processing the edge weights...")
          }
          if(is.vector(edge_weights)){
            ew_tmp <- edge_weights[which(ind_keep!=0)]
          } else{
            ew_tmp <- edge_weights[which(ind_keep!=0), , drop= FALSE]
          }
         }
         if(!is.null(data_tmp)){
          if(verbose == 2){
            message("Processing the data...")
          }
          add_obs_opts <- dots_list[["add_obs_options"]]
          if(is.null(add_obs_opts)){
            add_obs_opts <- list()
          }
          idx_obs_add <- (data_tmp[[".edge_number"]]%in%edge_keep)
          data_tmp_graph <- lapply(data_tmp, function(dat){dat[idx_obs_add]})
          data_tmp_graph[[".edge_number"]] <- match(data_tmp_graph[[".edge_number"]], edge_keep)
          class(data_tmp_graph) <- "metric_graph_data"
          add_obs_opts[["data"]] <- data_tmp_graph
         }
         if(length(graph$edges[which(ind_keep!=0)]) > 0){
          if(verbose > 0){
            message("Starting graph construction...")
          }
          dots_list[["edges"]] <- graph$edges[which(ind_keep!=0)]
          dots_list[["edge_weights"]] <- ew_tmp
          self$graphs[[k]] = do.call(metric_graph$new, dots_list)
          if(!is.null(data_tmp)){
            do.call(self$graphs[[k]]$add_observations, add_obs_opts)
          }
         }
       }
       for(i in self$n:1){
        if(is.null(self$graphs[[i]])){
          self$graphs[[i]] <- NULL
          self$n <- self$n - 1
        }
       }
       self$sizes <- components$csize
       self$lengths <- unlist(lapply(1:self$n,
                                     function(x) sum(self$graphs[[x]]$edge_lengths)))
       if(inherits(self$graphs[[1]]$get_edge_lengths(), "units")){
        units(self$lengths) <- units(self$graphs[[1]]$get_edge_lengths())
       }

       if(by_length) {
         reo <- order(self$lengths, decreasing = TRUE)
       } else {
         reo <- sort(self$sizes, decreasing = TRUE)
       }
       self$graphs <- self$graphs[reo]
       self$lengths <- self$lengths[reo]
       self$sizes <- self$sizes[reo]
     } else {
       self$graphs <- list(graph)
       self$lengths <- sum(graph$edge_lengths)
       self$sizes <- graph$nV
     }
   },

   #' @description Returns the largest component in the graph.
   #' @return A `metric_graph` object.
   get_largest = function() {
     return(self$graphs[[1]])
   },

   #' @description Plots all components.
   #' @param edge_colors A 3 x nc matrix with RGB values for the edge colors to
   #' be used when plotting each graph.
   #' @param vertex_colors A 3 x nc matrix with RGB values for the edge colors to
   #' be used when plotting each graph.
   #' @param ... Additional arguments for plotting the individual graphs.
   #' @return A `ggplot` object.
   plot = function(edge_colors = NULL, vertex_colors = NULL, ...) {

     if (is.null(edge_colors)) {
       edge_colors <- matrix(0, nrow = self$n, ncol = 3)
       if(self$n > 1) {
        for(i in 2:self$n) {
         edge_colors[i, ] = runif(3)
        }
       }
     } else {
       if (ncol(edge_colors)!= 3) {
         stop("edge_colors must have three columns!")
       }
       if (nrow(edge_colors)!= self$n) {
         stop("edge_colors must have the same number of rows as there are components!")
       }
     }
     if (is.null(vertex_colors)) {
       vertex_colors <- edge_colors
     } else {
       if (ncol(vertex_colors)!= 3) {
         stop("vertex_colors must have three columns!")
       }
       if (nrow(vertex_colors)!= self$n) {
         stop("vertex_colors must have the same number of rows as there are components!")
       }
     }
     p <- self$graphs[[1]]$plot(edge_color = rgb(edge_colors[1, 1],
                                                 edge_colors[1, 2],
                                                 edge_colors[1, 3]),
                                vertex_color = rgb(vertex_colors[1, 1],
                                                   vertex_colors[1, 2],
                                                   vertex_colors[1, 3]), ...)
     if (self$n > 1) {
       for(i in 2:self$n){
         suppressMessages(p <- self$graphs[[i]]$plot(edge_color = rgb(edge_colors[i, 1],
                                                     edge_colors[i, 2],
                                                     edge_colors[i, 3]),
                                    vertex_color = rgb(vertex_colors[i, 1],
                                                       vertex_colors[i, 2],
                                                       vertex_colors[i, 3]),
                                    p = p, ...))
       }
     }
     return(p)
   }))

Try the MetricGraph package in your browser

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

MetricGraph documentation built on April 3, 2025, 10:34 p.m.