bbecquet / Leaflet.PolylineDecorator

A plug-in for the JS map library Leaflet, allowing to define patterns (like dashes, arrows, icons, etc.) on Polylines.
MIT License
481 stars 114 forks source link

How do I make my arrowhead pointy? #114

Open jharrison opened 8 months ago

jharrison commented 8 months ago

I just want a simple triangular arrowhead that comes to a point, but when I try this:

this.polyline = L.polyline([this.tail, this.tip], { color: this.color }).addTo(this.map);
this.arrowHead = L.polylineDecorator(this.polyline, {
    patterns: [
        {
            offset: "100%",
            repeat: 0,
            symbol: L.Symbol.arrowHead({ pixelSize: 20, 
                pathOptions: { fillOpacity: 1, weight: 0, color: this.color } })
        }
    ]
}).addTo(this.map);

It comes out looking like this:

image

Giving the arrowhead 50% opacity reveals what's happening. The line extends all the way to the tip:

image

Is there a way to make the line stop short so it doesn't mess up the point? I've tried calculating an artificial end point but got bogged down in conversions between pixels, degrees, and meters. This seems like it should be pretty basic, so I figure I must be missing something.

jharrison commented 8 months ago

I solved the problem myself. Hopeful this will be useful to someone else and perhaps even make it into the code:

// the arrowhead is a triangle that comes to a fine point. We don't 
// want the line of the arrow extending all the way to that point or
// it messes up the tip. The solution is to stop the line at the back 
// of the arrowhead. 
const backOfArrowHead = calcBackOfArrowHead(map, tail, tip, arrowHeadSize, arrowHeadAngle);
polyline = L.polyline([tail, backOfArrowHead], { color: 'blue' }).addTo(map);

// Add the arrowhead using Leaflet.PolylineDecorator
arrowHead = L.polylineDecorator([tail, tip], {
    patterns: [
        {
            offset: "100%",
            repeat: 0,
            symbol: L.Symbol.arrowHead({ pixelSize:arrowHeadSize, pathOptions: { fillOpacity: 1, weight: 0, color: 'blue' } })
        }
    ]
}).addTo(map);

The calcBackOfArrowHead() function looks like this:

calcBackOfArrowHead(map, tail, tip, arrowHeadSize, arrowHeadAngle) {
    // Project LatLng points to pixel coordinates
    var tailP = map.project(tail);
    var tipP = map.project(tip);

    // Calculate the vector from tail to tip in pixel coordinates
    var vector = tipP.subtract(tailP);

    // Normalize the vector to a unit vector (length 1)
    var unitVector = vector.divideBy(vector.distanceTo([0, 0]));

    // Calculate a new point some distance back from the tip
    // The distance is the length of the adjacent side of a right triangle.
    // This is the right half of the arrowhead, pointing up and scaled to the unit circle.
    // We need the ratio d to help us find the bottom left corner, which is where
    // the line of the arrow needs to stop drawing.
    //   |\
    //   |θ\
    //   |  \ 1
    // d |   \
    //   |    \
    //   |_____\
    //

    const halfArrowHeadAngleRadians = arrowHeadAngle / 2 * (Math.PI / 180); // θ        
    const arrowHeadRatio = arrowHeadSize * Math.cos(halfArrowHeadAngleRadians); // d
    var newTipP = tipP.subtract(unitVector.multiplyBy(arrowHeadRatio));

    // Unproject the new pixel point back to LatLng coordinates
    var newTipLatLng = map.unproject(newTipP);

    return newTipLatLng;
}
jules43 commented 2 weeks ago

Out of interest, another thing that can help with this problem is to change the linecap to butt, removing the rounded tip from the line.