0

I have a problem with sorting points by the angle they create with the X axis. The points look like this

sample

Here is the code I have:

public static List<Point> SortPoints(List<Point> points)
        {
            List<Point> result = new List<Point>();
            List<KeyValuePair<Point, double>> valuePairs = new List<KeyValuePair<Point, double>>();
            foreach (var point in points)
            {
                valuePairs.Add(new KeyValuePair<Point, double>(point, Math.Atan2(point.Y, point.X)));
            }
            valuePairs = valuePairs.OrderByDescending(x => x.Value).ToList();
            foreach (var valuepair in valuePairs)
            {
                result.Add(valuepair.Key);
            }
            return result;
        }

It should sort points in the way, they close up. It works for most points, but it doesn't for some of them. It crashes mostly on these fragments:

failed fragments

Is my thinking correct for that kind of problem or do I miss something? I am still new to geometry in programming.

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/230327/discussion-on-question-by-maciejpuzianowski-sorting-points-by-polar-angle). – Samuel Liew Mar 24 '21 at 13:39

2 Answers2

0

C# TRANSLATION

public static List<Point> SortPoints(List<Point> points)
    {
        double[] pnt = new double[points.Count * 2];
        for (int z = 0, ii = 0; z < points.Count; z++, ii += 2)
        {
            pnt[ii] = points[z].X;
            pnt[ii + 1] = points[z].Y;
        }
        int n2 = pnt.Length;
        int n = n2 >> 1;
        int[] idx = new int[n];
        int i, j, k, i0, i1, e, m;
        double a, x0, y0, x1, y1, x, y;
        double[] ang = new double[n];
        int[] flag = new int[n];
        const double deg = Math.PI / 180.0;
        const double thr = 0.02 * deg;
        for (i = 0; i < n; i++) idx[i] = i + i;
        x0 = x1 = pnt[0];
        y0 = y1 = pnt[1];
        for (i = 0; i < n2;)
        {
            x = pnt[i]; i++;
            y = pnt[i]; i++;
            if (x0 > x) x0 = x;
            if (x1 < x) x1 = x;
            if (y0 > y) y0 = y;
            if (y1 < y) y1 = y;
        }
        x = 0.5 * (x0 + x1);
        y = 0.5 * (y0 + y1);
        for (i = 0, j = 0; i < n; i++, j += 2)
        {
            a = Math.Atan2(pnt[j + 1] - y, pnt[j + 0] - x);
            ang[i] = a;
        }
        for (e = 1, j = n; e != 0; j--)                                 // loop until jo swap occurs
            for (e = 0, i = 1; i < j; i++)                              // proces unsorted part of array
                if (ang[idx[i - 1] >> 1] > ang[idx[i] >> 1])              // condition if swap needed
                { e = idx[i - 1]; idx[i - 1] = idx[i]; idx[i] = e; e = 1; }
        for (i = 0; i < n; i++) flag[i] = 0;
        for (e = 0, j = 1, i = 1; i < n; i++)
            if (Math.Abs(ang[idx[i] >> 1] - ang[idx[i - 1] >> 1]) < thr)
            { flag[idx[i] >> 1] = j; flag[idx[i - 1] >> 1] = j; e = 1; }
            else if (e != 0) { e = 0; j++; }
        if (e != 0) j++; m = j;
        x = x0 + (0.3 * (x1 - x0));
        y = 0.5 * (y0 + y1);
        for (i = 0, j = 0; i < n; i++, j += 2)
            if (flag[i] != 0)                                      // only for problematic zones no need to recompute finished parts
            {
                a = Math.Atan2(pnt[j + 1] - y, pnt[j + 0] - x);                 // this might need handling edge cases of atan2
                ang[i] = a;
            }
        for (k = 0; k < n;)
        {
            for (; k < n; k++) if (flag[idx[k] >> 1] != 0)                     // zone start
                {
                    i0 = i1 = k;
                    for (; k < n; k++) if (flag[idx[k] >> 1] != 0) i1 = k;//           // zone end
                        else break;
                    // index (bubble) sort idx[] asc by ang[]
                    if (i0 != i1)
                        for (e = 1, j = i1 - i0 + 1; e > 0; j--)                          // loop until jo swap occurs
                            for (e = 0, i = i0 + 1; i < i0 + j; i++)                       // proces unsorted part of array
                                if (ang[idx[i - 1] >> 1] > ang[idx[i] >> 1])             // condition if swap needed
                                { e = idx[i - 1]; idx[i - 1] = idx[i]; idx[i] = e; e = 1; } // swap and allow to process array again
                                                                                            // different center for atan2 might reverse the ang order
                                                                                            // so test if start or end of zone is closer to the point before it
                    j = i0 - 1; if (j < 0) j = n - 1;                             // 45 deg is never at start or end of data so this should never happen
                    x = pnt[idx[j] + 0] - pnt[idx[i0] + 0];
                    y = pnt[idx[j] + 1] - pnt[idx[i0] + 1];
                    a = (x * x) + (y * y);
                    x = pnt[idx[j] + 0] - pnt[idx[i1] + 0];
                    y = pnt[idx[j] + 1] - pnt[idx[i1] + 1];
                    x = (x * x) + (y * y);
                    // reverse if not in correct order
                    if (x < a) for (; i0 < i1; i0++, i1--)
                        { j = idx[i0]; idx[i0] = idx[i1]; idx[i1] = j; }
                }
        }
        List<Point> result = new List<Point>();
        for (int h = 0; h < pnt.Length - 1; h += 2)
        {
            result.Add(new Point(pnt[h], pnt[h + 1]));
        }
        return result;
    }
