diff --git a/src/index.html b/src/index.html index 273e386..a1dfd6f 100644 --- a/src/index.html +++ b/src/index.html @@ -5,6 +5,7 @@ Планирование маршрутов + diff --git a/src/js/hamiltonPath.js b/src/js/hamiltonPath.js new file mode 100644 index 0000000..d75b1ba --- /dev/null +++ b/src/js/hamiltonPath.js @@ -0,0 +1,877 @@ +class GraphEdge { + /** + * @param {GraphVertex} startVertex + * @param {GraphVertex} endVertex + * @param {number} [weight=1] + */ + constructor(startVertex, endVertex, weight = 0) { + this.startVertex = startVertex; + this.endVertex = endVertex; + this.weight = weight; + } + + /** + * @return {string} + */ + getKey() { + const startVertexKey = this.startVertex.getKey(); + const endVertexKey = this.endVertex.getKey(); + + return `${startVertexKey}_${endVertexKey}`; + } + + /** + * @return {GraphEdge} + */ + reverse() { + const tmp = this.startVertex; + this.startVertex = this.endVertex; + this.endVertex = tmp; + + return this; + } + + /** + * @return {string} + */ + toString() { + return this.getKey(); + } +} + +class Comparator { + /** + * Constructor. + * @param {function(a: *, b: *)} [compareFunction] - It may be custom compare function that, let's + * say may compare custom objects together. + */ + constructor(compareFunction) { + this.compare = compareFunction || Comparator.defaultCompareFunction; + } + + /** + * Default comparison function. It just assumes that "a" and "b" are strings or numbers. + * @param {(string|number)} a + * @param {(string|number)} b + * @returns {number} + */ + static defaultCompareFunction(a, b) { + if (a === b) { + return 0; + } + + return a < b ? -1 : 1; + } + + /** + * Checks if two variables are equal. + * @param {*} a + * @param {*} b + * @return {boolean} + */ + equal(a, b) { + return this.compare(a, b) === 0; + } + + /** + * Checks if variable "a" is less than "b". + * @param {*} a + * @param {*} b + * @return {boolean} + */ + lessThan(a, b) { + return this.compare(a, b) < 0; + } + + /** + * Checks if variable "a" is greater than "b". + * @param {*} a + * @param {*} b + * @return {boolean} + */ + greaterThan(a, b) { + return this.compare(a, b) > 0; + } + + /** + * Checks if variable "a" is less than or equal to "b". + * @param {*} a + * @param {*} b + * @return {boolean} + */ + lessThanOrEqual(a, b) { + return this.lessThan(a, b) || this.equal(a, b); + } + + /** + * Checks if variable "a" is greater than or equal to "b". + * @param {*} a + * @param {*} b + * @return {boolean} + */ + greaterThanOrEqual(a, b) { + return this.greaterThan(a, b) || this.equal(a, b); + } + + /** + * Reverses the comparison order. + */ + reverse() { + const compareOriginal = this.compare; + this.compare = (a, b) => compareOriginal(b, a); + } +} + +class LinkedListNode { + constructor(value, next = null) { + this.value = value; + this.next = next; + } + + toString(callback) { + return callback ? callback(this.value) : `${this.value}`; + } +} + +class LinkedList { + /** + * @param {Function} [comparatorFunction] + */ + constructor(comparatorFunction) { + /** @var LinkedListNode */ + this.head = null; + + /** @var LinkedListNode */ + this.tail = null; + + this.compare = new Comparator(comparatorFunction); + } + + /** + * @param {*} value + * @return {LinkedList} + */ + prepend(value) { + // Make new node to be a head. + const newNode = new LinkedListNode(value, this.head); + this.head = newNode; + + // If there is no tail yet let's make new node a tail. + if (!this.tail) { + this.tail = newNode; + } + + return this; + } + + /** + * @param {*} value + * @return {LinkedList} + */ + append(value) { + const newNode = new LinkedListNode(value); + + // If there is no head yet let's make new node a head. + if (!this.head) { + this.head = newNode; + this.tail = newNode; + + return this; + } + + // Attach new node to the end of linked list. + this.tail.next = newNode; + this.tail = newNode; + + return this; + } + + /** + * @param {*} value + * @param {number} index + * @return {LinkedList} + */ + insert(value, rawIndex) { + const index = rawIndex < 0 ? 0 : rawIndex; + if (index === 0) { + this.prepend(value); + } else { + let count = 1; + let currentNode = this.head; + const newNode = new LinkedListNode(value); + while (currentNode) { + if (count === index) break; + currentNode = currentNode.next; + count += 1; + } + if (currentNode) { + newNode.next = currentNode.next; + currentNode.next = newNode; + } else { + if (this.tail) { + this.tail.next = newNode; + this.tail = newNode; + } else { + this.head = newNode; + this.tail = newNode; + } + } + } + return this; + } + + /** + * @param {*} value + * @return {LinkedListNode} + */ + delete(value) { + if (!this.head) { + return null; + } + + let deletedNode = null; + + // If the head must be deleted then make next node that is different + // from the head to be a new head. + while (this.head && this.compare.equal(this.head.value, value)) { + deletedNode = this.head; + this.head = this.head.next; + } + + let currentNode = this.head; + + if (currentNode !== null) { + // If next node must be deleted then make next node to be a next next one. + while (currentNode.next) { + if (this.compare.equal(currentNode.next.value, value)) { + deletedNode = currentNode.next; + currentNode.next = currentNode.next.next; + } else { + currentNode = currentNode.next; + } + } + } + + // Check if tail must be deleted. + if (this.compare.equal(this.tail.value, value)) { + this.tail = currentNode; + } + + return deletedNode; + } + + /** + * @param {Object} findParams + * @param {*} findParams.value + * @param {function} [findParams.callback] + * @return {LinkedListNode} + */ + find({ + value = undefined, + callback = undefined + }) { + if (!this.head) { + return null; + } + + let currentNode = this.head; + + while (currentNode) { + // If callback is specified then try to find node by callback. + if (callback && callback(currentNode.value)) { + return currentNode; + } + + // If value is specified then try to compare by value.. + if (value !== undefined && this.compare.equal(currentNode.value, value)) { + return currentNode; + } + + currentNode = currentNode.next; + } + + return null; + } + + /** + * @return {LinkedListNode} + */ + deleteTail() { + const deletedTail = this.tail; + + if (this.head === this.tail) { + // There is only one node in linked list. + this.head = null; + this.tail = null; + + return deletedTail; + } + + // If there are many nodes in linked list... + + // Rewind to the last node and delete "next" link for the node before the last one. + let currentNode = this.head; + while (currentNode.next) { + if (!currentNode.next.next) { + currentNode.next = null; + } else { + currentNode = currentNode.next; + } + } + + this.tail = currentNode; + + return deletedTail; + } + + /** + * @return {LinkedListNode} + */ + deleteHead() { + if (!this.head) { + return null; + } + + const deletedHead = this.head; + + if (this.head.next) { + this.head = this.head.next; + } else { + this.head = null; + this.tail = null; + } + + return deletedHead; + } + + /** + * @param {*[]} values - Array of values that need to be converted to linked list. + * @return {LinkedList} + */ + fromArray(values) { + values.forEach((value) => this.append(value)); + + return this; + } + + /** + * @return {LinkedListNode[]} + */ + toArray() { + const nodes = []; + + let currentNode = this.head; + while (currentNode) { + nodes.push(currentNode); + currentNode = currentNode.next; + } + + return nodes; + } + + /** + * @param {function} [callback] + * @return {string} + */ + toString(callback) { + return this.toArray().map((node) => node.toString(callback)).toString(); + } + + /** + * Reverse a linked list. + * @returns {LinkedList} + */ + reverse() { + let currNode = this.head; + let prevNode = null; + let nextNode = null; + + while (currNode) { + // Store next node. + nextNode = currNode.next; + + // Change next node of the current node so it would link to previous node. + currNode.next = prevNode; + + // Move prevNode and currNode nodes one step forward. + prevNode = currNode; + currNode = nextNode; + } + + // Reset head and tail. + this.tail = this.head; + this.head = prevNode; + + return this; + } +} + +class GraphVertex { + /** + * @param {*} value + */ + constructor(value) { + if (value === undefined) { + throw new Error('Graph vertex must have a value'); + } + + /** + * @param {GraphEdge} edgeA + * @param {GraphEdge} edgeB + */ + const edgeComparator = (edgeA, edgeB) => { + if (edgeA.getKey() === edgeB.getKey()) { + return 0; + } + + return edgeA.getKey() < edgeB.getKey() ? -1 : 1; + }; + + // Normally you would store string value like vertex name. + // But generally it may be any object as well + this.value = value; + this.edges = new LinkedList(edgeComparator); + } + + /** + * @param {GraphEdge} edge + * @returns {GraphVertex} + */ + addEdge(edge) { + this.edges.append(edge); + + return this; + } + + /** + * @param {GraphEdge} edge + */ + deleteEdge(edge) { + this.edges.delete(edge); + } + + /** + * @returns {GraphVertex[]} + */ + getNeighbors() { + const edges = this.edges.toArray(); + + /** @param {LinkedListNode} node */ + const neighborsConverter = (node) => { + return node.value.startVertex === this ? node.value.endVertex : node.value.startVertex; + }; + + // Return either start or end vertex. + // For undirected graphs it is possible that current vertex will be the end one. + return edges.map(neighborsConverter); + } + + /** + * @return {GraphEdge[]} + */ + getEdges() { + return this.edges.toArray().map((linkedListNode) => linkedListNode.value); + } + + /** + * @return {number} + */ + getDegree() { + return this.edges.toArray().length; + } + + /** + * @param {GraphEdge} requiredEdge + * @returns {boolean} + */ + hasEdge(requiredEdge) { + const edgeNode = this.edges.find({ + callback: (edge) => edge === requiredEdge, + }); + + return !!edgeNode; + } + + /** + * @param {GraphVertex} vertex + * @returns {boolean} + */ + hasNeighbor(vertex) { + const vertexNode = this.edges.find({ + callback: (edge) => edge.startVertex === vertex || edge.endVertex === vertex, + }); + + return !!vertexNode; + } + + /** + * @param {GraphVertex} vertex + * @returns {(GraphEdge|null)} + */ + findEdge(vertex) { + const edgeFinder = (edge) => { + return edge.startVertex === vertex || edge.endVertex === vertex; + }; + + const edge = this.edges.find({ + callback: edgeFinder + }); + + return edge ? edge.value : null; + } + + /** + * @returns {string} + */ + getKey() { + return this.value; + } + + /** + * @return {GraphVertex} + */ + deleteAllEdges() { + this.getEdges().forEach((edge) => this.deleteEdge(edge)); + + return this; + } + + /** + * @param {function} [callback] + * @returns {string} + */ + toString(callback) { + return callback ? callback(this.value) : `${this.value}`; + } +} + +class Graph { + /** + * @param {boolean} isDirected + */ + constructor(isDirected = false) { + this.vertices = {}; + this.edges = {}; + this.isDirected = isDirected; + } + + /** + * @param {GraphVertex} newVertex + * @returns {Graph} + */ + addVertex(newVertex) { + this.vertices[newVertex.getKey()] = newVertex; + + return this; + } + + /** + * @param {string} vertexKey + * @returns GraphVertex + */ + getVertexByKey(vertexKey) { + return this.vertices[vertexKey]; + } + + /** + * @param {GraphVertex} vertex + * @returns {GraphVertex[]} + */ + getNeighbors(vertex) { + return vertex.getNeighbors(); + } + + /** + * @return {GraphVertex[]} + */ + getAllVertices() { + return Object.values(this.vertices); + } + + /** + * @return {GraphEdge[]} + */ + getAllEdges() { + return Object.values(this.edges); + } + + /** + * @param {GraphEdge} edge + * @returns {Graph} + */ + addEdge(edge) { + // Try to find and end start vertices. + let startVertex = this.getVertexByKey(edge.startVertex.getKey()); + let endVertex = this.getVertexByKey(edge.endVertex.getKey()); + + // Insert start vertex if it wasn't inserted. + if (!startVertex) { + this.addVertex(edge.startVertex); + startVertex = this.getVertexByKey(edge.startVertex.getKey()); + } + + // Insert end vertex if it wasn't inserted. + if (!endVertex) { + this.addVertex(edge.endVertex); + endVertex = this.getVertexByKey(edge.endVertex.getKey()); + } + + // Check if edge has been already added. + if (this.edges[edge.getKey()]) { + throw new Error('Edge has already been added before'); + } else { + this.edges[edge.getKey()] = edge; + } + + // Add edge to the vertices. + if (this.isDirected) { + // If graph IS directed then add the edge only to start vertex. + startVertex.addEdge(edge); + } else { + // If graph ISN'T directed then add the edge to both vertices. + startVertex.addEdge(edge); + endVertex.addEdge(edge); + } + + return this; + } + + /** + * @param {GraphEdge} edge + */ + deleteEdge(edge) { + // Delete edge from the list of edges. + if (this.edges[edge.getKey()]) { + delete this.edges[edge.getKey()]; + } else { + throw new Error('Edge not found in graph'); + } + + // Try to find and end start vertices and delete edge from them. + const startVertex = this.getVertexByKey(edge.startVertex.getKey()); + const endVertex = this.getVertexByKey(edge.endVertex.getKey()); + + startVertex.deleteEdge(edge); + endVertex.deleteEdge(edge); + } + + /** + * @param {GraphVertex} startVertex + * @param {GraphVertex} endVertex + * @return {(GraphEdge|null)} + */ + findEdge(startVertex, endVertex) { + const vertex = this.getVertexByKey(startVertex.getKey()); + + if (!vertex) { + return null; + } + + return vertex.findEdge(endVertex); + } + + /** + * @return {number} + */ + getWeight() { + return this.getAllEdges().reduce((weight, graphEdge) => { + return weight + graphEdge.weight; + }, 0); + } + + /** + * Reverse all the edges in directed graph. + * @return {Graph} + */ + reverse() { + /** @param {GraphEdge} edge */ + this.getAllEdges().forEach((edge) => { + // Delete straight edge from graph and from vertices. + this.deleteEdge(edge); + + // Reverse the edge. + edge.reverse(); + + // Add reversed edge back to the graph and its vertices. + this.addEdge(edge); + }); + + return this; + } + + /** + * @return {object} + */ + getVerticesIndices() { + const verticesIndices = {}; + this.getAllVertices().forEach((vertex, index) => { + verticesIndices[vertex.getKey()] = index; + }); + + return verticesIndices; + } + + /** + * @return {*[][]} + */ + getAdjacencyMatrix() { + const vertices = this.getAllVertices(); + const verticesIndices = this.getVerticesIndices(); + + // Init matrix with infinities meaning that there is no ways of + // getting from one vertex to another yet. + const adjacencyMatrix = Array(vertices.length).fill(null).map(() => { + return Array(vertices.length).fill(Infinity); + }); + + // Fill the columns. + vertices.forEach((vertex, vertexIndex) => { + vertex.getNeighbors().forEach((neighbor) => { + const neighborIndex = verticesIndices[neighbor.getKey()]; + adjacencyMatrix[vertexIndex][neighborIndex] = this.findEdge(vertex, neighbor).weight; + }); + }); + + return adjacencyMatrix; + } + + /** + * @return {string} + */ + toString() { + return Object.keys(this.vertices).toString(); + } +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @param {GraphVertex} vertexCandidate + * @return {boolean} + */ +function isSafe(adjacencyMatrix, verticesIndices, cycle, vertexCandidate) { + const endVertex = cycle[cycle.length - 1]; + + // Get end and candidate vertices indices in adjacency matrix. + const candidateVertexAdjacencyIndex = verticesIndices[vertexCandidate.getKey()]; + const endVertexAdjacencyIndex = verticesIndices[endVertex.getKey()]; + + // Check if last vertex in the path and candidate vertex are adjacent. + if (adjacencyMatrix[endVertexAdjacencyIndex][candidateVertexAdjacencyIndex] === Infinity) { + return false; + } + + // Check if vertexCandidate is being added to the path for the first time. + const candidateDuplicate = cycle.find((vertex) => vertex.getKey() === vertexCandidate.getKey()); + + return !candidateDuplicate; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @return {boolean} + */ +function isCycle(adjacencyMatrix, verticesIndices, cycle) { + // Check if first and last vertices in hamiltonian path are adjacent. + + // Get start and end vertices from the path. + const startVertex = cycle[0]; + const endVertex = cycle[cycle.length - 1]; + + // Get start/end vertices indices in adjacency matrix. + const startVertexAdjacencyIndex = verticesIndices[startVertex.getKey()]; + const endVertexAdjacencyIndex = verticesIndices[endVertex.getKey()]; + + // Check if we can go from end vertex to the start one. + return adjacencyMatrix[endVertexAdjacencyIndex][startVertexAdjacencyIndex] !== Infinity; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {GraphVertex[]} vertices + * @param {object} verticesIndices + * @param {GraphVertex[][]} cycles + * @param {GraphVertex[]} cycle + */ +function hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle, +}) { + // Clone cycle in order to prevent it from modification by other DFS branches. + const currentCycle = [...cycle].map((vertex) => new GraphVertex(vertex.value)); + + if (vertices.length === currentCycle.length) { + // Hamiltonian path is found. + // Now we need to check if it is cycle or not. + if (isCycle(adjacencyMatrix, verticesIndices, currentCycle)) { + // Another solution has been found. Save it. + cycles.push(currentCycle); + } + return; + } + + for (let vertexIndex = 0; vertexIndex < vertices.length; vertexIndex += 1) { + // Get vertex candidate that we will try to put into next path step and see if it fits. + const vertexCandidate = vertices[vertexIndex]; + + // Check if it is safe to put vertex candidate to cycle. + if (isSafe(adjacencyMatrix, verticesIndices, currentCycle, vertexCandidate)) { + // Add candidate vertex to cycle path. + currentCycle.push(vertexCandidate); + + // Try to find other vertices in cycle. + hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle: currentCycle, + }); + + // BACKTRACKING. + // Remove candidate vertex from cycle path in order to try another one. + currentCycle.pop(); + } + } +} + +/** + * @param {Graph} graph + * @return {GraphVertex[][]} + */ +function hamiltonianCycle(graph) { + // Gather some information about the graph that we will need to during + // the problem solving. + const verticesIndices = graph.getVerticesIndices(); + const adjacencyMatrix = graph.getAdjacencyMatrix(); + const vertices = graph.getAllVertices(); + + // Define start vertex. We will always pick the first one + // this it doesn't matter which vertex to pick in a cycle. + // Every vertex is in a cycle so we can start from any of them. + const startVertex = vertices[0]; + + // Init cycles array that will hold all solutions. + const cycles = []; + + // Init cycle array that will hold current cycle path. + const cycle = [startVertex]; + + // Try to find cycles recursively in Depth First Search order. + hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle, + }); + + // Return found cycles. + return cycles; +} \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index d320396..39949d2 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -177,6 +177,21 @@ function initRecalculateWaypointsButton() { }); } +// Расчет расстояние между двумя точками заданными координатами на поверхности Земли +function calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371e3; // metres + const φ1 = lat1 * Math.PI / 180; // φ, λ in radians + const φ2 = lat2 * Math.PI / 180; + const Δφ = (lat2 - lat1) * Math.PI / 180; + const Δλ = (lon2 - lon1) * Math.PI / 180; + const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ / 2) * Math.sin(Δλ / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const d = R * c; // in metres + return d; +} + // Вычисление оптимального маршрута по начальной и конечной точке function recalculateWaypoints(waypoints) { if (!waypoints || waypoints.length < 2) { @@ -190,7 +205,25 @@ function recalculateWaypoints(waypoints) { } else if (waypoints.length == 3) { } else { - + // Создаем словарь с вершинами + let vertexes = {}; + for (const ind in waypoints) { + vertexes[ind] = new GraphVertex(ind); + } + // Формируем граф + const graph = new Graph(); + const len = waypoints.length; + for (let i = 0; i < len - 1; i++) { + for (let j = i + 1; j < len; j++) { + const point1 = waypoints[i]; + const point2 = waypoints[j]; + const d = calculateDistance(point1.location[0], point1.location[1], point2.location[0], point2.location[1]); + graph.addEdge(new GraphEdge(vertexes[i], vertexes[j], d)); + } + } + // Вычисляем Гамильтонов цикл для графа + const hamiltonianCycleSet = hamiltonianCycle(graph); + alert(hamiltonianCycleSet.length); } } } diff --git a/src/styles/styles.css b/src/styles/styles.css index 2997561..5f01b50 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -14,13 +14,6 @@ font-family: 'Segoe UI', Helvetica, 'Droid Sans', Tahoma, Geneva, sans-serif; } -.travel -{ - vertical-align: top; - width: 400px; - float: right; -} - .dark { background-color: #363e5b;