Skip to content

Commit fdcec46

Browse files
authored
Init webapp (#271)
* Init webapp * data folder * Update gitignore * x,y to latlon * Fixiaml
1 parent 855ee0e commit fdcec46

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ __pycache__
3737
images
3838
examples/debug
3939
examples/release
40+
41+
42+
webapp/data/*

webapp/index.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Road Network Visualization with OpenStreetMap</title>
7+
8+
<!-- Leaflet CSS and JS -->
9+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
10+
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
11+
12+
<!-- D3.js -->
13+
<script src="https://d3js.org/d3.v7.min.js"></script>
14+
15+
<link rel="stylesheet" href="styles.css" />
16+
</head>
17+
<body>
18+
19+
<!-- Map container -->
20+
<div id="map"></div>
21+
22+
<!-- Slider for time step -->
23+
<div class="slider-container">
24+
<label for="timeSlider">Time Step:</label>
25+
<input type="range" id="timeSlider" min="0" step="300" value="0" />
26+
<span id="timeLabel">Time Step: 0</span>
27+
</div>
28+
29+
<script src="script.js"></script>
30+
</body>
31+
</html>

webapp/script.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Initialize the Leaflet map
2+
const baseZoom = 13;
3+
const map = L.map('map').setView([44.4949, 11.3426], baseZoom); // Centered on Bologna, Italy
4+
5+
// Add OpenStreetMap tile layer with inverted grayscale effect
6+
const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
7+
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
8+
}).addTo(map);
9+
tileLayer.getContainer().style.filter = 'grayscale(100%) invert(100%)';
10+
11+
// Create an overlay for D3 visualizations
12+
L.svg().addTo(map);
13+
const overlay = d3.select(map.getPanes().overlayPane).select("svg");
14+
const g = overlay.append("g").attr("class", "leaflet-zoom-hide");
15+
16+
let nodes, edges, densities;
17+
let timeStep = 0;
18+
19+
// Load CSV data for nodes, edges, and densities
20+
Promise.all([
21+
d3.dsv(";", "./data/nodes.csv", parseNodes),
22+
d3.dsv(";", "./data/edges.csv", parseEdges),
23+
d3.dsv(";", "./data/densities.csv", parseDensity)
24+
]).then(([nodesData, edgesData, densityData]) => {
25+
nodes = nodesData;
26+
edges = edgesData;
27+
densities = densityData;
28+
29+
// console.log("Nodes:", nodes);
30+
// console.log("Edges:", edges);
31+
// console.log("Densities:", densities);
32+
33+
if (!nodes.length || !edges.length || !densities.length) {
34+
console.error("Missing CSV data.");
35+
return;
36+
}
37+
38+
// Create a map of nodes keyed by their id for quick lookup
39+
const nodeMap = new Map(nodes.map(d => [d.id, d]));
40+
41+
// Filter out edges whose nodes do not exist
42+
edges = edges.filter(d => nodeMap.has(d.u) && nodeMap.has(d.v));
43+
44+
// Create a color scale for density values using three color stops
45+
const colorScale = d3.scaleLinear()
46+
.domain([0, 0.5, 1])
47+
.range(["green", "yellow", "red"]);
48+
49+
// Function to project geographic coordinates into Leaflet's layer point coordinates
50+
function project(d) {
51+
return map.latLngToLayerPoint([d.y, d.x]);
52+
}
53+
54+
// D3 line generator to draw paths
55+
const lineGenerator = d3.line()
56+
.x(d => d[0])
57+
.y(d => d[1]);
58+
59+
// Draw edges as SVG paths
60+
const link = g.selectAll("path")
61+
.data(edges)
62+
.enter()
63+
.append("path")
64+
.attr("fill", "none")
65+
.attr("stroke", "white")
66+
.attr("stroke-dasharray", d =>
67+
d.name.toLowerCase().includes("autostrada") ? "4,4" : "none"
68+
)
69+
.style("pointer-events", "all")
70+
.style("cursor", "pointer")
71+
.on("click", function(event, d) {
72+
const densityData = densities.find(row => row.time === timeStep);
73+
const densityValue = densityData ? densityData.densities[edges.indexOf(d)] : "N/A";
74+
alert(`Edge ID: ${d.osm_id} - from ${d.u} to ${d.v}.\n\nDensity at time step ${timeStep}: ${densityValue}`);
75+
});
76+
77+
78+
// Draw nodes as SVG circles
79+
const node = g.selectAll("circle")
80+
.data(nodes)
81+
.enter()
82+
.append("circle")
83+
.attr("fill", "blue")
84+
.style("cursor", "pointer")
85+
.on("click", function(event, d) {
86+
alert(`Node ID: ${d.id}`);
87+
});
88+
89+
// Function to update node and edge positions, and color edges based on density
90+
function update() {
91+
// Project nodes to current map coordinates
92+
nodes.forEach(d => d.projected = project(d));
93+
94+
// Update edge paths
95+
link.attr("d", d => {
96+
if (d.geometry && d.geometry.length > 0) {
97+
const projectedCoords = d.geometry.map(pt => {
98+
const point = map.latLngToLayerPoint([pt.y, pt.x]);
99+
return [point.x, point.y];
100+
});
101+
return lineGenerator(projectedCoords);
102+
} else {
103+
// Fallback: draw a straight line between the two nodes
104+
const start = project(nodeMap.get(d.u));
105+
const end = project(nodeMap.get(d.v));
106+
return lineGenerator([[start.x, start.y], [end.x, end.y]]);
107+
}
108+
});
109+
110+
// Update node positions
111+
node.attr("cx", d => d.projected.x)
112+
.attr("cy", d => d.projected.y);
113+
114+
115+
// Update node radius based on zoom level
116+
function updateNodeRadius() {
117+
const zoomLevel = map.getZoom();
118+
const radiusScale = 3 + (zoomLevel - baseZoom);
119+
node.attr("r", radiusScale);
120+
}
121+
// Update edge stroke width based on zoom level
122+
function updateEdgeStrokeWidth() {
123+
const zoomLevel = map.getZoom();
124+
const strokeWidthScale = 3 + (zoomLevel - baseZoom);
125+
link.attr("stroke-width", strokeWidthScale);
126+
}
127+
128+
// Add event listener to map
129+
map.on('zoomend', function() {
130+
updateNodeRadius();
131+
updateEdgeStrokeWidth();
132+
});
133+
134+
// Initial render (default zoom level)
135+
updateNodeRadius();
136+
updateEdgeStrokeWidth();
137+
138+
updateDensityVisualization();
139+
}
140+
141+
map.on("zoomend", update);
142+
update(); // Initial render
143+
144+
// Update edge colors based on the current time step density data
145+
function updateDensityVisualization() {
146+
const currentDensityRow = densities.find(d => d.time === timeStep);
147+
if (!currentDensityRow) {
148+
console.error("No density data for time step:", timeStep);
149+
return;
150+
}
151+
const currentDensities = currentDensityRow.densities;
152+
153+
// For each edge, update the stroke color based on its density value
154+
edges.forEach((edge, index) => {
155+
let density = currentDensities[index];
156+
if (density === undefined || isNaN(density)) {
157+
console.warn(`Edge index ${index} has invalid density. Defaulting to 0.`);
158+
density = 0;
159+
}
160+
const color = colorScale(density);
161+
link.filter((d, i) => i === index)
162+
.attr("stroke", color);
163+
});
164+
}
165+
166+
// Set up the time slider based on the density data's maximum time value
167+
const maxTimeStep = d3.max(densities, d => d.time);
168+
const timeSlider = document.getElementById('timeSlider');
169+
const timeLabel = document.getElementById('timeLabel');
170+
// Round up max to the nearest 300 for step consistency
171+
timeSlider.max = Math.ceil(maxTimeStep / 300) * 300;
172+
timeSlider.step = 300;
173+
timeLabel.textContent = `Time Step: ${timeStep}`;
174+
175+
// Update the visualization when the slider value changes
176+
timeSlider.addEventListener('input', function() {
177+
timeStep = parseInt(timeSlider.value);
178+
timeLabel.textContent = `Time Step: ${timeStep}`;
179+
update();
180+
});
181+
}).catch(error => {
182+
console.error("Error loading CSV files:", error);
183+
});
184+
185+
// Parsing function for nodes CSV
186+
function parseNodes(d) {
187+
return {
188+
id: d.id,
189+
x: +d.lon, // Longitude
190+
y: +d.lat // Latitude
191+
};
192+
}
193+
194+
// Parsing function for edges CSV, including geometry parsing
195+
function parseEdges(d) {
196+
let geometry = [];
197+
if (d.geometry) {
198+
const coordsStr = d.geometry.replace(/^LINESTRING\s*\(/, '').replace(/\)$/, '');
199+
geometry = coordsStr.split(",").map(coordStr => {
200+
const coords = coordStr.trim().split(/\s+/);
201+
return { x: +coords[0], y: +coords[1] };
202+
});
203+
}
204+
return {
205+
osm_id: d.id,
206+
u: d.source_id,
207+
v: d.target_id,
208+
name: d.name,
209+
geometry: geometry
210+
};
211+
}
212+
213+
// Parsing function for density CSV
214+
function parseDensity(d) {
215+
const time = +d.time;
216+
const densities = Object.keys(d)
217+
.filter(key => key !== 'time')
218+
.map(key => {
219+
const val = d[key] ? d[key].trim() : "";
220+
return val === "" ? 0 : +val;
221+
});
222+
return { time, densities };
223+
}

webapp/styles.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
body, html {
2+
margin: 0;
3+
padding: 0;
4+
width: 100%;
5+
height: 100%;
6+
}
7+
8+
/* Fullscreen map */
9+
#map {
10+
width: 100%;
11+
height: 100vh;
12+
position: absolute;
13+
top: 0;
14+
left: 0;
15+
}
16+
17+
/* Slider container */
18+
.slider-container {
19+
position: absolute;
20+
bottom: 20px;
21+
left: 50%;
22+
transform: translateX(-50%);
23+
z-index: 1000;
24+
width: 80%;
25+
padding: 10px;
26+
background: rgba(255, 255, 255, 0.8);
27+
}
28+
29+
.slider-container input {
30+
width: 100%;
31+
}
32+
33+
/* SVG container on top of Leaflet */
34+
.leaflet-overlay-pane svg {
35+
position: absolute;
36+
}
37+
.leaflet-zoom-hide {
38+
pointer-events: auto;
39+
}
40+
41+
circle {
42+
fill: black;
43+
stroke: white;
44+
stroke-width: 1;
45+
}
46+

0 commit comments

Comments
 (0)