R/xmapbridge.R

Defines functions xmap.points xmap.plot xmap.project.delete xmap.graph.delete xmap.plot.delete xmap.project.list xmap.graph.list xmap.plot.list xmap.project.save xmap.graph.save xmap.plot.save xmap.project.load xmap.graph.load xmap.plot.load xmap.project.new xmap.graph.new xmap.plot.new xmap.col xmap.write.file.info xmap.read.file.info touch.project xmap.graph.valid xmap.project.valid make.xmap.child.file validate.x.and.y make.xmap.project.folder get.xmap.cache.dir xmap.debug

Documented in xmap.col xmap.debug xmap.graph.delete xmap.graph.list xmap.graph.load xmap.graph.new xmap.graph.save xmap.plot xmap.plot.delete xmap.plot.list xmap.plot.load xmap.plot.new xmap.plot.save xmap.points xmap.project.delete xmap.project.list xmap.project.load xmap.project.new xmap.project.save

#                               8             o      8               
#                               8                    8               
#  `o  o' ooYoYo. .oPYo. .oPYo. 8oPYo. oPYo. o8 .oPYo8 .oPYo. .oPYo. 
#   `bd'  8' 8  8 .oooo8 8    8 8    8 8  `'  8 8    8 8    8 8oooo8 
#   d'`b  8  8  8 8    8 8    8 8    8 8      8 8    8 8    8 8.     
#  o'  `o 8  8  8 `YooP8 8YooP' `YooP' 8      8 `YooP' `YooP8 `Yooo' 
#                        8                                  8        
#                        8                              YooP'
#
# R library for outputting graphs in the file structure required by
# the XMapBridge java application so that they can be displayed on
# top of the X:Map genome browser.
#
# Author:  Tim Yates and Crispin J Miller
# Date:    2008/08/13
# Licence: LGPL-v3

###############################################################################
## Our id classes
###############################################################################

setClass( "Project", 
          representation=representation( project="character" ),
          prototype( project="" ),
          validity=function( object ) { 
            .ret <- TRUE
            if( !.xmap.project.valid( object@project ) )
              .ret <- paste( object@project, "is not a valid project folder" )
            .ret
          } )
setClass( "Graph",
          contains="Project",
          representation=representation( graph="character", minx="numeric" ),
          prototype( graph="", minx=-1 ),
          validity=function( object ) { 
            .ret <- TRUE
            .path <- file.path( object@project, object@graph )
            if( !.xmap.graph.valid( .path ) )
              .ret <- paste( .path, "is not a valid graph folder" )
            if( object@minx < 0 )
              .ret <- paste( "'minx' must be a positive number" )
            .ret
          } )
setClass( "Plot",
          contains="Graph",
          representation=representation( plot="character" ),
          prototype( plot="" ),
          validity=function( object ) { 
            .ret <- TRUE
            .path <- file.path( object@project, object@graph, object@plot )
            if( !file.exists( .path ) )
              .ret <- paste( .path, "is not a valid plot file" )
            .ret
          } )

###############################################################################
## Our id methods
###############################################################################

xmap.debug <- function( idobject, newlines=FALSE ) {
  .ret <- "UNKNOWN"
  if( inherits( idobject, "Project" ) ) {
    .ret <- paste( "xmapbridge:Project(", idobject@project, ")" )
    if( newlines ) {
      .ret <- paste( .ret, "\n" )
    }
  }
  else if( inherits( idobject, "Graph" ) ) {
    .pref <- paste( "xmapbridge:Graph(", idobject@graph, "@", paste( format( idobject@minx, scientific=FALSE ), "bp", sep="", collapse="" ), ")" )
    .suff <- xmap.debug( as( idobject, "Project" ), newlines=newlines )
    if( newlines ) {
      .ret <- paste( .pref, "\n  in ->", .suff )
    }
    else {
      .ret <- paste( .pref, "in", .suff )
    }
  }
  else if( inherits( idobject, "Plot" ) ) {
    .pref <- paste( "xmapbridge:Plot(", idobject@plot, ")" )
    .suff <- xmap.debug( as( idobject, "Graph" ), newlines=newlines )
    if( newlines ) {
      .ret <- paste( .pref, "\n  in ->", .suff )
    }
    else {
      .ret <- paste( .pref, "in", .suff )
    }
  }
  .ret
}

