diff --git a/app.js b/app.js new file mode 100644 index 0000000..d0525f8 --- /dev/null +++ b/app.js @@ -0,0 +1,291 @@ + // Copyright (c) 2023, Sam Hadow + // + //app.js + // + // This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + // + // You should have received a copy of the GNU General Public License along with this program. If not, see . + +{ +// define svg in container +const svg = d3.select('#container') + .append('svg') + .attr('width', '100%') + .attr('height', '100%') + .attr('preserveAspectRatio', 'xMinYMin meet') + .classed('svg-content', true); + +// svg should fill available space +let width = window.innerWidth; +let height = window.innerHeight; +// svg viewbox +svg.attr('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`); + +// reload page when window is resized +// (resizes every elements and keeps svg consistent with background map) +window.onresize = function(){ location.reload(); } + + +// mouseover tooltip +// (box showing node name) +const tooltip = d3.select('#container') + .append("div") + .style("position", "absolute") + .style("visibility", "hidden") + .text("") + .attr("class", "tooltip") + .style("background-color", "white") + .style("border", "solid") + .style("border-width", "1px") + .style("border-radius", "5px") + .style("padding", "10px"); + + +// parsing data +Promise.all([ + d3.csv("data_nodes.csv"), // nodes + d3.csv("data_edges.csv") // edges +]).then( + +function (initialize) { + + let nodes = initialize[0]; + let edges = initialize[1]; + + const nodes_array = [] + nodes.forEach(function(row){ + nodes_array.push( + { + 'id':row.id, + 'y':row.latitude*(height/1747), + 'x':row.longitude*(width/2188), + 'name':row.name, + 'type':row.type + } + ); + }); + + const edges_array = [] + edges.forEach(function(row){ + edges_array.push( + { + 'id':row.id, + 'source':row.origin, + 'target':row.destination, + 'name':row.name, + 'type':row.type, + 'weight':parseFloat(row.weight) //we need floats not strings for additions and comparison operators + } + ); + }); + + +// handling clicks on radio buttons +let level = 0; // by default user is a beginner +const buttons = d3.selectAll('input'); +buttons.on('change', function(d) {level = this.value;}); + +// handling clicks on nodes +let selectedNode = null; //initializing 1st selected node + +function onClick(node) { + if (selectedNode === null) { + // on first click remember the selected node (only its id is enough) + selectedNode = d3.select(this).attr('id'); + } else { + // second click should trigger a path finding function + node2 = d3.select(this).attr('id') + path_finding(selectedNode, node2); + + // reset 1st selected node + selectedNode = null; + } +} + +function path_finding(node1, node2) { + + // reset color and width of every edge + svg.selectAll(".link") + .attr("stroke", "black") + .attr("stroke-width", 1.5); + + if (node1 != node2) { + // if we need to find a path + if (level==0) { + // beginner + current_path = path_to_follow(next_b, node1, node2); + estimated_time = adjacency_matrix_beginner[node1][node2]; + } else { + // expert + current_path = path_to_follow(next_e, node1, node2); + estimated_time = adjacency_matrix_expert[node1][node2]; + } + path_string = path_to_string(current_path, edges_array, nodes_array); + // we round estimated_time to upper integer as we don't want too many digits, it's just an estimation + path_string += 'Le trajet devrait vous prendre ' + Math.ceil(estimated_time) + 'minutes' + + target_name = nodes_array.find(node => node.id === node2).name; + // if target node has a name we append it to path_string + if (target_name != '') { + path_string += " jusqu'à " + target_name + } + + // then we fill corresponding html paragraph with generated string + document.getElementById("instructions").innerHTML = path_string + + // highlight edges in svg + // get id of each edge in path + var id_list = current_path.map(function(value) { return value[1];}); + svg.selectAll(".link") + .filter(function() { + return id_list.includes(d3.select(this).attr("id")); // filter links by id (only those in current path) + }) + .attr("stroke", "red") + .attr("stroke-width", 7); // make these links wider and red + + } else { + // names nodes, user is already here + document.getElementById("instructions").innerHTML = "Vous vous trouvez déjà ici." + } + +} + +// generating svg + // edges + // Draw the links + const link = svg.selectAll(".link") + .data(edges_array) + .enter().append("path") + .attr("class", "link") + .attr("stroke", "black") + .attr("stroke-width", 1.5) + .attr("fill", "none") + .attr("id", function(d) { + return d.id + }) + .attr("d", function(d) { + const x1 = nodes_array.find(node => node.id === d.source).x; + const y1 = nodes_array.find(node => node.id === d.source).y; + const x2 = nodes_array.find(node => node.id === d.target).x; + const y2 = nodes_array.find(node => node.id === d.target).y; + return `M ${x1},${y1} L ${x2},${y2}`; + }); + + // nodes + for (let i = 0; i < nodes_array.length; i++) { + if (nodes_array[i].type == 1) { + svg + .append('circle') + .attr('cx', nodes_array[i].x) + .attr('cy', nodes_array[i].y) + .attr('r', 14) + .attr('name',nodes_array[i].name) + .attr('id', nodes_array[i].id) + .style('fill', 'blue') + + //.on("click", onClick) + + .on("mouseover", function(){return tooltip.style("visibility", "visible");}) + .on("mousemove", function(){return tooltip.style("top", (event.pageY+30)+"px").style("left",(event.pageX)+"px") + .text("station: "+d3.select(this).attr("name"));}) + .on("mouseout", function(){return tooltip.style("visibility", "hidden");}); + } else { + svg + .append('circle') + .attr('cx', nodes_array[i].x) + .attr('cy', nodes_array[i].y) + .attr('r', 7) + .attr('name',nodes_array[i].name) + .attr('id', nodes_array[i].id) + .style('fill', 'green') + }; + }; + // handling click on nodes + const my_nodes = d3.selectAll('circle'); + my_nodes.on('click', onClick); + + +//adjacency matrix + const size = nodes_array.length; + let adjacency_matrix_beginner = Array(size).fill().map(()=>Array(size)); + let adjacency_matrix_expert = Array(size).fill().map(()=>Array(size)); + + let next_b = Array(size).fill().map(()=>Array(size).fill().map(()=>Array(2).fill())); + let next_e = Array(size).fill().map(()=>Array(size).fill().map(()=>Array(2).fill())); + + for (let j = 0; j < size; j++){ + adjacency_matrix_beginner[j][j]=0; + next_b[j][j][0]=j; + next_b[j][j][1]=-1; + + adjacency_matrix_expert[j][j]=0; + next_e[j][j][0]=j; + next_e[j][j][1]=-1; + } + + for (let k = 0; k < edges_array.length; k++){ + + temp_weight = edges_array[k].weight; + + // wait time (regardless of level) + // for lifts we'll add these numbers to the weight (in minutes) regardless of base time + // surface lift: 10 + // chairlift: 8 + // high speed chairlift: 5 + // gondola: 7 + // cable car: 7 + // free lift: 15 + if (edges_array[k].type == "surface lift") { + temp_weight += 10 + } else if (edges_array[k].type == "gondola") { + temp_weight += 7 + } else if (edges_array[k].type == "cable car") { + temp_weight += 7 + } else if (edges_array[k].type == "chairlift") { + temp_weight += 8 + } else if (edges_array[k].type == "high speed chairlift") { + temp_weight += 5 + } else if (edges_array[k].type == "free lift") { + temp_weight += 15 + } + + //expert + if (!(adjacency_matrix_expert[edges_array[k].source][edges_array[k].target] >= temp_weight ) ) { + adjacency_matrix_expert[edges_array[k].source][edges_array[k].target] = temp_weight; + next_b[edges_array[k].source][edges_array[k].target][0]=edges_array[k].target; + next_b[edges_array[k].source][edges_array[k].target][1]=edges_array[k].id; + } + + // beginner + // here we need to increase the weight according to difficulty if it's a slope + // factors: + // very easy (green): *1.2 + // easy (blue): *2 + // difficult (red): *3 + // very difficult (black): *4 + + if (edges_array[k].type == "very easy") { + temp_weight *= 1.2; + } else if (edges_array[k].type == "easy") { + temp_weight *= 2; + } else if (edges_array[k].type == "difficult") { + temp_weight *= 3; + } else if (edges_array[k].type == "very difficult") { + temp_weight *= 4; + } + // + if (!(adjacency_matrix_beginner[edges_array[k].source][edges_array[k].target] >= temp_weight ) ) { + adjacency_matrix_beginner[edges_array[k].source][edges_array[k].target] = temp_weight; + next_e[edges_array[k].source][edges_array[k].target][0]=edges_array[k].target; + next_e[edges_array[k].source][edges_array[k].target][1]=edges_array[k].id; + + } + } + + // obtains final next and adjacency matrices using Floyd Warshall algorithm + floyd_warshall(adjacency_matrix_beginner, next_b, size) + floyd_warshall(adjacency_matrix_expert, next_e, size) + +})} diff --git a/floyd_warshall.js b/floyd_warshall.js new file mode 100644 index 0000000..c825faa --- /dev/null +++ b/floyd_warshall.js @@ -0,0 +1,30 @@ + // Copyright (c) 2023, Sam Hadow + // + //floyd_warshall.js + // + // This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + // + // You should have received a copy of the GNU General Public License along with this program. If not, see . + +function floyd_warshall(matrix, next, size) { + // Floyd Warshall algorithm + for (let k = 0; k < size; k++){ + for (let i = 0; i < size; i++){ + for (let j = 0; j < size; j++){ + // we need to have matrix[i][k] and matrix[k][j] both defined + if (!((matrix[i][k] == undefined) || (matrix[k][j] == undefined)) ){ + // here we use !(a<=b) instead of a>b to handle situations where a is undefined + // (if a is undefined a <= any_value will be false) + if (!(matrix[i][j] <= matrix[i][k] + matrix[k][j])) { + matrix[i][j] = matrix[i][k] + matrix[k][j]; + next[i][j][0] = next[i][k][0] + next[i][j][1] = next[i][k][1] + } + } + } + } + } + +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..b5f35b3 --- /dev/null +++ b/main.js @@ -0,0 +1,36 @@ + // Copyright (c) 2023, Sam Hadow + // + //main.js + // + // This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + // + // You should have received a copy of the GNU General Public License along with this program. If not, see . + +const { app, BrowserWindow } = require('electron') + + +let mainWindow = null; + +app.whenReady().then(() => { + // We cannot require the screen module until the app is ready. + const { screen } = require('electron') + + // Create a window that fills the screen's available work area. + const primaryDisplay = screen.getPrimaryDisplay() + const { width, height } = primaryDisplay.workAreaSize + + +mainWindow = new BrowserWindow({ + width, + height, + autoHideMenuBar: true, + resizable: true, + //icon: 'logo.png', + frame: true + }) + + + mainWindow.loadFile('index.html') +}) diff --git a/path.js b/path.js new file mode 100644 index 0000000..f50f238 --- /dev/null +++ b/path.js @@ -0,0 +1,23 @@ + // Copyright (c) 2023, Sam Hadow + // + //path.js + // + // This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + // + // You should have received a copy of the GNU General Public License along with this program. If not, see . + +function path_to_follow(matrix,origin,target) { + // calculate edges to follow to reach target from origin (origin and target are nodes id) + // matrix should be next_b or next_e + // return a list with the id of these edges, and estimated travel time as first element. + let current_node = origin; + let path = [] + while (current_node != target) { + edge = matrix[current_node][target][1] + current_node = matrix[current_node][target][0] + path.push([current_node, edge]); + } + return path +} diff --git a/path_to_string.js b/path_to_string.js new file mode 100644 index 0000000..25e5d1c --- /dev/null +++ b/path_to_string.js @@ -0,0 +1,41 @@ + // Copyright (c) 2023, Sam Hadow + // + //path_to_string.js + // + // This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + // + // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + // + // You should have received a copy of the GNU General Public License along with this program. If not, see . + +function path_to_string(path, edges, nodes) { + // convert path (with only edges and nodes id) to a human-readable string + let current = null; + let string = '' + for (let i = 0; i < path.length; i++){ + current = edges.find(edge => edge.id === path[i][1]); + if (current.type == "very easy" || current.type == "easy" || current.type == "difficult" || current.type == "very difficult" ){ + string += i+1 +') Descendez la piste: ' + current.name + '
'; + } else if (current.type == "surface lift") { + string += i+1 +') Prenez le téléski: ' + current.name + '
'; + } else if (current.type == "gondola") { + string += i+1 +') Prenez la télécabine: ' + current.name + '
'; + } else if (current.type == "cable car") { + string += i+1 +') Prenez le téléphérique: ' + current.name + '
'; + } else if (current.type == "chairlift") { + string += i+1 +') Prenez le télésiège: ' + current.name + '
'; + } else if (current.type == "high speed chairlift") { + string += i+1 +') Prenez le télésiège express débrayable: ' + current.name + '
'; + } else if (current.type == "path") { + direction = nodes.find(node => node.id === current.target).name; + if (direction != '') { + string += i+1 +') Prenez le chemin en direction de: ' + direction + '
'; + } else { + string += i+1 +') Prenez un chemin
'; + } + } else if (current.type == "free lift") { + string += i+1 +') Prenez la remontée mécanique gratuite: ' + current.name + '
'; + } + } + return string +}