I need an algorithm to position lines on a set of points in a 2D environment. The algorithm should identify collinear points within a specified tolerance and place lines accordingly. Each resulting line should include its position, orientation, and length as part of the output. Points will be given in a table all being in order one after the other.
The points will be ordered in a sequence one after the other, just like this: Example of points placement & order
// Example input for testing
const table = [
{ Position: [0.849999487, -1.47224224] },
{ Position: [0.848479152, -1.01117814] },
{ Position: [0.842648506, -0.707066119] },
{ Position: [0.848704576, -0.489999771] },
{ Position: [0.845723033, -0.307818025] },
{ Position: [0.846934378, -0.149337426] },
{ Position: [0.859999716, 0] },
{ Position: [0.846934378, 0.149337381] },
{ Position: [0.845723033, 0.307817996] },
{ Position: [0.848704517, 0.489999801] },
{ Position: [0.842648506, 0.707066059] },
{ Position: [0.848479271, 1.01117814] },
{ Position: [0.599999666, 1.03923011] },
{ Position: [0.376222014, 1.03366148] },
{ Position: [0.184067041, 1.04389584] },
{ Position: [0, 1.0399996] },
{ Position: [-0.184066996, 1.04389584] },
{ Position: [-0.376221985, 1.03366148] },
{ Position: [-0.599999726, 1.03922999] },
{ Position: [-0.874190629, 1.04181993] },
{ Position: [-1.24099123, 1.04131532] }
];
Here's an example of what I'm looking for: Example of desired behavior
You can write the algorithm in any language of your preference.
Currently, I am using the area of the triangle formed by 3 consecutive points to check if it is lower than the tolerance. This works, but it's not perfect. I need a more robust method. I will show you below some of the results I get, and how they're problematic.
Errors with this algorithm:
1. The points are almost perfectly colinear, but when there's a change in direction the algorithm thinks that the next point is still part of the sequence, because of how close they are: Issue 1
2. The first & last point are considered to form a line, but they don't because of how much space there is between one and the other: Issue 2
function getBoundaryWalls2(points) {
const COLLINEAR_TOLERANCE = 0.05; // How strict the line detection is
const MIN_LINE_LENGTH = 1; // Minimum length for a valid line
const walls = [];
// Checks if 3 points form a nearly straight line by calculating triangle area
function areCollinear(p1, p2, p3) {
const x1 = p1.position.x, y1 = p1.position.y;
const x2 = p2.position.x, y2 = p2.position.y;
const x3 = p3.position.x, y3 = p3.position.y;
// If area of triangle is near 0, points are almost collinear
const area = Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2);
return area < COLLINEAR_TOLERANCE;
}
// Gets angle between two points in degreesS
function getAngle(p1, p2) {
const dx = p2.position.x - p1.position.x;
const dy = p2.position.y - p1.position.y;
return (Math.atan2(dy, dx) * 180 / Math.PI);
}
// Gets distance between two points
function getDistance(p1, p2) {
const dx = p2.position.x - p1.position.x;
const dy = p2.position.y - p1.position.y;
return Math.sqrt(dx * dx + dy * dy);
}
let i = 1;
while (i < points.length - 1) {
// Start a new potential line with two points
const currentLine = {
startPoint: points[i],
endPoint: points[i + 1],
points: [points[i], points[i + 1]]
};
// Try to extend the line by adding more collinear points
let j = i + 2;
while (j < points.length) {
if (areCollinear(currentLine.startPoint, currentLine.endPoint, points[j])) {
currentLine.endPoint = points[j];
currentLine.points.push(points[j]);
j++;
} else {
break;
}
}
// If line is long enough, create a wall
const length = getDistance(currentLine.startPoint, currentLine.endPoint);
if (length >= MIN_LINE_LENGTH) {
// Calculate wall center
const centerX = (currentLine.startPoint.position.x + currentLine.endPoint.position.x) / 2;
const centerY = (currentLine.startPoint.position.y + currentLine.endPoint.position.y) / 2;
// Get wall orientation
const orientation = getAngle(currentLine.startPoint, currentLine.endPoint);
// Create wall object
const wall = {
position: { x: centerX, y: centerY },
orientation: orientation,
length: length,
points: currentLine.points
};
walls.push(wall);
}
// Skip to next unprocessed point
i += currentLine.points.length - 1;
}
return walls;
}
Here is my C++ code to check for three points on a line
bool cLinify::isLine(
const cxy &p1,
const cxy &p2,
const cxy &p3)
{
const double COLLINEAR_TOLERANCE = 0.005;
// triangle area method
//(x2−x1)(y3−y1)=(x3−x1)(y2−y1)
// https://math.stackexchange.com/questions/38338/methods-for-showing-three-points-in-mathbbr2-are-colinear-or-not/221530#221530
return abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) < COLLINEAR_TOLERANCE;
}
Here is my code to find lines in between points using this method
void cLinify::solve()
{
cxy lp1, lp2;
for (int k = 0; k < myPoints.size() - 2; k++)
{
if( k == 0 )
{
lp1 = myPoints[0];
lp2 = myPoints[1];
continue;
}
if (isLine(myPoints[k-1], myPoints[k], myPoints[k + 1]))
{
// extend line
lp2 = myPoints[k+1];
}
else
{
// line ended
myLines.push_back(std::make_pair(lp1, lp2));
lp1 = lp2;
}
}
myLines.back() = std::make_pair(lp1, myPoints.back());
}
Here is the output produced from your sample points
The complete application code
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
#include <algorithm>
#include <wex.h> // https://github.com/JamesBremner/windex
#include "cStarterGUI.h"
#include "cxy.h" // https://github.com/JamesBremner/raven-set
struct cLinify
{
std::vector<cxy> myPoints;
std::vector<std::pair<cxy, cxy>> myLines;
cLinify(const std::vector<cxy> &points);
void solve();
void displayLines();
bool isLine(
const cxy &p1,
const cxy &p2,
const cxy &p3);
std::vector<double> box();
bool test();
std::vector<cxy>& getPoints()
{
return myPoints;
}
std::vector<std::pair<cxy, cxy>> getLines()
{
return myLines;
}
};
bool cLinify::isLine(
const cxy &p1,
const cxy &p2,
const cxy &p3)
{
const double COLLINEAR_TOLERANCE = 0.005;
// triangle area method
//(x2−x1)(y3−y1)=(x3−x1)(y2−y1)
// https://math.stackexchange.com/questions/38338/methods-for-showing-three-points-in-mathbbr2-are-colinear-or-not/221530#221530
return abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) < COLLINEAR_TOLERANCE;
}
cLinify::cLinify(const std::vector<cxy> &points)
: myPoints(points)
{
}
void cLinify::solve()
{
cxy lp1, lp2;
for (int k = 0; k < myPoints.size() - 2; k++)
{
if( k == 0 )
{
lp1 = myPoints[0];
lp2 = myPoints[1];
continue;
}
if (isLine(myPoints[k-1], myPoints[k], myPoints[k + 1]))
{
// extend line
lp2 = myPoints[k+1];
}
else
{
// line ended
myLines.push_back(std::make_pair(lp1, lp2));
lp1 = lp2;
}
}
myLines.back() = std::make_pair(lp1, myPoints.back());
}
void cLinify::displayLines()
{
for (auto &l : myLines) {
std::cout
<< "( " << l.first.x << ", " << l.first.y
<< " ) to ( " << l.second.x << ", " << l.second.y
<< " )\n";
}
}
std::vector<double> cLinify::box()
{
cxy topleft;
topleft = myPoints[0];
for (auto &p : myPoints)
{
if (p.x < topleft.x)
topleft.x = p.x;
if (p.y < topleft.y)
topleft.y = p.y;
}
cxy wh(0, 0);
for (auto &p : myPoints)
{
if (p.x - topleft.x > wh.x)
wh.x = p.x - topleft.x;
if (p.y - topleft.y > wh.y)
wh.y = p.y - topleft.y;
}
std::vector<double> ret;
double xscale = 400 / wh.x;
double yscale = 400 / wh.y;
if (yscale < xscale)
ret.push_back(yscale);
else
ret.push_back(xscale);
ret.push_back( topleft.x);
ret.push_back( topleft.y);
return ret;
}
bool cLinify::test()
{
cLinify L({cxy(1, 1),
cxy(2, 2),
cxy(3, 3)});
if (!L.isLine(cxy(1, 1), cxy(2, 2), cxy(3, 3)))
return false;
if (L.isLine(cxy(1, 1), cxy(2, 2), cxy(3, 3.1)))
return false;
std::cout << "unit test passed\n";
return true;
}
class cGUI : public cStarterGUI
{
public:
cGUI(cLinify &L)
: cStarterGUI(
"Linify",
{50, 50, 1000, 500}),
myLinify(L)
{
show();
run();
}
virtual void draw(wex::shapes &S);
private:
cLinify &myLinify;
};
void cGUI::draw(wex::shapes &S)
{
auto thebox = myLinify.box();
S.color(0);
for( auto& p : myLinify.getPoints() )
{
int x = 10 + thebox[0] * ( p.x - thebox[1] );
int y = 10 + thebox[0] * ( p.y - thebox[2] );
S.circle( x, y, 5 );
}
S.color(0x0000FF);
for( auto& l : myLinify.getLines())
{
std::vector<int> vl
{
10 + thebox[0] * ( l.first.x - thebox[1] ),
10 + thebox[0] * ( l.first.y - thebox[2] ),
10 + thebox[0] * ( l.second.x - thebox[1] ),
10 + thebox[0] * ( l.second.y - thebox[2] )
};
S.line(vl);
}
}
main()
{
/*
// Example input for testing
const table = [
{ Position: [0.849999487, -1.47224224] },
{ Position: [0.848479152, -1.01117814] },
{ Position: [0.842648506, -0.707066119] },
{ Position: [0.848704576, -0.489999771] },
{ Position: [0.845723033, -0.307818025] },
{ Position: [0.846934378, -0.149337426] },
{ Position: [0.859999716, 0] },
{ Position: [0.846934378, 0.149337381] },
{ Position: [0.845723033, 0.307817996] },
{ Position: [0.848704517, 0.489999801] },
{ Position: [0.842648506, 0.707066059] },
{ Position: [0.848479271, 1.01117814] },
{ Position: [0.599999666, 1.03923011] },
{ Position: [0.376222014, 1.03366148] },
{ Position: [0.184067041, 1.04389584] },
{ Position: [0, 1.0399996] },
{ Position: [-0.184066996, 1.04389584] },
{ Position: [-0.376221985, 1.03366148] },
{ Position: [-0.599999726, 1.03922999] },
{ Position: [-0.874190629, 1.04181993] },
{ Position: [-1.24099123, 1.04131532] }
];
*/
cLinify L({cxy(0.849999487, -1.47224224),
cxy(0.848479152, -1.01117814),
cxy(0.842648506, -0.707066119),
cxy(0.848704576, -0.489999771),
cxy(0.845723033, -0.307818025),
cxy(0.846934378, -0.149337426),
cxy(0.859999716, 0),
cxy(0.846934378, 0.149337381),
cxy(0.845723033, 0.307817996),
cxy(0.848704517, 0.489999801),
cxy(0.842648506, 0.707066059),
cxy(0.848479271, 1.01117814),
cxy(0.599999666, 1.03923011),
cxy(0.376222014, 1.03366148),
cxy(0.184067041, 1.04389584),
cxy(0, 1.0399996),
cxy(-0.184066996, 1.04389584),
cxy(-0.376221985, 1.03366148),
cxy(-0.599999726, 1.03922999),
cxy(-0.874190629, 1.04181993),
cxy(-1.24099123, 1.04131532)});
L.test();
L.solve();
L.displayLines();
cGUI theGUI(L);
return 0;
}
Complete VSCODE project at https://github.com/JamesBremner/linify