setMethod( "==", c( "Project", "Project" ), function( e1, e2 ) {   e1@project == e2@project } )
setMethod( "==", c( "Graph", "Graph" ),     function( e1, e2 ) { ( e1@project == e2@project ) && ( e1@graph == e2@graph ) && ( e1@minx == e2@minx ) } )
setMethod( "==", c( "Plot", "Plot" ),       function( e1, e2 ) { ( e1@project == e2@project ) && ( e1@graph == e2@graph ) && ( e1@minx == e2@minx ) && ( e1@plot == e2@plot ) } )
setMethod( "!=", c( "Project", "Project" ), function( e1, e2 ) {   e1@project != e2@project } )
setMethod( "!=", c( "Graph", "Graph" ),     function( e1, e2 ) { ( e1@project != e2@project ) || ( e1@graph != e2@graph ) || ( e1@minx != e2@minx ) } )
setMethod( "!=", c( "Plot", "Plot" ),       function( e1, e2 ) { ( e1@project != e2@project ) || ( e1@graph != e2@graph ) || ( e1@minx != e2@minx ) || ( e1@plot != e2@plot ) } )

if( is.null( getGeneric( "show" ) ) ) setGeneric( "show", function( object ) standardGeneric( "show" ) )
setMethod( "show", "Project", function( object ) { cat( xmap.debug( object, newlines=TRUE ) ) } )
setMethod( "show", "Graph",   function( object ) { cat( xmap.debug( object, newlines=TRUE ) ) } )
setMethod( "show", "Plot",    function( object ) { cat( xmap.debug( object, newlines=TRUE ) ) } )

setMethod( "as.character", "Project", function( x, ... ) { show( x ) } )
setMethod( "as.character", "Graph", function( x, ... ) { show( x ) } )
setMethod( "as.character", "Plot", function( x, ... ) { show( x ) } )

setGeneric( "xmap.project.folder", function( object, ... ) standardGeneric( "xmap.project.folder" ) ) ;
setMethod( "xmap.project.folder", "Project", function( object, ... ) { object@project } )
setMethod( "xmap.project.folder", "Graph",   function( object, ... ) { object@project } )
setMethod( "xmap.project.folder", "Plot",    function( object, ... ) { object@project } )

setGeneric( "xmap.graph.folder", function( object, ... ) standardGeneric( "xmap.graph.folder" ) ) ;
setMethod( "xmap.graph.folder", "Graph", function( object, ... ) { file.path( object@project, object@graph ) } )
setMethod( "xmap.graph.folder", "Plot",  function( object, ... ) { file.path( object@project, object@graph ) } )

setGeneric( "xmap.plot.file", function( object, ... ) standardGeneric( "xmap.plot.file" ) ) ;
setMethod( "xmap.plot.file", "Plot", function( object, ... ) { file.path( object@project, object@graph, object@plot ) } )

setGeneric( "xmap.graph.minx", function( object, ... ) standardGeneric( "xmap.graph.minx" ) ) ;
setMethod( "xmap.graph.minx", "Graph", function( object, ... ) { object@minx } )
setMethod( "xmap.graph.minx", "Plot",  function( object, ... ) { object@minx } )

###############################################################################
## Private Utility Methods
###############################################################################

.get.xmap.cache.dir <- function() {
  d <- Sys.getenv( "XMAP_BRIDGE_CACHE" )[[ 1 ]]
  if( d == "" ) {
    # Check for .xmb_cache folder in USER.HOME
    d <- file.path( path.expand( "~" ), ".xmb_cache" )
    if( !file.exists( d ) ) {
      .create <- readline( paste( "Cache environment variable not found.",
                                  "Is it O.K. to create", d, "? [y/N] " ) )
      if( substr( toupper( .create ), 1, 1 ) == "Y" ) {
        dir.create( d )
        if( !file.exists( d ) ) {
          stop( paste( "Sorry.  Could not create", d, "please check your system",
                       "permissions" ) ) ;
        }
      }
      else {
        stop( paste( "XMAP_BRIDGE_CACHE not set and no", d, "folder in home",
                     "directory. Please refer to the package installation",
                     "instructions for more details." ) )
      }
    }
  }
  d
}

