Optimally drawing round trip path between three to six points using brute force
A Pen by HARUN PEHLİVAN on CodePen.
| <header> | |
| <select> | |
| <option value="3">3 Points</option> | |
| <option value="4" selected>4 Points</option> | |
| <option value="5">5 Points</option> | |
| <option value="6">6 Points</option> | |
| </select> | |
| <button>Refresh</button> | |
| </header> | |
| <main></main> |
| console.clear(); | |
| const PI2 = Math.PI * 2; | |
| const PERMUTATIONS = { | |
| 3: [[0,1,2]], | |
| 4: [[0,1,2,3], [0,1,3,2], [0,2,1,3]], | |
| 5: [ | |
| [0,1,2,3,4], [0,1,2,4,3], [0,1,3,2,4], [0,1,3,4,2], [0,1,4,2,3], [0,1,4,3,2], | |
| [0,2,1,3,4], [0,2,1,4,3], [0,2,3,1,4], [0,2,4,1,3], | |
| [0,3,1,2,4], [0,3,2,1,4], | |
| ], | |
| 6: [ | |
| [0,1,2,3,4,5], [0,1,2,3,5,4], [0,1,2,4,3,5], [0,1,2,4,5,3], [0,1,2,5,3,4], [0,1,2,5,4,3], | |
| [0,1,3,2,4,5], [0,1,3,2,5,4], [0,1,3,4,2,5], [0,1,3,4,5,2], [0,1,3,5,2,4], [0,1,3,5,4,2], | |
| [0,1,4,2,3,5], [0,1,4,2,5,3], [0,1,4,3,2,5], [0,1,4,3,5,2], [0,1,4,5,2,3], [0,1,4,5,3,2], | |
| [0,1,5,2,3,4], [0,1,5,2,4,3], [0,1,5,3,2,4], [0,1,5,3,4,2], [0,1,5,4,2,3], [0,1,5,4,3,2], | |
| [0,2,1,3,4,5], [0,2,1,3,5,4], [0,2,1,4,3,5], [0,2,1,4,5,3], [0,2,1,5,3,4], [0,2,1,5,4,3], | |
| [0,2,3,1,4,5], [0,2,3,1,5,4], [0,2,3,4,1,5], [0,2,3,5,1,4], [0,2,4,1,3,5], [0,2,4,1,5,3], | |
| [0,2,4,3,1,5], [0,2,4,5,1,3], [0,2,5,1,3,4], [0,2,5,1,4,3], [0,2,5,3,1,4], [0,2,5,4,1,3], | |
| [0,3,1,2,4,5], [0,3,1,2,5,4], [0,3,1,4,2,5], [0,3,1,5,2,4], [0,3,2,1,4,5], [0,3,2,1,5,4], | |
| [0,3,2,4,1,5], [0,3,2,5,1,4], [0,3,4,1,2,5], [0,3,4,2,1,5], [0,3,5,1,2,4], [0,3,5,2,1,4], | |
| [0,4,1,2,3,5], [0,4,1,3,2,5], [0,4,2,1,3,5], [0,4,2,3,1,5], [0,4,3,1,2,5], [0,4,3,2,1,5], | |
| ] | |
| }; | |
| class MathHelper { | |
| static distance(x1, y1, x2, y2) { | |
| return Math.hypot(x1 - x2, y1 - y2); | |
| } | |
| static heron(len1, len2, len3) { | |
| let s = (len1 + len2 + len3) / 2; | |
| return Math.sqrt(s * len1 * len2 * len3); | |
| } | |
| // https://stackoverflow.com/a/2117523 | |
| static generateUUID() { | |
| return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { | |
| let r = Math.random() * 16 | 0; | |
| let v = c == 'x' ? r : (r & 0x3 | 0x8); | |
| return v.toString(16); | |
| }); | |
| } | |
| } | |
| class Point { | |
| constructor({ x, y }) { | |
| this.id = MathHelper.generateUUID(); | |
| this.relX = x; | |
| this.relY = y; | |
| } | |
| // relative vectors after all vectors are known | |
| relate({ realW, realH, relW, relH, minX, minY }) { | |
| this.relX = (this.relX - minX) / relW; | |
| this.relY = (this.relY - minY) / relH; | |
| this.setRealVectors(realW, realH); | |
| } | |
| // normal vectors relative to real realH and realW of canvas | |
| setRealVectors(realW, realH) { | |
| let a = 80; | |
| let b = a / 2; | |
| this.realX = this.relX * (realW - a) + b; | |
| this.realY = this.relY * (realH - a) + b; | |
| this.realDist = MathHelper.distance(realW / 2, realH / 2, this.realX, this.realY); | |
| } | |
| } | |
| class Route { | |
| constructor() { | |
| this.sequenceIds = []; | |
| this.sequences = {}; | |
| } | |
| sequence(idx) { | |
| let id = this.sequenceIds[idx]; | |
| return id ? this.sequences[id] : null; | |
| } | |
| traverse(points) { | |
| let permutations = PERMUTATIONS[Object.keys(points).length]; | |
| for (let i = 0; i < permutations.length; i++) { | |
| let dist = 0; | |
| let sequenceIdxs = permutations[i]; | |
| let sequence = { distance: 0, points: [] }; | |
| let id = MathHelper.generateUUID(); | |
| for (let j = 0; j < sequenceIdxs.length; j++) { | |
| let p1 = points[sequenceIdxs[j]]; | |
| let p2 = points[sequenceIdxs[(j + 1) % sequenceIdxs.length]]; | |
| let distance = MathHelper.distance(p1.realX, p1.realY, p2.realX, p2.realY); | |
| sequence.points.push(p1.id); | |
| sequence.distance += distance; | |
| } | |
| this.sequenceIds.push(id); | |
| this.sequences[id] = sequence; | |
| } | |
| this.sequenceIds.sort((a, b) => { | |
| let seqA = this.sequences[a]; | |
| let seqB = this.sequences[b]; | |
| if (seqA.distance > seqB.distance) return 1; | |
| if (seqA.distance < seqB.distance) return -1; | |
| return 0; | |
| }) | |
| } | |
| } | |
| class Visualizer { | |
| constructor(count) { | |
| document.querySelector('main').innerHTML = ''; | |
| this.canvases = []; | |
| this.contexts = []; | |
| this.current = 0; | |
| for (let i = 0; i < count; i++) { | |
| let div = document.createElement('div'); | |
| this.canvases[i] = document.createElement('canvas'); | |
| this.contexts[i] = this.canvases[i].getContext('2d'); | |
| div.appendChild(this.canvases[i]); | |
| document.querySelector('main').appendChild(div); | |
| } | |
| } | |
| get context() { return this.contexts[this.current]; } | |
| get canvas() { return this.canvases[this.current]; } | |
| update(idx) { | |
| this.current = idx; | |
| } | |
| size(width, height) { | |
| this.width = width; | |
| this.height = height; | |
| this.canvases.forEach((canvas) => { | |
| canvas.width = width; | |
| canvas.height = height; | |
| }); | |
| } | |
| text(text) { | |
| let span = document.createElement('span'); | |
| span.innerHTML = text; | |
| this.canvas.parentNode.appendChild(span); | |
| } | |
| clear() { | |
| this.contexts.forEach((context) => { context.clearRect(0, 0, this.width, this.height) }); | |
| } | |
| line(x1, y1, x2, y2, color = 'red', dotted) { | |
| this.context.strokeStyle = color; | |
| this.context.lineWidth = 2; | |
| if (dotted) this.context.setLineDash([2, 8]); | |
| this.context.beginPath(); | |
| this.context.moveTo(x1, y1); | |
| this.context.lineTo(x2, y2); | |
| this.context.stroke(); | |
| this.context.setLineDash([]); | |
| } | |
| point(x, y, fill = 'red') { | |
| this.context.fillStyle = fill; | |
| this.context.beginPath(); | |
| this.context.arc(x, y, 4, 0, PI2); | |
| this.context.fill(); | |
| } | |
| orbit(radius) { | |
| this.context.lineJoin = 'round'; | |
| this.context.lineWidth = 1; | |
| this.context.strokeStyle = 'rgba(255,255,255,0.2)'; | |
| this.context.beginPath(); | |
| this.context.arc(this.width / 2, this.height / 2, radius, 0, PI2); | |
| this.context.stroke(); | |
| } | |
| } | |
| class App { | |
| constructor() { | |
| this.overrideIdx = null; | |
| } | |
| print() { | |
| console.log(JSON.stringify(this.pointIds.map((pointId) => { | |
| let point = this.points[pointId]; | |
| return [ | |
| Math.round(point.relX * 100) / 100, | |
| Math.round(point.relY * 100) / 100 | |
| ]; | |
| }))); | |
| } | |
| run(count) { | |
| this.pointCount = count ? count : this.pointCount || 4; | |
| let cvsCount = PERMUTATIONS[this.pointCount].length; | |
| this.visualizer = new Visualizer(cvsCount); | |
| this.route = new Route(); | |
| this.generateCluster(); | |
| this.traverse(); | |
| this.drawSequences(); | |
| // this.print(); | |
| } | |
| get overrides() { | |
| return [ | |
| [[0,0],[1,1],[0.18,0.69],[0.55,0.75]], | |
| [[0.1,0.1],[0.6,0.4],[0.91,0.91],[0.82,0.8]], | |
| [[0.1,0.1],[0.9,0.4],[0.8,0.9],[0.4,0.7],], | |
| [[0.1,0.1],[0.9,0.45],[0.3,0.7],[0.2,0.8],], | |
| [[0,0],[1,0.91],[0.071,0.047],[0.42,1]], | |
| [[0,1],[1,0.74],[0.72,0],[0.75,0.76]], | |
| [[1,0],[0,1],[0.15,0.14],[0.68,0.09]], | |
| [[0,1],[1,0],[0.61,0.58],[0.35,0.5]], | |
| [[1,0.06],[0.21,0],[0.69,1],[0,0.44]], | |
| [[1,1],[0,0.17],[0.21,0],[0.78,0.6]], | |
| ]; | |
| } | |
| generateCluster() { | |
| this.initializeMinMax(); | |
| this.pointIds = []; | |
| this.points = {}; | |
| // For specific overriding of tough paths | |
| let override; | |
| if (typeof this.overrideIdx === 'number') { | |
| override = this.overrides[(this.overrideIdx) % this.overrides.length]; | |
| this.overrideIdx++; | |
| } | |
| for (let i = 0; i < this.pointCount; i++) { | |
| let x = override ? override[i][0] : Math.random(); | |
| let y = override ? override[i][1] : Math.random(); | |
| let point = new Point({ x, y }); | |
| this.pointIds.push(point.id); | |
| this.points[point.id] = point; | |
| this.updateMinMax(x, y); | |
| } | |
| let minDi = 400; | |
| let maxDi = 400; | |
| let diffDi = maxDi - minDi; | |
| let relW = this.maxX - this.minX; | |
| let relH = this.maxY - this.minY; | |
| let realW = relW * diffDi + minDi; | |
| let realH = relH * diffDi + minDi; | |
| this.visualizer.clear(); | |
| this.visualizer.size(realW, realH); | |
| let minX = this.minX; | |
| let minY = this.minY; | |
| this.pointIds.forEach((id) => { | |
| this.points[id].relate({ realW, realH, relW, relH, minX, minY }); | |
| }); | |
| } | |
| traverse() { | |
| let points = this.pointIds.map((id) => { return this.points[id]; }); | |
| this.route.traverse(points); | |
| } | |
| drawSequences() { | |
| for (let i = 0; i < this.route.sequenceIds.length; i++) { | |
| this.visualizer.update(i); | |
| let sequence = this.route.sequence(i); | |
| let color = i > 0 ? 'red' : 'white'; | |
| this.visualizer.text(Math.round(sequence.distance * 100) / 100); | |
| sequence.points.forEach((pointId, i) => { | |
| let point = this.points[pointId]; | |
| this.drawPath(pointId, sequence.points[(i + 1) % this.pointCount], color); | |
| this.visualizer.orbit(point.realDist); | |
| this.visualizer.point(point.realX, point.realY, color); | |
| }); | |
| } | |
| } | |
| drawPath(id1, id2, color, dashed) { | |
| let p1 = this.points[id1]; | |
| let p2 = this.points[id2]; | |
| this.visualizer.line(p1.realX, p1.realY, p2.realX, p2.realY, color, dashed); | |
| } | |
| initializeMinMax() { | |
| this.maxX = 0; | |
| this.maxY = 0; | |
| this.minX = Infinity; | |
| this.minY = Infinity; | |
| } | |
| updateMinMax(x, y) { | |
| this.maxX = Math.max(x, this.maxX); | |
| this.minX = Math.min(x, this.minX); | |
| this.maxY = Math.max(y, this.maxY); | |
| this.minY = Math.min(y, this.minY); | |
| } | |
| } | |
| let app = new App(); | |
| app.run(); | |
| document.querySelector('select').addEventListener('change', (e) => { | |
| app.run(parseInt(e.target.value)) | |
| }); | |
| document.querySelector('button').addEventListener('click', () => { app.run(); }); |
| html,body { | |
| height: 100%; | |
| } | |
| body { | |
| background: #121212; | |
| } | |
| header, main { | |
| text-align: center; | |
| width: 95%; | |
| max-width: 1200px; | |
| margin: 2rem auto; | |
| } | |
| button, select { | |
| border-radius: 4px; | |
| padding: 0.5rem 0.75rem; | |
| appearance: none; | |
| background: black; | |
| color: white; | |
| border: 2px solid #444; | |
| &:hover { | |
| border-color: white; | |
| } | |
| cursor: pointer; | |
| } | |
| main { | |
| user-select: none; | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-content: center; | |
| justify-content: center; | |
| div { | |
| padding: 0.5rem; | |
| box-sizing: border-box; | |
| position: relative; | |
| width: calc(50% - 1rem); | |
| @media (min-width: 700px) { | |
| width: calc(25% - 1rem); | |
| } | |
| span { | |
| color: white; | |
| font-size: 0.7rem; | |
| position: absolute; | |
| bottom: 0.75rem; | |
| right: 0.75rem; | |
| } | |
| } | |
| } | |
| canvas { | |
| display: block; | |
| height: auto; | |
| width: 100%; | |
| background: black; | |
| } |
Optimally drawing round trip path between three to six points using brute force
A Pen by HARUN PEHLİVAN on CodePen.