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.
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.
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.
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 - Working as expected
Filed #2 - Not quite working as expected. There are 3 cuts into the polygon on the south edge, and only 1 is being honored.
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!