.make.xmap.project.folder <- function() {
  .ok <- FALSE
  while( !.ok ) {
    .name <- file.path( .get.xmap.cache.dir(), paste( paste( format( Sys.time(), "%Y.%b.%d.%H.%M.%S" ), proc.time()[[1]], sep="_" ), ".xmb", sep="" ) )
    .ok <- !file.exists( .name )
  }
  dir.create( .name )
  .name
}

.validate.x.and.y <- function( x, y ) {
  if( missing( x ) ) {
    stop( "You need to specify values for x" )
  }
  if( !all( sapply( x, is.numeric ) ) ) {
    stop( "x seems to contain non-numerics" )
  }
  if( any( sapply( x, is.na ) ) ) {
    stop( "x cannot contain NA" )
  }
  if( any( sapply( x, is.nan ) ) ) {
    stop( "x cannot contain NaN" )
  }
  if( !all( sapply( x, is.finite ) ) ) {
    stop( "x cannot contain infinite numbers" )
  }
  if( missing( y ) ) {
    stop( "You need to specify values for y" )
  }
  if( !all( sapply( y, is.numeric ) ) ) {
    stop( "y seems to contain non-numerics" )
  }
  if( any( sapply( y, is.na ) ) ) {
    stop( "y cannot contain NA" )
  }
  if( any( sapply( y, is.nan ) ) ) {
    stop( "y cannot contain NaN" )
  }
  if( !all( sapply( y, is.finite ) ) ) {
    stop( "y cannot contain infinite numbers" )
  }
  if( length( x ) != length( y ) ) {
    stop( "'x' and 'y' lengths differ" )
  }
  if( length( x ) == 0 ) {
    stop( "lengths of 'x' and 'y' cannot be 0" )
  }
}

.make.xmap.child.file <- function( rootFolder, func ) {
  .x <- 1
  .ok <- FALSE
  .name <- ""
  .subname <- ""
  while( !.ok ) {
    .subname <- formatC( .x, width=8, flag="0" )
    .name <- file.path( rootFolder, .subname )
    .ok <- !file.exists( .name )
    .x <- .x + 1
  }
  func( .name )
  .subname
}

# Check to see if a folder is a valid project
.xmap.project.valid <- function( folder ) {
  file.exists( file.path( folder, "PROJECT" ) )
}

# Check to see if a folder is a valid graph
.xmap.graph.valid <- function( folder ) {
  file.exists( file.path( folder, "GRAPH" ) )
}

.touch.project <- function( itemid ) {
  .prj <- file.path( xmap.project.folder( itemid ), "PROJECT" )
  .data <- .xmap.read.file.info( .prj )
  .xmap.write.file.info( .prj, .data )
}

# read data from a file into a vector
.xmap.read.file.info <- function( file ) {
  if( is.character( file ) ) {
    file <- file( file, "r" )
    on.exit( close( file ) )
  }
  .ret <- list()
  if( !inherits( file, "connection" ) )
    stop( "'file' parameter must be a file object or a filename" )
  if( !isOpen( file, "r" ) ) {
    open( file, "r" )
    on.exit( close( file ) )
  }
  .ok <- TRUE
  while( .ok ) {
    .line <- readLines( file, 1 )
    if( length( .line ) == 0 ) {
      .ok <- FALSE
    }
    else if( .line[[1]] == '-' ) {
      .ok <- FALSE
    } 
    else {
      .elements <- strsplit( .line[[1]], "=" )
      .value <- paste( .elements[[1]][2:length(.elements[[1]])], collapse="=" )
      .ret[ .elements[[1]][1] ] <- tryCatch( { as.numeric( .value ) }, warning=function(e) { .value } )
    }
  }
  .ret
}

.xmap.write.file.info <- function( file, data ) {
  if( is.character( file ) ) {
    file <- file( file, "w" )
    on.exit( close( file ) )
  }
  .ret <- c()
  if( !inherits( file, "connection" ) )
    stop( "'file' parameter must be a file object or a filename" )
  if( !isOpen( file, "w" ) ) {
    open( file, "w" )
    on.exit( close( file ) )
  }
  data <- data[ sapply( data, function( a ) { !is.null( a ) } ) ]
  data <- lapply( data, function( a ) { ifelse( is.numeric( a ), format( a, scientific=FALSE ), a ) } )
  writeLines( paste( names( data ), data, sep="=" ), con=file )
}