-1

Ok I got it working sorting by atan2 angle by using 2 centers only (no need for 3 as I can detect the problem zones directly from the first center alone). This is the algorithm (shape must not self intersect and angle around selected centers must be monotonic !!!):

  1. compute BBOX (x0,y0,x1,y1) for your data

    this is needed to properly compute correct center locations for atan2 usage

  2. compute angle by atan2 for each point using BBOX center as center

    the center should be inside your shape so center of BBOX is the obvious choice. However as Yves Daoust pointed out this will not work for arbitrary concave shapes only for those shapes and centers where the angle is monotonic.

  3. sort your points by this angle

  4. detect problematic zones

    simply in problematic zones the consequent points after the sort has almost the same angle so just threshold that.

  5. compute atan2 angle for each problem zone with different center

    again center must be inside ... and should be shifted in any of the multiple of 90 degrees angle from original center. I chose shift toward x0 by 20% of shape x size. The bigger the shift the more ang difference the problem zones will get.

  6. sort the problem zones by new angle

  7. reverse problem zone order after sort if needed

    the shifted center might cause the angle direction reversal in comparison to original angles. So after sort if you compute distance between point before zone and zone first and last point if the last point of zone is closer it means you need to reverse the zone points order.

Here preview of output:

preview

Here C++/OpenGL/VCL code for this:

