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
+}