###############################################################################
## Colour manipulation method (to add alpha)
###############################################################################

xmap.col <- function( col, alpha ) {
  c.ints <- col2rgb( col, alpha=T )

  if( !missing( alpha ) ) { 
    c.ints[4,] <- alpha
  }
  apply( c.ints, 2, function( a ) { ( a[4] * 256 * 256 * 256 ) +
                                    ( a[1] * 256 * 256 ) +
                                    ( a[2] * 256 ) +
                                      a[3] } )
}

###############################################################################
## Creation Methods
###############################################################################

xmap.plot.new <- function( graphid, name, x, y, type=c("scatter","line","bar","step","area","steparea"), col=NULL, dp=2 ) {
  if( !is( graphid, "Graph" ) ) {
    stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
  }
  type <- match.arg( type )
  if( is.character( col ) ) { col <- xmap.col(col) }
  .validate.x.and.y( x, y )
  .graph <- xmap.graph.folder( graphid )
  if( !.xmap.graph.valid( .graph ) ) {
    stop( paste( graphid, "is not a valid graph" ) )
  }
  .filename <- .make.xmap.child.file( .graph, file.create )
  .plot <- new( "Plot", project=graphid@project, graph=graphid@graph, minx=graphid@minx, plot=.filename )
  .data <- list( NAME=name, TYPE=type, COLOR=col )
  xmap.plot.save( .plot, .data, x, y, dp )
  .plot
}
xmap.graph.new <- function( projectid, name, desc, min, max, chr, start, stop, ylab="value", species=c("homo_sapiens", "mus_musculus", "rattus_norvegicus") ) {
  if( !is( projectid, "Project" ) ) {
    stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
  }
  species <- match.arg( species )
  .prj <- xmap.project.folder( projectid )
  if( !.xmap.project.valid( .prj ) ) {
    stop( paste( projectid, "is not a valid project" ) )
  }

  if( is.nan( min ) )        { stop( "min cannot be NaN" ) ; }
  if( !is.numeric( min ) )   { stop( "min must be numeric" ) ; }
  if( is.nan( max ) )        { stop( "min cannot be NaN" ) ; }
  if( !is.numeric( max ) )   { stop( "min must be numeric" ) ; }
  if( is.nan( start ) )      { stop( "start cannot be NaN" ) ; }
  if( !is.numeric( start ) ) { stop( "start must be numeric" ) ; }
  if( is.nan( stop ) )       { stop( "stop cannot be NaN" ) ; }
  if( !is.numeric( stop ) )  { stop( "stop must be numeric" ) ; }
  if( start < 1 ) stop( "start cannot be less than 1" ) ;
  if( stop <= start ) stop( "stop must be greater than start" ) ;

  .data <- list( NAME=name, DESC=desc, MIN=min, MAX=max, CHR=chr, START=floor(start), STOP=ceiling(stop), YAXIS=ylab, SPECIES=species )
  .folder <- .make.xmap.child.file( .prj, dir.create )
  .path <- file.path( .prj, .folder, "GRAPH" )
  file.create( .path )
  .graph <- new( "Graph", project=projectid@project, graph=.folder, minx=start )
  xmap.graph.save( .graph, .data )
  .graph
}
xmap.project.new <- function( name ) {
  .folder <- .make.xmap.project.folder()
  .path <- file.path( .folder, "PROJECT" )
  file.create( .path )
  .data <- c( "NAME"=name )
  .project <- new( "Project", project=.folder )
  xmap.project.save( .project, .data )
  .project
}

###############################################################################
## Load Methods
###############################################################################