//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "Unit1.h"
#include "gl_simple.h"
#include "data.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
const int n2=sizeof(pnt)/sizeof(pnt[0]);    // size of pnt[]
const int n=n2>>1;                          // point in pnt[]
int idx[n];                                 // index sort
int ix=1055;                                // just debug X cursor on mouse wheel
//---------------------------------------------------------------------------
void compute()
    {
    int i,j,k,i0,i1,e,m;
    float a,x0,y0,x1,y1,x,y;
    float ang[n];   // atan2 angles per point
    int flag[n];    // 0 or problem zone ix per point
    const float deg=M_PI/180.0;
    const float thr=0.02*deg;
    // shuffle input data for debug as its already ordered
    for (i=0;i<n2;)
        {
        j=Random(n2)&0xFFFFFFFE;
        a=pnt[i]; pnt[i]=pnt[j]; pnt[j]=a; i++; j++;
        a=pnt[i]; pnt[i]=pnt[j]; pnt[j]=a; i++; j++;
        }
    // init index sort table
    for (i=0;i<n;i++) idx[i]=i+i;
    // compute BBOX of data
    x0=x1=pnt[0];
    y0=y1=pnt[1];
    for (i=0;i<n2;)
        {
        x=pnt[i]; i++;
        y=pnt[i]; i++;
        if (x0>x) x0=x;
        if (x1<x) x1=x;
        if (y0>y) y0=y;
        if (y1<y) y1=y;
        }
    // compute atan2 for center set to center of BBOX
    x=0.5*(x0+x1);
    y=0.5*(y0+y1);
    for (i=0,j=0;i<n;i++,j+=2)
        {
        a=atan2(pnt[j+1]-y,pnt[j+0]-x);                 // this might need handling edge cases of atan2
        ang[i]=a;
        }
    // index (bubble) sort idx[] asc by ang[]
    for (e=1,j=n;e;j--)                                 // loop until no swap occurs
     for (e=0,i=1;i<j;i++)                              // process unsorted part of array
      if (ang[idx[i-1]>>1]>ang[idx[i]>>1])              // condition if swap needed
      { e=idx[i-1]; idx[i-1]=idx[i]; idx[i]=e; e=1; }   // swap and allow to process array again
    // detect/label problematic zones m = number of zones +1
    for (i=0;i<n;i++) flag[i]=0;
    for (e=0,j=1,i=1;i<n;i++)
     if (fabs(ang[idx[i]>>1]-ang[idx[i-1]>>1])<thr)
      { flag[idx[i]>>1]=j; flag[idx[i-1]>>1]=j; e=1; }
      else if (e){ e=0; j++; }
    if (e) j++; m=j;
    // compute atan2 for center shifted toward x0
    // so it still inside but not too close to (0,0)
    // so there is some ang diference on problematic zones
    x=x0+(0.3*(x1-x0));
    y=0.5*(y0+y1);
    for (i=0,j=0;i<n;i++,j+=2)
     if (flag[i])                                       // only for problematic zones no need to recompute finished parts
        {
        a=atan2(pnt[j+1]-y,pnt[j+0]-x);                 // this might need handling edge cases of atan2
        ang[i]=a;
        }
    // loop through problematic zones
    for (k=0;k<n;)
        {
        for (;k<n;k++) if (flag[idx[k]>>1])                     // zone start
            {
            i0=i1=k;
            for (;k<n;k++) if (flag[idx[k]>>1]) i1=k;           // zone end
              else break;
            // index (bubble) sort idx[] asc by ang[]
            if (i0!=i1)
             for (e=1,j=i1-i0+1;e;j--)                          // loop until no swap occurs
              for (e=0,i=i0+1;i<i0+j;i++)                       // process unsorted part of array
               if (ang[idx[i-1]>>1]>ang[idx[i]>>1])             // condition if swap needed
                { e=idx[i-1]; idx[i-1]=idx[i]; idx[i]=e; e=1; } // swap and allow to process array again
            // different center for atan2 might reverse the ang order
            // so test if start or end of zone is closer to the point before it
            j=i0-1; if (j<0) j=n-1;                             // 45 deg is never at start or end of data so this should never happen
            x=pnt[idx[j]+0]-pnt[idx[i0]+0];
            y=pnt[idx[j]+1]-pnt[idx[i0]+1];
            a=(x*x)+(y*y);
            x=pnt[idx[j]+0]-pnt[idx[i1]+0];
            y=pnt[idx[j]+1]-pnt[idx[i1]+1];
            x=(x*x)+(y*y);
            // reverse if not in correct order
            if (x<a) for (;i0<i1;i0++,i1--)
             { j=idx[i0]; idx[i0]=idx[i1]; idx[i1]=j; }
            }
        }
    }
//---------------------------------------------------------------------------
void gl_draw()
    {
    int i,j;
    float a,da=1.0/float(n-1),x,y,r;
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_TEXTURE_2D);

    // set view to 2D
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glScalef(1.0/120.0,1.0/120.0,1.0);

    // render points from list
    glBegin(GL_POINTS);
