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 : '© <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 ( / ^ L I N E S T R I N G \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+ }
0 commit comments