xmap.plot.load <- function( plotid ) {
  if( !is( plotid, "Plot" ) ) {
    stop( paste( "plotid should be an object of class Plot, not", class( plotid ) ) )
  }
  .xmap.read.file.info( xmap.plot.file( plotid ) )
}
xmap.graph.load <- function( graphid ) {
  if( !is( graphid, "Graph" ) ) {
    stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
  }
  .xmap.read.file.info( file.path( xmap.graph.folder( graphid ), "GRAPH" ) )
}
xmap.project.load <- function( projectid ) {
  if( !is( projectid, "Project" ) ) {
    stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
  }
  .xmap.read.file.info( file.path( xmap.project.folder( projectid ), "PROJECT" ) )
}

###############################################################################
## Save Methods
###############################################################################

xmap.plot.save <- function( plotid, data, x, y, dp=2 ) {
  if( !is( plotid, "Plot" ) ) {
    stop( paste( "plotid should be an object of class Plot, not", class( plotid ) ) )
  }
  .validate.x.and.y( x, y )
  .file <- xmap.plot.file( plotid )
  .xmap.write.file.info( .file, data )
  write( "-", .file, append=TRUE )
  tryCatch( {
    mapply( function( a, b ) {
      a <- round( a - plotid@minx )
      b <- round( b, dp )
      r <- paste( paste( a, b, sep=",", collapse="," ), "\n", sep="", collapse="" )
      cat( r, file=.file, append=TRUE )
    }, x, y )
  }, warning=function(e) {
    stop( e$message )
  } )
  .touch.project( plotid )
}
xmap.graph.save <- function( graphid, data ) {
  if( !is( graphid, "Graph" ) ) {
    stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
  }
  .xmap.write.file.info( file.path( xmap.graph.folder( graphid ), "GRAPH" ), data )
  .touch.project( graphid )
}
xmap.project.save <- function( projectid, data ) {
  if( !is( projectid, "Project" ) ) {
    stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
  }
  .xmap.write.file.info( file.path( xmap.project.folder( projectid ), "PROJECT" ), data )
}

###############################################################################
## List methods
###############################################################################

xmap.plot.list <- function( graphid ) {
  if( !is( graphid, "Graph" ) ) {
    stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
  }
  .files <- list.files( xmap.graph.folder( graphid ) )
  if( length( .files ) != 0 ) {
    .files <- .files[ sapply( .files, function( name ) { 
      name != "GRAPH"
    } ) ]
    sapply( .files, function( f ) {
      new( "Plot", project=graphid@project, graph=graphid@graph, minx=graphid@minx, plot=f )
    } )
  }
}
xmap.graph.list <- function( projectid ) {
  if( !is( projectid, "Project" ) ) {
    stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
  }
  .prj <- xmap.project.folder( projectid )
  .files <- list.files( .prj )
  if( length( .files ) != 0 ) {
    .files <- .files[ sapply( .files, function( name ) { 
      .xmap.graph.valid( file.path( .prj, name ) ) 
    } ) ]
    .files <- sapply( .files, function( f ) { 
      f <- new( "Graph", project=projectid@project, graph=f, minx=0 )
    } )
    # load the minx value out of the data file
    sapply( .files, function( f ) {
      .info <- .xmap.read.file.info( file.path( xmap.graph.folder( f ), "GRAPH" ) )
      f@minx <- as.numeric( .info$START )
      f
    } )
  }
}
xmap.project.list <- function() {
  .files <- list.files( .get.xmap.cache.dir(), full.names=TRUE )
  if( length( .files ) != 0 ) {
    .files <- .files[ sapply( .files, .xmap.project.valid ) ]
    sapply( .files, function( f ) {
      f <- new( "Project", project=f )
    } )
  }
}

###############################################################################
## Delete Methods
###############################################################################

xmap.plot.delete <- function( plotid ) {
  if( !is( plotid, "Plot" ) ) {
    stop( paste( "plotid should be an object of class Plot, not", class( plotid ) ) )
  }
  unlink( xmap.plot.file( plotid ) )
}
xmap.graph.delete <- function( graphid ) {
  if( !is( graphid, "Graph" ) ) {
    stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
  }
  unlink( xmap.graph.folder( graphid ), recursive=TRUE )
}
xmap.project.delete <- function( projectid ) {
  if( !is( projectid, "Project" ) ) {
    stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
  }
  unlink( xmap.project.folder( projectid ), recursive=TRUE )
}

