292 lines
9.6 KiB
JavaScript
292 lines
9.6 KiB
JavaScript
|
// 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)
|
||
|
|
||
|
})}
|