Use PTv2 standard instead of DFS for finding segments
This commit is contained in:
		
							parent
							
								
									783f29ce68
								
							
						
					
					
						commit
						dd008d7ee2
					
				|  | @ -124,6 +124,17 @@ const matchStopNames = (fullName, abbrName) => | |||
|     ).length; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Determine if an OSM way is oneway or not. | ||||
|  * | ||||
|  * @param tags Set of tags of the way. | ||||
|  * @return True iff. the way is oneway. | ||||
|  */ | ||||
| const isOneWay = tags => | ||||
|     ('oneway' in tags && tags.oneway === 'yes') | ||||
|     || ('junction' in tags && tags.junction === 'roundabout') | ||||
|     || ('highway' in tags && tags.highway === 'motorway'); | ||||
| 
 | ||||
| /** | ||||
|  * Fetch stops, segments and lines in the network. | ||||
|  * | ||||
|  | @ -162,13 +173,10 @@ out body qt; | |||
|         return prev; | ||||
|     }, {}); | ||||
| 
 | ||||
|     // Result object containing all stops
 | ||||
|     // All stops in the network
 | ||||
|     const stops = {}; | ||||
| 
 | ||||
|     // Result object containing all segments between stops
 | ||||
|     const segments = {}; | ||||
| 
 | ||||
|     // Result object containing all lines
 | ||||
|     // All transport lines of the network
 | ||||
|     const lines = {}; | ||||
| 
 | ||||
|     for (let routeMaster of routeMasters) | ||||
|  | @ -234,7 +242,7 @@ URI: ${osmViewNode}/${stop.id} | |||
| 
 | ||||
|                         if (candidates.length === 0) | ||||
|                         { | ||||
|                             console.warn('No candidate found in TaM data.'); | ||||
|                             console.warn('No candidate found in TaM data.'); | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|  | @ -272,171 +280,142 @@ ${joinSentence(Array.from(candidate.directions), ', ', ' or ')} | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Add missing segments between stops
 | ||||
|         // Reconstruct the line’s route from stop to stop
 | ||||
|         const routes = []; | ||||
| 
 | ||||
|         for (let route of rawRoutes) | ||||
|         { | ||||
|             const {from, to} = route.tags; | ||||
| 
 | ||||
|             const stops = route.members | ||||
|                 .filter(({role}) => role === 'stop') | ||||
|                 .map(({ref}) => elements[ref]) | ||||
|                 .filter(stop => 'ref' in stop.tags) | ||||
|                 .map(stop => ({ | ||||
|                     id: stop.id, | ||||
|                     lat: stop.lat, | ||||
|                     lon: stop.lon, | ||||
|                     ref: stop.tags.ref, | ||||
|                     name: stop.tags.name, | ||||
|                 })); | ||||
|             // Human-readable description of the route for errors
 | ||||
|             const routeDescription = `line ${lineRef}’s route from \
 | ||||
| “${route.tags.from}” to “${route.tags.to}”`;
 | ||||
| 
 | ||||
|             const ways = route.members | ||||
|                 .filter(({role}) => role === '') | ||||
|                 .map(({ref}) => ref); | ||||
|             // Check that the route consists of a block of stops and platforms
 | ||||
|             // followed by a block of routes as dictated by PTv2
 | ||||
|             const relationPivot = route.members.findIndex( | ||||
|                 ({role}) => role === '' | ||||
|             ); | ||||
| 
 | ||||
|             // Construct a graph with all connected nodes
 | ||||
|             const nodeNeighbors = new Map(); | ||||
| 
 | ||||
|             for (let id of ways) | ||||
|             if (!route.members.slice(0, relationPivot).every( | ||||
|                 ({role}) => role === 'stop' || role === 'platform' | ||||
|             )) | ||||
|             { | ||||
|                 const {type, nodes, tags} = elements[id]; | ||||
| 
 | ||||
|                 const isOneWay = ( | ||||
|                     tags.oneway === 'yes' | ||||
|                     || tags.junction === 'roundabout' | ||||
|                 ); | ||||
| 
 | ||||
|                 const canGoBackward = ( | ||||
|                     !isOneWay | ||||
|                     || parseInt(tags['lanes:psv:backward'], 10) > 0 | ||||
|                 ); | ||||
| 
 | ||||
|                 if (type === 'way' && nodes.length >= 1) | ||||
|                 { | ||||
|                     let previousNode = nodes[0]; | ||||
| 
 | ||||
|                     if (!nodeNeighbors.has(previousNode)) | ||||
|                     { | ||||
|                         nodeNeighbors.set(previousNode, new Set()); | ||||
|                     } | ||||
| 
 | ||||
|                     for (let node of nodes.slice(1)) | ||||
|                     { | ||||
|                         if (!nodeNeighbors.has(node)) | ||||
|                         { | ||||
|                             nodeNeighbors.set(node, new Set()); | ||||
|                         } | ||||
| 
 | ||||
|                         nodeNeighbors.get(previousNode).add(node); | ||||
| 
 | ||||
|                         if (canGoBackward) | ||||
|                         { | ||||
|                             nodeNeighbors.get(node).add(previousNode); | ||||
|                         } | ||||
| 
 | ||||
|                         previousNode = node; | ||||
|                     } | ||||
|                 } | ||||
|                 throw new Error(`Members with invalid roles in between stops
 | ||||
| of ${routeDescription}`);
 | ||||
|             } | ||||
| 
 | ||||
|             // Find way from first stop through the end using DFS
 | ||||
|             const numberOfStops = stops.length; | ||||
|             let currentStopIndex = 0; | ||||
| 
 | ||||
|             while (currentStopIndex + 1 < numberOfStops) | ||||
|             if (!route.members.slice(relationPivot).every( | ||||
|                 ({role}) => role === '' | ||||
|             )) | ||||
|             { | ||||
|                 const currentStop = stops[currentStopIndex]; | ||||
|                 const nextStop = stops[currentStopIndex + 1]; | ||||
|                 const segmentId = `${currentStop.ref}-${nextStop.ref}`; | ||||
|                 throw new Error(`Members with invalid roles inside the path
 | ||||
| of ${routeDescription}`);
 | ||||
|             } | ||||
| 
 | ||||
|                 if (!(segmentId in segments)) | ||||
|             // List of stops in the route, expected to be in the timetable
 | ||||
|             // order as per PTv2 and to be traversed in order by the sequence
 | ||||
|             // of ways extracted below
 | ||||
|             const stops = route.members.slice(0, relationPivot) | ||||
|                 .filter(({role}) => role === 'stop') | ||||
|                 .map(({ref}) => ref); | ||||
| 
 | ||||
|             // List of ways making up the route’s path through its stops
 | ||||
|             // with each way connected to the next through a single point
 | ||||
|             const ways = route.members.slice(relationPivot) | ||||
|                 .map(({ref}) => ref); | ||||
| 
 | ||||
|             // Merge all used ways in a single path
 | ||||
|             let path = []; | ||||
|             let currentNode = stops[0]; | ||||
| 
 | ||||
|             for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) | ||||
|             { | ||||
|                 const {nodes: wayNodes, tags: wayTags} | ||||
|                     = elements[ways[wayIndex]]; | ||||
|                 const wayNodesSet = new Set(wayNodes); | ||||
| 
 | ||||
|                 const curNodeIndex = wayNodes.indexOf(currentNode); | ||||
| 
 | ||||
|                 // If not the last way, find a connection point to the next way
 | ||||
|                 // (there should be exactly one)
 | ||||
|                 let nextNode = null; | ||||
|                 let nextNodeIndex = null; | ||||
| 
 | ||||
|                 if (wayIndex + 1 < ways.length) | ||||
|                 { | ||||
|                     const visitedEdges = new Set(); | ||||
|                     const stack = [{ | ||||
|                         currentNode: currentStop.id, | ||||
|                         segment: [currentStop.id], | ||||
|                     }]; | ||||
|                     const nextNodeCandidates = elements[ways[wayIndex + 1]] | ||||
|                         .nodes.filter(node => wayNodesSet.has(node)); | ||||
| 
 | ||||
|                     let found = false; | ||||
| 
 | ||||
|                     while (stack.length !== 0) | ||||
|                     if (nextNodeCandidates.length !== 1) | ||||
|                     { | ||||
|                         const {currentNode, segment} = stack.pop(); | ||||
| 
 | ||||
|                         if (currentNode === nextStop.id) | ||||
|                         { | ||||
|                             // Arrived at next stop
 | ||||
|                             segments[segmentId] = { | ||||
|                                 nodes: segment.map(id => | ||||
|                                 { | ||||
|                                     const {lat, lon} = elements[id]; | ||||
|                                     return {lat, lon}; | ||||
|                                 }), | ||||
|                                 length: geolib.getPathLength(segment.map(id => | ||||
|                                 { | ||||
|                                     const {lat, lon} = elements[id]; | ||||
|                                     return {latitude: lat, longitude: lon}; | ||||
|                                 })), | ||||
|                                 lines: new Set([lineRef]), | ||||
|                             }; | ||||
| 
 | ||||
|                             found = true; | ||||
|                             break; | ||||
|                         } | ||||
| 
 | ||||
|                         const neighbors = nodeNeighbors.get(currentNode) || []; | ||||
| 
 | ||||
|                         for (let nextNode of neighbors) | ||||
|                         { | ||||
|                             const edge = `${currentNode}-${nextNode}`; | ||||
| 
 | ||||
|                             if (!visitedEdges.has(edge)) | ||||
|                             { | ||||
|                                 visitedEdges.add(edge); | ||||
|                                 stack.push({ | ||||
|                                     currentNode: nextNode, | ||||
|                                     segment: segment.concat([nextNode]), | ||||
|                                 }); | ||||
|                             } | ||||
|                         } | ||||
|                         throw new Error(`There should be exactly one point
 | ||||
| connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${routeDescription}, | ||||
| but there are ${nextNodeCandidates.length}`);
 | ||||
|                     } | ||||
| 
 | ||||
|                     if (!found) | ||||
|                     { | ||||
|                         throw new Error(`No way between stop \
 | ||||
| “${currentStop.name}” (${currentStop.id}) and stop “${nextStop.name}” \ | ||||
| (${nextStop.id}) on line ${lineRef}’s route from “${from}” to “${to}”`);
 | ||||
|                     } | ||||
|                     nextNode = nextNodeCandidates[0]; | ||||
|                     nextNodeIndex = wayNodes.indexOf(nextNode); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     segments[segmentId].lines.add(lineRef); | ||||
|                     nextNodeIndex = wayNodes.length; | ||||
|                 } | ||||
| 
 | ||||
|                 ++currentStopIndex; | ||||
|                 if (curNodeIndex < nextNodeIndex) | ||||
|                 { | ||||
|                     // Use the way in its normal direction
 | ||||
|                     path = path.concat( | ||||
|                         wayNodes.slice(curNodeIndex, nextNodeIndex) | ||||
|                     ); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // Use the way in the reverse direction
 | ||||
|                     if (isOneWay(wayTags)) | ||||
|                     { | ||||
|                         throw new Error(`Way n°${wayIndex} in
 | ||||
| ${routeDescription} is one-way and cannot be used in reverse.`);
 | ||||
|                     } | ||||
| 
 | ||||
|                     path = path.concat( | ||||
|                         wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1) | ||||
|                                 .reverse() | ||||
|                     ); | ||||
|                 } | ||||
| 
 | ||||
|                 currentNode = nextNode; | ||||
|             } | ||||
| 
 | ||||
|             // Split the path into segments between stops
 | ||||
|             const segments = []; | ||||
| 
 | ||||
|             for (let stopIndex = 0; stopIndex + 1 < stops.length; ++stopIndex) | ||||
|             { | ||||
|                 segments.push(path.slice( | ||||
|                     path.indexOf(stops[stopIndex]), | ||||
|                     path.indexOf(stops[stopIndex + 1] + 1), | ||||
|                 ).map(id => ({ | ||||
|                     lat: elements[id].lat, | ||||
|                     lon: elements[id].lon, | ||||
|                 }))); | ||||
|             } | ||||
| 
 | ||||
|             routes.push({ | ||||
|                 from, to, | ||||
|                 name: route.tags.name, | ||||
|                 segments, | ||||
|                 stops: stops.map(id => elements[id].tags.ref), | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         // Construct line objects
 | ||||
|         const routes = rawRoutes.map(route => ({ | ||||
|             from: route.tags.from, | ||||
|             to: route.tags.to, | ||||
| 
 | ||||
|             // Retrieve each stop’s information (stop order in the relation is
 | ||||
|             // assumed to reflect reality)
 | ||||
|             stops: route.members | ||||
|                 .filter(({role}) => role === 'stop') | ||||
|                 .map(({ref}) => elements[ref]) | ||||
|                 .filter(stop => 'ref' in stop.tags) | ||||
|                 .map(stop => stop.tags.ref), | ||||
|         })); | ||||
| 
 | ||||
|         lines[lineRef] = { | ||||
|             color, | ||||
|             routes, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return {stops, segments, lines}; | ||||
|     return {stops, lines}; | ||||
| }; | ||||
| 
 | ||||
| exports.fetch = fetch; | ||||
|  |  | |||
							
								
								
									
										291546
									
								
								back/data/network.json
								
								
								
								
							
							
						
						
									
										291546
									
								
								back/data/network.json
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -23,7 +23,7 @@ const fs = require('fs'); | |||
| 
 | ||||
|                 return value; | ||||
|             }, | ||||
|             '    ' | ||||
|             4 | ||||
|         ) | ||||
|     ); | ||||
| })(); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue