├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── NEWS ├── NEWS.md ├── R ├── sync.R └── utils.R ├── README.Rmd ├── README.md ├── inst └── htmlwidgets │ └── lib │ └── Leaflet.Sync │ ├── L.Map.Sync.js │ ├── LICENSE │ └── bower.json ├── leafsync.Rproj └── man ├── figures └── README-sync.png └── latticeView.Rd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^\.travis\.yml$ 2 | ^CODE_OF_CONDUCT\.md$ 3 | ^README\.Rmd$ 4 | ^.*\.Rproj$ 5 | ^\.Rproj\.user$ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | language: r 3 | sudo: required 4 | dist: xenial 5 | cache: packages 6 | latex: true 7 | 8 | r: 9 | - release 10 | - devel 11 | - oldrel 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who 4 | contribute through reporting issues, posting feature requests, updating documentation, 5 | submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for 8 | everyone, regardless of level of experience, gender, gender identity and expression, 9 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 10 | 11 | Examples of unacceptable behavior by participants include the use of sexual language or 12 | imagery, derogatory comments or personal attacks, trolling, public or private harassment, 13 | insults, or other unprofessional conduct. 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 16 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 17 | Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed 18 | from the project team. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 21 | opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the Contributor Covenant 24 | (http://contributor-covenant.org), version 1.0.0, available at 25 | http://contributor-covenant.org/version/1/0/0/ 26 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: leafsync 2 | Type: Package 3 | Title: Small Multiples for Leaflet Web Maps 4 | Version: 0.1.1.9002 5 | Authors@R: c( 6 | person("Tim", "Appelhans", role = c("aut", "cre"), email = "tim.appelhans@gmail.com"), 7 | person("Kenton", "Russell", role = c("aut")), 8 | person("Christoph", "Stepper", role = c("ctb")), 9 | person("Jan Pieter", "Wagmeester", role = c("ctb"), comment = "Leaflet.Sync plugin") 10 | ) 11 | Maintainer: Tim Appelhans 12 | Description: Create small multiples of several leaflet web maps with (optional) 13 | synchronised panning and zooming control. When syncing is enabled all maps 14 | respond to mouse actions on one map. This allows side-by-side comparisons 15 | of different attributes of the same geometries. Syncing can be adjusted 16 | so that any combination of maps can be synchronised. 17 | License: MIT + file LICENSE 18 | URL: https://github.com/r-spatial/leafsync 19 | BugReports: https://github.com/r-spatial/leafsync/issues 20 | Depends: 21 | R (>= 3.1.0), 22 | methods 23 | Imports: 24 | htmltools (>= 0.3), 25 | htmlwidgets, 26 | leaflet (>= 2.0.1) 27 | Encoding: UTF-8 28 | LazyData: true 29 | RoxygenNote: 7.1.2 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Tim Appelhans 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(latticeView) 4 | export(latticeview) 5 | export(sync) 6 | importFrom(methods,slot) 7 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | leafsync 0.1.1 2 | 3 | new features: 4 | 5 | * new argument `full.height` to control widget sizing. Default is `TRUE`. #1 6 | 7 | leafsync 0.1.0 8 | 9 | * Initial release 10 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | ## leafsync 0.1.1.9002 (2022-03-03) 2 | 3 | #### ✨ features and improvements 4 | 5 | * new argument `between` to specify space between panels (in pixels). 6 | 7 | #### 🐛 bug fixes 8 | 9 | * wrap panels into one div to avoid wrapped text around panels. #5 10 | 11 | #### 💬 documentation etc 12 | 13 | #### 🍬 miscellaneous 14 | 15 | 16 | ## leafsync 0.1.1 17 | 18 | new features: 19 | 20 | * new argument `full.height` to control widget sizing. Default is `TRUE`. #1 21 | 22 | ## leafsync 0.1.0 23 | 24 | * Initial release 25 | -------------------------------------------------------------------------------- /R/sync.R: -------------------------------------------------------------------------------- 1 | #' View two or more (possibly synchronised) mapview or leaflet maps 2 | #' 3 | #' @description 4 | #' This function produces a lattice like view of two or more maps. It is possible to sync 5 | #' any combination of panels or all or none. For synchronising all panels it 6 | #' is best to use the provided convenience function \code{sync}. 7 | #' 8 | #' @param ... any number of mapview or leaflet objects or a list thereof 9 | #' @param ncol how many columns should be plotted 10 | #' @param full.height \code{logical}; should the produced lattice view be 11 | #' scaled to the complete height of the viewer (or browser); default to \code{TRUE}. 12 | #' If \code{FALSE}, the height of each panel falls back to the default 13 | #' sizing policy height of leaflet (400px). 14 | #' @param sync whether to synchronise zoom and pan for certain elements. 15 | #' Possible values are "all" (default) to sync all maps, "none" to disable 16 | #' synchronisation or a list of panel numbers, e.g. \code{list(c(1, 3), c(2, 4))} 17 | #' will synchronise panels 1 & 3 and panels 2 & 4. Panels are drawn from top right 18 | #' to bottom left. 19 | #' @param sync.cursor whether to show cursor position in synced panels (default TRUE). 20 | #' @param no.initial.sync whether to sync the initial view (default TRUE). 21 | #' @param between a named list with components "x" and "y" specifying the space 22 | #' between panels in pixels . "x" refers to CSS property "margin-right", 23 | #' "y" refers to CSS property "margin-top". 24 | #' Default is `list(x = "2px", y = "2px")`. 25 | #' 26 | #' @examples 27 | #' if (interactive()) { 28 | #' library(leaflet) 29 | #' library(leafsync) 30 | #' 31 | #' m1 = leaflet() %>% 32 | #' addTiles() %>% 33 | #' addPolygons(data = gadmCHE) 34 | #' 35 | #' m2 = leaflet() %>% 36 | #' addProviderTiles("Esri.WorldImagery") %>% 37 | #' addCircleMarkers(data = breweries91, 38 | #' color = "black", 39 | #' opacity = 0.9, 40 | #' fillColor = "white", 41 | #' fillOpacity = 0.9) 42 | #' 43 | #' # synced 44 | #' sync(m1, m2) 45 | #' sync(m1, m2, no.initial.sync = TRUE) 46 | #' 47 | #' # not synced 48 | #' latticeview(m1, m2) 49 | #' latticeview(m1, m2, ncol = 1) 50 | #' } 51 | #' 52 | #' @export latticeView 53 | #' @name latticeView 54 | #' @aliases latticeView 55 | #' 56 | 57 | latticeView <- function(..., 58 | ncol = 2, 59 | full.height = TRUE, 60 | sync = "none", 61 | sync.cursor = FALSE, 62 | no.initial.sync = TRUE, 63 | between = list(x = "2px", y = "2px")) { 64 | 65 | ## convert all ... objects to list or extract list if list was passed 66 | ls <- list(...) 67 | if (length(ls) == 1) ls <- ls[[1]] 68 | 69 | for (i in seq(ls)) { 70 | # convert each list element to class leaflet (if necessary) 71 | if (inherits(ls[[i]], "mapview")) ls[[i]] <- mapview2leaflet(ls[[i]]) 72 | if (length(ls[[i]]$dependencies) == 0) { 73 | ls[[i]]$dependencies = list() 74 | } 75 | 76 | # modify the sizingPolicy for full screen heights 77 | hght = "400px" 78 | if (full.height == TRUE) { 79 | nrow = round(length(ls) / ncol, 0) 80 | hght = 100 / nrow - 1 81 | ls[[i]]$sizingPolicy$defaultHeight = paste0(hght, "vh") 82 | } 83 | 84 | #ls[[i]]$dependencies[[length(ls[[i]]$dependencies) + 1]] <- sync_dep 85 | # give a "unique" id to each leaflet map for lookup 86 | if (is.null(ls[[i]]$elementId)) { 87 | # use unique same id generator as htmlwidgets 88 | # https://github.com/ramnathv/htmlwidgets/blob/master/R/htmlwidgets.R#L165 89 | ls[[i]]$elementId <- paste("htmlwidget", as.integer(stats::runif(1, 1, 10000)), sep = "-") 90 | } 91 | } 92 | 93 | ## calculate div width depending on ncol and set div style 94 | between = utils::modifyList( 95 | list(x = "2px", y = "2px") 96 | , between 97 | ) 98 | wdth <- paste0("width:", round(1 / ncol * 100, 0) - 1, "%;") 99 | styl <- paste0( 100 | "display:inline;" 101 | , wdth 102 | , "float:left;border-width:1px 1px 1px 1px;" 103 | , "margin-right:" 104 | , between$x 105 | , ";margin-top:" 106 | , between$y 107 | , ";" 108 | ) 109 | 110 | ## htmltools stuff ... ? 111 | tg <- lapply(seq(ls), function(i) { 112 | htmltools::tags$div(style = styl, ls[[i]]) 113 | }) 114 | 115 | ## string operations for syncing, depending on sync argument 116 | ## initialize sync_string as empty 117 | sync_strng <- "" 118 | if (!is.list(sync) && sync == "all") { 119 | sync = list(seq(ls)) 120 | } 121 | if (is.list(sync)) { 122 | for (i in seq(sync)) { 123 | synci <- sync[[i]] 124 | sync_grid <- expand.grid(synci,synci,KEEP.OUT.ATTRS = FALSE) 125 | sync_strng <- c(sync_strng,apply( 126 | sync_grid, 127 | MARGIN = 1, 128 | function(combo){ 129 | # don't sync to self 130 | if (combo[1] != combo[2]) { 131 | return(sprintf( 132 | "leaf_widgets['%s'].sync(leaf_widgets['%s'],{syncCursor: %s, noInitialSync: %s});", 133 | ls[[combo[1]]]$elementId, 134 | ls[[combo[2]]]$elementId, 135 | tolower(as.logical(sync.cursor)), 136 | tolower(as.logical(no.initial.sync)) 137 | )) 138 | } 139 | return("") 140 | } 141 | )) 142 | } 143 | } 144 | sync_strng <- paste0(sync_strng, collapse = "\n") 145 | 146 | tl <- htmltools::attachDependencies( 147 | htmltools::tagList( 148 | tg, 149 | htmlwidgets::onStaticRenderComplete( 150 | paste0('var leaf_widgets = {}; 151 | Array.prototype.map.call( 152 | document.querySelectorAll(".leaflet"), 153 | function(ldiv){ 154 | if (HTMLWidgets.find("#" + ldiv.id) && HTMLWidgets.find("#" + ldiv.id).getMap()) { 155 | leaf_widgets[ldiv.id] = HTMLWidgets.find("#" + ldiv.id).getMap(); 156 | } 157 | } 158 | ); 159 | ', 160 | sync_strng 161 | ) 162 | ) 163 | ), 164 | dependencyLeafletsync() 165 | ) 166 | 167 | return( 168 | htmltools::browsable( 169 | htmltools::div( 170 | tl 171 | , id = paste( 172 | "htmlwidget" 173 | , as.integer(stats::runif(1, 1, 10000)) 174 | , sep = "-" 175 | ) 176 | , style = paste0( 177 | "width: 100%; height: " 178 | , hght 179 | , "; position: relative" 180 | , "; background-color: transparent" 181 | , ";" 182 | ) 183 | , class = "leaflet html-widget html-widget-static-bound leaflet-container leaflet-touch leaflet-fade-anim leaflet-grab leaflet-touch-drag leaflet-touch-zoom" 184 | , tabindex = "0" 185 | ) 186 | ) 187 | ) 188 | } 189 | 190 | #' @describeIn latticeView alias for ease of typing 191 | #' @aliases latticeview 192 | #' @export latticeview 193 | latticeview <- function(...) latticeView(...) 194 | 195 | #' @describeIn latticeView convenience function for syncing maps 196 | #' @aliases sync 197 | #' @export sync 198 | sync <- function(..., 199 | ncol = 2, 200 | full.height = TRUE, 201 | sync = "all", 202 | sync.cursor = TRUE, 203 | no.initial.sync = TRUE, 204 | between = list(x = "2px", y = "2px")) { 205 | 206 | latticeView( 207 | ... 208 | , ncol = ncol 209 | , full.height = full.height 210 | , sync = sync 211 | , sync.cursor = sync.cursor 212 | , no.initial.sync = no.initial.sync 213 | , between = between 214 | ) 215 | 216 | } 217 | 218 | 219 | # Reduce(paste0, sapply(seq(ls), function(i) { 220 | # first <- do.call(c, lapply(seq(ls), function(j) { 221 | # paste0("leaf_widgets[", j - 1, "]") 222 | # })) 223 | # paste0(first, ".sync(leaf_widgets[", i - 1, "]);") 224 | # })) 225 | 226 | 227 | # ## more htmltools stuff... ?? 228 | # tl <- htmltools::tagList( 229 | # # htmltools::tags$head(htmltools::tags$script( 230 | # # type="text/javascript", 231 | # # src="https://cdn.rawgit.com/turban/Leaflet.Sync/master/L.Map.Sync.js" 232 | # # )), 233 | # tg, 234 | # htmlwidgets::onStaticRenderComplete( 235 | # paste0('var leaf_widgets = Array.prototype.map.call( 236 | # document.querySelectorAll(".leaflet"), 237 | # function(ldiv){ 238 | # return HTMLWidgets.find("#" + ldiv.id).getMap(); 239 | # } 240 | # );', 241 | # sync_strng 242 | # ) 243 | # ) 244 | # ) 245 | 246 | 247 | # htmltools::tags$head(htmltools::tags$script( 248 | # type="text/javascript", 249 | # src="https://cdn.rawgit.com/turban/Leaflet.Sync/master/L.Map.Sync.js" 250 | # )), 251 | 252 | 253 | # Provide Leaflet.sync Dependency 254 | dependencyLeafletsync <- function(){ 255 | htmltools::htmlDependency( 256 | name = "Leaflet.Sync" 257 | ,version = "0.0.5" 258 | ,src = c(file = system.file("htmlwidgets/lib/Leaflet.Sync", 259 | package = "leafsync")) 260 | ,script = "L.Map.Sync.js" 261 | ) 262 | } 263 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | ### mapview to leaflet 2 | #' @importFrom methods slot 3 | mapview2leaflet <- function(x) { 4 | methods::slot(x, "map") 5 | } 6 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r setup, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | eval = FALSE, 11 | comment = "#>", 12 | fig.path = "man/figures/README-", 13 | out.width = "100%" 14 | ) 15 | ``` 16 | 17 | # leafsync - (Synced) small multiples of leaflet maps 18 | 19 | [![Travis build status](https://travis-ci.org/r-spatial/leafsync.svg?branch=master)](https://travis-ci.org/r-spatial/leafsync) 20 | [![monthly](http://cranlogs.r-pkg.org/badges/leafsync)](https://www.rpackages.io/package/leafsync) 21 | [![total](http://cranlogs.r-pkg.org/badges/grand-total/leafsync)](https://www.rpackages.io/package/leafsync) 22 | [![CRAN](http://www.r-pkg.org/badges/version/leafsync?color=009999)](https://cran.r-project.org/package=leafsync) 23 | [![status](https://tinyverse.netlify.com/badge/leafsync)](https://CRAN.R-project.org/package=leafsync) 24 | 25 | `leafsync` is a plugin for [`leaflet`](https://github.com/rstudio/leaflet) to produce potentially synchronised small multiples of leaflet web maps wrapping [`Leaflet.Sync`](https://github.com/jieter/Leaflet.Sync). 26 | 27 | 28 | ## Installation 29 | 30 | You can install the released version of leafsync from [CRAN](https://CRAN.R-project.org) with: 31 | 32 | ``` r 33 | install.packages("leafsync") 34 | ``` 35 | 36 | ## Example 37 | 38 | ``` r 39 | library(sp) 40 | library(raster) 41 | library(mapview) 42 | 43 | data(meuse) 44 | coordinates(meuse) <- ~x+y 45 | proj4string(meuse) <- CRS("+init=epsg:28992") 46 | 47 | ## view different aspects of same data set 48 | m1 <- mapview(meuse, zcol = "soil", burst = TRUE) 49 | m2 <- mapview(meuse, zcol = "lead") 50 | m3 <- mapview(meuse, zcol = "landuse", map.types = "Esri.WorldImagery") 51 | m4 <- mapview(meuse, zcol = "dist.m") 52 | 53 | sync(m1, m2, m3, m4) # 4 panels synchronised 54 | ``` 55 | 56 | ![](man/figures/README-sync.png) 57 | 58 | ### Code of Conduct 59 | 60 | Please note that the 'leafsync' project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # leafsync - (Synced) small multiples of leaflet maps 5 | 6 | [![Travis build 7 | status](https://travis-ci.org/r-spatial/leafsync.svg?branch=master)](https://travis-ci.org/r-spatial/leafsync) 8 | [![monthly](http://cranlogs.r-pkg.org/badges/leafsync)](https://www.rpackages.io/package/leafsync) 9 | [![total](http://cranlogs.r-pkg.org/badges/grand-total/leafsync)](https://www.rpackages.io/package/leafsync) 10 | [![CRAN](http://www.r-pkg.org/badges/version/leafsync?color=009999)](https://cran.r-project.org/package=leafsync) 11 | [![status](https://tinyverse.netlify.com/badge/leafsync)](https://CRAN.R-project.org/package=leafsync) 12 | 13 | `leafsync` is a plugin for 14 | [`leaflet`](https://github.com/rstudio/leaflet) to produce potentially 15 | synchronised small multiples of leaflet web maps wrapping 16 | [`Leaflet.Sync`](https://github.com/jieter/Leaflet.Sync). 17 | 18 | ## Installation 19 | 20 | You can install the released version of leafsync from 21 | [CRAN](https://CRAN.R-project.org) with: 22 | 23 | ``` r 24 | install.packages("leafsync") 25 | ``` 26 | 27 | ## Example 28 | 29 | ``` r 30 | library(sp) 31 | library(raster) 32 | library(mapview) 33 | 34 | data(meuse) 35 | coordinates(meuse) <- ~x+y 36 | proj4string(meuse) <- CRS("+init=epsg:28992") 37 | 38 | ## view different aspects of same data set 39 | m1 <- mapview(meuse, zcol = "soil", burst = TRUE) 40 | m2 <- mapview(meuse, zcol = "lead") 41 | m3 <- mapview(meuse, zcol = "landuse", map.types = "Esri.WorldImagery") 42 | m4 <- mapview(meuse, zcol = "dist.m") 43 | 44 | sync(m1, m2, m3, m4) # 4 panels synchronised 45 | ``` 46 | 47 | ![](man/figures/README-sync.png) 48 | 49 | ### Code of Conduct 50 | 51 | Please note that the ‘leafsync’ project is released with a [Contributor 52 | Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project 53 | you agree to abide by its terms. 54 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/Leaflet.Sync/L.Map.Sync.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Extends L.Map to synchronize the interaction on one map to one or more other maps. 3 | */ 4 | 5 | (function () { 6 | var NO_ANIMATION = { 7 | animate: false, 8 | reset: true, 9 | disableViewprereset: true 10 | }; 11 | 12 | L.Sync = function () {}; 13 | /* 14 | * Helper function to compute the offset easily. 15 | * 16 | * The arguments are relative positions with respect to reference and target maps of 17 | * the point to sync. If you provide ratioRef=[0, 1], ratioTarget=[1, 0] will sync the 18 | * bottom left corner of the reference map with the top right corner of the target map. 19 | * The values can be less than 0 or greater than 1. It will sync points out of the map. 20 | */ 21 | L.Sync.offsetHelper = function (ratioRef, ratioTarget) { 22 | var or = L.Util.isArray(ratioRef) ? ratioRef : [0.5, 0.5]; 23 | var ot = L.Util.isArray(ratioTarget) ? ratioTarget : [0.5, 0.5]; 24 | return function (center, zoom, refMap, targetMap) { 25 | var rs = refMap.getSize(); 26 | var ts = targetMap.getSize(); 27 | var pt = refMap.project(center, zoom) 28 | .subtract([(0.5 - or[0]) * rs.x, (0.5 - or[1]) * rs.y]) 29 | .add([(0.5 - ot[0]) * ts.x, (0.5 - ot[1]) * ts.y]); 30 | return refMap.unproject(pt, zoom); 31 | }; 32 | }; 33 | 34 | 35 | L.Map.include({ 36 | sync: function (map, options) { 37 | this._initSync(); 38 | options = L.extend({ 39 | noInitialSync: false, 40 | syncCursor: false, 41 | syncCursorMarkerOptions: { 42 | radius: 10, 43 | fillOpacity: 0.3, 44 | color: '#da291c', 45 | fillColor: '#fff' 46 | }, 47 | offsetFn: function (center, zoom, refMap, targetMap) { 48 | // no transformation at all 49 | return center; 50 | } 51 | }, options); 52 | 53 | // prevent double-syncing the map: 54 | if (this._syncMaps.indexOf(map) === -1) { 55 | this._syncMaps.push(map); 56 | this._syncOffsetFns[L.Util.stamp(map)] = options.offsetFn; 57 | } 58 | 59 | if (!options.noInitialSync) { 60 | map.setView( 61 | options.offsetFn(this.getCenter(), this.getZoom(), this, map), 62 | this.getZoom(), NO_ANIMATION); 63 | } 64 | if (options.syncCursor) { 65 | if (typeof map.cursor === 'undefined') { 66 | map.cursor = L.circleMarker([0, 0], options.syncCursorMarkerOptions).addTo(map); 67 | } 68 | 69 | this._cursors.push(map.cursor); 70 | 71 | this.on('mousemove', this._cursorSyncMove, this); 72 | this.on('mouseout', this._cursorSyncOut, this); 73 | } 74 | 75 | // on these events, we should reset the view on every synced map 76 | // dragstart is due to inertia 77 | this.on('resize zoomend', this._selfSetView); 78 | this.on('moveend', this._syncOnMoveend); 79 | this.on('dragend', this._syncOnDragend); 80 | return this; 81 | }, 82 | 83 | 84 | // unsync maps from each other 85 | unsync: function (map) { 86 | var self = this; 87 | 88 | if (this._cursors) { 89 | this._cursors.forEach(function (cursor, indx, _cursors) { 90 | if (cursor === map.cursor) { 91 | _cursors.splice(indx, 1); 92 | } 93 | }); 94 | } 95 | 96 | // TODO: hide cursor in stead of moving to 0, 0 97 | if (map.cursor) { 98 | map.cursor.setLatLng([0, 0]); 99 | } 100 | 101 | if (this._syncMaps) { 102 | this._syncMaps.forEach(function (synced, id) { 103 | if (map === synced) { 104 | delete self._syncOffsetFns[L.Util.stamp(map)]; 105 | self._syncMaps.splice(id, 1); 106 | } 107 | }); 108 | } 109 | 110 | if (!this._syncMaps || this._syncMaps.length == 0) { 111 | // no more synced maps, so these events are not needed. 112 | this.off('resize zoomend', this._selfSetView); 113 | this.off('moveend', this._syncOnMoveend); 114 | this.off('dragend', this._syncOnDragend); 115 | } 116 | 117 | return this; 118 | }, 119 | 120 | // Checks if the map is synced with anything or a specifyc map 121 | isSynced: function (otherMap) { 122 | var has = (this.hasOwnProperty('_syncMaps') && Object.keys(this._syncMaps).length > 0); 123 | if (has && otherMap) { 124 | // Look for this specific map 125 | has = false; 126 | this._syncMaps.forEach(function (synced) { 127 | if (otherMap == synced) { has = true; } 128 | }); 129 | } 130 | return has; 131 | }, 132 | 133 | 134 | // Callbacks for events... 135 | _cursorSyncMove: function (e) { 136 | this._cursors.forEach(function (cursor) { 137 | cursor.setLatLng(e.latlng); 138 | }); 139 | }, 140 | 141 | _cursorSyncOut: function (e) { 142 | this._cursors.forEach(function (cursor) { 143 | // TODO: hide cursor in stead of moving to 0, 0 144 | cursor.setLatLng([0, 0]); 145 | }); 146 | }, 147 | 148 | _selfSetView: function (e) { 149 | // reset the map, and let setView synchronize the others. 150 | this.setView(this.getCenter(), this.getZoom(), NO_ANIMATION); 151 | }, 152 | 153 | _syncOnMoveend: function (e) { 154 | if (this._syncDragend) { 155 | // This is 'the moveend' after the dragend. 156 | // Without inertia, it will be right after, 157 | // but when inertia is on, we need this to detect that. 158 | this._syncDragend = false; // before calling setView! 159 | this._selfSetView(e); 160 | this._syncMaps.forEach(function (toSync) { 161 | toSync.fire('moveend'); 162 | }); 163 | } 164 | }, 165 | 166 | _syncOnDragend: function (e) { 167 | // It is ugly to have state, but we need it in case of inertia. 168 | this._syncDragend = true; 169 | }, 170 | 171 | 172 | // overload methods on originalMap to replay interactions on _syncMaps; 173 | _initSync: function () { 174 | if (this._syncMaps) { 175 | return; 176 | } 177 | var originalMap = this; 178 | 179 | this._syncMaps = []; 180 | this._cursors = []; 181 | this._syncOffsetFns = {}; 182 | 183 | L.extend(originalMap, { 184 | setView: function (center, zoom, options, sync) { 185 | // Use this sandwich to disable and enable viewprereset 186 | // around setView call 187 | function sandwich (obj, fn) { 188 | var viewpreresets = []; 189 | var doit = options && options.disableViewprereset && obj && obj._events; 190 | if (doit) { 191 | // The event viewpreresets does an invalidateAll, 192 | // that reloads all the tiles. 193 | // That causes an annoying flicker. 194 | viewpreresets = obj._events.viewprereset; 195 | obj._events.viewprereset = []; 196 | } 197 | var ret = fn(obj); 198 | if (doit) { 199 | // restore viewpreresets event to its previous values 200 | obj._events.viewprereset = viewpreresets; 201 | } 202 | return ret; 203 | } 204 | 205 | // Looks better if the other maps 'follow' the active one, 206 | // so call this before _syncMaps 207 | var ret = sandwich(this, function (obj) { 208 | return L.Map.prototype.setView.call(obj, center, zoom, options); 209 | }); 210 | 211 | if (!sync) { 212 | originalMap._syncMaps.forEach(function (toSync) { 213 | sandwich(toSync, function (obj) { 214 | return toSync.setView( 215 | originalMap._syncOffsetFns[L.Util.stamp(toSync)](center, zoom, originalMap, toSync), 216 | zoom, options, true); 217 | }); 218 | }); 219 | } 220 | 221 | return ret; 222 | }, 223 | 224 | panBy: function (offset, options, sync) { 225 | if (!sync) { 226 | originalMap._syncMaps.forEach(function (toSync) { 227 | toSync.panBy(offset, options, true); 228 | }); 229 | } 230 | return L.Map.prototype.panBy.call(this, offset, options); 231 | }, 232 | 233 | _onResize: function (event, sync) { 234 | if (!sync) { 235 | originalMap._syncMaps.forEach(function (toSync) { 236 | toSync._onResize(event, true); 237 | }); 238 | } 239 | return L.Map.prototype._onResize.call(this, event); 240 | }, 241 | 242 | _stop: function (sync) { 243 | L.Map.prototype._stop.call(this); 244 | if (!sync) { 245 | originalMap._syncMaps.forEach(function (toSync) { 246 | toSync._stop(true); 247 | }); 248 | } 249 | } 250 | }); 251 | 252 | originalMap.dragging._draggable._updatePosition = function () { 253 | L.Draggable.prototype._updatePosition.call(this); 254 | var self = this; 255 | originalMap._syncMaps.forEach(function (toSync) { 256 | L.DomUtil.setPosition(toSync.dragging._draggable._element, self._newPos); 257 | toSync.eachLayer(function (layer) { 258 | if (layer._google !== undefined) { 259 | var offsetFn = originalMap._syncOffsetFns[L.Util.stamp(toSync)]; 260 | var center = offsetFn(originalMap.getCenter(), originalMap.getZoom(), originalMap, toSync); 261 | layer._google.setCenter(center); 262 | } 263 | }); 264 | toSync.fire('move'); 265 | }); 266 | }; 267 | } 268 | }); 269 | })(); 270 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/Leaflet.Sync/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 Bjorn Sandvik / Jan Pieter Waagmeester 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/Leaflet.Sync/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Leaflet.Sync", 3 | "version": "0.2.2", 4 | "homepage": "https://github.com/turban/Leaflet.Sync", 5 | "authors": [ 6 | "Bjorn Sandvik", 7 | "Jan Pieter Waagmeester " 8 | ], 9 | "description": "Sync interaction across multiple Leaflet maps", 10 | "main": "L.Map.Sync.js", 11 | "keywords": [ 12 | "Leaflet", 13 | "map", 14 | "synchronisation" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "test" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /leafsync.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | StripTrailingWhitespace: Yes 16 | 17 | BuildType: Package 18 | PackageInstallArgs: --no-multiarch --with-keep.source 19 | PackageCheckArgs: --as-cran 20 | PackageRoxygenize: rd,collate,namespace 21 | -------------------------------------------------------------------------------- /man/figures/README-sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-spatial/leafsync/e4c25d9cfbf3da2ec1ee4e1048944d81450e70d3/man/figures/README-sync.png -------------------------------------------------------------------------------- /man/latticeView.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sync.R 3 | \name{latticeView} 4 | \alias{latticeView} 5 | \alias{latticeview} 6 | \alias{sync} 7 | \title{View two or more (possibly synchronised) mapview or leaflet maps} 8 | \usage{ 9 | latticeView( 10 | ..., 11 | ncol = 2, 12 | full.height = TRUE, 13 | sync = "none", 14 | sync.cursor = FALSE, 15 | no.initial.sync = TRUE, 16 | between = list(x = "2px", y = "2px") 17 | ) 18 | 19 | latticeview(...) 20 | 21 | sync( 22 | ..., 23 | ncol = 2, 24 | full.height = TRUE, 25 | sync = "all", 26 | sync.cursor = TRUE, 27 | no.initial.sync = TRUE, 28 | between = list(x = "2px", y = "2px") 29 | ) 30 | } 31 | \arguments{ 32 | \item{...}{any number of mapview or leaflet objects or a list thereof} 33 | 34 | \item{ncol}{how many columns should be plotted} 35 | 36 | \item{full.height}{\code{logical}; should the produced lattice view be 37 | scaled to the complete height of the viewer (or browser); default to \code{TRUE}. 38 | If \code{FALSE}, the height of each panel falls back to the default 39 | sizing policy height of leaflet (400px).} 40 | 41 | \item{sync}{whether to synchronise zoom and pan for certain elements. 42 | Possible values are "all" (default) to sync all maps, "none" to disable 43 | synchronisation or a list of panel numbers, e.g. \code{list(c(1, 3), c(2, 4))} 44 | will synchronise panels 1 & 3 and panels 2 & 4. Panels are drawn from top right 45 | to bottom left.} 46 | 47 | \item{sync.cursor}{whether to show cursor position in synced panels (default TRUE).} 48 | 49 | \item{no.initial.sync}{whether to sync the initial view (default TRUE).} 50 | 51 | \item{between}{a named list with components "x" and "y" specifying the space 52 | between panels in pixels . "x" refers to CSS property "margin-right", 53 | "y" refers to CSS property "margin-top". 54 | Default is `list(x = "2px", y = "2px")`.} 55 | } 56 | \description{ 57 | This function produces a lattice like view of two or more maps. It is possible to sync 58 | any combination of panels or all or none. For synchronising all panels it 59 | is best to use the provided convenience function \code{sync}. 60 | } 61 | \section{Functions}{ 62 | \itemize{ 63 | \item \code{latticeview}: alias for ease of typing 64 | 65 | \item \code{sync}: convenience function for syncing maps 66 | }} 67 | 68 | \examples{ 69 | if (interactive()) { 70 | library(leaflet) 71 | library(leafsync) 72 | 73 | m1 = leaflet() \%>\% 74 | addTiles() \%>\% 75 | addPolygons(data = gadmCHE) 76 | 77 | m2 = leaflet() \%>\% 78 | addProviderTiles("Esri.WorldImagery") \%>\% 79 | addCircleMarkers(data = breweries91, 80 | color = "black", 81 | opacity = 0.9, 82 | fillColor = "white", 83 | fillOpacity = 0.9) 84 | 85 | # synced 86 | sync(m1, m2) 87 | sync(m1, m2, no.initial.sync = TRUE) 88 | 89 | # not synced 90 | latticeview(m1, m2) 91 | latticeview(m1, m2, ncol = 1) 92 | } 93 | 94 | } 95 | --------------------------------------------------------------------------------