// Vendored from leaflet-polylineoffset@1.1.1 (MIT). // // Why this lives in-tree instead of being an npm dep: // • The published package would require --legacy-peer-deps because of an // unrelated React-17 peer-dep conflict elsewhere in the project, and we // don't want a renderer plugin to force a global resolver flag. // • It's frozen upstream (no meaningful updates since 2020), tiny, and // has zero runtime deps besides leaflet (already in package.json). // // What it does: // Monkey-patches L.Polyline so that any path passed with a numeric // `offset` in pathOptions is rendered shifted perpendicular to its // direction of travel by that many pixels (positive = right of travel, // negative = left). Used by Dispatch.js's Compare → Combined view to // render planned + actual as parallel rails when they share the same // road geometry; without this they overlap and read as one polyline. // // Import once for the side effect: // import '../../../utils/leafletPolylineOffset'; // // Then add to any pathOptions: // pathOptions={{ ..., offset: 5 }} // // Plays nicely with both SVG and Canvas renderers. import L from 'leaflet'; L.PolylineOffset = { translatePoint(pt, dist, radians) { return L.point(pt.x + dist * Math.cos(radians), pt.y + dist * Math.sin(radians)); }, offsetPointLine(points, distance) { const l = points.length; if (l < 2) { throw new Error('Line should be defined by at least 2 points'); } let a = points[0]; let b; const offsetAngle = Math.PI / 2; const offsetSegments = []; for (let i = 1; i < l; i++) { b = points[i]; // Each segment's offset angle is perpendicular to its direction. const segAngle = Math.atan2(b.y - a.y, b.x - a.x); offsetSegments.push({ offsetAngle: segAngle - offsetAngle, original: [a, b], offset: [ this.translatePoint(a, distance, segAngle - offsetAngle), this.translatePoint(b, distance, segAngle - offsetAngle) ] }); a = b; } return offsetSegments; }, // Find the intersection of two segments by extending them to infinity // along their direction, then walking along segment 1 by parameter t. // Returns null when the segments are parallel (no intersection). intersection(l1a, l1b, l2a, l2b) { const line1 = this.segmentAsVector(l1a, l1b); const line2 = this.segmentAsVector(l2a, l2b); const denom = -line2.x * line1.y + line1.x * line2.y; if (denom === 0) return null; const s = (-line1.y * (l1a.x - l2a.x) + line1.x * (l1a.y - l2a.y)) / denom; const t = (line2.x * (l1a.y - l2a.y) - line2.y * (l1a.x - l2a.x)) / denom; if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { return L.point(l1a.x + t * line1.x, l1a.y + t * line1.y); } return null; }, segmentAsVector(a, b) { return L.point(b.x - a.x, b.y - a.y); }, // Walk the offset segments and join adjacent ones at their intersection // points (mitered corners). When two consecutive segments don't intersect // within their bounds (sharp turn, or co-linear), fall back to the offset // endpoint so the polyline doesn't gap. joinLineSegments(segments) { const joined = []; let last = segments[0].offset; joined.push(last[0]); for (let i = 1; i < segments.length; i++) { const next = segments[i].offset; const inter = this.intersection(last[0], last[1], next[0], next[1]); if (inter) { joined.push(inter); } else { joined.push(last[1]); } last = next; } joined.push(last[1]); return joined; }, offsetPoints(points, offset) { if (!points || points.length < 2) return points; const offsets = this.offsetPointLine(points, offset); return this.joinLineSegments(offsets); }, // Operates on a ring of LatLngs by projecting → offsetting → unprojecting, // since leaflet polyline math is in screen pixels but our points are LatLng. offsetLatLngs(map, latlngs, offset) { const points = latlngs.map((ll) => map.latLngToLayerPoint(ll)); const offsetPts = this.offsetPoints(points, offset); return offsetPts.map((p) => map.layerPointToLatLng(p)); } }; // Patch Polyline._projectLatlngs (used by both SVG and Canvas renderers) so // that when an offset is set, the projected ring is offset before clipping. // We keep the original on _projectLatlngsOriginal so we can call through. const originalProject = L.Polyline.prototype._projectLatlngs; L.Polyline.prototype._projectLatlngs = function patchedProject(latlngs, result, projectedBounds) { const offset = this.options.offset; if (!offset || typeof offset !== 'number') { return originalProject.call(this, latlngs, result, projectedBounds); } // Recurse for multi-ring polylines (shouldn't happen for simple lines, // but the leaflet API allows it). const flat = latlngs[0] instanceof L.LatLng; if (!flat) { for (let i = 0; i < latlngs.length; i++) { this._projectLatlngs(latlngs[i], result, projectedBounds); } return undefined; } const projected = latlngs.map((ll) => this._map.latLngToLayerPoint(ll)); const offsetted = L.PolylineOffset.offsetPoints(projected, offset); // Update projectedBounds with each offset point so the renderer's // viewport-clipping check still works. for (let i = 0; i < offsetted.length; i++) { projectedBounds.extend(offsetted[i]); } result.push(offsetted); return undefined; }; export default L;