javascripttypescriptleafletreact-leaflet

Implementing a Creeping Line Ahead (CLA) to Tasking Area Search Pattern Transition in Leaflet.js


I have a requirement to draw a Creeping Line Ahead (CLA) pattern using Leaflet.js over a tasking area (squares, rectangles, and generic polygons). The CLA pattern includes parallel lines with shorter straight-line segments than the total width of the area to be searched.

Here's an example of the CLA pattern: enter image description here.

The search pattern options include:

Here's the relevant code I've written:

<!DOCTYPE html>
<html>
<head>
    <title>Leaflet Creeping Line Ahead Pattern</title>
    <!-- Include Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
    <!-- Include Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
    <style>
        /* Set the size of the map */
        #map {
            height: 500px;
            width: 100%;
        }
    </style>
</head>
<body>
    <h2>Leaflet Creeping Line Ahead Pattern</h2>
    <!-- Create a div element to hold the map -->
    <div id="map"></div>

    <script>
        // Initialize the map and set its view to a given location and zoom level
        var map = L.map('map').setView([9.5415, 35.2651], 14);

        // Add an OpenStreetMap layer to the map
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(map);

        // Define the coordinates of the rectangle
        const rectangleCoords = [
            [9.531347, 35.25444], // top-left corner (latitude, longitude)
            [9.552678, 35.25444], // top-right corner (latitude, longitude)
            [9.552678, 35.27577], // bottom-right corner (latitude, longitude)
            [9.531347, 35.27577]  // bottom-left corner (latitude, longitude)
        ];

        // Create a polygon (rectangle) using the provided coordinates
        const rectangle = L.polygon(rectangleCoords, {
            color: 'blue', // Color of the rectangle border
            weight: 3, // Thickness of the rectangle border
            fillColor: 'blue', // Fill color of the rectangle
            fillOpacity: 0.2 // Opacity of the fill color
        }).addTo(map);

        // Function to draw the creeping line ahead pattern
        function drawCLA(bounds, start, initialHeading, firstTurn, trackSpacing, distanceFromEdge) {
            // Get the start and end points of the rectangle's longer side (e.g. east-west direction)
            let startPoint = start;
            let endPoint = [
                start[0] + (bounds.getNorthEast().lng - bounds.getSouthWest().lng) * Math.cos(initialHeading * Math.PI / 180),
                start[1] + (bounds.getNorthEast().lng - bounds.getSouthWest().lng) * Math.sin(initialHeading * Math.PI / 180)
            ];
            
            // Calculate the length of the rectangle's longer side
            const lineLength = bounds.getNorthEast().lng - bounds.getSouthWest().lng;

            // Initialize an array to hold the drawn lines
            const lines = [];
            const greyColor = 'grey';

            // Draw parallel lines
            while (startPoint[0] <= bounds.getNorthEast().lat) {
                // Draw a line from the current start point to the end point
                const line = L.polyline([startPoint, endPoint], {
                    color: greyColor,
                    weight: 2
                }).addTo(map);
                lines.push(line);

                // Calculate the next start and end points
                startPoint = [
                    startPoint[0] + trackSpacing,
                    startPoint[1]
                ];
                
                endPoint = [
                    endPoint[0] + trackSpacing,
                    endPoint[1]
                ];
            }

            return lines;
        }

        // Define the commence search point (CSP)
        const csp = [9.531347, 35.25444]; // CSP at the top-left corner of the rectangle

        // Set the initial heading, first turn, and track spacing
        const initialHeading = 90; // East direction (heading in degrees)
        const firstTurn = 'left'; // Direction of the first turn ('left' or 'right')
        const trackSpacing = 0.0003; // Spacing between each parallel line segment
        const distanceFromEdge = 0.0005; // Distance from the edge

        // Draw the creeping line ahead pattern inside the rectangle
        drawCLA(rectangle.getBounds(), csp, initialHeading, firstTurn, trackSpacing, distanceFromEdge);

        // Zoom the map to fit the bounds of the rectangle
        map.fitBounds(rectangle.getBounds());
    </script>
</body>
</html>

However, my current implementation does not produce the expected CLA pattern. Any help on how to correctly implement the CLA pattern using Leaflet.js would be greatly appreciated.


