292 lines
9.6 KiB
JavaScript
Raw Normal View History

2023-05-23 03:04:36 +02:00
// 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 <https://www.gnu.org/licenses/>.
{
// 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)
})}