// code written for jquery 3.4.1 // click handle functions function emptyClickHandleFunc(event) { console.log("empty click handle function"); } function displaySolution(tour) { var delayMS = 300; for (var i = 0; i < tour.length - 1; i++) { //drawLine(vert[0], vert[1], nextVert[0], nextVert[1], baobabOrange, "15px"); setTimeout(function(myi) { var vert = vertices[tour[myi]]; var nextVert = vertices[tour[myi + 1]]; drawLine(vert[0], vert[1], nextVert[0], nextVert[1], baobabOrange, "15px"); }, (i + 1)*delayMS, i); } // draw the final edge back to the first vertex firstVert = vertices[tour[0]]; lastVert = vertices[tour[tour.length - 1]]; drawLine(lastVert[0], lastVert[1], firstVert[0], firstVert[1], baobabOrange, "15px"); return (tour.length - 1) * delayMS; } var waitingForSolver = false; function solveTSP(event) { if ( !waitingForSolver ) { console.log("solving TSP"); setAntsSolvingState(); waitingForSolver = true; $.ajax({ url : "https://europe-west1-sistemas-baobab.cloudfunctions.net/ACOTSPSolve", type : 'POST', data : JSON.stringify({ "params" : [ {"Vertices" : vertices, "UserTour" : userTour} ], "method" : "ACOTSPSolver.Solve", "id" : "1", }), dataType : "json", contentType : "application/json", success : function(result) { sloverSuccessFunc(result); waitingForSolver = false; }, }); } } // eucDist computes the Euclidean distance between two 2D points, where each point is an array of two Numbers. function eucDist(a, b) { var distX = b[0] - a[0]; var distY = b[1] - a[1]; return Math.sqrt(distX*distX + distY*distY); } function computeLength(tour) { var len = 0; for ( var i = 0; i < tour.length - 1 ; i++ ) { len += eucDist(vertices[tour[i]], vertices[tour[i+1]]); } len += eucDist(vertices[tour[tour.length - 1]], vertices[tour[0]]); return len; } var bestSolution = null; function saveBestSolution(result) { console.log(result); var r = JSON.parse(result.result); if ( bestSolution == null ) { bestSolution = r; } else { var bestLen = computeLength(bestSolution.Tour); var newLen = computeLength(r.Tour); if ( newLen < bestLen ) { bestSolution = r; } } } function displayFinalSolution() { var delayOfSolDispMS = displaySolution(bestSolution.Tour); setTimeout(function() { var acoLen = Math.floor(computeLength(bestSolution.Tour)); var usrLen = 0; if ( checkUserTour() ) { usrLen = Math.floor(computeLength(userTour)); } if( usrLen > 0 ) { $("#userStatField").text(usrLen); } $("#acotspStatField").text(acoLen); if(acoLen < usrLen || usrLen == 0) { $("#instructField") .text("ANTS WIN! Click on the canvas to play again."); } else if(usrLen < acoLen) { $("#instructField") .text("YOU WIN! Click on the canvas to play again."); } else if(usrLen == acoLen) { $("#instructField") .text("It's a DRAW! Click on the canvas to play again."); } }, delayOfSolDispMS); rectClickHandleFunc = resetApp; } var sloverSuccessFunc = saveBestSolution; function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); } // checkCollision checks whether a vertex added at (x, y) would result in a // collision with an existing vertex. a collision is assumed if the bounding // square of a vertex(assumed to be circular) collides with another bounding // square. Returns Boolean, true if collision function checkCollision(x, y) { for (var v = 0; v < vertices.length; v++) { var vert = vertices[v]; if ((Math.abs(vert[0] - x) < vertexRadius) || (Math.abs(vert[1] - y) < vertexRadius)) { return true; } } return false; } var maxCollisionChecks = 10000; // addRandVertex makes a fixed number of collision checks and adds a vertex at a // random position that doesn't cause a collision. function addRandVertex() { var svg = $("#appSVG")[0]; var svgWidth = svg.width.baseVal.value; var svgHeight = svg.height.baseVal.value; var rx = getRandomInt(svgWidth); var ry = getRandomInt(svgHeight); for (var i = 0; i < maxCollisionChecks; i++) { if (!checkCollision(rx, ry)) { break; } rx = getRandomInt(svgWidth); ry = getRandomInt(svgHeight); } setTimeout(function() { addVertex(rx, ry, baobabOrange); }, 400); } function randomGraph(event) { console.log("creating random Graph"); var nVertices = vertices.length; // TODO handle case where nVertices == Vreq if (nVertices < Vreq) { var nVertsToAdd = Vreq - nVertices; for (var v = 0; v < nVertsToAdd; v++) { addRandVertex(); } } if( state != "CreatingRoute" ) { setCreateRouteState(); } } function resetApp(event) { console.log("resetting App"); location.reload(); } function greyOut(someButton) { $(someButton).prop("disabled", true); $(someButton).css("opacity", 0.3); } function enableBtn(someButton) { $(someButton).prop("disabled", false); $(someButton).css("opacity", 1); } function makeBlank(someField) { $(someField).text("-"); } var baobabOrange = "#f58220"; var vertices = []; // array of int[2] arrays var userTour = []; // array of int // checkUserTour returns false if the userTour is not a valid solution to the TSP. function checkUserTour() { if ( userTour.length != Vreq ) { return false; } for ( var i = 1; i < userTour.length; i++ ) { if ( userTour[i] > Vreq - 1 ) { return false; } if ( userTour[i] < 0 ) { return false; } for ( var j = 0; j < i; j++ ) { if ( userTour[i] == userTour[j] ) { return false; } } } if ( userTour[0] > Vreq - 1 ) { return false; } if ( userTour[0] < 0 ) { return false; } return true; } // checkUserFinished returns false if the user is not finished with the game. It doesn't mean he has a correct solution. function checkNewVertex(vertexIndex) { if ( userTour.length == Vreq - 1 && vertexIndex == firstVertex ) { return true; } if ( vertexIndex == firstVertex ) { return false; } for ( var i = 0; i < userTour.length; i++ ) { if ( vertexIndex == userTour[i] ) { return false; } } return true; } // TODO enforce this limit var Vreq = 20; var vertexRadius = 20; var state = "Idle"; var vertexClickEventFunc = function(layer, vertex) { console.log("event click"); vertex.animateLayer(layer, {fillStyle : 'violet'}, 0); } function enableDoneBtn() { enableBtn($("#solveBtn")); doneClickHandleFunc = setCompleteState; } function disableDoneBtn() { greyOut($("#solveBtn")); doneClickHandleFunc = emptyClickHandleFunc; } function enableRndBtn() { enableBtn($("#randomBtn")); rndClickHandleFunc = randomGraph; } function disableRndBtn() { greyOut($("#randomBtn")); rndClickHandleFunc = emptyClickHandleFunc; } function enableResetBtn() { enableBtn("#resetBtn"); resetClickHandleFunc = resetApp; } function disableResetBtn() { greyOut("#resetBtn"); resetClickHandleFunc = emptyClickHandleFunc; } // States function setIdleState() { state = "Idle" vertices = []; userTour = []; $("#instructField") .text("Let's draw " + Vreq.toString() + " dots together, or press the dice icon."); enableRndBtn(); disableDoneBtn(); enableResetBtn(); makeBlank($("#userStatField")); makeBlank($("#acotspStatField")); } function setBuildingStateInstructionField() { var nVertices = vertices.length; var nVToPlace = Vreq - nVertices; $("#instructField") .text( nVToPlace.toString() + " dots to go, or press the dice icon."); } function setBuildingState() { state = "Building" setBuildingStateInstructionField(); enableRndBtn(); disableDoneBtn(); enableResetBtn(); makeBlank($("#userStatField")); makeBlank($("#acotspStatField")); } var selectedVert1 = null; var vertexClickHandleFunc = function( event) { console.log("empty vertexClickHandleFunc"); } function drawLine(x1, y1, x2, y2, edgeColor, strokeWidth) { var svg = document.querySelector('#appSVG'); // TODO use createElementNS? var newElement = document.createElementNS("http://www.w3.org/2000/svg", 'line'); // Create a path in SVG's namespace newElement.setAttribute("x1", x1); newElement.setAttribute("y1", y1); newElement.setAttribute("x2", x2); newElement.setAttribute("y2", y2); /* TODO newElement.addEventListener("click", { handleEvent: function(e) {edgeClickHandleFunc(e);} }); */ newElement.style.stroke = edgeColor; // Set stroke colour newElement.style.strokeWidth = strokeWidth; // Set stroke width newElement.style.strokeLinecap = "round"; newElement.style.opacity = 0.5; svg.appendChild(newElement); } var selectVertColor = "#00FF00"; var userEdgeColor = "#0000FF"; var vertInTourColor = "lightblue"; var firstVertex = null; function setCreateRouteState() { state = "CreatingRoute" $("#instructField") .text( "Find the shortest path through all the dots and finish where you started."); disableRndBtn(); enableDoneBtn(); enableResetBtn(); makeBlank($("#userStatField")); makeBlank($("#acotspStatField")); canvasClickFunc = function(e) { console.log("different event"); } vertexClickHandleFunc = function(event) { // TODO prevent drawing multiple edges. // TODO prevent adding the same vertex to a tour twice console.log(event); if (selectedVert1 == null) { selectedVert1 = event.target; selectedVert1.style.fill = selectVertColor; var newVertInd = Number(selectedVert1.dataset.verticesIndex); firstVertex = newVertInd; } else { if ( event.target != selectedVert1 ) { var newVertInd = Number(event.target.dataset.verticesIndex); if ( checkNewVertex(newVertInd) ) { userTour.push(Number(selectedVert1.dataset.verticesIndex)); if ( userTour.length == Vreq ) { setCompleteState(); } selectedVert1.style.fill = vertInTourColor; var selectedVert2 = event.target; selectedVert2.style.fill = selectVertColor; x1 = selectedVert1.cx.baseVal.value; y1 = selectedVert1.cy.baseVal.value; x2 = selectedVert2.cx.baseVal.value; y2 = selectedVert2.cy.baseVal.value; drawLine(x1, y1, x2, y2, userEdgeColor, "10px"); var svg = $("#appSVG")[0]; svg.removeChild(selectedVert1); svg.appendChild(selectedVert1); selectedVert1 = selectedVert2; } } } } rectClickHandleFunc = function( e) { console.log("empty rectClickHandleFunc"); } } function setAntsSolvingState() { state = "Solving" $("#instructField").text("Ants at work!"); disableRndBtn(); disableDoneBtn(); disableResetBtn(); // TODO get the length of the route of the user makeBlank($("#userStatField")); makeBlank($("#acotspStatField")); } function saveAndDisplay(result) { saveBestSolution(result); displayFinalSolution(); } function tryToWin() { // To retry one time if the user's solution is better, delete the following 5 lines and uncomment the rest of this function. if ( waitingForSolver ) { sloverSuccessFunc = saveAndDisplay; } else { displayFinalSolution(); } /* if ( !waitingForSolver ) { if ( checkUserTour() ) { var usrLen = computeLength(userTour); var acoLen = computeLength(bestSolution.Tour); if ( usrLen < acoLen ) { sloverSuccessFunc = saveAndDisplay; solveTSP(); } else { displayFinalSolution(); } } else { displayFinalSolution(); } } else { sloverSuccessFunc = saveAndDisplay; } */ } function setCompleteState() { state = "Complete" disableRndBtn(); disableDoneBtn(); enableResetBtn(); // TODO Maybe, show elapsed time during solving? $("#instructField") .text("Ants are searching for their way home..."); rectClickHandleFunc = emptyClickHandleFunc; tryToWin(); } function load() { setIdleState(); } // TODO handle intersections function addVertex(x, y, fillColor) { if ( vertices.length < Vreq ) { var svg = document.querySelector('#appSVG'); // TODO use createElementNS? var newElement = document.createElementNS("http://www.w3.org/2000/svg", 'circle'); // Create a path in SVG's namespace newElement.setAttribute("cx", x); newElement.setAttribute("cy", y); newElement.setAttribute("r", vertexRadius); newElement.setAttribute("class", "Vertex"); newElement.addEventListener( "click", {handleEvent : function(e) { vertexClickHandleFunc(e); }}); newElement.style.fill = fillColor; // Set stroke colour newElement.style.strokeWidth = "5px"; // Set stroke width newElement.style.opacity = 0.7; vertices.push([ x, y ]); newElement.setAttribute("data-vertices-index", vertices.length - 1); // attribute automatically mapped to // newElement.dataset.verticesIndex svg.appendChild(newElement); if( state == "Building" ) { setBuildingStateInstructionField(); } if ( vertices.length >= Vreq ) { solveTSP(); setCreateRouteState(); } else { setBuildingState(); } } } // handler for clicks on the SVGs main rectangle. var rectClickHandleFunc = function(event) { console.log(event); x = event.offsetX; y = event.offsetY; addVertex(x, y, "red"); // TODO what happens if we reach Vreq here addRandVertex(); } function rectClick(event) { rectClickHandleFunc(event); } function rndClick(event) { rndClickHandleFunc(event); } function doneClick(event) { doneClickHandleFunc(event); } function resetClick(event) { resetClickHandleFunc(event); }