Solution

  • There are some issues with the way you put the problem; for one, you seem to use longitude and latitude as x anf y coordinates. This is wrong in general, even if the area is small enough for the curvature of Earth to be ignored (if it's large enough not to be ignored, a horizontal line, to be taken along the great circle differs significantly from the parallel). But even if we go with horizontal lines along the parallel, the length of an arc of meridian R * Δlatitude, where R is the radius of the Earth, while the length of an arc of parallel is R * cos(latitude) * Δlongitude. There's no solution for this in the problem data, since parameters like trackSpacing and distanceFromEdge are given in latitude/longitude units rather than distances (e.g., miles, kilometers). Still, the example is given at low latitudes, where cos(latitude) is close enough to 1.

    I also don't fully understand how distanceFromEdge should work; if you "enter" at a corner of the rectangle, the first line will always be on the edge, the distance could be taken only from the second line.

    Now, if the paths are always parallel to the x and y axes, i.e., the initialHeading is one of 0, 90, 180, 270 (or -90), the solution is not very complicated, the only challenge is to compute the intersection of the current line with the border of the rectangle minus the distanceFromEdge. I implemented that in the function computeLineEnd, which is the central point of the code; there are 8 test cases of the code, based on entry points in the four corners of the bounding rectangle and initial line being along one or the other of the two rectangle edges that intersect in that corner. Those eight test cases can be selected from the top right of the page. The function computeCLAPolyline returns the polyline for the CLA run; it is then drawn on the map, with the entry point marked by a green circle and the exit point by a red one.

    // Initialize the map and set its view to a given location and zoom level
    const map = L.map('map').setView([9.5415, 35.2651], 14);
    
    // Add an OpenStreetMap layer to the map
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
    
    const _bottom = 9.531347, // min latitude
        _top = 9.552678, // max latitude
        _left = 35.25444, // min longitude
        _right = 35.27577;  // max longitude
    
    // Define the coordinates of the rectangle
    const rectangleCoords = [
        [_bottom, _left], // bottom-left corner (latitude, longitude)
        [_top, _left], // top-left corner (latitude, longitude)
        [_top, _right], // top-right corner (latitude, longitude)
        [_bottom, _right]  // bottom-right corner (latitude, longitude)
    ];
    
    // Create a polygon (rectangle) using the provided coordinates
    const rectangle = L.polygon(rectangleCoords, {
        color: 'blue', // Color of the rectangle border
        weight: 0.5, // Thickness of the rectangle border
        fillColor: 'blue', // Fill color of the rectangle
        fillOpacity: 0.2 // Opacity of the fill color
    }).addTo(map);
    
    const MAX_LINES = 1000; // safety, to prevent infinite loop
    let claPath = null, circleEntry = null, circleExit = null;
    function drawCLA(csp, initialHeading, firstTurn, trackSpacing = 0.0004, distanceFromEdge = 0.0005){
        // Draw the creeping line ahead pattern inside the rectangle
        claPath?.remove();
        circleEntry?.remove();
        circleExit?.remove();
    
        const polyLine = computeCLAPolyline(rectangle.getBounds(), csp,
            initialHeading, firstTurn, trackSpacing, distanceFromEdge);
        if(polyLine.length === 0){
            return;
        }
    
        claPath = L.polyline(polyLine, {color: 'gray', weight: 2});
        claPath.addTo(map);
        // entry point
        circleEntry = L.circle(polyLine[0], 30, {color: 'green', fillColor: 'green', fillOpacity: 1});
        circleEntry.addTo(map);
        // exit point
        circleExit = L.circle(polyLine[polyLine.length-1], 30, {color: 'red', fillColor: 'red', fillOpacity: 1});
        circleExit.addTo(map);
    
    }
    
    document.querySelector('#tests').addEventListener('change',
        function(ev){
            const [sMarginV, sMarginH, sHeading, firstTurn] = ev.target.value.split(',');
            const marginH = sMarginH === 'left' ? _left : _right;
            const marginV = sMarginV === 'top' ? _top : _bottom;
            const initialHeading = parseFloat(sHeading);
            drawCLA([marginV, marginH], initialHeading, firstTurn);
        }
    );
    
    // Zoom the map to fit the bounds of the rectangle
    map.fitBounds(rectangle.getBounds());
    
    document.querySelector('#tests').dispatchEvent(new Event('change'));
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    function isInside({x, y}, boundsLines){
        return x >= boundsLines.constY1.x[0] && x <= boundsLines.constY1.x[1] &&
            y >= boundsLines.constX1.y[0] && y <= boundsLines.constX1.y[1];
    }
    
    function computeLineEnd({x0, y0}, {mSin, mCos,  maxLength = 1/0}, boundsLines, distanceFromEdge){
        //find the first intersection with the bounds of the line starting from (x0, y0) with slope mCos/mSin
        // the line equation: (y - y0) * mCos = (x-x0) * mSin
        const intersections = [];
        if(Math.abs(mSin) < 1e-10){
            mSin = 0;
        }
        if(Math.abs(mCos) < 1e-10){
            mCos = 0;
        }
    
        if(mCos !== 0){
            for(const boundLine of [boundsLines.constX1, boundsLines.constX2]){
                const xSol = boundLine.x + boundLine.marginSign * (distanceFromEdge || 0);
                if(mCos * (xSol - x0) > 0){
                    const ySol = y0 + (xSol - x0) * mSin / mCos;
                    if(ySol >= boundLine.y[0] && ySol <= boundLine.y[1]){
                        const delta2 = Math.sqrt((xSol - x0) ** 2 + (ySol - y0) ** 2);
                        if(delta2 > 1e-10 && isInside({x: xSol, y: ySol}, boundsLines)){
                            intersections.push({x: xSol, y: ySol, delta2});
                        }
                    }
                }
            }
        }
    
        if(mSin !== 0){
            for(const boundLine of [boundsLines.constY1, boundsLines.constY2]){
                const ySol = boundLine.y + boundLine.marginSign * (distanceFromEdge || 0);
                if(mSin * (ySol - y0) > 0){
                    const xSol = x0 + (ySol - y0) * mCos / mSin;
                    if(xSol >= boundLine.x[0] && xSol <= boundLine.x[1]){
                        const delta2 = Math.sqrt((xSol - x0) ** 2 + (ySol - y0) ** 2);
                        if(delta2 > 1e-10 && isInside({x: xSol, y: ySol}, boundsLines)){
                            intersections.push({x: xSol, y: ySol, delta2})
                        }
                    }
                }
            }
        }
    
        if(intersections.length > 1){
            intersections.sort(({delta2: a}, {delta2: b}) => b - a);
        }
        const firstIntersection =  intersections[0];
        if(firstIntersection && firstIntersection.delta2 > maxLength && distanceFromEdge !== false){
            return {x: x0 + maxLength * mCos, y: y0 + maxLength * mSin, delta2: maxLength};
        }
        return  firstIntersection;
    }
    
    function computeCLAPolyline(bounds, start, initialHeading, firstTurn, trackSpacing, distanceFromEdge) {
        const P1 = bounds.getNorthWest();
        const P2 = bounds.getNorthEast();
        const P3 = bounds.getSouthEast();
        const P4 = bounds.getSouthWest();
    
        const boundsLines = {
            constY1: {
                y: P1.lat,
                x: [P1.lng, P2.lng],
                marginSign: -1,
            },
            constY2: {
                y: P3.lat,
                x: [P4.lng, P3.lng],
                marginSign: 1
            },
            constX1: {
                x: P2.lng,
                y: [P3.lat, P2.lat],
                marginSign: -1
            },
            constX2: {
                x: P4.lng,
                y: [P4.lat, P1.lat],
                marginSign: 1
            }
        };
    
        let startPoint = start,
            startPointNoMargin = start;
        let lineAngle = (90-initialHeading) * Math.PI / 180;
        let maxLength = 1/0;
        let endOfLine = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
            {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, distanceFromEdge);
    
        if(!endOfLine){
            return [];
        }
        const resultPolyLine = [startPoint];
    
        let endOfLineNoMargin = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
            {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, false);
    
        let endPoint = [endOfLine.y, endOfLine.x];
        let endPointNoMargin = [endOfLineNoMargin.y, endOfLineNoMargin.x];
    
        let turn = firstTurn,
            turnIndex = 0;
    
        for(let i = 0; i < MAX_LINES; i++){
            lineAngle += turn === 'left' ? Math.PI / 2 : -Math.PI / 2;
            startPoint = endPoint;
            startPointNoMargin = endPointNoMargin;
            maxLength = maxLength === 1 / 0 ? trackSpacing : 1 / 0;
            endOfLine = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
                {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, distanceFromEdge);
            if(!endOfLine){
                resultPolyLine.push(startPointNoMargin);
                return resultPolyLine;
            }
            resultPolyLine.push(startPoint);
            endOfLineNoMargin = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
                {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, false);
    
            endPoint = [endOfLine.y, endOfLine.x];
            endPointNoMargin = [endOfLineNoMargin.y, endOfLineNoMargin.x];
    
            if(maxLength !== 1/0 && maxLength - endOfLine.delta2 > 1e-10){
                resultPolyLine.push(endPointNoMargin);
                return resultPolyLine;
            }
            turnIndex++;
            if(turnIndex % 2 === 0){
                turn = turn === 'left' ? 'right' : 'left';
            }
        }
    
        return [];
    }
    #map {
        height: 500px;
        width: 100%;
    }
    <link href="https://unpkg.com/leaflet/dist/leaflet.css" rel="stylesheet"/>
    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
    
    <h2>Leaflet Creeping Line Ahead Pattern</h2>
    <!-- Create a div element to hold the map -->
    <div id="map"></div>
    <select id="tests" style="position: absolute; top:1em; right:0">
        <option selected value="bottom,left,90,left">(bottom-left) -> right</option>
        <option value="bottom,left,0,right">(bottom-left) -> up</option>
        <option value="top,left,90,right">(top-left) -> right</option>
        <option value="top,left,180,left">(top-left) -> down</option>
        <option value="bottom,right,-90,right">(bottom-right) -> left</option>
        <option value="bottom,right,0,left">(bottom-right) -> up</option>
        <option value="top,right,-90,left">(top-right) -> left</option>
        <option value="top,right,180,right">(top-right) -> down</option>
    </select>

    or as jsFiddle

    If the paths are allowed to be oblique, the computation is much more complicated, because you have to look ahead after the current line, and end it before the edge, so that the next line also fits. Here's an implementation of that idea, with random deviations from vertical/horizontal lines:

    // Initialize the map and set its view to a given location and zoom level
    const map = L.map('map').setView([9.5415, 35.2651], 14);
    
    // Add an OpenStreetMap layer to the map
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
    
    const _bottom = 9.531347, // min latitude
        _top = 9.552678, // max latitude
        _left = 35.25444, // min longitude
        _right = 35.27577;  // max longitude
    
    // Define the coordinates of the rectangle
    const rectangleCoords = [
        [_bottom, _left], // bottom-left corner (latitude, longitude)
        [_top, _left], // top-left corner (latitude, longitude)
        [_top, _right], // top-right corner (latitude, longitude)
        [_bottom, _right]  // bottom-right corner (latitude, longitude)
    ];
    
    // Create a polygon (rectangle) using the provided coordinates
    const rectangle = L.polygon(rectangleCoords, {
        color: 'blue', // Color of the rectangle border
        weight: 0.5, // Thickness of the rectangle border
        fillColor: 'blue', // Fill color of the rectangle
        fillOpacity: 0.2 // Opacity of the fill color
    }).addTo(map);
    
    
    const MAX_LINES = 1000; // safety, to prevent infinite loop
    let claPath = null, circleEntry = null, circleExit = null;
    function drawCLA(csp, initialHeading, firstTurn, trackSpacing = 0.0004, distanceFromEdge = 0.0005){
        // Draw the creeping line ahead pattern inside the rectangle
        claPath?.remove();
        circleEntry?.remove();
        circleExit?.remove();
        console.log({csp, initialHeading, firstTurn}); // save problematic random values
    
        const polyLine = computeCLAPolyline(rectangle.getBounds(), csp,
            initialHeading, firstTurn, trackSpacing, distanceFromEdge);
        if(polyLine.length === 0){
            return;
        }
    
        claPath = L.polyline(polyLine, {color: 'gray', weight: 2});
        claPath.addTo(map);
        // entry point
        circleEntry = L.circle(polyLine[0], 30, {color: 'green', fillColor: 'green', fillOpacity: 1});
        circleEntry.addTo(map);
        // exit point
        circleExit = L.circle(polyLine[polyLine.length-1], 30, {color: 'red', fillColor: 'red', fillOpacity: 1});
        circleExit.addTo(map);
    }
    
    let maxObliqueness = null,
        marginH = null,
        marginV = null,
        mainHeading = null,
        changeSign = null,
        firstTurn = null;
    
    function runRandomTest(){
        if([maxObliqueness, marginH, marginV, mainHeading, changeSign, firstTurn].includes(null)){
            return;
        }
        drawCLA([marginV, marginH], mainHeading+changeSign*Math.floor(1+Math.random()*maxObliqueness), firstTurn);
    }
    
    document.querySelector('#run').addEventListener('click', runRandomTest);
    
    document.querySelector('#tests').addEventListener('change',
        function(ev){
            const [sMarginV, sMarginH, sHeading, sChangeSign, sFirstTurn] = ev.target.value.split(',');
            marginH = sMarginH === 'left' ? _left : _right;
            marginV = sMarginV === 'top' ? _top : _bottom;
            mainHeading = parseFloat(sHeading);
            changeSign = parseFloat(sChangeSign);
            firstTurn = sFirstTurn;
    
            runRandomTest();
        }
    );
    
    document.querySelector('#maxObliqueness').addEventListener('change',
        function(ev){
            maxObliqueness = parseFloat(ev.target.value);
            runRandomTest()
        }
    );
    
    // Zoom the map to fit the bounds of the rectangle
    map.fitBounds(rectangle.getBounds());
    
    // initialize data
    document.querySelector('#tests').dispatchEvent(new Event('change'));
    document.querySelector('#maxObliqueness').dispatchEvent(new Event('change'));
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    
    function isInside({x, y}, boundsLines){
        return x >= boundsLines.constY1.x[0] && x <= boundsLines.constY1.x[1] &&
            y >= boundsLines.constX1.y[0] && y <= boundsLines.constX1.y[1];
    }
    
    function computeLineEnd({x0, y0}, {mSin, mCos,  maxLength = 1/0}, boundsLines, distanceFromEdge){
        //find the first intersection with the bounds of the line starting from (x0, y0) with slope mCos/mSin
        // the line equation: (y - y0) * mCos = (x-x0) * mSin
        // or x = x0 + t * mCos; y = y0 + t * mSin with t >=0
        if(Math.abs(mSin) < 1e-10){
            mSin = 0;
        }
        if(Math.abs(mCos) < 1e-10){
            mCos = 0;
        }
    
        const intersections = [];
        if(mCos !== 0){
            for(const boundLine of [boundsLines.constX1, boundsLines.constX2]){
                const xSol = boundLine.x + boundLine.marginSign * (distanceFromEdge || 0);
                if(mCos * (xSol - x0) > 0){
                    const ySol = y0 + (xSol - x0) * mSin / mCos;
                    if(ySol >= boundLine.y[0] && ySol <= boundLine.y[1]){
                        const delta2 = Math.sqrt((xSol - x0) ** 2 + (ySol - y0) ** 2);
                        if(delta2 > 1e-10 && isInside({x: xSol, y: ySol}, boundsLines)){
                            intersections.push({x: xSol, y: ySol, delta2});
                        }
                    }
                }
            }
        }
    
        if(mSin !== 0){
            for(const boundLine of [boundsLines.constY1, boundsLines.constY2]){
                const ySol = boundLine.y + boundLine.marginSign * (distanceFromEdge || 0);
                if(mSin * (ySol - y0) > 0){
                    const xSol = x0 + (ySol - y0) * mCos / mSin;
                    if(xSol >= boundLine.x[0] && xSol <= boundLine.x[1]){
                        const delta2 = Math.sqrt((xSol - x0) ** 2 + (ySol - y0) ** 2);
                        if(delta2 > 1e-10 && isInside({x: xSol, y: ySol}, boundsLines)){
                            intersections.push({x: xSol, y: ySol, delta2})
                        }
                    }
                }
            }
        }
    
        if(intersections.length > 1){
            intersections.sort(({delta2: a}, {delta2: b}) => b - a);
        }
        const firstIntersection =  intersections[0];
        if(firstIntersection && firstIntersection.delta2 > maxLength && distanceFromEdge !== false){
            return {x: x0 + maxLength * mCos, y: y0 + maxLength * mSin, delta2: maxLength};
        }
        return  firstIntersection;
    }
    
    function computeLineEndWithParamStart({x0: [ax, bx], y0: [ay, by], maxT}, {mSin, mCos, maxLength}, boundsLines, distanceFromEdge){
        const tSols = [];
        for(const boundLine of [boundsLines.constX1, boundsLines.constX2]){
            const xSol = boundLine.x + boundLine.marginSign * distanceFromEdge;
            const t = (xSol - bx - maxLength * mCos) / ax;
            if(t >= 0 && t <= maxT){
                const ySol = ay * t + by + maxLength * mSin;
                if(isInside({x: xSol, y: ySol}, boundsLines)){
                    tSols.push(t);
                }
            }
        }
    
        for(const boundLine of [boundsLines.constY1, boundsLines.constY2]){
            const ySol = boundLine.y + boundLine.marginSign * distanceFromEdge;
            const t = (ySol - by - maxLength * mSin) / ay;
            if(t >= 0 && t <= maxT){
                const xSol = ax * t + bx + maxLength * mCos;
                if(isInside({x: xSol, y: ySol}, boundsLines)){
                    tSols.push(t);
                }
            }
        }
    
        if(tSols.length === 0){
            return null;
        }
        return Math.max(...tSols)
    }
    
    
    function computeNextTwoLines({x0, y0}, {mSin, mCos}, {mSin2, mCos2, maxLength2}, boundsLines, distanceFromEdge){
        const sol = computeLineEnd({x0, y0}, {mSin, mCos}, boundsLines, distanceFromEdge);
        if(!sol){
            return sol;
        }
        const maxT = sol.delta2;
    
        const t = computeLineEndWithParamStart(
            {x0: [mCos, x0], y0: [mSin, y0], maxT},
            {mSin: mSin2, mCos: mCos2, maxLength: maxLength2}, boundsLines, distanceFromEdge);
    
        if(t === null){
            return null;
        }
        else{
            return {
                x: x0 + t * mCos,
                y: y0 + t * mSin,
                x2: x0 + t * mCos + maxLength2 * mCos2,
                y2: y0 + t * mSin + maxLength2 * mSin2
            }
        }
    }
    
    // Function to draw the creeping line ahead pattern
    function computeCLAPolyline(bounds, start, initialHeading, firstTurn, trackSpacing, distanceFromEdge) {
        const P1 = bounds.getNorthWest();
        const P2 = bounds.getNorthEast();
        const P3 = bounds.getSouthEast();
        const P4 = bounds.getSouthWest();
    
        const boundsLines = {
            constY1: {
                y: P1.lat,
                x: [P1.lng, P2.lng],
                marginSign: -1,
            },
            constY2: {
                y: P3.lat,
                x: [P4.lng, P3.lng],
                marginSign: 1
            },
            constX1: {
                x: P2.lng,
                y: [P3.lat, P2.lat],
                marginSign: -1
            },
            constX2: {
                x: P4.lng,
                y: [P4.lat, P1.lat],
                marginSign: 1
            }
        };
    
        // Get the start and end points of the rectangle's longer side (e.g. east-west direction)
        let startPoint = start,
            startPointNoMargin = start;
    
        let lineAngle = (90-initialHeading) * Math.PI / 180;
        let maxLength = 1/0;
        let endOfLine = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
            {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, distanceFromEdge);
    
    
        if(!endOfLine){
            return [];
        }
        const resultPolyLine = [startPoint];
    
        let endOfLineNoMargin = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
            {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, false);
    
        let endPoint = [endOfLine.y, endOfLine.x];
        let endPointNoMargin = [endOfLineNoMargin.y, endOfLineNoMargin.x];
        let prevPointNoMargin = null;
    
        let turn = firstTurn,
            turnIndex = 0;
    
        for(let i = 0; i < MAX_LINES; i++){
            let previousLineAngle = lineAngle;
            lineAngle += turn === 'left' ? Math.PI / 2 : -Math.PI / 2;
            startPoint = endPoint;
            startPointNoMargin = endPointNoMargin;
            maxLength = maxLength === 1 / 0 ? trackSpacing : 1 / 0;
            endOfLine = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
                {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, distanceFromEdge);
            if(endOfLine && (maxLength === 1/0 || maxLength - endOfLine.delta2 < 1e-10)){
                resultPolyLine.push(startPoint);
                endOfLineNoMargin = computeLineEnd({x0: startPoint[1], y0: startPoint[0]},
                    {mSin: Math.sin(lineAngle), mCos: Math.cos(lineAngle), maxLength}, boundsLines, false);
    
                endPoint = [endOfLine.y, endOfLine.x];
                //L.circle(endPoint, maxLength !== 1/0 ? 20: 30, {color: maxLength !== 1/0 ? 'magenta' : 'orange', fillColor: maxLength !== 1/0 ? 'magenta' : 'orange', fillOpacity: 1}).addTo(map);
                endPointNoMargin = [endOfLineNoMargin.y, endOfLineNoMargin.x];
                prevPointNoMargin = null;
            }
            else{
                let sol2 = null;
                const mSin = Math.sin(previousLineAngle),
                    mCos = Math.cos(previousLineAngle);
                const startPoint2 = resultPolyLine[resultPolyLine.length - 1];
                if(i % 2 === 0 && Math.abs(mSin) > 1e-5 && Math.abs(mCos) > 1e-5){
                    sol2 = computeNextTwoLines({x0: startPoint2[1], y0: startPoint2[0]},
                        {mSin, mCos},
                        {mSin2: Math.sin(lineAngle), mCos2: Math.cos(lineAngle), maxLength2: trackSpacing},
                        boundsLines, distanceFromEdge);
                }
                if(sol2){
                    startPoint = [sol2.y, sol2.x];
                    endPoint = [sol2.y2, sol2.x2];
    
                    const sol2NoMargin = computeNextTwoLines({x0: startPoint2[1], y0: startPoint2[0]},
                        {mSin, mCos},
                        {mSin2: Math.sin(lineAngle), mCos2: Math.cos(lineAngle), maxLength2: trackSpacing},
                        boundsLines, 0);
                    if(sol2NoMargin){
                        prevPointNoMargin = [sol2NoMargin.y, sol2NoMargin.x];
                        endPointNoMargin = [sol2NoMargin.y2, sol2NoMargin.x2];
                    }
                    resultPolyLine.push(startPoint);
                }
                else{
                    if(prevPointNoMargin){
                        resultPolyLine.push(prevPointNoMargin);
                    }
                    resultPolyLine.push(startPointNoMargin);
                    return resultPolyLine;
                }
            }
    
            turnIndex++;
            if(turnIndex % 2 === 0){
                turn = turn === 'left' ? 'right' : 'left';
            }
        }
    
        return resultPolyLine;
    }
    #map {
        height: 500px;
        width: 100%;
    }
    <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
    
    <h2>Leaflet Creeping Line Ahead Pattern</h2>
    <!-- Create a div element to hold the map -->
    <div id="map"></div>
    <div style="position: absolute; top:1em; right:0; text-align: right">
    <select id="maxObliqueness">
        <option selected value="15">Random obliqueness: up to 15 deg</option>
        <option value="30">Random obliqueness: up to 30 deg</option>
        <option value="45">Random obliqueness: up to 45 deg</option>
    </select><br>
    <button id="run">Rerun</button>
    <select id="tests">
        <option selected value="bottom,left,90,-1,left">(bottom-left) -> right</option>
        <option value="bottom,left,0,1,right">(bottom-left) -> up</option>
        <option value="top,left,90,1,right">(top-left) -> right</option>
        <option value="top,left,180,-1,left">(top-left) -> down</option>
        <option value="bottom,right,-90,1,right">(bottom-right) -> left</option>
        <option value="bottom,right,0,-1,left">(bottom-right) -> up</option>
        <option value="top,right,-90,-1,left">(top-right) -> left</option>
        <option value="top,right,180,1,right">(top-right) -> down</option>
    </select>
    </div>
    or as jsFiddle

    These should be considered as a starting point for more exact versions, that would take into consideration the cos(latitude) term, or even the curvature of the Earth, which is probably much more complicated.