| 
									
										
										
										
											2020-07-16 23:41:05 +00:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |  * @fileoverview | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  |  * | 
					
						
							| 
									
										
										
										
											2020-07-16 23:41:05 +00:00
										 |  |  |  |  * Extract static information about the TaM network from OpenStreetMap (OSM): | 
					
						
							|  |  |  |  |  * tram and bus lines, stops and routes. | 
					
						
							|  |  |  |  |  * | 
					
						
							| 
									
										
										
										
											2021-05-11 13:35:03 +00:00
										 |  |  |  |  * Functions in this file also report inconsistencies in OSM data. | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  |  * | 
					
						
							|  |  |  |  |  * Because of the static nature of this data, it is cached in a | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  * version-controlled file `network.json` next to this file. To update it, | 
					
						
							|  |  |  |  |  * run the `script/update-network.js` script. | 
					
						
							| 
									
										
										
										
											2020-07-16 23:41:05 +00:00
										 |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-11 19:39:24 +00:00
										 |  |  |  | import * as turfHelpers from "@turf/helpers"; | 
					
						
							|  |  |  |  | import turfLength from "@turf/length"; | 
					
						
							|  |  |  |  | import * as util from "../util.js"; | 
					
						
							|  |  |  |  | import * as osm from "./sources/osm.js"; | 
					
						
							| 
									
										
										
										
											2020-01-14 23:19:26 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  * Route of a line of a transport network. | 
					
						
							|  |  |  |  |  * @typedef {Object} Route | 
					
						
							|  |  |  |  |  * @property {string} from Name of the starting point of the route. | 
					
						
							|  |  |  |  |  * @property {string} to Name of the ending point of the route. | 
					
						
							|  |  |  |  |  * @property {string?} via Optional name of a major intermediate | 
					
						
							|  |  |  |  |  * stop of the route. | 
					
						
							|  |  |  |  |  * @property {string} name Human-readable name of the line. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Line of a transport network. | 
					
						
							|  |  |  |  |  * @typedef {Object} Line | 
					
						
							|  |  |  |  |  * @property {string} color Hexadecimal color code of this line. | 
					
						
							|  |  |  |  |  * @property {Array.<Route>} routes Routes of this line. | 
					
						
							| 
									
										
										
										
											2020-01-14 23:19:26 +00:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Stop in a transport network (as a GeoJSON feature point). | 
					
						
							|  |  |  |  |  * @typedef {Object} Stop | 
					
						
							|  |  |  |  |  * @property {string} type Always equal to "Feature". | 
					
						
							|  |  |  |  |  * @property {string} id Stop identifier (unique in each network.). | 
					
						
							|  |  |  |  |  * @property {Object} properties | 
					
						
							|  |  |  |  |  * @property {string} properties.name Human-readable name of the stop. | 
					
						
							|  |  |  |  |  * @property {string} properties.node Associated node ID in OpenStreetMap. | 
					
						
							|  |  |  |  |  * @property {Array.<Array.[string,number]>} properties.routes | 
					
						
							|  |  |  |  |  * List of transport lines using this stop (as pairs of | 
					
						
							|  |  |  |  |  * line and route identifiers). | 
					
						
							|  |  |  |  |  * @property {Object} geometry | 
					
						
							|  |  |  |  |  * @property {string} geometry.type Always equal to "Point" | 
					
						
							|  |  |  |  |  * @property {Array.<number>} geometry.coordinates | 
					
						
							|  |  |  |  |  * Longitude and latitude of the stop point. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Planned segment routing between two stops of a transport network | 
					
						
							|  |  |  |  |  * (as a GeoJSON feature line string). | 
					
						
							|  |  |  |  |  * @typedef {Object} Segment | 
					
						
							|  |  |  |  |  * @property {string} type Always equal to "Feature". | 
					
						
							|  |  |  |  |  * @property {string} id Segment identifier (format: `{begin}-{end}`). | 
					
						
							|  |  |  |  |  * @property {Object} properties | 
					
						
							|  |  |  |  |  * @property {string} properties.begin ID of the stop at the beginning. | 
					
						
							|  |  |  |  |  * @property {string} properties.end ID of the stop at the end. | 
					
						
							|  |  |  |  |  * @property {number} properties.length Length of this segment (meters). | 
					
						
							|  |  |  |  |  * @property {Array.<Array.[string,number]>} properties.routes | 
					
						
							|  |  |  |  |  * List of transport lines using this segment (as pairs of | 
					
						
							|  |  |  |  |  * line identifiers and line direction numbers). | 
					
						
							|  |  |  |  |  * @property {Object} geometry | 
					
						
							|  |  |  |  |  * @property {string} geometry.type Always equal to "LineString". | 
					
						
							|  |  |  |  |  * @property {Array.<Array.<number>>} geometry.coordinates | 
					
						
							|  |  |  |  |  * Sequence of points forming this segment (as longitude/latitude pairs). | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |  * Edge of the graph for navigating between stops. | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  * @typedef {Object} NavigationEdge | 
					
						
							|  |  |  |  |  * @property {string} type Always equal to "Feature". | 
					
						
							|  |  |  |  |  * @property {Object} properties | 
					
						
							|  |  |  |  |  * @property {string} properties.begin ID of the stop or node at the beginning. | 
					
						
							|  |  |  |  |  * @property {string} properties.end ID of the stop or node at the end. | 
					
						
							|  |  |  |  |  * @property {number} properties.length Length of this edge (meters). | 
					
						
							|  |  |  |  |  * @property {Object} geometry | 
					
						
							|  |  |  |  |  * @property {string} geometry.type Always equal to "LineString". | 
					
						
							|  |  |  |  |  * @property {Array.<Array.<number>>} geometry.coordinates | 
					
						
							|  |  |  |  |  * Sequence of points forming this edge (as longitude/latitude pairs). | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Graph for navigating between stops. | 
					
						
							|  |  |  |  |  * @typedef {Object.<string,Object.<string,NavigationEdge>>} Navigation | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Information about a public transport network. | 
					
						
							|  |  |  |  |  * @typedef {Object} Network | 
					
						
							|  |  |  |  |  * @property {Object.<string,Stop>} stops List of stops. | 
					
						
							|  |  |  |  |  * @property {Object.<string,Line>} lines List of lines. | 
					
						
							|  |  |  |  |  * @property {Object.<string,Segment>} segments List of segments. | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |  * @property {Navigation} navigation | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  * Graph for out-of-route navigation between stops. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Retrieve raw routes, ways and stops from OpenStreetMap for the given | 
					
						
							|  |  |  |  |  * transport lines. | 
					
						
							|  |  |  |  |  * @param {Array.<string>} lineRefs List of lines to fetch. | 
					
						
							|  |  |  |  |  * @return {Array.<Object>} List of objects returned by OSM. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | // Retrieve routes, ways and stops from OpenStreetMap
 | 
					
						
							|  |  |  |  | const queryLines = async lineRefs => { | 
					
						
							|  |  |  |  |     return (await osm.runQuery(`[out:json];
 | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  | // Find the public transport lines bearing the requested references
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  | relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"]; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  | ( | 
					
						
							|  |  |  |  |     ._; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Recursively fetch routes, ways and stops inside the lines
 | 
					
						
							|  |  |  |  |     >>; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Find adjacent tracks (used for out-of-route navigation)
 | 
					
						
							|  |  |  |  |     complete { | 
					
						
							|  |  |  |  |         way(around:0)[railway="tram"]; | 
					
						
							|  |  |  |  |     }; | 
					
						
							|  |  |  |  |     >; | 
					
						
							|  |  |  |  | ); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | out body qt; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | `)).elements;
 | 
					
						
							|  |  |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Assemble information about lines, stops and segments from the raw | 
					
						
							|  |  |  |  |  * OpenStreetMap data. | 
					
						
							|  |  |  |  |  * @param {Array.<Object>} elementsList List of nodes retrieved from OSM. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID. | 
					
						
							|  |  |  |  |  * @return {Object} Assembled information about lines, stops and segments. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const processRoutes = (elementsList, elementsById) => { | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |     const lines = {}; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     const stops = {}; | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |     const segments = {}; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     const routeMasters = elementsList.filter(osm.isTransportLine); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  |     // Extract lines, associated stops and planned routes
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |     for (const routeMaster of routeMasters) { | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |         const lineRef = routeMaster.tags.ref; | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |         const color = routeMaster.tags.colour || "#000000"; | 
					
						
							| 
									
										
										
										
											2020-07-22 21:10:43 +00:00
										 |  |  |  |         const routes = []; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |         for (const [routeRef, data] of routeMaster.members.entries()) { | 
					
						
							|  |  |  |  |             const routeId = data.ref; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |             const route = elementsById[routeId]; | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |             const { from, via, to, name } = route.tags; | 
					
						
							|  |  |  |  |             const state = route.tags.state || "normal"; | 
					
						
							| 
									
										
										
										
											2020-07-22 21:10:43 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |             // Add missing stops to the global stops object
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |             for (const { ref, role } of route.members) { | 
					
						
							|  |  |  |  |                 if (role === "stop") { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                     const stop = elementsById[ref]; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     if (!("ref" in stop.tags)) { | 
					
						
							| 
									
										
										
										
											2020-07-22 21:10:43 +00:00
										 |  |  |  |                         throw new Error(`Stop ${stop.id}
 | 
					
						
							|  |  |  |  | (${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing | 
					
						
							|  |  |  |  | a “ref” tag`);
 | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     if (!(stop.tags.ref in stops)) { | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |                         stops[stop.tags.ref] = turfHelpers.point([ | 
					
						
							|  |  |  |  |                             stop.lon, | 
					
						
							|  |  |  |  |                             stop.lat | 
					
						
							|  |  |  |  |                         ], { | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |                             name: stop.tags.name, | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                             node: ref.toString(), | 
					
						
							| 
									
										
										
										
											2020-07-27 19:01:21 +00:00
										 |  |  |  |                             routes: [[lineRef, routeRef]], | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                         }, { | 
					
						
							|  |  |  |  |                             id: stop.tags.ref, | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |                         }); | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     } else { | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |                         stops[stop.tags.ref].properties.routes.push([ | 
					
						
							|  |  |  |  |                             lineRef, | 
					
						
							|  |  |  |  |                             routeRef | 
					
						
							|  |  |  |  |                         ]); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             // 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( | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 ({ role }) => role === "" | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             ); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             if (!route.members.slice(0, relationPivot).every( | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 ({ role }) => role === "stop" || role === "platform" | 
					
						
							|  |  |  |  |             )) { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                 throw new Error(`Members with invalid roles in between stops
 | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  | of ${name}`);
 | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             if (!route.members.slice(relationPivot).every( | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 ({ role }) => role === "" | 
					
						
							|  |  |  |  |             )) { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                 throw new Error(`Members with invalid roles inside the path
 | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  | of ${name}`);
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             // 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
 | 
					
						
							| 
									
										
										
										
											2020-07-22 21:10:43 +00:00
										 |  |  |  |             const lineStops = route.members.slice(0, relationPivot) | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 .filter(({ role }) => role === "stop") | 
					
						
							|  |  |  |  |                 .map(({ ref }) => ref); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             // 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) | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 .map(({ ref }) => ref); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             // Merge all used ways in a single path
 | 
					
						
							|  |  |  |  |             let path = []; | 
					
						
							| 
									
										
										
										
											2020-07-22 21:10:43 +00:00
										 |  |  |  |             let currentNode = lineStops[0]; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |             for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 const way = elementsById[ways[wayIndex]]; | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 const { nodes: wayNodes } = way; | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                 const wayNodesSet = new Set(wayNodes); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                 const curNodeIndex = wayNodes.indexOf(currentNode); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                 // If not the last way, find a connection point to the next way
 | 
					
						
							|  |  |  |  |                 // (there should be exactly one)
 | 
					
						
							|  |  |  |  |                 let nextNode = null; | 
					
						
							|  |  |  |  |                 let nextNodeIndex = null; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 if (wayIndex + 1 < ways.length) { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                     const nextNodeCandidates = elementsById[ways[wayIndex + 1]] | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                         .nodes.filter(node => wayNodesSet.has(node)); | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     if (nextNodeCandidates.length !== 1) { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                         throw new Error(`There should be exactly one point
 | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  | connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name}, | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  | but there are ${nextNodeCandidates.length}`);
 | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                     nextNode = nextNodeCandidates[0]; | 
					
						
							|  |  |  |  |                     nextNodeIndex = wayNodes.indexOf(nextNode); | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 } else { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                     nextNodeIndex = wayNodes.length; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |                 } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 if (curNodeIndex < nextNodeIndex) { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                     // Use the way in its normal direction
 | 
					
						
							|  |  |  |  |                     path = path.concat( | 
					
						
							|  |  |  |  |                         wayNodes.slice(curNodeIndex, nextNodeIndex) | 
					
						
							|  |  |  |  |                     ); | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 } else { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                     // Use the way in the reverse direction
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     if (osm.isOneWay(way)) { | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                         throw new Error(`Way n°${wayIndex} in
 | 
					
						
							| 
									
										
										
										
											2020-07-17 10:13:25 +00:00
										 |  |  |  | ${name} is one-way and cannot be used in reverse.`);
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |                     path = path.concat( | 
					
						
							|  |  |  |  |                         wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1) | 
					
						
							| 
									
										
										
										
											2020-07-16 22:16:54 +00:00
										 |  |  |  |                             .reverse() | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |                     ); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |                 currentNode = nextNode; | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             // Split the path into segments between stops
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |             for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 const begin = elementsById[lineStops[stopIdx]].tags.ref; | 
					
						
							| 
									
										
										
										
											2020-07-24 23:16:36 +00:00
										 |  |  |  |                 const beginIdx = path.indexOf(lineStops[stopIdx]); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 const end = elementsById[lineStops[stopIdx + 1]].tags.ref; | 
					
						
							| 
									
										
										
										
											2020-07-24 23:16:36 +00:00
										 |  |  |  |                 const endIdx = path.indexOf( | 
					
						
							|  |  |  |  |                     lineStops[stopIdx + 1], | 
					
						
							|  |  |  |  |                     beginIdx | 
					
						
							|  |  |  |  |                 ) + 1; | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                 const id = `${begin}-${end}`; | 
					
						
							| 
									
										
										
										
											2020-07-24 23:16:36 +00:00
										 |  |  |  |                 const nodesIds = path.slice(beginIdx, endIdx); | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 if (id in segments) { | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |                     if (!util.arraysEqual( | 
					
						
							|  |  |  |  |                         nodesIds, | 
					
						
							|  |  |  |  |                         segments[id].properties.nodesIds | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                     )) { | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |                         throw new Error(`Segment ${id} is defined as a
 | 
					
						
							|  |  |  |  | different sequence of nodes in two or more lines.`);
 | 
					
						
							|  |  |  |  |                     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |                     segments[id].properties.routes.push([lineRef, routeRef]); | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 } else { | 
					
						
							|  |  |  |  |                     segments[id] = turfHelpers.lineString(nodesIds.map( | 
					
						
							|  |  |  |  |                         nodeId => [ | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                             elementsById[nodeId].lon, | 
					
						
							|  |  |  |  |                             elementsById[nodeId].lat | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                         ] | 
					
						
							|  |  |  |  |                     ), { | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |                         // Keep track of the original sequence of nodes to
 | 
					
						
							|  |  |  |  |                         // compare with duplicates
 | 
					
						
							| 
									
										
										
										
											2020-07-24 23:02:07 +00:00
										 |  |  |  |                         nodesIds, | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                         routes: [[lineRef, routeRef]], | 
					
						
							|  |  |  |  |                         begin: begin, | 
					
						
							|  |  |  |  |                         end: end, | 
					
						
							|  |  |  |  |                     }, { id }); | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                     segments[id].properties.length = ( | 
					
						
							|  |  |  |  |                         1000 * turfLength(segments[id])); | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             routes.push({ | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |                 from, | 
					
						
							|  |  |  |  |                 via, | 
					
						
							|  |  |  |  |                 to, | 
					
						
							|  |  |  |  |                 name, | 
					
						
							|  |  |  |  |                 state | 
					
						
							| 
									
										
										
										
											2020-07-16 20:56:39 +00:00
										 |  |  |  |             }); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |         lines[lineRef] = { | 
					
						
							|  |  |  |  |             color, | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |             routes | 
					
						
							| 
									
										
										
										
											2020-01-14 13:08:08 +00:00
										 |  |  |  |         }; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |     // Remove OSM nodes from segments that were only used for checking validity
 | 
					
						
							| 
									
										
										
										
											2020-07-25 16:05:43 +00:00
										 |  |  |  |     for (const segment of Object.values(segments)) { | 
					
						
							| 
									
										
										
										
											2020-07-25 14:28:51 +00:00
										 |  |  |  |         delete segment.properties.nodesIds; | 
					
						
							| 
									
										
										
										
											2020-07-17 21:48:32 +00:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     return { lines, stops, segments }; | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |  * Create a graph for navigating between stops. | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  * @param {Array.<Object>} elementsList List of nodes retrieved from OSM. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID. | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |  * @return {Object} Resulting graph and reverse arcs. | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | const createNavigationGraph = (elementsList, elementsById) => { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     const navigation = {}; | 
					
						
							|  |  |  |  |     const navigationReverse = {}; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Create graph nodes from OSM nodes
 | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  |     for (const obj of elementsList) { | 
					
						
							|  |  |  |  |         if (obj.type === "node") { | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |             navigation[obj.id] = {}; | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             navigationReverse[obj.id] = new Set(); | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     // Link up graph edges with OSM ways
 | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  |     for (const obj of elementsList) { | 
					
						
							|  |  |  |  |         if (obj.type === "way") { | 
					
						
							|  |  |  |  |             const oneWay = osm.isOneWay(obj); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |             for (let i = 0; i + 1 < obj.nodes.length; ++i) { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 const from = obj.nodes[i].toString(); | 
					
						
							|  |  |  |  |                 let to = obj.nodes[i + 1].toString(); | 
					
						
							|  |  |  |  |                 let path = [from, to]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                 // Make sure we can’t jump between rails at railway crossings
 | 
					
						
							|  |  |  |  |                 if (i + 2 < obj.nodes.length | 
					
						
							|  |  |  |  |                         && osm.isRailwayCrossing(elementsById[to])) { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                     const next = obj.nodes[i + 2].toString(); | 
					
						
							|  |  |  |  |                     path = [from, to, next]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                     to = next; | 
					
						
							|  |  |  |  |                     i += 1; | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |                 navigation[from][to] = path; | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 navigationReverse[to].add(from); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                 if (!oneWay) { | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                     const reversePath = [...path]; | 
					
						
							|  |  |  |  |                     reversePath.reverse(); | 
					
						
							|  |  |  |  |                     navigation[to][from] = reversePath; | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                     navigationReverse[from].add(to); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 } | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |     return { navigation, navigationReverse }; | 
					
						
							|  |  |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | /** | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |  * Identify intermediate nodes of the navigation graph that can be simplified. | 
					
						
							|  |  |  |  |  * @param {Set.<string>} stopsSet OSM IDs of stop nodes. | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |  * @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. | 
					
						
							|  |  |  |  |  * @return {Set.<string>} Set of compressible nodes. | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  | const findCompressibleNodes = (stopsSet, navigation, navigationReverse) => { | 
					
						
							|  |  |  |  |     const compressible = new Set(); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |     for (const nodeId in navigation) { | 
					
						
							|  |  |  |  |         if (stopsSet.has(nodeId)) { | 
					
						
							|  |  |  |  |             // Keep stop nodes
 | 
					
						
							|  |  |  |  |             continue; | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         const entries = navigationReverse[nodeId]; | 
					
						
							|  |  |  |  |         const exits = new Set(Object.keys(navigation[nodeId])); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         // Keep split nodes, i.e. nodes with at least two exit nodes
 | 
					
						
							|  |  |  |  |         // and one entry node that are all distinct from each other
 | 
					
						
							|  |  |  |  |         if (entries.size >= 1) { | 
					
						
							|  |  |  |  |             if (exits.size >= 3) { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 continue; | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             let isSplit = false; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (exits.size === 2) { | 
					
						
							|  |  |  |  |                 for (const entry of entries) { | 
					
						
							|  |  |  |  |                     if (!exits.has(entry)) { | 
					
						
							|  |  |  |  |                         isSplit = true; | 
					
						
							|  |  |  |  |                         break; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (isSplit) { | 
					
						
							|  |  |  |  |                 continue; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         // Keep junction nodes, i.e. nodes with at least two entry nodes
 | 
					
						
							|  |  |  |  |         // and one exit node that are all distinct from each other
 | 
					
						
							|  |  |  |  |         if (exits.size >= 1) { | 
					
						
							|  |  |  |  |             if (entries.size >= 3) { | 
					
						
							|  |  |  |  |                 continue; | 
					
						
							|  |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             let isJunction = false; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (entries.size === 2) { | 
					
						
							|  |  |  |  |                 for (const exit of exits) { | 
					
						
							|  |  |  |  |                     if (!entries.has(exit)) { | 
					
						
							|  |  |  |  |                         isJunction = true; | 
					
						
							|  |  |  |  |                         break; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                     } | 
					
						
							|  |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (isJunction) { | 
					
						
							|  |  |  |  |                 continue; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         // Compress all other nodes
 | 
					
						
							|  |  |  |  |         compressible.add(nodeId); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return compressible; | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Remove nodes that are not used to link up two kept nodes. | 
					
						
							|  |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. | 
					
						
							|  |  |  |  |  * @param {Set.<string>} compressible Set of nodes that will not be kept. | 
					
						
							|  |  |  |  |  * @return {boolean} True if some dead-ends were removed. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const removeDeadEnds = (navigation, navigationReverse, compressible) => { | 
					
						
							|  |  |  |  |     let didRemove = false; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |     // Find dead-ends starting from kept nodes
 | 
					
						
							|  |  |  |  |     for (const beginId in navigation) { | 
					
						
							|  |  |  |  |         if (compressible.has(beginId)) { | 
					
						
							|  |  |  |  |             continue; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         const begin = navigation[beginId]; | 
					
						
							|  |  |  |  |         const stack = []; | 
					
						
							|  |  |  |  |         const parent = {[beginId]: beginId}; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         for (const succId in begin) { | 
					
						
							|  |  |  |  |             if (compressible.has(succId)) { | 
					
						
							|  |  |  |  |                 stack.push(succId); | 
					
						
							|  |  |  |  |                 parent[succId] = beginId; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         while (stack.length > 0) { | 
					
						
							|  |  |  |  |             const endId = stack.pop(); | 
					
						
							|  |  |  |  |             const end = navigation[endId]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (compressible.has(endId)) { | 
					
						
							|  |  |  |  |                 let hasSuccessor = false; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 for (const succId in end) { | 
					
						
							|  |  |  |  |                     if (succId !== parent[endId]) { | 
					
						
							|  |  |  |  |                         parent[succId] = endId; | 
					
						
							|  |  |  |  |                         stack.push(succId); | 
					
						
							|  |  |  |  |                         hasSuccessor = true; | 
					
						
							|  |  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                 } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 if (!hasSuccessor) { | 
					
						
							|  |  |  |  |                     // Remove the dead-end path
 | 
					
						
							|  |  |  |  |                     let trackback = endId; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                     while (trackback !== beginId) { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                         navigationReverse[trackback].delete(parent[trackback]); | 
					
						
							|  |  |  |  |                         delete navigation[parent[trackback]][trackback]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                         trackback = parent[trackback]; | 
					
						
							|  |  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                     didRemove = true; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 } | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |     // Find dead-ends starting from compressible source nodes
 | 
					
						
							|  |  |  |  |     for (const beginId in navigation) { | 
					
						
							|  |  |  |  |         if (!compressible.has(beginId)) { | 
					
						
							|  |  |  |  |             continue; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         if (navigationReverse[beginId].size > 0) { | 
					
						
							|  |  |  |  |             continue; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         const begin = navigation[beginId]; | 
					
						
							|  |  |  |  |         const stack = []; | 
					
						
							|  |  |  |  |         const parent = {[beginId]: beginId}; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         for (const succId in begin) { | 
					
						
							|  |  |  |  |             stack.push(succId); | 
					
						
							|  |  |  |  |             parent[succId] = beginId; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         while (stack.length > 0) { | 
					
						
							|  |  |  |  |             const endId = stack.pop(); | 
					
						
							|  |  |  |  |             const end = navigation[endId]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (compressible.has(endId)) { | 
					
						
							|  |  |  |  |                 for (const succId in end) { | 
					
						
							|  |  |  |  |                     if (succId !== parent[endId]) { | 
					
						
							|  |  |  |  |                         parent[succId] = endId; | 
					
						
							|  |  |  |  |                         stack.push(succId); | 
					
						
							|  |  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             } else { | 
					
						
							|  |  |  |  |                 // Remove the dead-end path
 | 
					
						
							|  |  |  |  |                 let trackback = endId; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 while (trackback !== beginId) { | 
					
						
							|  |  |  |  |                     navigationReverse[trackback].delete(parent[trackback]); | 
					
						
							|  |  |  |  |                     delete navigation[parent[trackback]][trackback]; | 
					
						
							|  |  |  |  |                     trackback = parent[trackback]; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                 } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 didRemove = true; | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |     return didRemove; | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Compress the given set of nodes. | 
					
						
							|  |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. | 
					
						
							|  |  |  |  |  * @param {Set.<string>} compressible Set of nodes to compress. | 
					
						
							|  |  |  |  |  * @return {boolean} True if some nodes were compressed. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const removeCompressibleNodes = (navigation, navigationReverse, compressible) => { | 
					
						
							|  |  |  |  |     let didCompress = false; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |     for (const beginId in navigation) { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         if (compressible.has(beginId)) { | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |             continue; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |         // Start a DFS from each kept node
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |         const begin = navigation[beginId]; | 
					
						
							|  |  |  |  |         const stack = []; | 
					
						
							|  |  |  |  |         const parent = {[beginId]: beginId}; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         for (const succId in begin) { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (compressible.has(succId)) { | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                 stack.push(succId); | 
					
						
							|  |  |  |  |                 parent[succId] = beginId; | 
					
						
							|  |  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         while (stack.length > 0) { | 
					
						
							|  |  |  |  |             const endId = stack.pop(); | 
					
						
							|  |  |  |  |             const end = navigation[endId]; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |             if (!compressible.has(endId)) { | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                 // Found another kept node
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 // Collect and remove intermediate path
 | 
					
						
							|  |  |  |  |                 let path = []; | 
					
						
							|  |  |  |  |                 let trackback = endId; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 do { | 
					
						
							|  |  |  |  |                     const segment = [...navigation[parent[trackback]][trackback]]; | 
					
						
							|  |  |  |  |                     segment.reverse(); | 
					
						
							|  |  |  |  |                     path = path.concat(segment.slice(0, -1)); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                     navigationReverse[trackback].delete(parent[trackback]); | 
					
						
							|  |  |  |  |                     delete navigation[parent[trackback]][trackback]; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                     trackback = parent[trackback]; | 
					
						
							|  |  |  |  |                 } while (trackback !== beginId); | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 // Make sure not to add loops if we’re compressing a cycle
 | 
					
						
							|  |  |  |  |                 if (endId !== beginId) { | 
					
						
							|  |  |  |  |                     path.push(beginId); | 
					
						
							|  |  |  |  |                     path.reverse(); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                     begin[endId] = path; | 
					
						
							|  |  |  |  |                     navigationReverse[endId].add(beginId); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 didCompress = true; | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |             } else { | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  |                 // Continue the traversal down compressible nodes
 | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 let isFirst = true; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |                 for (const succId in end) { | 
					
						
							|  |  |  |  |                     if (succId !== parent[endId]) { | 
					
						
							|  |  |  |  |                         if (isFirst) { | 
					
						
							|  |  |  |  |                             parent[succId] = endId; | 
					
						
							|  |  |  |  |                             stack.push(succId); | 
					
						
							|  |  |  |  |                             isFirst = false; | 
					
						
							|  |  |  |  |                         } else { | 
					
						
							|  |  |  |  |                             throw new Error(`Multiple successors in \
 | 
					
						
							|  |  |  |  | non-junction node ${endId}`);
 | 
					
						
							|  |  |  |  |                         } | 
					
						
							|  |  |  |  |                     } | 
					
						
							|  |  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  |             } | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-23 12:59:08 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     return didCompress; | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Find nodes in the graph that have no exits nor entries and remove them. | 
					
						
							|  |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const cleanUpIsolatedNodes = (navigation, navigationReverse) => { | 
					
						
							|  |  |  |  |     for (const nodeId in navigation) { | 
					
						
							|  |  |  |  |         if ( | 
					
						
							|  |  |  |  |             Object.keys(navigation[nodeId]).length === 0 | 
					
						
							|  |  |  |  |             && navigationReverse[nodeId].size === 0 | 
					
						
							|  |  |  |  |         ) { | 
					
						
							|  |  |  |  |             delete navigation[nodeId]; | 
					
						
							|  |  |  |  |             delete navigationReverse[nodeId]; | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Remove and relink nodes that connect only two nodes or less. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Stop>} stops List of stops. | 
					
						
							|  |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const compressNavigationGraph = (stops, navigation, navigationReverse) => { | 
					
						
							|  |  |  |  |     const stopsSet = new Set( | 
					
						
							|  |  |  |  |         Object.values(stops).map(stop => stop.properties.node) | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |     let compressible = null; | 
					
						
							|  |  |  |  |     let didCompress = true; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     while (didCompress) { | 
					
						
							|  |  |  |  |         let didRemove = true; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         while (didRemove) { | 
					
						
							|  |  |  |  |             compressible = findCompressibleNodes( | 
					
						
							|  |  |  |  |                 stopsSet, navigation, navigationReverse | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |             didRemove = removeDeadEnds( | 
					
						
							|  |  |  |  |                 navigation, navigationReverse, compressible | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         didCompress = removeCompressibleNodes( | 
					
						
							|  |  |  |  |             navigation, navigationReverse, compressible | 
					
						
							|  |  |  |  |         ); | 
					
						
							|  |  |  |  |         cleanUpIsolatedNodes(navigation, navigationReverse); | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-05-16 10:01:51 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Transform navigation graph edges into GeoJSON segments. | 
					
						
							|  |  |  |  |  * @param {Navigation} navigation Input navigation graph. | 
					
						
							|  |  |  |  |  * @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const makeNavigationSegments = (navigation, elementsById) => { | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |     for (const [beginId, begin] of Object.entries(navigation)) { | 
					
						
							|  |  |  |  |         for (const endId in begin) { | 
					
						
							|  |  |  |  |             begin[endId] = turfHelpers.lineString(begin[endId].map( | 
					
						
							|  |  |  |  |                 nodeId => [ | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |                     elementsById[nodeId].lon, | 
					
						
							|  |  |  |  |                     elementsById[nodeId].lat | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  |                 ] | 
					
						
							|  |  |  |  |             ), { | 
					
						
							|  |  |  |  |                 begin: beginId, | 
					
						
							|  |  |  |  |                 end: endId, | 
					
						
							|  |  |  |  |             }); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             begin[endId].properties.length = 1000 * turfLength(begin[endId]); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * Fetch information about the network. | 
					
						
							|  |  |  |  |  * @param {Array.<string>} lineRefs List of lines to fetch. | 
					
						
							|  |  |  |  |  * @returns {Network} Network metadata extracted from OSM. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | export const fetch = async lineRefs => { | 
					
						
							|  |  |  |  |     const elementsList = await queryLines(lineRefs); | 
					
						
							|  |  |  |  |     const elementsById = elementsList.reduce((prev, elt) => { | 
					
						
							|  |  |  |  |         prev[elt.id] = elt; | 
					
						
							|  |  |  |  |         return prev; | 
					
						
							|  |  |  |  |     }, {}); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     const { lines, stops, segments } = processRoutes(elementsList, elementsById); | 
					
						
							| 
									
										
										
										
											2021-05-22 22:45:09 +00:00
										 |  |  |  |     const { navigation, navigationReverse } = createNavigationGraph( | 
					
						
							|  |  |  |  |         elementsList, elementsById | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     compressNavigationGraph(stops, navigation, navigationReverse); | 
					
						
							|  |  |  |  |     makeNavigationSegments(navigation, elementsById); | 
					
						
							| 
									
										
										
										
											2021-05-21 21:59:51 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     return { navigation, lines, stops, segments }; | 
					
						
							| 
									
										
										
										
											2020-01-14 23:19:26 +00:00
										 |  |  |  | }; |