2

I'm trying to build something using Leaf.js and (as suggested by ChatGPT) Turf.

I have a map and GeoJSON polygon plotted on it just fine:

<script type="application/javascript">
    var initialGeoJSON = <?=$mapregion->geojson?>;
    var map;
    let mapboxToken = '<?=MAPBOX_TOKEN?>';
    let pathWidth = 39;

    $(document).ready(function () {
        // Initialize the map with satellite view as the default
        map = L.map('map').setView([40.9479, -87.1856], 15);

        // Create a satellite tile layer using Mapbox Satellite
        L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
            maxZoom: 19,
            id: 'mapbox/satellite-streets-v12', // Mapbox Satellite tile layer
            accessToken: mapboxToken, // Use the global variable for Mapbox access token
        }).addTo(map);

        var initialPoly = L.geoJSON(initialGeoJSON, {
            editable: true,
        });

        initialPoly.addTo(map);

        centerMapToPolygon(initialPoly);

        function centerMapToPolygon(polygonLayer) {
            var bounds = polygonLayer.getBounds();
            var centerLatLng = bounds.getCenter();
            map.setView(centerLatLng, 13);
        }
    });
</script>

This is working fine.

The rendered map

However, where I'm really getting stuck, and likely due to my own lack of math skills beyond intermediate algebra and due to being overwhelmed by my day job and lack of sleep, I can't seem to figure out how to do this next part.

Since I know the path (pathWidth) and I know the shape of the polygon, I figure it should be relatively simple to plot those path lines across the polygon.

Think of it as a simple portrayal of the rows of crops that might be in that field.

I've tried a few different things, and again, likely because I'm exhausted and overwhelmed already, I haven't been able to get anything to work even remotely close.

Since the polygons are never going to be perfectly rectangular or even oriented in the way this one is, this problem gets even more challenging and demands dynamic handling of the shapes.

Example: Another sample map rendering

One thought I had, ignoring that last paragraph, was that I could somehow drop a grid with a known spacing (pathWidth) over the polygon then keep only the overlapping bits. However, I got absolutely nowhere with that.

I'm about to give up on this idea entirely, so my last-ditch attempt at making any progress is coming to you lovely, intelligent humans.

Educate me, please.

Update!

This is turning out to be more involved than a simple operation, but I'm making progress! I'll share the solution when I'm done.

map of plotted paths

Update 2: I've made some pretty good progress, but I'm not very satisfied with my code. This is all a lot of hack and stab and slash until I got something working, and I need to go back and get it cleaned up.

