docs: private List<TimeStep<State, Observation, Path>> createTimeSteps(
              List<Observation> filteredGPXEntries, List<Collection<QueryResult>> queriesPerEntry,
              QueryGraph queryGraph) {
          final int n = filteredGPXEntries.size();
          if (queriesPerEntry.size() != n) {
              throw new IllegalArgumentException(
                      "filteredGPXEntries and queriesPerEntry must have same size.");
          }
  
          final List<TimeStep<State, Observation, Path>> timeSteps = new ArrayList<>();
          for (int i = 0; i < n; i++) {
  
              Observation gpxEntry = filteredGPXEntries.get(i);
              final Collection<QueryResult> queryResults = queriesPerEntry.get(i);
  
              List<State> candidates = new ArrayList<>();
              for (QueryResult qr : queryResults) {
                  int closestNode = qr.getClosestNode();
                  if (queryGraph.isVirtualNode(closestNode)) {
                      // get virtual edges:
                      List<VirtualEdgeIteratorState> virtualEdges = new ArrayList<>();
                      EdgeIterator iter = queryGraph.createEdgeExplorer().setBaseNode(closestNode);
                      while (iter.next()) {
                          if (!queryGraph.isVirtualEdge(iter.getEdge())) {
                              throw new RuntimeException("Virtual nodes must only have virtual edges "
                                      + "to adjacent nodes.");
                          }
                          virtualEdges.add((VirtualEdgeIteratorState)
                                  queryGraph.getEdgeIteratorState(iter.getEdge(), iter.getAdjNode()));
                      }
                      if (virtualEdges.size() != 2) {
                          throw new RuntimeException("Each virtual node must have exactly 2 "
                                  + "virtual edges (reverse virtual edges are not returned by the "
                                  + "EdgeIterator");
                      }
  
                      // Create a directed candidate for each of the two possible directions through
                      // the virtual node. This is needed to penalize U-turns at virtual nodes
                      // (see also #51). We need to add candidates for both directions because
                      // we don't know yet which is the correct one. This will be figured
                      // out by the Viterbi algorithm.
                      //
                      // Adding further candidates to explicitly allow U-turns through setting
                      // incomingVirtualEdge==outgoingVirtualEdge doesn't make sense because this
                      // would actually allow to perform a U-turn without a penalty by going to and
                      // from the virtual node through the other virtual edge or its reverse edge.
                      VirtualEdgeIteratorState e1 = virtualEdges.get(0);
                      VirtualEdgeIteratorState e2 = virtualEdges.get(1);
                      for (int j = 0; j < 2; j++) {
                          // get favored/unfavored edges:
                          VirtualEdgeIteratorState incomingVirtualEdge = j == 0 ? e1 : e2;
                          VirtualEdgeIteratorState outgoingVirtualEdge = j == 0 ? e2 : e1;
                          // create candidate
                          QueryResult vqr = new QueryResult(qr.getQueryPoint().lat, qr.getQueryPoint().lon);
                          vqr.setQueryDistance(qr.getQueryDistance());
                          vqr.setClosestNode(qr.getClosestNode());
                          vqr.setWayIndex(qr.getWayIndex());
                          vqr.setSnappedPosition(qr.getSnappedPosition());
                          vqr.setClosestEdge(qr.getClosestEdge());
                          vqr.calcSnappedPoint(distanceCalc);
                          State candidate = new State(gpxEntry, vqr, incomingVirtualEdge,
                                  outgoingVirtualEdge);
                          candidates.add(candidate);
                      }
                  } else {
                      // Create an undirected candidate for the real node.
                      State candidate = new State(gpxEntry, qr);
                      candidates.add(candidate);
                  }
              }
  
              final TimeStep<State, Observation, Path> timeStep = new TimeStep<>(gpxEntry, candidates);
              timeSteps.add(timeStep);
          }
          return timeSteps;
      }