// 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) })}