//  glBegin(GL_LINE_STRIP);
//  glBegin(GL_TRIANGLE_FAN);
    glColor3f(0.0,0.0,0.0);
    glVertex2f(0.0,0.0);
    for (a=0.0,i=0;i<n;i++,a+=da)
        {
        glColor3f(a,a,a);
        glVertex2fv(pnt+idx[i]);
        }
    glEnd();

    // render debug index (on mouse wheel)
    x=pnt[idx[ix]+0];
    y=pnt[idx[ix]+1];
    r=5.0;
    glBegin(GL_LINES);
    glColor3f(0.0,1.0,0.0);
    glVertex2f(x-r,y-r);
    glVertex2f(x+r,y+r);
    glVertex2f(x-r,y+r);
    glVertex2f(x+r,y-r);
    glEnd();

    glFinish();
    SwapBuffers(hdc);
    Form1->Caption=ix;
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
    {
    // Init of program
    gl_init(Handle);    // init OpenGL
    compute();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
    {
    // Exit of program
    gl_exit();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
    {
    // repaint
    gl_draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
    {
    // resize
    gl_resize(ClientWidth,ClientHeight);
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormMouseWheel(TObject *Sender, TShiftState Shift, int WheelDelta, TPoint &MousePos, bool &Handled)
    {
    Handled=true;
    int dix=1; if (Shift.Contains(ssShift)) dix=10;
    if (WheelDelta>0){ ix+=dix; if (ix>=n) ix=  0; }
    if (WheelDelta<0){ ix-=dix; if (ix< 0) ix=n-1; }
    gl_draw();
    }
//---------------------------------------------------------------------------

Just ignore the VCL and OpenGL stuff. The only important stuff here is the function compute() which works as described above. The global variables just above it. I used index sort however so the points order is not changed at all instead idx[i] holds the index of i-th point in the input data. I wanted to keep this as simple as I could so no dynamic allocations nor containers or funny stuff... I used your data as input in form:

float pnt[]=
    {
    -98.622,0.4532042,
    -98.622,1.64291,
    -98.612097,3.0877569,
    ...
    -98.618994,-3.2649391,
    -98.6260115,-1.9205891
    };

The gl_simple.h I used for OpenGL can be found here:

I tested this by using the cursor (green cross on image) and mouse wheel through the whole shape looking for jumping back and forward ... In previous versions I got the flag[] array as global and rendered different colors for the problem zones so I can debug directly them and not looking for whole shape again and again ... Also I used bubble sort ... in case you got huge data use quick sort instead... however this data you provided is computed instantly on my old computer so I didn't bother to add recursion.

For arbitrary concave non self-intersecting shapes you need to use different approach like for example connected component analysis:

  1. for each point compute its 2 nearest neighbor points

    this is very slow and should be speed-ed up by using spatial sorting of points to lower the complexity.

    in case of very non uniform sampling the two closest points should not lie on or near the same direction!!! So dot between their unit direction should be as far from +1.0 as it can.

  2. select any start point and add it to output

  3. select one of its yet unused neighbors and add it to output

  4. set link between selected and its predecessing point as used.

  5. loop #4 until you get to the starting point again

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • What does this "e" condition means here `for (e=1,j=n;e;j--)`? How int can be a boolean? – maciejpuzianowski Mar 24 '21 at 10:56
  • @maciejpuzianowski its the bubble sort ending condition... no need to iterate if data is already sorted (no swap occurs in last pass)... and yes `int`s can be `boolean` ... so condition `(e)` means: `e==0` is `false` and `e!=0` is `true` .. that is common in C/C++ languages so you can rewrite it to `(e!=0)` which yelds the same result as `(e)` – Spektre Mar 24 '21 at 11:12
  • I have translated your precious code to c#, however I get an `IndexOutOfRangeException` in `if (ang[idx[i-1]>>1]>ang[idx[i]>>1]) // condition if swap needed { e=idx[i-1]; idx[i-1]=idx[i]; idx[i]=e; e=1; } // swap and allow to process array again` that if. I will post my translation. – maciejpuzianowski Mar 24 '21 at 14:17
  • @maciejpuzianowski my input points are 1D array holding `{ x0,y0, x1,y1, x2,y2, ...}` so first point is at index 0, next at index 2, next at index 4, next at index 6 ... so at start I feed the `idx[] = { 0,2,4,6,8,... }` now when you wan tot access i-th point use `pnt[idx[i]]` however all the other tables are not 2D vectors but only scalars so index must be divided by 2 for those: `ang[idx[i]>>1] , flag[idx[i]>>1]` ... now if you have index out of range there might be 2 common reasons: 1. your language indexes starts from 1 instead of 0 – Spektre Mar 24 '21 at 14:21
  • @maciejpuzianowski 2. you mess something up causing the `idx[]` array holds wrong values at some point (like wrong swap code ... or some typo or whatever) ... just to be sure the bubble sort for loop start with `i=1` not `0` ... as I use `i` and `i-1` in comparison so if you start from `0` you got a problem – Spektre Mar 24 '21 at 14:21
  • @maciejpuzianowski from a quick look at your code are you sure that `pnt.Length;` return the number of `double`s in array and not the allocated size in `Bytes`? maybe safer would be `n2 = points.Count * 2` – Spektre Mar 24 '21 at 14:29
  • 1
    I found the bug, I have just mistyped one variable. Your code works brilliant. Thank you very much for your help. You are the boss. – maciejpuzianowski Mar 24 '21 at 17:04