├── 100TestCoords.txt ├── Proof.png ├── README.md ├── tsp.py └── tsp_solver.py /100TestCoords.txt: -------------------------------------------------------------------------------- 1 | 40.24297393200541,-77.0035012238999 2 | 40.21507783613489,-77.02347645316718 3 | 40.27114249775027,-76.94895753529786 4 | 40.219669,-76.983865 5 | 40.28537106536488,-77.01798809196181 6 | 40.21972186603164,-77.05748074636104 7 | 40.22377448009034,-76.9763224380628 8 | 40.24789451117517,-77.00188146655874 9 | 40.290991,-76.980329 10 | 40.28181729272107,-76.97920259698685 11 | 40.26706128426313,-76.97164203459519 12 | 40.2121119678532,-77.01051992262413 13 | 40.23911815610897,-77.00827038390398 14 | 40.25816756120965,-77.04182916504384 15 | 40.22169191405075,-76.95669954451313 16 | 40.22613823720287,-76.94130506764961 17 | 40.30418438658337,-76.9339222335575 18 | 40.22076237515356,-76.96507116498196 19 | 40.21549157397697,-77.00638073914823 20 | 40.2870128046609,-76.98622269896552 21 | 40.28035228999222,-77.0318434759776 22 | 40.287275,-76.954654 23 | 40.23733232298675,-76.96228068862203 24 | 40.27686003404827,-77.00872029498481 25 | 40.25009662521456,-77.00881027700079 26 | 40.23250544727463,-77.08086438042314 27 | 40.25844423023904,-77.01177964608638 28 | 40.21431915345849,-77.04101954530871 29 | 40.2211723391992,-77.07375983350437 30 | 40.22593160321316,-77.00215142761607 31 | 40.23575288813444,-76.9801026326832 32 | 40.24146352015563,-76.92149645950285 33 | 40.2732909932155,-76.96570126371493 34 | 40.28715991831912,-76.97650244999063 35 | 40.23892287465784,-77.05289342417957 36 | 40.22599269868361,-77.04209903708336 37 | 40.29049978533623,-76.94391596335895 38 | 40.23594578920235,-76.92131636656372 39 | 40.213877,-76.998131 40 | 40.22939996561175,-76.96435104815956 41 | 40.279216,-76.941373 42 | 40.24888913603001,-76.9259986970467 43 | 40.22064676700747,-77.0061107874955 44 | 40.26690247188268,-77.02194693487259 45 | 40.23207252948577,-77.00710060728665 46 | 40.235648,-76.972185 47 | 40.24758736271338,-77.02680533797088 48 | 40.21875226477405,-77.0170883368984 49 | 40.22893127859683,-76.92041589790273 50 | 40.27980636358748,-76.96021029396303 51 | 40.29457593846715,-76.93428238213355 52 | 40.23315112265849,-77.03733120898971 53 | 40.26723838821673,-76.98145267375445 54 | 40.23899854692659,-76.94112500382299 55 | 40.25450320452236,-76.92428786624959 56 | 40.23885467710176,-76.95291862455093 57 | 40.24598802710373,-77.01618857515554 58 | 40.24169590151379,-76.98676269003892 59 | 40.25674308013073,-77.01960763424171 60 | 40.2389542807885,-77.09273433210042 61 | 40.269662,-77.048675 62 | 40.2528043259795,-76.9512981944639 63 | 40.233440968437,-76.9502178958029 64 | 40.217332,-77.029014 65 | 40.23288938093179,-76.93509271255768 66 | 40.24547513477401,-76.93527278525717 67 | 40.26164003438222,-76.93725356747514 68 | 40.26005495122646,-76.96597130503282 69 | 40.25241790551942,-76.99126252243641 70 | 40.25986042688429,-76.94571654862438 71 | 40.28614363590564,-76.93122108548621 72 | 40.21081883097627,-77.01780814148351 73 | 40.25251760673532,-77.00179147940628 74 | 40.307766994543,-76.93986454961838 75 | 40.258809,-76.976472 76 | 40.27721702283007,-76.94922761364288 77 | 40.23955963501814,-76.93230155186146 78 | 40.24626013296728,-77.06773412953513 79 | 40.247572,-77.051496 80 | 40.29965264252392,-76.93545285769187 81 | 40.22363674205992,-77.01996752958651 82 | 40.24159438683109,-77.02590564840074 83 | 40.226147,-76.984807 84 | 40.207399,-77.029168 85 | 40.24409732537843,-76.99612215439676 86 | 40.25356363147802,-76.96138052134617 87 | 40.25345564955763,-77.04182916504384 88 | 40.22586690760417,-76.9705619159629 89 | 40.27594830416179,-76.97110197647527 90 | 40.23922797287306,-77.07223095209821 91 | 40.23227839183281,-76.92563852412567 92 | 40.24864974055637,-77.04515754473128 93 | 40.28050217287626,-76.9292402057204 94 | 40.24639615506855,-76.96669141229134 95 | 40.25217,-77.031472 96 | 40.27351792154369,-76.92581861071844 97 | 40.2330180777503,-77.02716521192674 98 | 40.270068,-76.991212 99 | 40.22596638370256,-77.0314836159336 100 | 40.22638027267461,-76.92969040847879 -------------------------------------------------------------------------------- /Proof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kneckter/AutoSortCoords/274491b6241a0ed9d4fad9b7f4b2c74a71422164/Proof.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archived 2 | This project is now archived and replaced with the SpawnpointClusterTool located at https://github.com/Kneckter/SpawnpointClusterTool. 3 | 4 | # TSP Py 5 | A python script that will sort coordinates by using the TSP Solver module to make the shortest route through a list of coordinates. I did not write this script but I wanted to show it in this repository since it works better than my VBS macro. I did modified it slightly so it does not need Tkinter. 6 | 7 | Proof of concent is shown in the image below. The first image is a path of 400 coordinates generated from RDM Tools. The second image is a path of the same coordinates using the AutoSortCoords macro. The last image is the path using the same coordinates and the pythong script. 8 | 9 | ![alt text](https://raw.githubusercontent.com/Kneckter/AutoSortCoords/python/Proof.png) 10 | 11 | # Get Started With Python 12 | To get started using the python script, you can download the two python files or `git clone https://github.com/Kneckter/AutoSortCoords/ -b python` for this repository branch. The script requires python's `matplotlib` package for calculating the route. You can install it with `pip install matplotlib`. Note that I have only tested this with Python 2.7 13 | 14 | Once you have the files on your system, create a text file of your coordinates named `infile.txt` that has one coordinate pair per line like this: 15 | ``` 16 | 15.0001,-25.0001 17 | 15.0002,-25.0002 18 | 15.0003,-25.0003 19 | ``` 20 | 21 | Then run the command `python tsp.py infile.txt > outfile.txt` to sort the coordinates and output them to outfile.txt. The outfile will be formatted as one coordinate pair per line like the infile. 22 | 23 | ## Notes 24 | This has been tested on Ubuntu 18.04 with Python 2.7. 25 | 26 | Thanks to https://github.com/dmishin/tsp-solver for the tsp-solver module. 27 | Thanks to http://www.gpsvisualizer.com/, you can upload your coordinates and get images like the ones in this post. 28 | -------------------------------------------------------------------------------- /tsp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import random, numpy, math, copy 3 | import matplotlib 4 | matplotlib.use('Agg') 5 | import matplotlib.pyplot as plt 6 | import sys 7 | from tsp_solver import solve_tsp 8 | 9 | assert(len(sys.argv) >= 2) 10 | infile = sys.argv[1] 11 | points = [] 12 | with (open(infile,'r')) as f: 13 | for line in f: 14 | line = line.rstrip('\n') 15 | (lat,lon) = [numpy.float64(x) for x in line.split(',')] 16 | points.append((lat,lon)) 17 | 18 | tour = [i for i in range(len(points))] 19 | 20 | D = numpy.zeros((len(points),len(points))) 21 | for i in range(len(points)): 22 | for j in range(len(points)): 23 | D[i][j]=numpy.linalg.norm(numpy.subtract(points[i],points[j])) 24 | 25 | tour = solve_tsp( D ) 26 | 27 | plt.plot([points[tour[i]][0] for i in range(len(points))], [points[tour[i]][1] 28 | for i in range(len(points))], 'xb-'); 29 | for i in tour: 30 | print("%s,%s" % (points[i][0].astype(str), points[i][1].astype(str))) 31 | plt.show() 32 | -------------------------------------------------------------------------------- /tsp_solver.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | from itertools import islice 3 | from array import array as pyarray 4 | ################################################################################ 5 | # A simple algorithm for solving the Travelling Salesman Problem 6 | # Finds a suboptimal solution 7 | ################################################################################ 8 | if "xrange" not in globals(): 9 | #py3 10 | xrange = range 11 | else: 12 | #py2 13 | pass 14 | 15 | 16 | def optimize_solution( distances, connections, endpoints ): 17 | """Tries to optimize solution, found by the greedy algorithm""" 18 | N = len(connections) 19 | path = restore_path( connections, endpoints ) 20 | def ds(i,j): #distance between ith and jth points of path 21 | pi = path[i] 22 | pj = path[j] 23 | if pi < pj: 24 | return distances[pj][pi] 25 | else: 26 | return distances[pi][pj] 27 | 28 | d_total = 0.0 29 | optimizations = 0 30 | for a in xrange(N-1): 31 | b = a+1 32 | for c in xrange( b+2, N-1): 33 | d = c+1 34 | delta_d = ds(a,b)+ds(c,d) -( ds(a,c)+ds(b,d)) 35 | if delta_d > 0: 36 | d_total += delta_d 37 | optimizations += 1 38 | connections[path[a]].remove(path[b]) 39 | connections[path[a]].append(path[c]) 40 | connections[path[b]].remove(path[a]) 41 | connections[path[b]].append(path[d]) 42 | 43 | connections[path[c]].remove(path[d]) 44 | connections[path[c]].append(path[a]) 45 | connections[path[d]].remove(path[c]) 46 | connections[path[d]].append(path[b]) 47 | path[:] = restore_path( connections, endpoints ) 48 | 49 | return optimizations, d_total 50 | 51 | def restore_path( connections, endpoints ): 52 | """Takes array of connections and returns a path. 53 | Connections is array of lists with 1 or 2 elements. 54 | These elements are indices of teh vertices, connected to this vertex 55 | Guarantees that first index < last index 56 | """ 57 | if endpoints is None: 58 | #there are 2 nodes with valency 1 - start and end. Get them. 59 | start, end = [idx 60 | for idx, conn in enumerate(connections) 61 | if len(conn)==1 ] 62 | else: 63 | start, end = endpoints 64 | 65 | path = [start] 66 | prev_point = None 67 | cur_point = start 68 | while True: 69 | next_points = [pnt for pnt in connections[cur_point] 70 | if pnt != prev_point ] 71 | if not next_points: break 72 | next_point = next_points[0] 73 | path.append(next_point) 74 | prev_point, cur_point = cur_point, next_point 75 | return path 76 | 77 | def _assert_triangular(distances): 78 | """Ensure that matrix is left-triangular at least. 79 | """ 80 | for i, row in enumerate(distances): 81 | if len(row) < i: raise ValueError( "Distance matrix must be left-triangular at least. Row {row} must have at least {i} items".format(**locals())) 82 | 83 | 84 | def pairs_by_dist(N, distances): 85 | """returns list of coordinate pairs (i,j), sorted by distances; such that i < j""" 86 | #Sort coordinate pairs by distance 87 | indices = [] 88 | for i in xrange(N): 89 | for j in xrange(i): 90 | indices.append(i*N+j) 91 | 92 | indices.sort(key = lambda ij: distances[ij//N][ij%N]) 93 | return ((ij//N,ij%N) for ij in indices) 94 | 95 | def solve_tsp( distances, optim_steps=3, pairs_by_dist=pairs_by_dist, endpoints=None ): 96 | """Given a distance matrix, finds a solution for the TSP problem. 97 | Returns list of vertex indices. 98 | Guarantees that the first index is lower than the last 99 | 100 | :arg: distances : left-triangular matrix of distances. array of arrays 101 | :arg: optim_steps (int) number of additional optimization steps, allows to improve solution but costly. 102 | :arg: pairs_by_dist (function) an implementtion of the pairs_by_dist function. for optimization purposes. 103 | :arg: endpoinds : None or pair (int,int) 104 | """ 105 | N = len(distances) 106 | if N == 0: return [] 107 | if N == 1: return [0] 108 | 109 | _assert_triangular(distances) 110 | 111 | #State of the TSP solver algorithm. 112 | node_valency = pyarray('i', [2])*N #Initially, each node has 2 sticky ends 113 | if endpoints is not None: 114 | start, end = endpoints 115 | if start == end: raise ValueError("start=end is not supported") 116 | node_valency[start]=1 117 | node_valency[end]=1 118 | 119 | 120 | #for each node, stores 1 or 2 connected nodes 121 | connections = [[] for i in xrange(N)] 122 | 123 | def join_segments(sorted_pairs): 124 | #segments of nodes. Initially, each segment contains only 1 node 125 | segments = [ [i] for i in xrange(N) ] 126 | 127 | def possible_edges(): 128 | #Generate sequence of graph edges, that are possible and connect different segments. 129 | #print("#### sorted pairs:", sorted_pairs) 130 | for ij in sorted_pairs: 131 | i,j = ij 132 | #if both start and end could have connections, 133 | # and both nodes connect to a different segments: 134 | if node_valency[i] and node_valency[j] and\ 135 | (segments[i] is not segments[j]): 136 | yield ij 137 | 138 | def connect_vertices(i,j): 139 | node_valency[i] -= 1 140 | node_valency[j] -= 1 141 | connections[i].append(j) 142 | connections[j].append(i) 143 | #Merge segment J into segment I. 144 | seg_i = segments[i] 145 | seg_j = segments[j] 146 | if len(seg_j) > len(seg_i): 147 | seg_i, seg_j = seg_j, seg_i 148 | i, j = j, i 149 | for node_idx in seg_j: 150 | segments[node_idx] = seg_i 151 | seg_i.extend(seg_j) 152 | 153 | def edge_connects_endpoint_segments(i,j): 154 | #return True, if given ede merges 2 segments that have endpoints in them 155 | si,sj = segments[i],segments[j] 156 | ss,se = segments[start], segments[end] 157 | return (si is ss) and (sj is se) or (sj is ss) and (si is se) 158 | 159 | 160 | #Take first N-1 possible edge. they are already sorted by distance 161 | edges_left = N-1 162 | for i,j in possible_edges(): 163 | if endpoints and edges_left!=1 and edge_connects_endpoint_segments(i,j): 164 | #print(f"#### disallow {i}, {j} because premature termination") 165 | continue #don't allow premature path termination 166 | 167 | 168 | #print(f"####add edge {i}, {j} of len {distances[i][j]}") 169 | 170 | connect_vertices(i,j) 171 | edges_left -= 1 172 | if edges_left == 0: 173 | break 174 | 175 | #invoke main greedy algorithm 176 | join_segments(pairs_by_dist(N, distances)) 177 | 178 | #now call additional optiomization procedure. 179 | for passn in range(optim_steps): 180 | nopt, dtotal = optimize_solution( distances, connections, endpoints ) 181 | if nopt == 0: 182 | break 183 | #restore path from the connections map (graph) and return it 184 | return restore_path( connections, endpoints=endpoints ) --------------------------------------------------------------------------------