diff --git a/src/public/index.html b/src/public/index.html index 5ab3e43..c857243 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -20,6 +20,7 @@ + @@ -2288,6 +2289,7 @@ } // add node neighbours + var polylineOffset = 0; const neighbours = node.neighbours ?? []; for(const neighbour of neighbours){ @@ -2319,8 +2321,12 @@ ], { color: '#2563eb', opacity: 0.5, + offset: polylineOffset, }).addTo(neighboursLayerGroup); + // increase offset so next neighbour does not overlay other neighbours from self + polylineOffset += 2; + // default to showing distance in meters var distance = `${distanceInMeters} meters`; diff --git a/src/public/plugins/leaflet.polylineoffset.js b/src/public/plugins/leaflet.polylineoffset.js new file mode 100644 index 0000000..b8bae65 --- /dev/null +++ b/src/public/plugins/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, options) { + var offsetSegments = this.offsetPointLine(L.LineUtil.simplify(pts, options.smoothFactor), options.offset); + return this.joinLineSegments(offsetSegments, options.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 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));