def main(argv): # Initialize from command-line parameters: if len(argv) < 7: syntax(0) dbServer = argv[1] networkName = argv[2] userName = argv[3] password = argv[4] gdbFilename = argv[5] gdbPathMatch = argv[6] sourceID = 0 endTime = 86400 refTime = None problemReport = False if len(argv) > 6: i = 7 while i < len(argv): if argv[i] == "-s" and i < len(argv) - 1: sourceID = int(argv[i + 1]) i += 1 elif argv[i] == "-t" and i < len(argv) - 1: refTime = datetime.strptime(argv[i + 1], '%H:%M:%S') i += 1 elif argv[i] == "-e" and i < len(argv) - 1: endTime = int(argv[i + 1]) i += 1 elif argv[i] == "-p": problemReport = True i += 1 if refTime is None and not problemReport: print("ERROR: No reference time is specified.") syntax(1) # Get the database connected: print("INFO: Connect to database...", file = sys.stderr) database = vista_network.connect(dbServer, userName, password, networkName) # Read in the topology from the VISTA database: print("INFO: Read topology from database...", file = sys.stderr) vistaGraph = vista_network.fillGraph(database) # Read in the GPS track information: print("INFO: Read GDB GPS track '%s'..." % gdbFilename, file = sys.stderr) gpsTracks = gdb_extracted.fillFromFile(gdbFilename, vistaGraph.gps) # Restore the path match: print("INFO: Read the GDB path-match file '%s'..." % gdbPathMatch, file = sys.stderr) with open(gdbPathMatch, 'r') as inFile: nodes = path_engine.readStandardDump(vistaGraph, gpsTracks, inFile, lambda x: str(x)) # Assumption: Each shapeID corresponds with one trip that will be reported in the output. # And, each route corresponds with one trip. # Filter out nodes that have one or zero links: for shapeID in nodes.keys(): ctr = 0 for node in nodes[shapeID]: ctr += len(node.routeInfo) if ctr <= 1: print("INFO: Filtering out shapeID %s." % str(shapeID), file = sys.stderr) del nodes[shapeID] del gpsTracks[shapeID] # Deal with Problem Report: if problemReport: print("INFO: Output problem report CSV...", file = sys.stderr) problem_report.problemReport(nodes, vistaGraph) print("INFO: Done.", file = sys.stderr) return # TODO: The logic below is a hack to create unique routes given GDB IDs. There are several # long-term problems with this, including the idea that it is impossible to reuse common # routes (each instance is its own route) and there are assumptions about vehicle ID # numbering in the generated vehicles. # Fabricate routes: routes = {} ctr = 1 # We'll be making arbitrary route IDs: for shapeID in gpsTracks: routes[ctr] = gtfs.RoutesEntry(ctr, shapeID, "") ctr += 1 # Let vehicle IDs be in a different number range: vehCtr = int(ctr / 10000) vehCtr += 10000 # Fabricate trips and stop times: trips = {} stopTimes = {} for routeID in routes: trips[vehCtr] = gtfs.TripsEntry(vehCtr, routes[routeID], "", gpsTracks[routes[routeID].shortName]) stopTimes[trips[vehCtr]] = list() # Fake the system by having no stops defined. vehCtr += 1 tripIDs = trips.keys() tripIDs.sort() # Output the routes file: print("INFO: Dumping public.bus_route.csv...", file = sys.stderr) with open("public.bus_route.csv", 'w') as outFile: transit_gtfs.dumpBusRoutes(trips, userName, networkName, outFile) # Output the routes_link file: print("INFO: Dumping public.bus_route_link.csv...", file = sys.stderr) with open("public.bus_route_link.csv", 'w') as outFile: transit_gtfs.dumpBusRouteLinks(trips, stopTimes, nodes, vistaGraph, 1, False, userName, networkName, refTime, endTime, False, False, False, False, outFile) print("INFO: Dumping public.bus_frequency.csv...", file = sys.stderr) with open("public.bus_frequency.csv", 'w') as outFile: transit_gtfs._outHeader("public.bus_frequency", userName, networkName, outFile) print("\"route\",\"period\",\"frequency\",\"offsettime\",\"preemption\"", file = outFile) for tripID in tripIDs: departureTime = trips[tripID].shapeEntries[0].time timeDiff = departureTime - refTime print("%d,1,86400,%d,0" % (tripID, timeDiff.days * 24 * 3600 + timeDiff.seconds), file = outFile) print("INFO: Dumping public.bus_period.csv...", file = sys.stderr) with open("public.bus_period.csv", 'w') as outFile: transit_gtfs._outHeader("public.bus_period", userName, networkName, outFile) print("\"id\",\"starttime\",\"endtime\"", file = outFile) print("1,0,%d" % endTime, file = outFile) # Now we need to write out to the travel_time output: print("INFO: Dumping public.travel_time.csv...", file = sys.stderr) with open("public.travel_time.csv", 'w') as outFile: transit_gtfs._outHeader("public.travel_time", userName, networkName, outFile) print("\"departure_time\",\"vehicle_id\",\"route_id\",\"exittime\",\"linkid\",\"arrivaltime\",\"sourceid\"", file = outFile) for tripID in tripIDs: nodeList = nodes[trips[tripID].route.shortName] "@type nodeList: list<path_engine.PathEnd>" departureTime = trips[tripID].shapeEntries[0].time lastTime = trips[tripID].shapeEntries[-1].time # Add the first link to the file: timeDiff = departureTime - refTime timeDiffLast = lastTime - refTime outStr = "%d,%d,%d,%d,%d,%d,%d" % (timeDiff.days * 24 * 3600 + timeDiff.seconds, trips[tripID].route.routeID, tripID, timeDiffLast.days * 24 * 3600 + timeDiffLast.seconds, nodeList[0].pointOnLink.link.id, timeDiff.days * 24 * 3600 + timeDiff.seconds, sourceID) print(outStr, file = outFile) for node in nodeList: "@type node: path_engine.PathEnd" if len(node.routeInfo) > 0: # TODO: Deal with midnight if the time is before refTime. arrivalTime = node.shapeEntry.time for link in node.routeInfo: arrivalTimeSec = 3600 * arrivalTime.hour + 60 * arrivalTime.minute + arrivalTime.second # TODO: We need to make vehicleID, routeID and tripID be consistent. timeDiffArr = arrivalTime - refTime outStr = "%d,%d,%d,%d,%d,%d,%d" % (timeDiff.days * 24 * 3600 + timeDiff.seconds, trips[tripID].route.routeID, tripID, timeDiffLast.days * 24 * 3600 + timeDiffLast.seconds, link.id, timeDiffArr.days * 24 * 3600 + timeDiffArr.seconds, sourceID) print(outStr, file = outFile) print("INFO: Done.", file = sys.stderr)
def dumpBusRouteLinks(gtfsTrips, gtfsStopTimes, gtfsNodes, vistaNetwork, stopSearchRadius, excludeUpstream, userName, networkName, startTime, endTime, widenBegin, widenEnd, excludeBegin, excludeEnd, outFile=sys.stdout): """ dumpBusRouteLinks dumps out a public.bus_route_link.csv file contents. This also will remove all stop times and trips that fall outside of the valid evaluation interval as dictated by the exclusion parameters. @type gtfsTrips: dict<int, gtfs.TripsEntry> @type gtfsStopTimes: dict<TripsEntry, list<StopTimesEntry>> @type gtfsNodes: dict<int, list<path_engine.PathEnd>> @type vistaNetwork: graph.GraphLib @type stopSearchRadius: float @type excludeUpstream: boolean @type userName: str @type networkName: str @type startTime: datetime @type endTime: datetime @type widenBegin: bool @type widenEnd: bool @type excludeBegin: bool @type excludeEnd: bool @type outFile: file @return A mapping of stopID to points-on-links plus the start and end times adjusted for warm-up and cool-down (if widenBegin or widenEnd is True) @rtype (dict<int, graph.PointOnLink>, datetime, datetime) """ _outHeader("public.bus_route_link", userName, networkName, outFile) print('"route","sequence","link","stop","dwelltime",', file=outFile) # Set up the output: ret = {} "@type ret: dict<int, graph.PointOnLink>" warmupStartTime = startTime cooldownEndTime = endTime # Initialize the path engine for use later: pathEngine = path_engine.PathEngine(stopSearchRadius, stopSearchRadius, stopSearchRadius, sys.float_info.max, sys.float_info.max, stopSearchRadius, 1, 1, 1, sys.maxsize, sys.maxsize) pathEngine.limitClosestPoints = 8 pathEngine.limitSimultaneousPaths = 6 pathEngine.maxHops = 12 pathEngine.logFile = None # Suppress the log outputs for the path engine; enough stuff will come from other sources. problemReportNodes = {} "@type problemReportNodes: dict<?, path_engine.PathEnd>" tripIDs = compat.listkeys(gtfsTrips) tripIDs.sort() for tripID in tripIDs: if gtfsTrips[tripID].shapeEntries[0].shapeID not in gtfsNodes: # This happens if the incoming files contain a subset of all available topology. print( "WARNING: Skipping route for trip %d because no points are available." % tripID, file=sys.stderr) continue treeNodes = gtfsNodes[gtfsTrips[tripID].shapeEntries[0].shapeID] "@type treeNodes: list<path_engine.PathEnd>" # Step 1: Find the longest distance of contiguous valid links within the shape for each trip: startIndex = -1 curIndex = 0 linkCount = 0 totalLinks = 0 longestStart = -1 longestEnd = len(treeNodes) longestDist = sys.float_info.min longestLinkCount = 0 while curIndex <= len(treeNodes): if (curIndex == len(treeNodes)) or ( curIndex == 0) or treeNodes[curIndex].restart: totalLinks += 1 linkCount += 1 if (curIndex > startIndex) and (startIndex >= 0): # We have a contiguous interval. See if it wins: if treeNodes[curIndex - 1].totalDist - treeNodes[ startIndex].totalDist > longestDist: longestStart = startIndex longestEnd = curIndex longestDist = treeNodes[ curIndex - 1].totalDist - treeNodes[startIndex].totalDist longestLinkCount = linkCount linkCount = 0 # This happens if it is time to start a new interval: startIndex = curIndex else: totalLinks += len(treeNodes[curIndex].routeInfo) linkCount += len(treeNodes[curIndex].routeInfo) curIndex += 1 if longestStart >= 0: # We have a valid path. See if it had been trimmed down and report it. if (longestStart > 0) or (longestEnd < len(treeNodes)): print("WARNING: For shape ID %s from seq. %d through %d, %.2g%% of %d links will be used." \ % (str(treeNodes[longestStart].shapeEntry.shapeID), treeNodes[longestStart].shapeEntry.shapeSeq, treeNodes[longestEnd - 1].shapeEntry.shapeSeq, 100 * float(longestLinkCount) / float(totalLinks), totalLinks), file = sys.stderr) # Step 2: Ignore routes that are entirely outside our valid time interval. flag = False if len(gtfsStopTimes[gtfsTrips[tripID]]) == 0: # This will happen if we don't have stops defined. In this case, we want to go ahead and process the bus_route_link # outputs because we don't know if the trip falls in or out of the valid time range. flag = True else: for stopEntry in gtfsStopTimes[gtfsTrips[tripID]]: if stopEntry.arrivalTime >= startTime and stopEntry.arrivalTime <= endTime: flag = True break if not flag: # This will be done silently because (depending upon the valid interval) there could be # hundreds of these in a GTFS set. continue # Step 3: Match up stops to that contiguous list: # At this point, we're doing something with this. print("INFO: -- Matching stops for trip %d --" % tripID, file=sys.stderr) stopTimes = gtfsStopTimes[gtfsTrips[tripID]] "@type stopTimes: list<gtfs.StopTimesEntry>" # Isolate the relevant VISTA tree nodes: (Assume from above that this is a non-zero length array) ourGTFSNodes = treeNodes[longestStart:longestEnd] # We are going to recreate a small VISTA network from ourGTFSNodes and then match up the stops to that. # First, prepare the small VISTA network: vistaSubset = graph.GraphLib(vistaNetwork.gps.latCtr, vistaNetwork.gps.lngCtr) vistaNodePrior = None "@type vistaNodePrior: graph.GraphNode" # Build a list of links: outLinkIDList = [] "@type outLinkList: list<int>" # Plop in the start node: vistaNodePrior = graph.GraphNode( ourGTFSNodes[0].pointOnLink.link.origNode.id, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLat, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLng) vistaSubset.addNode(vistaNodePrior) outLinkIDList.append(ourGTFSNodes[0].pointOnLink.link.id) # Link together nodes as we traverse through them: for ourGTFSNode in ourGTFSNodes: "@type ourGTFSNode: path_engine.PathEnd" # There should only be one destination link per VISTA node because this comes form our tree. # If there is no link or we're repeating the first one, then there were no new links assigned. if (len(ourGTFSNode.routeInfo) < 1) or ((len(outLinkIDList) == 1) \ and (ourGTFSNode.routeInfo[0].id == ourGTFSNodes[0].pointOnLink.link.id)): continue for link in ourGTFSNode.routeInfo: "@type link: graph.GraphLink" if link.id not in vistaNetwork.linkMap: print( "WARNING: In finding bus route links, link ID %d is not found in the VISTA network." % link.id, file=sys.stderr) continue origVistaLink = vistaNetwork.linkMap[link.id] "@type origVistaLink: graph.GraphLink" if origVistaLink.origNode.id not in vistaSubset.nodeMap: # Create a new node: vistaNode = graph.GraphNode( origVistaLink.origNode.id, origVistaLink.origNode.gpsLat, origVistaLink.origNode.gpsLng) vistaSubset.addNode(vistaNode) else: # The path evidently crosses over itself. Reuse an existing node. vistaNode = vistaSubset.nodeMap[ origVistaLink.origNode.id] # We shall label our links as indices into the stage we're at in ourGTFSNodes links. This will allow for access later. if outLinkIDList[-1] not in vistaSubset.linkMap: vistaSubset.addLink( graph.GraphLink(outLinkIDList[-1], vistaNodePrior, vistaNode)) vistaNodePrior = vistaNode outLinkIDList.append(link.id) # And then finish off the graph with the last link: if ourGTFSNode.pointOnLink.link.destNode.id not in vistaSubset.nodeMap: vistaNode = graph.GraphNode( ourGTFSNode.pointOnLink.link.destNode.id, ourGTFSNode.pointOnLink.link.destNode.gpsLat, ourGTFSNode.pointOnLink.link.destNode.gpsLng) vistaSubset.addNode(vistaNode) if outLinkIDList[-1] not in vistaSubset.linkMap: vistaSubset.addLink( graph.GraphLink(outLinkIDList[-1], vistaNodePrior, vistaNode)) # Then, prepare the stops as GTFS shapes entries: print("INFO: Mapping stops to VISTA network...", file=sys.stderr) gtfsShapes = [] gtfsStopsLookup = {} "@type gtfsStopsLookup: dict<int, gtfs.StopTimesEntry>" # Append an initial dummy shape to force routing through the path start: gtfsShapes.append( gtfs.ShapesEntry( -1, -1, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLat, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLng)) # Append all of the stops: for gtfsStopTime in stopTimes: "@type gtfsStopTime: gtfs.StopTimesEntry" gtfsShapes.append( gtfs.ShapesEntry(-1, gtfsStopTime.stopSeq, gtfsStopTime.stop.gpsLat, gtfsStopTime.stop.gpsLng)) gtfsStopsLookup[gtfsStopTime.stopSeq] = gtfsStopTime # Append a trailing dummy shape to force routing through the path end: gtfsShapes.append( gtfs.ShapesEntry( -1, -1, ourGTFSNodes[-1].pointOnLink.link.destNode.gpsLat, ourGTFSNodes[-1].pointOnLink.link.destNode.gpsLng)) # Find a path through our prepared node map subset: resultTree = pathEngine.constructPath(gtfsShapes, vistaSubset) "@type resultTree: list<path_engine.PathEnd>" # Strip off the dummy ends: del resultTree[-1] del resultTree[0] if len(resultTree) > 0: resultTree[0].prevTreeNode = None # So now we should have one tree entry per matched stop. # Deal with Problem Report: # TODO: The Problem Report will include all nodes on each path regardless of valid time interval; # However; we will not have gotten here if the trip was entirely outside of it. if problemReport: revisedNodeList = {} prevNode = None "@type revisedNodeList = list<path_engine.PathEnd>" for stopNode in resultTree: # Reconstruct a tree node in terms of the original network. newShape = gtfs.ShapesEntry( gtfsTrips[tripID].shapeEntries[0].shapeID, stopNode.shapeEntry.shapeSeq, stopNode.shapeEntry.lat, stopNode.shapeEntry.lng, False) origLink = vistaNetwork.linkMap[ stopNode.pointOnLink.link.id] newPointOnLink = graph.PointOnLink( origLink, stopNode.pointOnLink.dist, stopNode.pointOnLink.nonPerpPenalty, stopNode.pointOnLink.refDist) newNode = path_engine.PathEnd(newShape, newPointOnLink) newNode.restart = False newNode.totalCost = stopNode.totalCost newNode.totalDist = stopNode.totalDist newNode.routeInfo = [] for link in stopNode.routeInfo: newNode.routeInfo.append(vistaNetwork.linkMap[link.id]) newNode.prevTreeNode = prevNode prevNode = newNode revisedNodeList[stopNode.shapeEntry.shapeSeq] = newNode problemReportNodes[gtfsTrips[tripID].shapeEntries[0]. shapeID] = revisedNodeList # Walk through our output link list and see where the resultTree entries occur: resultIndex = 0 stopMatches = [] "@type stopMatches: list<StopMatch>" rejectFlag = False for linkID in outLinkIDList: curResultIndex = resultIndex # This routine will advance resultIndex only if a stop is found for linkID, and will exit out when # no more stops are found for linkID. stopMatch = StopMatch(linkID) "@type stopMatch: StopMatch" stopMatches.append(stopMatch) while curResultIndex < len(resultTree): if resultTree[ curResultIndex].pointOnLink.link.id == linkID: # Only pay attention to this stop if it is within the valid time range: gtfsStopTime = gtfsStopsLookup[ resultTree[resultIndex].shapeEntry.shapeSeq] if excludeBegin and gtfsStopTime.arrivalTime < startTime or excludeEnd and gtfsStopTime.arrivalTime > endTime: # Throw away this entire route because it is excluded and part of it falls outside: print( "INFO: Excluded because of activity outside of the valid time range.", file=sys.stderr) del stopMatches[:] rejectFlag = True break elif (widenBegin or gtfsStopTime.arrivalTime >= startTime) and ( widenEnd or gtfsStopTime.arrivalTime <= endTime): if (stopMatch.bestTreeEntry is None) \ or (resultTree[resultIndex].pointOnLink.refDist < stopMatch.bestTreeEntry.pointOnLink.refDist): # Log the best match: stopMatch.bestTreeEntry = resultTree[ resultIndex] stopMatch.matchCtr += 1 resultIndex = curResultIndex + 1 curResultIndex += 1 if (stopMatch and stopMatch.matchCtr == 0) \ or ((curResultIndex < len(resultTree)) and (resultTree[resultIndex].pointOnLink.link.id == linkID)): continue # We have gotten to the end of matched link(s). break if rejectFlag: break # Then, output the results out if we are supposed to. foundStopSet = set() "@type foundStopSet: set<int>" if not rejectFlag: outSeqCtr = longestStart minTime = warmupStartTime maxTime = cooldownEndTime foundValidStop = False for stopMatch in stopMatches: if stopMatch.matchCtr > 1: # Report duplicates: print("WARNING: %d stops have been matched for TripID %d, LinkID %d. Keeping Stop %d, Stop Seq %d" \ % (stopMatch.matchCtr, tripID, stopMatch.linkID, gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID, stopMatch.bestTreeEntry.shapeEntry.shapeSeq), file = sys.stderr) # TODO: This is a problem because VISTA only allows one stop per link. So, the stop that is closest to # the link is the one that is the winner and the rest are ignored. We don't yet do anything intelligent with dwell # times, etc. if stopMatch.matchCtr > 0: # Report the best match: foundStopSet.add( stopMatch.bestTreeEntry.shapeEntry.shapeSeq ) # Check off this stop sequence. foundValidStop = True print( '"%d","%d","%d","%d","%d",' % (tripID, outSeqCtr, stopMatch.linkID, gtfsStopsLookup[stopMatch.bestTreeEntry. shapeEntry.shapeSeq].stop.stopID, DWELLTIME_DEFAULT), file=outFile) if gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID in ret \ and ret[gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID].link.id \ != stopMatch.bestTreeEntry.pointOnLink.link.id: print("WARNING: stopID %d is attempted to be assigned to linkID %d, but it had already been assigned to linkID %d." \ % (gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID, stopMatch.bestTreeEntry.pointOnLink.link.id, ret[gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID].link.id), file = sys.stderr) # TODO: This is a tricky problem. This means that among multiple bus routes, the same stop had been # found to best fit two different links. I don't exactly know the best way to resolve this, other # than (for NMC analyses) to create a "fake" stop that's tied with the new link. else: ret[gtfsStopsLookup[stopMatch.bestTreeEntry. shapeEntry.shapeSeq].stop. stopID] = stopMatch.bestTreeEntry.pointOnLink # Check on the minimum/maximum time range: gtfsStopTime = gtfsStopsLookup[ stopMatch.bestTreeEntry.shapeEntry.shapeSeq] minTime = min(gtfsStopTime.arrivalTime, minTime) maxTime = max(gtfsStopTime.arrivalTime, maxTime) else: # The linkID has nothing to do with any points in consideration. Report it without a stop: if foundValidStop or not excludeUpstream: print('"%d","%d","%d",,,' % (tripID, outSeqCtr, stopMatch.linkID), file=outFile) outSeqCtr += 1 # TODO: For start time estimation (as reported in the public.bus_frequency.csv output), it may be # ideal to keep track of linear distance traveled before the first valid stop. # Widen out the valid interval if needed: warmupStartTime = min(minTime, warmupStartTime) cooldownEndTime = max(maxTime, cooldownEndTime) # Are there any stops left over? If so, report them to say that they aren't in the output file. startGap = -1 endGap = -1 for gtfsStopTime in stopTimes: "@type gtfsStopTime: gtfs.StopTimesEntry" flag = False if gtfsStopTime.stopSeq not in foundStopSet: # This stop is unaccounted for: if startGap < 0: startGap = gtfsStopTime.stopSeq endGap = gtfsStopTime.stopSeq # Old message is very annoying, especially if the underlying topology is a subset of shapefile # geographic area and there's a ton of them. That's why there is the new range message as shown below. # print("WARNING: Trip tripID %d, stopID %d stop seq. %d will not be in the bus_route_link file." % (tripID, # gtfsStopTime.stop.stopID, gtfsStopTime.stopSeq), file = sys.stderr) if problemReport: revisedNodeList = problemReportNodes[ gtfsTrips[tripID].shapeEntries[0].shapeID] if gtfsStopTime.stopSeq not in revisedNodeList: # Make a dummy "error" node for reporting. newShape = gtfs.ShapesEntry( gtfsTrips[tripID].shapeEntries[0].shapeID, gtfsStopTime.stopSeq, gtfsStopTime.stop.gpsLat, gtfsStopTime.stop.gpsLng, False) newPointOnLink = graph.PointOnLink(None, 0) newPointOnLink.pointX = gtfsStopTime.stop.pointX newPointOnLink.pointY = gtfsStopTime.stop.pointY newNode = path_engine.PathEnd( newShape, newPointOnLink) newNode.restart = True revisedNodeList[gtfsStopTime.stopSeq] = newNode else: flag = True if (flag or gtfsStopTime.stopSeq == stopTimes[-1].stopSeq) and startGap >= 0: subStr = "Seqs. %d-%d" % ( startGap, endGap) if startGap != endGap else "Seq. %d" % startGap print( "WARNING: Trip ID %d, Stop %s will not be in the bus_route_link file." % (tripID, subStr), file=sys.stderr) startGap = -1 else: print("WARNING: No links for tripID %d." % tripID, file=sys.stderr) # Deal with Problem Report: if problemReport: print("INFO: Output problem report CSV...", file=sys.stderr) problemReportNodesOut = {} for shapeID in problemReportNodes: seqs = compat.listkeys(problemReportNodes[shapeID]) seqs.sort() ourTgtList = [] for seq in seqs: ourTgtList.append(problemReportNodes[shapeID][seq]) problemReportNodesOut[shapeID] = ourTgtList problem_report.problemReport(problemReportNodesOut, vistaNetwork) return (ret, warmupStartTime, cooldownEndTime)
def dumpBusRouteLinks(gtfsTrips, gtfsStopTimes, gtfsNodes, vistaNetwork, stopSearchRadius, excludeUpstream, userName, networkName, startTime, endTime, widenBegin, widenEnd, excludeBegin, excludeEnd, outFile = sys.stdout): """ dumpBusRouteLinks dumps out a public.bus_route_link.csv file contents. This also will remove all stop times and trips that fall outside of the valid evaluation interval as dictated by the exclusion parameters. @type gtfsTrips: dict<int, gtfs.TripsEntry> @type gtfsStopTimes: dict<TripsEntry, list<StopTimesEntry>> @type gtfsNodes: dict<int, list<path_engine.PathEnd>> @type vistaNetwork: graph.GraphLib @type stopSearchRadius: float @type excludeUpstream: boolean @type userName: str @type networkName: str @type startTime: datetime @type endTime: datetime @type widenBegin: bool @type widenEnd: bool @type excludeBegin: bool @type excludeEnd: bool @type outFile: file @return A mapping of stopID to points-on-links plus the start and end times adjusted for warm-up and cool-down (if widenBegin or widenEnd is True) @rtype (dict<int, graph.PointOnLink>, datetime, datetime) """ _outHeader("public.bus_route_link", userName, networkName, outFile) print('"route","sequence","link","stop","dwelltime",', file = outFile) # Set up the output: ret = {} "@type ret: dict<int, graph.PointOnLink>" warmupStartTime = startTime cooldownEndTime = endTime # Initialize the path engine for use later: pathEngine = path_engine.PathEngine(stopSearchRadius, stopSearchRadius, stopSearchRadius, sys.float_info.max, sys.float_info.max, stopSearchRadius, 1, 1, 1, sys.maxint, sys.maxint) pathEngine.limitClosestPoints = 8 pathEngine.limitSimultaneousPaths = 6 pathEngine.maxHops = 12 pathEngine.logFile = None # Suppress the log outputs for the path engine; enough stuff will come from other sources. problemReportNodes = {} "@type problemReportNodes: dict<?, path_engine.PathEnd>" tripIDs = gtfsTrips.keys() tripIDs.sort() for tripID in tripIDs: if gtfsTrips[tripID].shapeEntries[0].shapeID not in gtfsNodes: # This happens if the incoming files contain a subset of all available topology. print("WARNING: Skipping route for trip %d because no points are available." % tripID, file = sys.stderr) continue treeNodes = gtfsNodes[gtfsTrips[tripID].shapeEntries[0].shapeID] "@type treeNodes: list<path_engine.PathEnd>" # Step 1: Find the longest distance of contiguous valid links within the shape for each trip: startIndex = -1 curIndex = 0 linkCount = 0 totalLinks = 0 longestStart = -1 longestEnd = len(treeNodes) longestDist = sys.float_info.min longestLinkCount = 0 while curIndex <= len(treeNodes): if (curIndex == len(treeNodes)) or (curIndex == 0) or treeNodes[curIndex].restart: totalLinks += 1 linkCount += 1 if (curIndex > startIndex) and (startIndex >= 0): # We have a contiguous interval. See if it wins: if treeNodes[curIndex - 1].totalDist - treeNodes[startIndex].totalDist > longestDist: longestStart = startIndex longestEnd = curIndex longestDist = treeNodes[curIndex - 1].totalDist - treeNodes[startIndex].totalDist longestLinkCount = linkCount linkCount = 0 # This happens if it is time to start a new interval: startIndex = curIndex else: totalLinks += len(treeNodes[curIndex].routeInfo) linkCount += len(treeNodes[curIndex].routeInfo) curIndex += 1 if longestStart >= 0: # We have a valid path. See if it had been trimmed down and report it. if (longestStart > 0) or (longestEnd < len(treeNodes)): print("WARNING: For shape ID %s from seq. %d through %d, %.2g%% of %d links will be used." \ % (str(treeNodes[longestStart].shapeEntry.shapeID), treeNodes[longestStart].shapeEntry.shapeSeq, treeNodes[longestEnd - 1].shapeEntry.shapeSeq, 100 * float(longestLinkCount) / float(totalLinks), totalLinks), file = sys.stderr) # Step 2: Ignore routes that are entirely outside our valid time interval. flag = False if len(gtfsStopTimes[gtfsTrips[tripID]]) == 0: # This will happen if we don't have stops defined. In this case, we want to go ahead and process the bus_route_link # outputs because we don't know if the trip falls in or out of the valid time range. flag = True else: for stopEntry in gtfsStopTimes[gtfsTrips[tripID]]: if stopEntry.arrivalTime >= startTime and stopEntry.arrivalTime <= endTime: flag = True break if not flag: # This will be done silently because (depending upon the valid interval) there could be # hundreds of these in a GTFS set. continue # Step 3: Match up stops to that contiguous list: # At this point, we're doing something with this. print("INFO: -- Matching stops for trip %d --" % tripID, file = sys.stderr) stopTimes = gtfsStopTimes[gtfsTrips[tripID]] "@type stopTimes: list<gtfs.StopTimesEntry>" # Isolate the relevant VISTA tree nodes: (Assume from above that this is a non-zero length array) ourGTFSNodes = treeNodes[longestStart:longestEnd] # We are going to recreate a small VISTA network from ourGTFSNodes and then match up the stops to that. # First, prepare the small VISTA network: vistaSubset = graph.GraphLib(vistaNetwork.gps.latCtr, vistaNetwork.gps.lngCtr) vistaNodePrior = None "@type vistaNodePrior: graph.GraphNode" # Build a list of links: outLinkIDList = [] "@type outLinkList: list<int>" # Plop in the start node: vistaNodePrior = graph.GraphNode(ourGTFSNodes[0].pointOnLink.link.origNode.id, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLat, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLng) vistaSubset.addNode(vistaNodePrior) outLinkIDList.append(ourGTFSNodes[0].pointOnLink.link.id) # Link together nodes as we traverse through them: for ourGTFSNode in ourGTFSNodes: "@type ourGTFSNode: path_engine.PathEnd" # There should only be one destination link per VISTA node because this comes form our tree. # If there is no link or we're repeating the first one, then there were no new links assigned. if (len(ourGTFSNode.routeInfo) < 1) or ((len(outLinkIDList) == 1) \ and (ourGTFSNode.routeInfo[0].id == ourGTFSNodes[0].pointOnLink.link.id)): continue for link in ourGTFSNode.routeInfo: "@type link: graph.GraphLink" if link.id not in vistaNetwork.linkMap: print("WARNING: In finding bus route links, link ID %d is not found in the VISTA network." % link.id, file = sys.stderr) continue origVistaLink = vistaNetwork.linkMap[link.id] "@type origVistaLink: graph.GraphLink" if origVistaLink.origNode.id not in vistaSubset.nodeMap: # Create a new node: vistaNode = graph.GraphNode(origVistaLink.origNode.id, origVistaLink.origNode.gpsLat, origVistaLink.origNode.gpsLng) vistaSubset.addNode(vistaNode) else: # The path evidently crosses over itself. Reuse an existing node. vistaNode = vistaSubset.nodeMap[origVistaLink.origNode.id] # We shall label our links as indices into the stage we're at in ourGTFSNodes links. This will allow for access later. if outLinkIDList[-1] not in vistaSubset.linkMap: vistaSubset.addLink(graph.GraphLink(outLinkIDList[-1], vistaNodePrior, vistaNode)) vistaNodePrior = vistaNode outLinkIDList.append(link.id) # And then finish off the graph with the last link: if ourGTFSNode.pointOnLink.link.destNode.id not in vistaSubset.nodeMap: vistaNode = graph.GraphNode(ourGTFSNode.pointOnLink.link.destNode.id, ourGTFSNode.pointOnLink.link.destNode.gpsLat, ourGTFSNode.pointOnLink.link.destNode.gpsLng) vistaSubset.addNode(vistaNode) if outLinkIDList[-1] not in vistaSubset.linkMap: vistaSubset.addLink(graph.GraphLink(outLinkIDList[-1], vistaNodePrior, vistaNode)) # Then, prepare the stops as GTFS shapes entries: print("INFO: Mapping stops to VISTA network...", file = sys.stderr) gtfsShapes = [] gtfsStopsLookup = {} "@type gtfsStopsLookup: dict<int, gtfs.StopTimesEntry>" # Append an initial dummy shape to force routing through the path start: gtfsShapes.append(gtfs.ShapesEntry(-1, -1, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLat, ourGTFSNodes[0].pointOnLink.link.origNode.gpsLng)) # Append all of the stops: for gtfsStopTime in stopTimes: "@type gtfsStopTime: gtfs.StopTimesEntry" gtfsShapes.append(gtfs.ShapesEntry(-1, gtfsStopTime.stopSeq, gtfsStopTime.stop.gpsLat, gtfsStopTime.stop.gpsLng)) gtfsStopsLookup[gtfsStopTime.stopSeq] = gtfsStopTime # Append a trailing dummy shape to force routing through the path end: gtfsShapes.append(gtfs.ShapesEntry(-1, -1, ourGTFSNodes[-1].pointOnLink.link.destNode.gpsLat, ourGTFSNodes[-1].pointOnLink.link.destNode.gpsLng)) # Find a path through our prepared node map subset: resultTree = pathEngine.constructPath(gtfsShapes, vistaSubset) "@type resultTree: list<path_engine.PathEnd>" # Strip off the dummy ends: del resultTree[-1] del resultTree[0] if len(resultTree) > 0: resultTree[0].prevTreeNode = None # So now we should have one tree entry per matched stop. # Deal with Problem Report: # TODO: The Problem Report will include all nodes on each path regardless of valid time interval; # However; we will not have gotten here if the trip was entirely outside of it. if problemReport: revisedNodeList = {} prevNode = None "@type revisedNodeList = list<path_engine.PathEnd>" for stopNode in resultTree: # Reconstruct a tree node in terms of the original network. newShape = gtfs.ShapesEntry(gtfsTrips[tripID].shapeEntries[0].shapeID, stopNode.shapeEntry.shapeSeq, stopNode.shapeEntry.lat, stopNode.shapeEntry.lng, False) origLink = vistaNetwork.linkMap[stopNode.pointOnLink.link.id] newPointOnLink = graph.PointOnLink(origLink, stopNode.pointOnLink.dist, stopNode.pointOnLink.nonPerpPenalty, stopNode.pointOnLink.refDist) newNode = path_engine.PathEnd(newShape, newPointOnLink) newNode.restart = False newNode.totalCost = stopNode.totalCost newNode.totalDist = stopNode.totalDist newNode.routeInfo = [] for link in stopNode.routeInfo: newNode.routeInfo.append(vistaNetwork.linkMap[link.id]) newNode.prevTreeNode = prevNode prevNode = newNode revisedNodeList[stopNode.shapeEntry.shapeSeq] = newNode problemReportNodes[gtfsTrips[tripID].shapeEntries[0].shapeID] = revisedNodeList # Walk through our output link list and see where the resultTree entries occur: resultIndex = 0 stopMatches = [] "@type stopMatches: list<StopMatch>" rejectFlag = False for linkID in outLinkIDList: curResultIndex = resultIndex # This routine will advance resultIndex only if a stop is found for linkID, and will exit out when # no more stops are found for linkID. stopMatch = StopMatch(linkID) "@type stopMatch: StopMatch" stopMatches.append(stopMatch) while curResultIndex < len(resultTree): if resultTree[curResultIndex].pointOnLink.link.id == linkID: # Only pay attention to this stop if it is within the valid time range: gtfsStopTime = gtfsStopsLookup[resultTree[resultIndex].shapeEntry.shapeSeq] if excludeBegin and gtfsStopTime.arrivalTime < startTime or excludeEnd and gtfsStopTime.arrivalTime > endTime: # Throw away this entire route because it is excluded and part of it falls outside: print("INFO: Excluded because of activity outside of the valid time range.", file = sys.stderr) del stopMatches[:] rejectFlag = True break elif (widenBegin or gtfsStopTime.arrivalTime >= startTime) and (widenEnd or gtfsStopTime.arrivalTime <= endTime): if (stopMatch.bestTreeEntry is None) \ or (resultTree[resultIndex].pointOnLink.refDist < stopMatch.bestTreeEntry.pointOnLink.refDist): # Log the best match: stopMatch.bestTreeEntry = resultTree[resultIndex] stopMatch.matchCtr += 1 resultIndex = curResultIndex + 1 curResultIndex += 1 if (stopMatch and stopMatch.matchCtr == 0) \ or ((curResultIndex < len(resultTree)) and (resultTree[resultIndex].pointOnLink.link.id == linkID)): continue # We have gotten to the end of matched link(s). break if rejectFlag: break # Then, output the results out if we are supposed to. foundStopSet = set() "@type foundStopSet: set<int>" if not rejectFlag: outSeqCtr = longestStart minTime = warmupStartTime maxTime = cooldownEndTime foundValidStop = False for stopMatch in stopMatches: if stopMatch.matchCtr > 1: # Report duplicates: print("WARNING: %d stops have been matched for TripID %d, LinkID %d. Keeping Stop %d, Stop Seq %d" \ % (stopMatch.matchCtr, tripID, stopMatch.linkID, gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID, stopMatch.bestTreeEntry.shapeEntry.shapeSeq), file = sys.stderr) # TODO: This is a problem because VISTA only allows one stop per link. So, the stop that is closest to # the link is the one that is the winner and the rest are ignored. We don't yet do anything intelligent with dwell # times, etc. if stopMatch.matchCtr > 0: # Report the best match: foundStopSet.add(stopMatch.bestTreeEntry.shapeEntry.shapeSeq) # Check off this stop sequence. foundValidStop = True print('"%d","%d","%d","%d","%d",' % (tripID, outSeqCtr, stopMatch.linkID, gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID, DWELLTIME_DEFAULT), file = outFile) if gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID in ret \ and ret[gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID].link.id \ != stopMatch.bestTreeEntry.pointOnLink.link.id: print("WARNING: stopID %d is attempted to be assigned to linkID %d, but it had already been assigned to linkID %d." \ % (gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID, stopMatch.bestTreeEntry.pointOnLink.link.id, ret[gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID].link.id), file = sys.stderr) # TODO: This is a tricky problem. This means that among multiple bus routes, the same stop had been # found to best fit two different links. I don't exactly know the best way to resolve this, other # than (for NMC analyses) to create a "fake" stop that's tied with the new link. else: ret[gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq].stop.stopID] = stopMatch.bestTreeEntry.pointOnLink # Check on the minimum/maximum time range: gtfsStopTime = gtfsStopsLookup[stopMatch.bestTreeEntry.shapeEntry.shapeSeq] minTime = min(gtfsStopTime.arrivalTime, minTime) maxTime = max(gtfsStopTime.arrivalTime, maxTime) else: # The linkID has nothing to do with any points in consideration. Report it without a stop: if foundValidStop or not excludeUpstream: print('"%d","%d","%d",,,' % (tripID, outSeqCtr, stopMatch.linkID), file = outFile) outSeqCtr += 1 # TODO: For start time estimation (as reported in the public.bus_frequency.csv output), it may be # ideal to keep track of linear distance traveled before the first valid stop. # Widen out the valid interval if needed: warmupStartTime = min(minTime, warmupStartTime) cooldownEndTime = max(maxTime, cooldownEndTime) # Are there any stops left over? If so, report them to say that they aren't in the output file. startGap = -1 endGap = -1 for gtfsStopTime in stopTimes: "@type gtfsStopTime: gtfs.StopTimesEntry" flag = False if gtfsStopTime.stopSeq not in foundStopSet: # This stop is unaccounted for: if startGap < 0: startGap = gtfsStopTime.stopSeq endGap = gtfsStopTime.stopSeq # Old message is very annoying, especially if the underlying topology is a subset of shapefile # geographic area and there's a ton of them. That's why there is the new range message as shown below. # print("WARNING: Trip tripID %d, stopID %d stop seq. %d will not be in the bus_route_link file." % (tripID, # gtfsStopTime.stop.stopID, gtfsStopTime.stopSeq), file = sys.stderr) if problemReport: revisedNodeList = problemReportNodes[gtfsTrips[tripID].shapeEntries[0].shapeID] if gtfsStopTime.stopSeq not in revisedNodeList: # Make a dummy "error" node for reporting. newShape = gtfs.ShapesEntry(gtfsTrips[tripID].shapeEntries[0].shapeID, gtfsStopTime.stopSeq, gtfsStopTime.stop.gpsLat,gtfsStopTime.stop.gpsLng, False) newPointOnLink = graph.PointOnLink(None, 0) newPointOnLink.pointX = gtfsStopTime.stop.pointX newPointOnLink.pointY = gtfsStopTime.stop.pointY newNode = path_engine.PathEnd(newShape, newPointOnLink) newNode.restart = True revisedNodeList[gtfsStopTime.stopSeq] = newNode else: flag = True if (flag or gtfsStopTime.stopSeq == stopTimes[-1].stopSeq) and startGap >= 0: subStr = "Seqs. %d-%d" % (startGap, endGap) if startGap != endGap else "Seq. %d" % startGap print("WARNING: Trip ID %d, Stop %s will not be in the bus_route_link file." % (tripID, subStr), file = sys.stderr) startGap = -1 else: print("WARNING: No links for tripID %d." % tripID, file = sys.stderr) # Deal with Problem Report: if problemReport: print("INFO: Output problem report CSV...", file = sys.stderr) problemReportNodesOut = {} for shapeID in problemReportNodes: seqs = problemReportNodes[shapeID].keys() seqs.sort() ourTgtList = [] for seq in seqs: ourTgtList.append(problemReportNodes[shapeID][seq]) problemReportNodesOut[shapeID] = ourTgtList problem_report.problemReport(problemReportNodesOut, vistaNetwork) return (ret, warmupStartTime, cooldownEndTime)
def main(argv): # Initialize from command-line parameters: if len(argv) < 7: syntax(0) dbServer = argv[1] networkName = argv[2] userName = argv[3] password = argv[4] csvFilename = argv[5] csvPathMatch = argv[6] sourceID = 0 endTime = 86400 refTime = None problemReport = False if len(argv) > 6: i = 7 while i < len(argv): if argv[i] == "-s" and i < len(argv) - 1: sourceID = int(argv[i + 1]) i += 1 elif argv[i] == "-t" and i < len(argv) - 1: refTime = datetime.strptime(argv[i + 1], '%H:%M:%S') i += 1 elif argv[i] == "-e" and i < len(argv) - 1: endTime = int(argv[i + 1]) i += 1 elif argv[i] == "-p": problemReport = True i += 1 if refTime is None and not problemReport: print("ERROR: No reference time is specified.") syntax(1) # Get the database connected: print("INFO: Connect to database...", file=sys.stderr) database = vista_network.connect(dbServer, userName, password, networkName) # Read in the topology from the VISTA database: print("INFO: Read topology from database...", file=sys.stderr) vistaGraph = vista_network.fillGraph(database) # Read in the GPS track information: print("INFO: Read ArcGIS CSV GPS track '%s'..." % csvFilename, file=sys.stderr) gpsTracks = arcgiscsv_extracted.fillFromFile(csvFilename, vistaGraph.gps) # Restore the path match: print("INFO: Read the ArcGIS CSV path-match file '%s'..." % csvPathMatch, file=sys.stderr) with open(csvPathMatch, 'r') as inFile: nodes = path_engine.readStandardDump(vistaGraph, gpsTracks, inFile, lambda x: str(x)) # Assumption: Each shapeID corresponds with one trip that will be reported in the output. # And, each route corresponds with one trip. # Filter out nodes that have one or zero links: for shapeID in compat.listkeys(nodes): ctr = 0 for node in nodes[shapeID]: ctr += len(node.routeInfo) if ctr <= 1: print("INFO: Filtering out shapeID %s." % str(shapeID), file=sys.stderr) del nodes[shapeID] del gpsTracks[shapeID] # Deal with Problem Report: if problemReport: print("INFO: Output problem report CSV...", file=sys.stderr) problem_report.problemReport(nodes, vistaGraph) print("INFO: Done.", file=sys.stderr) return # TODO: The logic below is a hack to create unique routes given GDB IDs. There are several # long-term problems with this, including the idea that it is impossible to reuse common # routes (each instance is its own route) and there are assumptions about vehicle ID # numbering in the generated vehicles. # Fabricate routes: routes = {} ctr = 1 # We'll be making arbitrary route IDs: for shapeID in gpsTracks: routes[ctr] = gtfs.RoutesEntry(ctr, shapeID, "") ctr += 1 # Let vehicle IDs be in a different number range: vehCtr = int(ctr / 10000) vehCtr += 10000 # Fabricate trips and stop times: trips = {} stopTimes = {} for routeID in routes: trips[vehCtr] = gtfs.TripsEntry(vehCtr, routes[routeID], "", gpsTracks[routes[routeID].shortName]) stopTimes[trips[vehCtr]] = list( ) # Fake the system by having no stops defined. vehCtr += 1 tripIDs = compat.listkeys(trips) tripIDs.sort() # Output the routes file: print("INFO: Dumping public.bus_route.csv...", file=sys.stderr) with open("public.bus_route.csv", 'w') as outFile: transit_gtfs.dumpBusRoutes(trips, userName, networkName, outFile) # Output the routes_link file: print("INFO: Dumping public.bus_route_link.csv...", file=sys.stderr) with open("public.bus_route_link.csv", 'w') as outFile: transit_gtfs.dumpBusRouteLinks(trips, stopTimes, nodes, vistaGraph, 1, False, userName, networkName, refTime, endTime, False, False, False, False, outFile) print("INFO: Dumping public.bus_frequency.csv...", file=sys.stderr) with open("public.bus_frequency.csv", 'w') as outFile: transit_gtfs._outHeader("public.bus_frequency", userName, networkName, outFile) print( "\"route\",\"period\",\"frequency\",\"offsettime\",\"preemption\"", file=outFile) for tripID in tripIDs: departureTime = trips[tripID].shapeEntries[0].time timeDiff = departureTime - refTime print("%d,1,86400,%d,0" % (tripID, timeDiff.days * 24 * 3600 + timeDiff.seconds), file=outFile) print("INFO: Dumping public.bus_period.csv...", file=sys.stderr) with open("public.bus_period.csv", 'w') as outFile: transit_gtfs._outHeader("public.bus_period", userName, networkName, outFile) print("\"id\",\"starttime\",\"endtime\"", file=outFile) print("1,0,%d" % endTime, file=outFile) # Now we need to write out to the travel_time output: print("INFO: Dumping public.travel_time.csv...", file=sys.stderr) with open("public.travel_time.csv", 'w') as outFile: transit_gtfs._outHeader("public.travel_time", userName, networkName, outFile) print( "\"departure_time\",\"vehicle_id\",\"route_id\",\"exittime\",\"linkid\",\"arrivaltime\",\"sourceid\"", file=outFile) for tripID in tripIDs: nodeList = nodes[trips[tripID].route.shortName] "@type nodeList: list<path_engine.PathEnd>" departureTime = trips[tripID].shapeEntries[0].time lastTime = trips[tripID].shapeEntries[-1].time # Add the first link to the file: timeDiff = departureTime - refTime timeDiffLast = lastTime - refTime outStr = "%d,%d,%d,%d,%d,%d,%d" % ( timeDiff.days * 24 * 3600 + timeDiff.seconds, trips[tripID].route.routeID, tripID, timeDiffLast.days * 24 * 3600 + timeDiffLast.seconds, nodeList[0].pointOnLink.link.id, timeDiff.days * 24 * 3600 + timeDiff.seconds, sourceID) print(outStr, file=outFile) for node in nodeList: "@type node: path_engine.PathEnd" if len(node.routeInfo) > 0: # TODO: Deal with midnight if the time is before refTime. arrivalTime = node.shapeEntry.time for link in node.routeInfo: arrivalTimeSec = 3600 * arrivalTime.hour + 60 * arrivalTime.minute + arrivalTime.second # TODO: We need to make vehicleID, routeID and tripID be consistent. timeDiffArr = arrivalTime - refTime outStr = "%d,%d,%d,%d,%d,%d,%d" % ( timeDiff.days * 24 * 3600 + timeDiff.seconds, trips[tripID].route.routeID, tripID, timeDiffLast.days * 24 * 3600 + timeDiffLast.seconds, link.id, timeDiffArr.days * 24 * 3600 + timeDiffArr.seconds, sourceID) print(outStr, file=outFile) print("INFO: Done.", file=sys.stderr)