<script type="application/javascript">
    var initialGeoJSON = <?=$mapregion->geojson?>;
    var map;
    let envelope;
    let mapboxToken = '<?=MAPBOX_TOKEN?>';
    let pathWidth = 39;

    $(document).ready(function () {
        // Initialize the map with satellite view as the default
        map = L.map('map').setView([40.9479, -87.1856], 15);

        // Create a satellite tile layer using Mapbox Satellite
        L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
            maxZoom: 19,
            id: 'mapbox/satellite-streets-v12', // Mapbox Satellite tile layer
            accessToken: mapboxToken, // Use the global variable for Mapbox access token
        }).addTo(map);

        var initialPoly = L.geoJSON(initialGeoJSON, {
            editable: true,
        });

        initialPoly.addTo(map);


        var longestEdge = null;
        var coordinates = initialGeoJSON.geometry.coordinates[0];

        envelope = turf.envelope(initialGeoJSON);
        coordinates = envelope.geometry.coordinates[0];


        for (var i = 0; i < coordinates.length - 1; i++)
        {
            var pair = [
                coordinates[i],
                coordinates[i + 1]
            ];

            var lengthAndHeading = calculateEdgeLengthAndHeading(pair);

            if (longestEdge != null) {
                if (longestEdge.distance < lengthAndHeading.distance) {
                    longestEdge = lengthAndHeading;
                }
            } else {
                longestEdge = lengthAndHeading;
            }
        }

        // Plot the parallel line on the map
        var latLngs = longestEdge.coordinates.map(function (coord) {
            return [coord[1], coord[0]];
        });
        var polyline = L.polyline(latLngs, {color: 'yellow'}).addTo(map);

        var heading = longestEdge.headings;

        // Calculate perpendicular angle to longest edge's heading
        var perpendicularAngle = (heading + 90) % 360;

        var thisLine = longestEdge;
        var parallelLine = null;
        var offsetDistance = pathWidth / 2;
        var linesPlotted = 0;


        var colors = ['gray', 'white'];

        var keepGoing = true;

        do {
            // Calculate distance to offset parallel line from longest edge
            if (linesPlotted >= 1) {
                offsetDistance = pathWidth;
            }

            if (thisLine.coordinates == undefined && thisLine.geometry != undefined) {
                thisLine.coordinates = thisLine.geometry.coordinates;
            }

            var startPoint = thisLine.coordinates[0];
            var endPoint = thisLine.coordinates[1];

            var startParallel = turf.destination(turf.point(startPoint), offsetDistance, perpendicularAngle, {units: 'feet'}).geometry.coordinates;
            var endParallel = turf.destination(turf.point(endPoint), offsetDistance, perpendicularAngle, {units: 'feet'}).geometry.coordinates;

            parallelLine = turf.lineString([startParallel, endParallel]);

            var intersected = turf.lineIntersect(parallelLine, initialGeoJSON.geometry);

            var color = colors.shift();
            colors.push(color);

            if (linesPlotted % 10 == 0) color = 'black';

            if (intersected.features.length >= 2) {
                for (var i = 0; i < intersected.features.length; i += 2) {
                    var intersectionStart = intersected.features[i].geometry.coordinates;
                    var intersectionEnd = intersected.features[i + 1].geometry.coordinates;


                    var line = turf.lineString([intersectionStart, intersectionEnd]);
                    var latLngs = line.geometry.coordinates.map(function (coord) {
                        return [coord[1], coord[0]];
                    });

                    var polyline = L.polyline(latLngs, {color: color}).addTo(map);
                }
                linesPlotted++;
            } else if (intersected.features.length == 1) {
                var latLngs = parallelLine.geometry.coordinates.map(function (coord) {
                    return [coord[1], coord[0]];
                });
                var polyline = L.polyline(latLngs, {color: color}).addTo(map);
                linesPlotted++;
            } else if (linesPlotted == 0) {
                heading = (heading + 180) % 360;
                perpendicularAngle = (heading + 90) % 360;
            } else {
                keepGoing = false;
                break;
            }


            if (linesPlotted > 0) {
                thisLine = parallelLine;

            }
        } while (keepGoing);


        function calculateEdgeLengthAndHeading(coordinates) {
            var edgeLengths = [];
            var edgeHeadings = [];

            for (var i = 0; i < coordinates.length - 1; i++) {
                var startPoint = coordinates[i];
                var endPoint = coordinates[i + 1];

                var distance = turf.distance(turf.point(startPoint), turf.point(endPoint), { units: 'feet' });
                var bearing = turf.bearing(turf.point(startPoint), turf.point(endPoint));

                edgeLengths.push(distance);
                edgeHeadings.push(bearing);
            }

            if (edgeLengths.length == 1) edgeLengths = edgeLengths.pop();
            if (edgeHeadings.length == 1) edgeHeadings = edgeHeadings.pop();

            return { distance: edgeLengths, headings: edgeHeadings, coordinates: coordinates };
        }


        centerMapToPolygon(initialPoly);

        function centerMapToPolygon(polygonLayer) {
            var bounds = polygonLayer.getBounds();
            var centerLatLng = bounds.getCenter();
            map.setView(centerLatLng, 13);
        }

        function calculateHeading(startPoint, endPoint) {
            // Calculate the heading between two points
            var deltaX = endPoint[0] - startPoint[0];
            var deltaY = endPoint[1] - startPoint[1];

            var angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
            var heading = (angle + 360) % 360;

            return heading;
        }

        function findLongestEdge(coordinates) {
            var longestEdge = null;
            var longestEdgeLength = 0;

            for (var i = 0; i < coordinates.length - 1; i++) {
                var startPoint = coordinates[i];
                var endPoint = coordinates[i + 1];
                var edge = turf.lineString([startPoint, endPoint]);

                var edgeLength = turf.length(edge, { units: 'feet' });
                if (edgeLength > longestEdgeLength) {
                    longestEdgeLength = edgeLength;
                    longestEdge = edge;
                }
            }

            return longestEdge;
        }

    });
</script>

And here's some sample images from it: Field #1 Field #1 - Working as expected

Field #2 Filed #2 - Not quite working as expected. There are 3 cuts into the polygon on the south edge, and only 1 is being honored.

Field #4 Field #4 - Works

Field #5 Field #5 - Works

Field #6 Field #6 - Works

Field #7 Field #7 - Works

Field #8 Field #8 - Works

Field #9 Field #9 - Works

Field #10 Field #10 - Works

It seems only the fields with more than one cut into the polygon are the ones that suffer, and I can probably work with that for now. But if anyone has any thoughts about what I'm missing here, I'd be more than happy to hear them!

Skudd
  • 684
  • 2
  • 12
  • 28

1 Answers1

-1

In my experience, headland field boundary recordings can have a lot of noise, so it may work better with recorded data to take all the segments, modulus them to [0,90) degrees, and make a histogram of the total path length in 5 or 10 degree bins. It seems like this could produce a set of options for the operator to pick between the top three angles.

theSparky
  • 440
  • 3
  • 13
  • That's not a horrible idea, but it needs a baseline to work from. I can record the completed work and compare to the initial generated path and make revisions from there. – Skudd Aug 24 '23 at 17:04