Frontend Final Project Description
Here is the guide to our frontend code for our Geotourism final project.
- Creating Vertex and Graph Classes
- Storing vertices into each graph
- Heuristic Calculation Method
- Drawing the Heuristic Graph
- Moving onto the Game function
- Defining Adjacency Relationships for the Heuristic Graph
- Drawing the User's Graph
- Handling Mouse Event functions
- Mouse Down
- Mouse Move
- Mouse Up
- Extra Handler: Handling the Reset Button
- Utilizing Event Handlers
- Page Transitions
- Page Transitions in HTML
- POST Request
- Styling
Creating Vertex and Graph Classes
- class Vertex allows the coder to easily add in a new vertex on the graph by simply using JSON and writing {id: "", x: , y: }. This allows us to manipulate the points on the graph much more easily.
-
class Graph allows the coder to easily add in a graph. We added two graphs: one for allowing the user to draw their own line connections, another for displaying the shortest path using heuristic algorithm. Only one graph can be displayed on the canvas at a time.
- calculateDistance() function is used to calculate distances in the heuristic graph.
- calculateTotalDistance() function is used to calculate distances in the user interactive graph.
// Vertex class to represent each HTML element
class Vertex {
constructor(id, x, y) {
this.id = id; // id of the vertex
this.x = x; // x-coordinate of the vertex
this.y = y; // y-coordinate of the vertex
this.adjacent = []; // array to store adjacent vertices
this.connected = false; // flag to indicate if vertex is connected
}
// Function to add an adjacent vertex
addAdjacent(vertex) {
this.adjacent.push(vertex);
}
}
// Graph class to hold all the vertices
class Graph {
constructor() {
this.vertices = []; // array to store all vertices
this.map = {}; // hash map to store vertices by their ids
}
// Function to add a vertex to the graph
addVertex(vertex) {
this.vertices.push(vertex);
this.map[vertex.id] = vertex; // add vertex to the map
}
// Function to check if all vertices are connected
checkAllVerticesConnected() {
const visited = new Set(); // Set to store visited vertices
const stack = []; // Stack for DFS traversal
// Start DFS from the first vertex in the graph
stack.push(graph.vertices[0]);
while (stack.length > 0) {
const vertex = stack.pop();
visited.add(vertex);
// Add all adjacent unvisited vertices to the stack
for (const adjacentVertex of vertex.adjacent) {
if (!visited.has(adjacentVertex)) {
stack.push(adjacentVertex);
}
}
}
// Check if all vertices are visited
return visited.size === graph.vertices.length;
}
// Function to calculate the Euclidean distance between two vertices
calculateDistance(v1, v2) {
const dx = v1.x - v2.x;
const dy = v1.y - v2.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Function to calculate the total distance of all lines
calculateTotalDistance() {
let totalDistance = 0;
for (const vertex of this.vertices) {
for (const adjacentVertex of vertex.adjacent) {
totalDistance += this.calculateDistance(vertex, adjacentVertex);
}
}
return totalDistance;
}
}
Creating things using the Graph and Vertex classes
-
vertices array stores all of the points that are to be graphed on the canvas. The array can be manipulated based on user's menu selection(something that we will go over later in detail)
- Note that points are still in the array but commented out, this allows the coder to know what locations are included in the game
- const heuristic creates the graph of the shortest route among all points
- const graph creates the graph where the user can interact and connect points using drag and drop lines
// Define the vertices as an array of objects, points are commented out since user picks these points, not us
let vertices = [
// { id: "A", x: 150, y: 200 },
// { id: "B", x: 90, y: 200 },
// { id: "C", x: 95, y: 220 },
// { id: "D", x: 165, y: 230 },
// { id: "E", x: 316, y: 225 },
// { id: "F", x: 100, y: 276 },
// { id: "G", x: 235, y: 260 },
// { id: "H", x: 265, y: 270 },
// { id: "I", x: 360, y: 320 },
// { id: "J", x: 370, y: 340 },
// { id: "O", x: 330, y: 360 },
// { id: "R", x: 310, y: 390 },
// { id: "T", x: 360, y: 385 },
// { id: "V", x: 360, y: 460 },
// { id: "W", x: 270, y: 480 },
// { id: "Z", x: 120, y: 530 },
// { id: "MissionTrails", x: 640, y: 50},
// { id: "Walmart", x: 500, y: 590},
// { id: "Costco", x: 670, y: 190}
// Add more vertices here as needed
];
// Create heuristic graph
const heuristic = new Graph();
// Create the user drawing graph
const graph = new Graph();
Storing vertices into each graph
- The vertices array is stored into both graphs in order to allow the graphs to be able to plot specific points.
- Because both graphs will be using the same points at all times, two for loops are used to iterate through and store each point in the array vertices in each graph.
// Loop through the vertices array and create a new Vertex object for each one
for (const vertex of vertices) {
const newVertex = new Vertex(vertex.id, vertex.x, vertex.y);
graph.addVertex(newVertex);
}
// Loop through the vertices array and create a new Vertex object for each one
for (const vertex of vertices) {
const newVertex = new Vertex(vertex.id, vertex.x, vertex.y);
heuristic.addVertex(newVertex);
}
// Function to generate all possible paths that visit all vertices exactly once
function generatePaths(graph) {
const paths = [];
const visited = new Set();
function dfs(path) {
if (path.length === graph.vertices.length) {
paths.push(path);
return;
}
graph.vertices.forEach((vertex) => {
if (!visited.has(vertex)) {
visited.add(vertex);
dfs([...path, vertex]);
visited.delete(vertex);
}
});
}
graph.vertices.forEach((vertex) => {
visited.add(vertex);
dfs([vertex]);
visited.delete(vertex);
});
return paths;
}
// Function to calculate the total distance of heuristic path
function getPathDistance(path) {
let distance = 0;
for (let i = 0; i < path.length - 1; i++) {
distance += heuristic.calculateDistance(path[i], path[i+1]);
}
return distance;
}
// Function to draw the shortest path on the canvas
function drawShortestPath(graph) {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
// Generate all possible paths and find the shortest one
const paths = generatePaths(heuristic);
let shortestPath = null;
let shortestDistance = Infinity;
paths.forEach((path) => {
const distance = getPathDistance(path);
if (distance < shortestDistance) {
shortestPath = path;
shortestDistance= distance;
}
});
// Store the pixel length in a global variable called path_length
const path_length = shortestDistance;
// Log the pixel length to the console
console.log("Pixel length of shortest path:", path_length);
shortestDistanceResult.textContent = ((path_length*2)/54).toFixed(2);
dummyCalcD = ((path_length*2)/54).toFixed(2);
// Draw all vertices as black circles
graph.vertices.forEach((vertex) => {
ctx.beginPath();
ctx.arc(vertex.x, vertex.y, 10, 0, 2 * Math.PI);
ctx.fillStyle = "#000000";
ctx.fill();
ctx.closePath();
});
// Draw the path as a red line
ctx.beginPath();
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 3;
for (let i = 0; i < shortestPath.length - 1; i++) {
const current = shortestPath[i];
const next = shortestPath[i+1];
ctx.moveTo(current.x, current.y);
ctx.lineTo(next.x, next.y);
}
ctx.stroke();
ctx.closePath();
}
// Draw the shortest path on the canvas
// This is an example of how this function would be called. ***heuristic*** refers to the new Graph created using the Graph class
drawShortestPath(heuristic);
Moving onto the Game function
The game() function is a function that encloses all of the action that the user does. This includes multiple event handlers that are responsible for the lines dragging and dropping. It is also reponsible for drawing the graph that the user interacts with. This is essentially a separate dimension from the heuristic diagram which is not able to interact with users.
- We enclosed all of the user actions into one function in order to allow the user to first select their points and draw the graph using the user's selected points.
- Without game() the user would be unable to see the points they selected on the menu selection page.
For a time we will be explaining code from the game() function
// Initialize variables
let selectedVertex = null;
let lineStartX = null;
let lineStartY = null;
let lineEndX = null;
let lineEndY = null;
let allVerticesConnected = graph.checkAllVerticesConnected();
// Draw the user drawing graph
drawGraph(graph);
Defining Adjacency Relationships for the Heuristic Graph
- This algorithm allows for calculation of a heuristic - It essentially finds the closest point from one starting point(vertex) to another. Then, it does the same thing again with the end point becoming the start point in the calculation.
- This defining of adjacency relationships is what gives the heuristic graph its heuristic characteristics.
// Define adjacency relationships
heuristic.vertices.forEach((vertex) => {
const closestPoints = vertices
.filter((p) => p.id !== vertex.id)
.sort((a, b) => heuristic.calculateDistance(vertex, a) - heuristic.calculateDistance(vertex, b))
.slice(0, 2);
closestPoints.forEach((point) => {
const adjacentVertex = heuristic.map[point.id];
vertex.addAdjacent(adjacentVertex);
});
});
Drawing the User's Graph
- function drawGraph() is very similar to function drawShortestPath() for the heuristic graph. It essentially draws out the user's interactive graph onto the canvas.
- It is as convenient as drawShortestPath() and can be called multiple times to the coder's desire without problems occuring.
// Function to draw the graph on the canvas
function drawGraph(graph) {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
// Draw all vertices as black circles
graph.vertices.forEach((vertex) => {
ctx.beginPath();
ctx.arc(vertex.x, vertex.y, 10, 0, 2 * Math.PI);
ctx.fillStyle = vertex.connected ? "#00FF00" : "#000000";
ctx.fill();
ctx.closePath();
});
// Draw the connected lines
ctx.beginPath();
ctx.strokeStyle = "#0000FF";
ctx.lineWidth = 3;
graph.vertices.forEach((vertex) => {
vertex.adjacent.forEach((adjacentVertex) => {
ctx.moveTo(vertex.x, vertex.y);
ctx.lineTo(adjacentVertex.x, adjacentVertex.y);
});
});
ctx.stroke();
ctx.closePath();
}
// Draw the user drawing graph
// This is an example of how this function would be called. ***graph*** refers to the new Graph created using the Graph class
drawGraph(graph);
Handling Mouse Event functions
We will next be talking about how the user is able to interact using mouse moving and clicking. We are using event handlers and event listeners to be able to track down mouse interactions.
- We will be handling the following mouse events:
- Mouse Down
- Mouse Move
- Mouse Up
- What to do if reset button is clicked
Mouse Down
Mouse Down event is handled in the handleMouseDown() function.
- The function first checks if all vertices are connected. If all vertices are connected(allVerticesConnected === true) then this function is disabled(which can prevent the user from interacting with the graph once it is completed)
- This function's main functionality is to record down the coordinates of the vertex(point) in which the user clicked down on as a starting point.
// Function to handle the mouse down event
function handleMouseDown(e) {
if (allVerticesConnected) {
return; // Return early if all vertices are already connected
}
const canvas = e.target;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Find the vertex that the user clicked on (if any)
const vertex = graph.vertices.find((vertex) => {
const dx = vertex.x - mouseX;
const dy = vertex.y - mouseY;
return dx * dx + dy * dy <= 100; // check if the click is within the vertex's radius
});
if (vertex) {
// Store the selected vertex and the starting position of the line
selectedVertex = vertex;
lineStartX = vertex.x;
lineStartY = vertex.y;
// Add mouse move and mouse up event listeners
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("mouseup", handleMouseUp);
}
}
Mouse Move
Mouse Move event is handled in the handleMouseMove() function.
- The function essentially creates the line dragging effect when the user drags his mouse. The starting point is on the point the user clicked on, and the end point of the line follows the user's cursor.
- This line can also be customized, such as changing its color or width.
// Function to handle the mouse move event
function handleMouseMove(e) {
const canvas = e.target;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Update the line end position
lineEndX = mouseX;
lineEndY = mouseY;
// Redraw the canvas
drawGraph(graph);
// Draw the temporary line from the selected vertex to the mouse position
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.moveTo(lineStartX, lineStartY);
ctx.lineTo(lineEndX, lineEndY);
ctx.stroke();
ctx.closePath();
}
Mouse Up
Mouse Up event is handled in the handleMouseUp() function.
- This function creates the "snapping effect," where the line finally connects onto another vertex(point) on the graph if the mouse is released.
- It is able to do this by detecting if the line is within 20 pixels of a point once the mouse is released.
- If the line is within 20 pixels of a point, the line snaps onto the corresponding point with the addAdjacent functions(addAdjacent function is found in class Vertex).
- This function also detects to see if all points are connected.
- If all points are connected, the status is updated in the console, and the user is allowed to finish the game.
- The total distance of all the lines formed are also calculated
- The total distance is converted to miles when displaying it in HTML.
- It is able to do this by detecting if the line is within 20 pixels of a point once the mouse is released.
// Function to handle the mouse up event
function handleMouseUp(e) {
const canvas = e.target;
// Find the vertex that the user released the mouse on (if any)
const vertex = graph.vertices.find((vertex) => {
const dx = vertex.x - lineEndX;
const dy = vertex.y - lineEndY;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= 20; // check if the release point is within 20 pixels of the vertex
});
if (vertex && !vertex.connected) {
// Connect the line to the snapped vertex
selectedVertex.addAdjacent(vertex);
vertex.addAdjacent(selectedVertex);
// Set the vertices as connected
selectedVertex.connected = true;
vertex.connected = true;
// Redraw the canvas with the updated graph and line connection
drawGraph(graph);
// Check if all vertices are connected
allVerticesConnected = graph.checkAllVerticesConnected();
console.log("All vertices connected:", allVerticesConnected);
// allows user to finish if all points connected
if(allVerticesConnected === true){
finishButton.style.display = "block";
}
// Calculate and update the total distance
const totalDistance = graph.calculateTotalDistance();
totalDistanceDisplay.textContent = (totalDistance/54).toFixed(2);
dummyTotalD = (totalDistance/54).toFixed(2);
}
// Reset the line positions and remove the event listeners
lineStartX = null;
lineStartY = null;
lineEndX = null;
lineEndY = null;
selectedVertex = null;
canvas.removeEventListener("mousemove", handleMouseMove);
canvas.removeEventListener("mouseup", handleMouseUp);
}
Extra Handler: Handling the Reset Button
- This event handler handleResetButtonClick() responds when the reset button is clicked on. When the reset button is clicked, all lines on the graph are erased and the total distance of all lines is reset to 0.
- In addition, the function that checks for connection of all vertices is set to false so that the user can continue to draw lines after a reset.
- The finish button is also hidden to prevent the user from submitting an unfinished route.
// Function to handle the reset button click event
function handleResetButtonClick() {
// Clear the canvas
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Reset all vertices
for (const vertex of graph.vertices) {
vertex.connected = false;
vertex.adjacent = [];
}
// Reset the total distance
document.getElementById("totalDistance").textContent = "0.00";
// Redraw the empty canvas
drawGraph(graph);
// Reset the allVerticesConnected flag
allVerticesConnected = graph.checkAllVerticesConnected();
// hides finish button if lines are reset
finishButton.style.display = "none";
}
// Add event listeners
canvas.addEventListener("mousedown", handleMouseDown);
resetButton.addEventListener("click", handleResetButtonClick);
submitButton.addEventListener("click", handleResetButtonClick);
Page Transitions
- We are now outside of function game() and will now explore the code for page transitions.
- function gameScreen() has a parameter which can help indicate the status of the page(whether the user is playing, on the start screen, or on the end screen, etc.)
- Different pages will appear and disappear corresponding to which button the user presses on.
- If the Finish Game button is pressed, the heuristic graph is drawn onto the canvas.
- In this place we added our scoring algorithm to be able to display the score of the user.
- The scoring is in accordance to how far away the user is from the shortest distance possible. The closer the route is to the shortest route, the higher the score.
- The list of locations are also displayed using a for loop, and are shown on the form.
// Initially hides end page and game page and finish button
endPage.style.display = "none";
gamePage.style.display = "none";
finishButton.style.display = "none";
finishForm.style.display = "none";
menuPage.style.display = "none";
// Function switches screen based on status parameter
function gameScreen(status){
if(status === 1){
startPage.style.display = "none";
menuPage.style.display = "block";
}
if(status === 2){
finishForm.style.display = "block";
resetButton.style.display = "none";
finishButton.style.display = "none";
// Draw the shortest path on the canvas
drawShortestPath(heuristic);
// Calculates the score
let intShortestDistance=parseInt(shortestDistanceResult.innerHTML);
let intUserDistance=parseInt(totalDistanceDisplay.innerHTML);
let score = (Math.pow(2,vertices.length)*1000*Math.pow(Math.E,(2*(Math.log(intUserDistance/intShortestDistance))))).toFixed(2);
// Invalidates score in case distance is less than shortest distance. Displays score if otherwise
if(intShortestDistance>intUserDistance){
totalDistanceDisplay.textContent = "**error**";
scoreDisplay.textContent = "NA";
} else{
// Adds bonus score if user chooses more points(since more points means higher difficulty)
if(vertices.length > 5){
score = (score*(1+0.12*(vertices.length-5))).toFixed(2);
}
scoreDisplay.textContent = score.toString();
dummyScore = score;
}
// for loop to display locations on the form
locationDisplay = "";
for(let i=0; i<(locationNames.length-1); i++){
locationDisplay += locationNames[i] + ", ";
}
locationDisplay += locationNames[locationNames.length-1];
locationList.textContent = locationDisplay;
}
if(status === 3){
window.location.reload();
}
}
Page Transitions in HTML
- We will now show how page transitions are created to ensure a smooth user experience.
- Multiple divs are used to separate the start, playing, menu, and end screens.
- Liquid is used to utilize the menu selection page and functionality(which is in a separate file). Liquid makes the code more clear with less clutter. It also reduces the hassle of copying and pasting and having to reformat the code.
<div id="start-page">
<div class="button-container">
<button class="gen-button" onclick="gameScreen(1)" id="start-button">Start Game</button>
</div>
</div>
<div id="menu-selection-page">
<h2 style="color:white; text-align: center;">Please select 2-10 locations you would like to visit.</h2>
<p style="color:white; text-align: center;">Selection of locations is limited to 10 locations.</p>
<!-- Use liquid to incorporate menu code for easier access, removed so that fastpages will not have an error -->
<!-- ***liquid goes here*** -->
</div>
<div id="end-page">
<h1>Thank you for playing!</h1>
<p>If you would like to play another round, please click the button below.</p>
<div class="button-container">
<button class="gen-button" onclick="gameScreen(3)">Return to Game Page</button>
</div>
<h3>If you would like to return to the home page, please click <span id="linked-gametohome"><a href="/VSCode-Fastpages/index">here</a></span>.</h3>
</div>
</head>
<body>
<div id="finish-form">
<h3 style="color:white;">Game over, please record your score!</h3>
<p style="color:white;">The shortest route is shown on the map with the red lines</p>
<form action="javascript:userCreate()">
<p><label>
Username:
<input type="text" name="username" id="username" placeholder="Enter username here" required>
</label></p>
<p><label>
Total Distance of your route: <span id="totalDistance">0.00</span> miles
</label></p>
<p><label>
Total Distance of shortest route: <span id="totalDistanceClosest">0.00</span> miles
</label></p>
<p><label>
Calculated score: <span id="scoring">0</span>
</label></p>
<p><label>
Locations visited: <span id="locationList">NA</span>
</label></p>
<p>
<!-- Popup message on button click -->
<button onclick="alert('Your score has been posted!')" id="form-submit-button">Submit</button>
</p>
</form>
</div>
<div id="game-page">
<div class="button-container">
<button id="game-finish-button" class="gen-button" onclick="gameScreen(2)">Finish Game</button>
<button id="resetButton" class="gen-button">Reset</button>
</div>
<canvas id="canvas" width="1072" height="829"></canvas>
</div>
POST Request
- At the end of a game, the user is prompted to submit their data to a database. This is done through the POST request code.
- Dummy variables for score, total distance, and calculated distance are created in order to prevent conflicts with the HTML strings from each element with the corresponding id.
- These are automatic inputs, and the user does not need to input anything for these categories.
- The list locationNames is added into body as it is without the need for input from the user.
- Only the username requires input from the user in the form.
- Dummy variables for score, total distance, and calculated distance are created in order to prevent conflicts with the HTML strings from each element with the corresponding id.
// prepare URL's to allow easy switch from deployment and localhost
// const url = "http://localhost:8086/api/leaderboardUser";
const url = "https://school.aipad-techs.com/api/leaderboardUser/api/leaderboardUser";
const createGame = url + '/addscore';
// Function creates POST request
function userCreate(){
// Get the data
const body = {
name: document.getElementById("username").value,
score: Math.round(dummyScore),
locations: locationNames,
tot_distance: Math.round(dummyTotalD),
calc_distance: Math.round(dummyCalcD)
};
console.log(body);
const requestOptions = {
method: 'POST',
body: JSON.stringify(body),
mode: 'cors',
cache: 'default',
//credentials: 'include',
headers: {
"content-type": "application/json",
'Authorization': 'Bearer my-token',
},
};
// URL for Create API
// Fetch API call to the database to create a new user
fetch(createGame, requestOptions)
.then(response => {
// trap error response from Web API
if (response.status !== 200) {
const errorMsg = 'Database create error: ' + response.status;
console.log(errorMsg);
return;
}
// response contains valid result
response.json().then(data => {
console.log(data);
})
})
finishForm.style.display = "none";
gamePage.style.display = "none";
endPage.style.display = "block";
}
#title-thing{
text-align: center;
color: white;
}
.button-container{
width: fit-content;
margin: 0 auto;
}
.gen-button{
display: block;
width: 200px;
padding: 15px 0;
text-align: center;
margin: 20px 10px;
background: transparent;
border-radius: 25px;
border: 2px solid #009614;
color: #fff;
font-weight: bold;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.6s;
color: white;
}
.gen-button:hover{
background-color: #009614;
}
#canvas{
border: 1px solid #000000;
background-image: url('SDmap.png');
background-position: center;
}
#linked-gametohome{
background-color: yellow;
}
#end-page{
text-align: center;
color: white;
}
#game-page{
text-align: center;
color: white;
}
#finish-form{
text-align: center;
background-color: transparent;
}
form {
background: transparent; /* Adjust the alpha value (0.5) to control the transparency */
}
label {
display: block;
color: white;
font-family: Helvetica, arial;
}
p{
width: 100%;
}
#username{
background: transparent;
border: none;
outline: auto;
font-size: 1em;
padding:0 35px 0 5px;
color: white;
}
#form-submit-button{
display: block;
width: 200px;
padding: 15px 0;
text-align: center;
margin: 20px auto;
background: transparent;
border-radius: 25px;
border: 2px solid #009614;
color: #fff;
font-weight: bold;
cursor: pointer;
overflow: hidden;
transition: all 0.6s;
color: white;
}
#form-submit-button:hover{
background-color: #009614;
}
.banner{
width: 100%;
height: 200% !important;
background-image: linear-gradient(rgba(0,0,0,0.75), rgba(0,0,0,0.75)),url(homepg.jpg);
background-size: cover;
background-position: center !important ;
}