###############################################################################
## Utility methods to let people use this like the existing plot and points
## graph methods
###############################################################################

.xmap.env <- new.env( hash=TRUE, parent=emptyenv() )

xmap.plot <- function( x, y, 
                      species=c("homo_sapiens", "mus_musculus", "rattus_norvegicus"), 
                      chr, xlim, ylim, main, sub, 
                      type=c("scatter","line","bar","step","area","steparea"), 
                      xlab, ylab="value", col, dp=2, projectid ) {
  .validate.x.and.y( x, y )
  species <- match.arg(species)
  if( missing( chr ) ) {
    stop( "You need to specify a chromosome for this plot" )
  }
  .project.created <- FALSE
  if( missing( projectid ) ) {
    projectid <- .xmap.env$project
    if( is.null( projectid ) || !.xmap.project.valid( xmap.project.folder( projectid ) ) ) {
      projectid <- xmap.project.new( format( Sys.time(), "Project created %d %b %Y" ) )
      .xmap.env$project <- projectid
      .project.created <- TRUE
    }
  }
  else {
    if( !is( projectid, "Project" ) ) {
      stop( paste( "projectid should be an object of class Project, not", class( projectid ) ) )
    }
  }
  tryCatch( {
    if( !.xmap.project.valid( xmap.project.folder( projectid ) ) ) {
      rm( "project", envir=.xmap.env )
      stop( paste( projectid, "is not a valid project" ) )
    }
    if( missing( col ) )  { col <- NULL }
    if( missing( sub ) )  { sub <- NULL }
    if( missing( main ) ) { main <- format( Sys.time(), "Graph created %H:%M:%S" ) }
    if( missing( xlim ) ) { xlim <- range( x ) }
    .xmap.env$xlim <- xlim
    if( missing( ylim ) ) { ylim <- range( c( 0, y ) ) }
    tryCatch( {
      graph <- xmap.graph.new( projectid, main, sub, ylim[ 1 ], ylim[ 2 ], chr, xlim[ 1 ], xlim[ 2 ], ylab, species )
      .xmap.env$graph <- graph
      .xmap.env$plotNo <- 1
      xmap.points( x, y, type, xlab, col, dp )
    },
    error=function( err ) {
      if( exists( "graph" ) ) {
        xmap.graph.delete( graph )
        rm( "graph", envir=.xmap.env )
      }
      stop( paste( err$message, "[dg]" ) )
    } )
  },
  error=function( err ) {
    if( .project.created ) {
      xmap.project.delete( projectid )
      rm( "project", envir=.xmap.env )
      stop( paste( err$message, "[dp]" ) )
    }
    stop( err$message )
  } )
}

xmap.points <- function( x, y, 
                        type=c("scatter","line","bar","step","area","steparea"), 
                        xlab, col, dp=2, graphid ) {
  .validate.x.and.y( x, y )
  if( missing( graphid ) ) {
    graphid <- .xmap.env$graph
    if( is.null( graphid ) || !.xmap.graph.valid( xmap.graph.folder( graphid ) ) ) {
      stop( "You must call xmap.plot or specify a graph to call xmap.points" )
    }
  }
  else {
    if( !is( graphid, "Graph" ) ) {
      stop( paste( "graphid should be an object of class Graph, not", class( graphid ) ) )
    }
  }
  if( !.xmap.project.valid( xmap.project.folder( graphid ) ) ) {
    stop( "Project for graph invalid" )
    rm( "project", envir=.xmap.env ) ;
  }
  .plotNo <- .xmap.env$plotNo
  if( is.null( .plotNo ) ) {
    .plotNo <- 1
  }
  if( missing( xlab ) ) { xlab <- paste( "Plot", .plotNo )  }
  if( missing( col ) ) { col <- NULL }
  tryCatch( {
    .plot <- xmap.plot.new( graphid, xlab, x, y, type, col, dp )
    .xmap.env$plotNo <- .plotNo + 1
    .plot
  },
  error=function( err ) {
    if( exists( ".plot" ) ) {
      xmap.plot.delete( .plot )
    }
    stop( err$message )
  } )
}

Try the xmapbridge package in your browser

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

xmapbridge documentation built on Nov. 1, 2018, 2:05 a.m.