diff --git a/R/dependencies.R b/R/dependencies.R
index 1bfc4c706..3fa118304 100644
--- a/R/dependencies.R
+++ b/R/dependencies.R
@@ -25,6 +25,9 @@ leafletDependencies <- list(
omnivore = function() {
leafletOmnivoreDependencies()
},
+ polylineoffset = function() {
+ leafletPolylineoffsetDependencies()
+ },
# the ones below are not really expected to be used directly
# but are included for completeness sake.
diff --git a/R/layers.R b/R/layers.R
index ca958da1f..e62544cfd 100644
--- a/R/layers.R
+++ b/R/layers.R
@@ -1116,6 +1116,9 @@ addCircles <- function(
#' @param smoothFactor how much to simplify the polyline on each zoom level
#' (more means better performance and less accurate representation)
#' @param highlightOptions Options for highlighting the shape on mouse over.
+#' @param offset relative pixel offset from their actual LatLngs. The offset
+#' value can be either negative or positive, for left- or right-side offset,
+#' and remains constant across zoom levels.
#' @param noClip whether to disable polyline clipping
#' @describeIn map-layers Add polylines to the map
#' @export
@@ -1137,15 +1140,27 @@ addPolylines <- function(
labelOptions = NULL,
options = pathOptions(),
highlightOptions = NULL,
- data = getMapData(map)
+ data = getMapData(map),
+ offset = NULL
) {
if (missing(labelOptions)) labelOptions <- labelOptions()
+ if (is.numeric(offset)) {
+ if (offset == 0) offset <- NULL
+ } else {
+ offset <- NULL
+ }
+
options <- c(options, filterNULL(list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
- fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
+ fill = fill, fillColor = fillColor, fillOpacity = fillOpacity, offset = offset,
dashArray = dashArray, smoothFactor = smoothFactor, noClip = noClip
)))
+
+ if(!is.null(offset)){
+ map$dependencies <- c(map$dependencies, leafletPolylineoffsetDependencies())
+ }
+
pgons <- derivePolygons(data, lng, lat, missing(lng), missing(lat), "addPolylines")
invokeMethod(map, data, "addPolylines", pgons, layerId, group, options,
popup, popupOptions, safeLabel(label, data), labelOptions, highlightOptions) %>%
diff --git a/R/plugin-polylineoffset.R b/R/plugin-polylineoffset.R
new file mode 100644
index 000000000..e5bc47716
--- /dev/null
+++ b/R/plugin-polylineoffset.R
@@ -0,0 +1,10 @@
+leafletPolylineoffsetDependencies <- function() {
+ list(
+ htmltools::htmlDependency(
+ "leaflet-polylineoffset",
+ "1.1.1",
+ system.file("htmlwidgets/lib/leaflet-polylineoffset", package = "leaflet"),
+ script = "leaflet.polylineoffset.js"
+ )
+ )
+}
diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js
index c6c233bff..e8164eceb 100644
--- a/inst/htmlwidgets/leaflet.js
+++ b/inst/htmlwidgets/leaflet.js
@@ -1649,9 +1649,9 @@ methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options,
* @param lat Array of arrays of latitude coordinates for polylines
* @param lng Array of arrays of longitude coordinates for polylines
*/
-methods.addPolylines = function (polygons, layerId, group, options, popup, popupOptions, label, labelOptions, highlightOptions) {
+methods.addPolylines = function (polygons, layerId, group, options, popup, popupOptions, label, labelOptions, highlightOptions, offset) {
if (polygons.length > 0) {
- var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("popupOptions", popupOptions).col("label", label).col("labelOptions", labelOptions).col("highlightOptions", highlightOptions).cbind(options);
+ var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("popupOptions", popupOptions).col("label", label).col("labelOptions", labelOptions).col("highlightOptions", highlightOptions).col("offset", offset).cbind(options);
addLayers(this, "shape", df, function (df, i) {
var shapes = df.get(i, "shapes");
diff --git a/inst/htmlwidgets/leaflet.yaml b/inst/htmlwidgets/leaflet.yaml
index 0cfedbe93..1505ea57a 100644
--- a/inst/htmlwidgets/leaflet.yaml
+++ b/inst/htmlwidgets/leaflet.yaml
@@ -22,3 +22,7 @@ dependencies:
version: 1.3.1 # match leaflet version
src: "htmlwidgets/lib/rstudio_leaflet"
stylesheet: rstudio_leaflet.css
+ - name: leaflet-polylineoffset
+ version: 1.1.1 # match leaflet version
+ src: "htmlwidgets/lib/leaflet-polylineoffset"
+ script: leaflet.polylineoffset.js
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/LICENSE b/inst/htmlwidgets/lib/leaflet-polylineoffset/LICENSE
new file mode 100644
index 000000000..f3794ddde
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Benjamin Becquet
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/README.md b/inst/htmlwidgets/lib/leaflet-polylineoffset/README.md
new file mode 100644
index 000000000..ad72f532a
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/README.md
@@ -0,0 +1,49 @@
+Leaflet Polyline Offset
+===
+Works with Leaflet >= 1.0.
+
+This plugin adds to Leaflet `Polyline`s the ability to be drawn with a relative pixel offset, without modifying their actual `LatLng`s. The offset value can be either negative or positive, for left- or right-side offset, and remains constant across zoom levels.
+
+## Install with NPM
+
+```bash
+npm install leaflet-polylineoffset
+```
+
+## Use cases and demos
+
+Line offsetting is the process of drawing a line parallel to an existant one, at a fixed distance. It's not a simple (x,y) translation of the whole shape, as it shouldn't overlap. It can be used to visually emphasize different properties of the same linear feature, or achieve complex composite styling.
+
+This plugin brings this feature to Leaflet, to apply to client-side vectors.
+
+Demos are clearer than words:
+* [Basic demo](http://bbecquet.github.io/Leaflet.PolylineOffset/examples/example.html). The dashed line is the "model", with no offset applied. Red is with a -10px offset, green is with a 5px offset. The three are distinct `Polyline` objects but uses the same coordinate array.
+* [Cycle lanes](http://bbecquet.github.io/Leaflet.PolylineOffset/examples/example_cycle.html). Drawing a road with two directions of cycle lanes, a main one and one shared.
+* [Bus lines](http://bbecquet.github.io/Leaflet.PolylineOffset/examples/example_bus.html). A more complex demo. Offsets are computed automatically depending on the number of bus lines using the same segment. Other non-offset polylines are used to achieve the white and black outline effect.
+
+## Usage
+
+The plugin adds offset capabilities directly to the `L.Polyline` class.
+```javascript
+// Instantiate a normal Polyline with an 'offset' options, in pixels.
+var pl = L.polyline([[48.8508, 2.3455], [48.8497, 2.3504], [48.8494, 2.35654]], {
+ offset: 5
+});
+
+// Setting the 'offset' property through the 'setStyle' method won't work.
+// If you want to set the offset afterwards, use 'setOffset'.
+pl.setOffset(-10);
+
+// To cancel the offset, simply set it to 0
+pl.setOffset(0);
+```
+
+## License
+MIT.
+
+## Contributors
+* [Benjamin Becquet](https://github.com/bbecquet) (original author)
+* [sanderd17](https://github.com/sanderd17)
+* [jellevoost](https://github.com/jellevoost)
+* [ghybs](https://github.com/ghybs)
+* [BartWaardenburg](https://github.com/BartWaardenburg)
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example.html b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example.html
new file mode 100644
index 000000000..5fdbf911e
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ Leaflet Polyline Offset example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_bus.html b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_bus.html
new file mode 100644
index 000000000..56d901d83
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_bus.html
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+ Leaflet Polyline Offset - Bus lines example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_cycle.html b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_cycle.html
new file mode 100644
index 000000000..cfa7877c2
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/examples/example_cycle.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ Leaflet Polyline Offset - Cycle lanes example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/leaflet.polylineoffset.js b/inst/htmlwidgets/lib/leaflet-polylineoffset/leaflet.polylineoffset.js
new file mode 100644
index 000000000..855abadb8
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/leaflet.polylineoffset.js
@@ -0,0 +1,227 @@
+(function (factory, window) {
+ if (typeof define === 'function' && define.amd) {
+ define(['leaflet'], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory(require('leaflet'));
+ }
+ if (typeof window !== 'undefined' && window.L) {
+ window.L.PolylineOffset = factory(L);
+ }
+}(function (L) {
+
+function forEachPair(list, callback) {
+ if (!list || list.length < 1) { return; }
+ for (var i = 1, l = list.length; i < l; i++) {
+ callback(list[i-1], list[i]);
+ }
+}
+
+/**
+Find the coefficients (a,b) of a line of equation y = a.x + b,
+or the constant x for vertical lines
+Return null if there's no equation possible
+*/
+function lineEquation(pt1, pt2) {
+ if (pt1.x === pt2.x) {
+ return pt1.y === pt2.y ? null : { x: pt1.x };
+ }
+
+ var a = (pt2.y - pt1.y) / (pt2.x - pt1.x);
+ return {
+ a: a,
+ b: pt1.y - a * pt1.x,
+ };
+}
+
+/**
+Return the intersection point of two lines defined by two points each
+Return null when there's no unique intersection
+*/
+function intersection(l1a, l1b, l2a, l2b) {
+ var line1 = lineEquation(l1a, l1b);
+ var line2 = lineEquation(l2a, l2b);
+
+ if (line1 === null || line2 === null) {
+ return null;
+ }
+
+ if (line1.hasOwnProperty('x')) {
+ return line2.hasOwnProperty('x')
+ ? null
+ : {
+ x: line1.x,
+ y: line2.a * line1.x + line2.b,
+ };
+ }
+ if (line2.hasOwnProperty('x')) {
+ return {
+ x: line2.x,
+ y: line1.a * line2.x + line1.b,
+ };
+ }
+
+ if (line1.a === line2.a) {
+ return null;
+ }
+
+ var x = (line2.b - line1.b) / (line1.a - line2.a);
+ return {
+ x: x,
+ y: line1.a * x + line1.b,
+ };
+}
+
+function translatePoint(pt, dist, heading) {
+ return {
+ x: pt.x + dist * Math.cos(heading),
+ y: pt.y + dist * Math.sin(heading),
+ };
+}
+
+var PolylineOffset = {
+ offsetPointLine: function(points, distance) {
+ var offsetSegments = [];
+
+ forEachPair(points, L.bind(function(a, b) {
+ if (a.x === b.x && a.y === b.y) { return; }
+
+ // angles in (-PI, PI]
+ var segmentAngle = Math.atan2(a.y - b.y, a.x - b.x);
+ var offsetAngle = segmentAngle - Math.PI/2;
+
+ offsetSegments.push({
+ offsetAngle: offsetAngle,
+ original: [a, b],
+ offset: [
+ translatePoint(a, distance, offsetAngle),
+ translatePoint(b, distance, offsetAngle)
+ ]
+ });
+ }, this));
+
+ return offsetSegments;
+ },
+
+ offsetPoints: function(pts, offset) {
+ var offsetSegments = this.offsetPointLine(pts, offset);
+ return this.joinLineSegments(offsetSegments, offset);
+ },
+
+ /**
+ Join 2 line segments defined by 2 points each with a circular arc
+ */
+ joinSegments: function(s1, s2, offset) {
+ // TODO: different join styles
+ return this.circularArc(s1, s2, offset)
+ .filter(function(x) { return x; })
+ },
+
+ joinLineSegments: function(segments, offset) {
+ var joinedPoints = [];
+ var first = segments[0];
+ var last = segments[segments.length - 1];
+
+ if (first && last) {
+ joinedPoints.push(first.offset[0]);
+ forEachPair(segments, L.bind(function(s1, s2) {
+ joinedPoints = joinedPoints.concat(this.joinSegments(s1, s2, offset));
+ }, this));
+ joinedPoints.push(last.offset[1]);
+ }
+
+ return joinedPoints;
+ },
+
+ segmentAsVector: function(s) {
+ return {
+ x: s[1].x - s[0].x,
+ y: s[1].y - s[0].y,
+ };
+ },
+
+ getSignedAngle: function(s1, s2) {
+ const a = this.segmentAsVector(s1);
+ const b = this.segmentAsVector(s2);
+ return Math.atan2(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y);
+ },
+
+ /**
+ Interpolates points between two offset segments in a circular form
+ */
+ circularArc: function(s1, s2, distance) {
+ // if the segments are the same angle,
+ // there should be a single join point
+ if (s1.offsetAngle === s2.offsetAngle) {
+ return [s1.offset[1]];
+ }
+
+ const signedAngle = this.getSignedAngle(s1.offset, s2.offset);
+ // for inner angles, just find the offset segments intersection
+ if ((signedAngle * distance > 0) &&
+ (signedAngle * this.getSignedAngle(s1.offset, [s1.offset[0], s2.offset[1]]) > 0)) {
+ return [intersection(s1.offset[0], s1.offset[1], s2.offset[0], s2.offset[1])];
+ }
+
+ // draws a circular arc with R = offset distance, C = original meeting point
+ var points = [];
+ var center = s1.original[1];
+ // ensure angles go in the anti-clockwise direction
+ var rightOffset = distance > 0;
+ var startAngle = rightOffset ? s2.offsetAngle : s1.offsetAngle;
+ var endAngle = rightOffset ? s1.offsetAngle : s2.offsetAngle;
+ // and that the end angle is bigger than the start angle
+ if (endAngle < startAngle) {
+ endAngle += Math.PI * 2;
+ }
+ var step = Math.PI / 8;
+ for (var alpha = startAngle; alpha < endAngle; alpha += step) {
+ points.push(translatePoint(center, distance, alpha));
+ }
+ points.push(translatePoint(center, distance, endAngle));
+
+ return rightOffset ? points.reverse() : points;
+ }
+}
+
+// Modify the L.Polyline class by overwriting the projection function
+L.Polyline.include({
+ _projectLatlngs: function (latlngs, result, projectedBounds) {
+ var isFlat = latlngs.length > 0 && latlngs[0] instanceof L.LatLng;
+
+ if (isFlat) {
+ var ring = latlngs.map(L.bind(function(ll) {
+ var point = this._map.latLngToLayerPoint(ll);
+ if (projectedBounds) {
+ projectedBounds.extend(point);
+ }
+ return point;
+ }, this));
+
+ // Offset management hack ---
+ if (this.options.offset) {
+ ring = L.PolylineOffset.offsetPoints(ring, this.options.offset);
+ }
+ // Offset management hack END ---
+
+ result.push(ring.map(function (xy) {
+ return L.point(xy.x, xy.y);
+ }));
+ } else {
+ latlngs.forEach(L.bind(function(ll) {
+ this._projectLatlngs(ll, result, projectedBounds);
+ }, this));
+ }
+ }
+});
+
+L.Polyline.include({
+ setOffset: function(offset) {
+ this.options.offset = offset;
+ this.redraw();
+ return this;
+ }
+});
+
+return PolylineOffset;
+
+}, window));
diff --git a/inst/htmlwidgets/lib/leaflet-polylineoffset/package.json b/inst/htmlwidgets/lib/leaflet-polylineoffset/package.json
new file mode 100644
index 000000000..54fa89572
--- /dev/null
+++ b/inst/htmlwidgets/lib/leaflet-polylineoffset/package.json
@@ -0,0 +1,58 @@
+{
+ "_from": "leaflet-polylineoffset",
+ "_id": "leaflet-polylineoffset@1.1.1",
+ "_inBundle": false,
+ "_integrity": "sha512-WcEjAROx9IhIVwSMoFy9p2QBCG9YeuGtJl4ZdunIgj4xbCdTrUkBj8JdonUeCyLPnD2/Vrem/raOPHm5LvebSw==",
+ "_location": "/leaflet-polylineoffset",
+ "_phantomChildren": {},
+ "_requested": {
+ "type": "tag",
+ "registry": true,
+ "raw": "leaflet-polylineoffset",
+ "name": "leaflet-polylineoffset",
+ "escapedName": "leaflet-polylineoffset",
+ "rawSpec": "",
+ "saveSpec": null,
+ "fetchSpec": "latest"
+ },
+ "_requiredBy": [
+ "#USER",
+ "/"
+ ],
+ "_resolved": "https://registry.npmjs.org/leaflet-polylineoffset/-/leaflet-polylineoffset-1.1.1.tgz",
+ "_shasum": "021b717d4f7d462f742c1ac1ac0e83e8eef74615",
+ "_spec": "leaflet-polylineoffset",
+ "_where": "C:\\Users\\Fernando.Munozmendez\\OneDrive - AECOM Directory\\Projects\\Leaflet_dev\\leaflet",
+ "author": {
+ "name": "Benjamin Becquet"
+ },
+ "bugs": {
+ "url": "https://github.com/bbecquet/Leaflet.PolylineOffset/issues"
+ },
+ "bundleDependencies": false,
+ "deprecated": false,
+ "description": "Apply a relative pixel offset to polylines without changing their coordinates.",
+ "directories": {
+ "example": "examples"
+ },
+ "homepage": "https://github.com/bbecquet/Leaflet.PolylineOffset#readme",
+ "keywords": [
+ "map",
+ "leaflet",
+ "polyline",
+ "offset",
+ "layer",
+ "vector"
+ ],
+ "license": "MIT",
+ "main": "leaflet.polylineoffset.js",
+ "name": "leaflet-polylineoffset",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/bbecquet/Leaflet.PolylineOffset.git"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "version": "1.1.1"
+}
diff --git a/javascript/src/methods.js b/javascript/src/methods.js
index 153b1b816..01ae266a9 100644
--- a/javascript/src/methods.js
+++ b/javascript/src/methods.js
@@ -458,7 +458,7 @@ methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, c
* @param lat Array of arrays of latitude coordinates for polylines
* @param lng Array of arrays of longitude coordinates for polylines
*/
-methods.addPolylines = function(polygons, layerId, group, options, popup, popupOptions, label, labelOptions, highlightOptions) {
+methods.addPolylines = function(polygons, layerId, group, options, popup, popupOptions, label, labelOptions, highlightOptions, offset) {
if(polygons.length>0) {
let df = new DataFrame()
.col("shapes", polygons)
@@ -469,6 +469,7 @@ methods.addPolylines = function(polygons, layerId, group, options, popup, popupO
.col("label", label)
.col("labelOptions", labelOptions)
.col("highlightOptions", highlightOptions)
+ .col("offset", offset)
.cbind(options);
addLayers(this, "shape", df, function(df, i) {
diff --git a/man/leafletDependencies.Rd b/man/leafletDependencies.Rd
index 54d8959ad..10221830d 100644
--- a/man/leafletDependencies.Rd
+++ b/man/leafletDependencies.Rd
@@ -4,7 +4,7 @@
\name{leafletDependencies}
\alias{leafletDependencies}
\title{Various leaflet dependency functions for use in downstream packages}
-\format{An object of class \code{list} of length 13.}
+\format{An object of class \code{list} of length 14.}
\usage{
leafletDependencies
}
diff --git a/man/map-layers.Rd b/man/map-layers.Rd
index 01e1a94b2..69a679dff 100644
--- a/man/map-layers.Rd
+++ b/man/map-layers.Rd
@@ -70,7 +70,7 @@ addPolylines(map, lng = NULL, lat = NULL, layerId = NULL,
fillOpacity = 0.2, dashArray = NULL, smoothFactor = 1,
noClip = FALSE, popup = NULL, popupOptions = NULL, label = NULL,
labelOptions = NULL, options = pathOptions(),
- highlightOptions = NULL, data = getMapData(map))
+ highlightOptions = NULL, data = getMapData(map), offset = NULL)
addRectangles(map, lng1, lat1, lng2, lat2, layerId = NULL,
group = NULL, stroke = TRUE, color = "#03F", weight = 5,
@@ -204,6 +204,10 @@ pattern}}
\item{noClip}{whether to disable polyline clipping}
+\item{offset}{relative pixel offset from their actual LatLngs. The offset
+value can be either negative or positive, for left- or right-side offset,
+and remains constant across zoom levels.}
+
\item{lng1, lat1, lng2, lat2}{latitudes and longitudes of the south-west and
north-east corners of rectangles}
diff --git a/scripts/offset_example.R b/scripts/offset_example.R
new file mode 100644
index 000000000..b6eeeaa04
--- /dev/null
+++ b/scripts/offset_example.R
@@ -0,0 +1,7 @@
+# example
+
+leaflet(atlStorms2005[1,]) %>%
+ addTiles() %>%
+ addPolylines(color = "blue", offset = 10) %>%
+ addPolylines(color = "black", offset = 0)
+