Пример #1
0
 public synchronized void initIndexes() {
   if (vertexIndex != null) {
     return;
   }
   graphService.setLoadLevel(LoadLevel.DEBUG);
   Graph graph = graphService.getGraph();
   vertexIndex = new STRtree();
   edgeIndex = new STRtree();
   for (Vertex v : graph.getVertices()) {
     Envelope vertexEnvelope = new Envelope(v.getCoordinate());
     vertexIndex.insert(vertexEnvelope, v);
     for (Edge e : v.getOutgoing()) {
       Envelope envelope;
       Geometry geometry = e.getGeometry();
       if (geometry == null) {
         envelope = vertexEnvelope;
       } else {
         envelope = geometry.getEnvelopeInternal();
       }
       edgeIndex.insert(envelope, e);
     }
   }
   vertexIndex.build();
   edgeIndex.build();
 }
  @Override
  public List<GraphPath> plan(
      String fromPlace,
      String toPlace,
      List<String> intermediates,
      Date targetTime,
      TraverseOptions options) {

    if (options.getModes().contains(TraverseMode.TRANSIT)) {
      throw new UnsupportedOperationException("TSP is not supported for transit trips");
    }

    ArrayList<String> notFound = new ArrayList<String>();
    Vertex fromVertex = getVertexForPlace(fromPlace, options);
    if (fromVertex == null) {
      notFound.add("from");
    }
    Vertex toVertex = getVertexForPlace(toPlace, options);
    if (toVertex == null) {
      notFound.add("to");
    }
    ArrayList<Vertex> intermediateVertices = new ArrayList<Vertex>();

    int i = 0;
    for (String intermediate : intermediates) {
      Vertex vertex = getVertexForPlace(intermediate, options);
      if (vertex == null) {
        notFound.add("intermediate." + i);
      } else {
        intermediateVertices.add(vertex);
      }
      i += 1;
    }

    if (notFound.size() > 0) {
      throw new VertexNotFoundException(notFound);
    }

    if (_graphService.getCalendarService() != null)
      options.setCalendarService(_graphService.getCalendarService());

    options.setTransferTable(_graphService.getGraph().getTransferTable());
    GraphPath path =
        _routingService.route(
            fromVertex,
            toVertex,
            intermediateVertices,
            (int) (targetTime.getTime() / 1000),
            options);

    return Arrays.asList(path);
  }
 @Override
 public void validate(String name, String value) throws ParameterException {
   if (!GraphService.routerIdLegal(value)) {
     String msg = String.format("%s: '%s' is not a valid router ID.", name, value);
     throw new ParameterException(msg);
   }
 }
Пример #4
0
  public RaptorStateSet getStateSet(RoutingRequest options) {

    final Graph graph;
    if (options.rctx == null) {
      graph = graphService.getGraph(options.getRouterId());
      options.setRoutingContext(graph);
      options.rctx.pathParsers =
          new PathParser[] {new BasicPathParser(), new NoThruTrafficPathParser()};
    } else {
      graph = options.rctx.graph;
    }

    RaptorData data = graph.getService(RaptorDataService.class).getData();

    // we multiply the initial walk distance to account for epsilon dominance.
    options.setMaxWalkDistance(options.getMaxWalkDistance() * WALK_EPSILON);

    RoutingRequest walkOptions = options.clone();
    walkOptions.rctx.pathParsers = new PathParser[0];
    TraverseModeSet modes = options.getModes().clone();
    modes.setTransit(false);
    walkOptions.setModes(modes);
    RaptorSearch search = new RaptorSearch(data, options);

    for (int i = 0; i < options.getMaxTransfers() + 2; ++i) {
      if (!round(data, options, walkOptions, search, i)) break;
    }
    RaptorStateSet result = new RaptorStateSet();
    result.statesByStop = search.statesByStop;
    return result;
  }
Пример #5
0
  /**
   * Get edges inside a bbox.
   *
   * @return
   */
  @Secured({"ROLE_USER"})
  @GET
  @Path("/edges")
  @Produces({MediaType.APPLICATION_JSON})
  public Object getEdges(
      @QueryParam("lowerLeft") String lowerLeft,
      @QueryParam("upperRight") String upperRight,
      @QueryParam("exactClass") String className,
      @QueryParam("skipTransit") boolean skipTransit,
      @QueryParam("skipStreets") boolean skipStreets,
      @QueryParam("skipNoGeometry") boolean skipNoGeometry) {

    initIndexes();

    Envelope envelope = getEnvelope(lowerLeft, upperRight);

    EdgeSet out = new EdgeSet();
    Graph graph = graphService.getGraph();

    @SuppressWarnings("unchecked")
    List<Edge> query = edgeIndex.query(envelope);
    out.edges = new ArrayList<WrappedEdge>();
    for (Edge e : query) {
      if (skipStreets && (e instanceof StreetEdge)) continue;
      if (skipTransit && !(e instanceof StreetEdge)) continue;
      if (skipNoGeometry && e.getGeometry() == null) continue;
      if (className != null && !e.getClass().getName().endsWith("." + className)) continue;
      out.edges.add(new WrappedEdge(e, graph.getIdForEdge(e)));
    }
    return out.withGraph(graph);
  }
Пример #6
0
  @Secured({"ROLE_USER"})
  @GET
  @Path("/annotations")
  @Produces({MediaType.APPLICATION_JSON})
  public Object getAnnotations() {
    Graph graph = graphService.getGraph();
    List<GraphBuilderAnnotation> builderAnnotations = graph.getBuilderAnnotations();

    List<Annotation> out = new ArrayList<Annotation>();
    for (GraphBuilderAnnotation annotation : builderAnnotations) {
      Annotation outAnnotation = new Annotation();
      out.add(outAnnotation);
      outAnnotation.annotation = annotation.getVariety().name();
      Collection<Object> referencedObjects = annotation.getReferencedObjects();
      for (Object object : referencedObjects) {
        AnnotationObject annotationObj = new AnnotationObject();
        applyObjectToAnnotation(graph, annotationObj, object);
        outAnnotation.addObject(annotationObj);
      }
    }

    Annotations annotations = new Annotations();
    annotations.annotations = out;
    return annotations;
  }
  /**
   * Get polygons covering the components of the graph. The largest component (in terms of number of
   * nodes) will not overlap any other components (it will have holes); the others may overlap each
   * other.
   *
   * @param modes
   * @return
   */
  @Secured({"ROLE_USER"})
  @GET
  @Path("/polygons")
  @Produces({MediaType.APPLICATION_JSON})
  public GraphComponentPolygons getComponentPolygons(
      @DefaultValue("TRANSIT,WALK") @QueryParam("modes") TraverseModeSet modes,
      @QueryParam(RequestInf.DATE) String date,
      @QueryParam(RequestInf.TIME) String time,
      @DefaultValue("") @QueryParam(RequestInf.BANNED_ROUTES) String bannedRoutes) {

    TraverseOptions options = new TraverseOptions(modes);
    options.bannedRoutes = new HashSet<RouteSpec>();
    if (bannedRoutes.length() > 0) {
      for (String element : bannedRoutes.split(",")) {
        String[] routeSpec = element.split("_", 2);
        if (routeSpec.length != 2) {
          throw new IllegalArgumentException("AgencyId or routeId not set in bannedRoutes list");
        }
        options.bannedRoutes.add(new RouteSpec(routeSpec[0], routeSpec[1]));
      }
    }

    long dateTime = DateUtils.toDate(date, time).getTime();
    if (cachedPolygons == null || dateTime != cachedDateTime || !options.equals(cachedOptions)) {
      cachedOptions = options;
      cachedDateTime = dateTime;
      Graph graph = graphService.getGraph();
      if (graphService.getCalendarService() != null) {
        options.setCalendarService(graphService.getCalendarService());
      }
      options.setServiceDays(dateTime, graph.getAgencyIds());
      cachedPolygons = AnalysisUtils.getComponentPolygons(graph, options, dateTime);
    }

    GraphComponentPolygons out = new GraphComponentPolygons();
    out.components = new ArrayList<GraphComponent>();

    for (Geometry geometry : cachedPolygons) {
      GraphComponent component = new GraphComponent();
      component.polygon = geometry;
      out.components.add(component);
    }

    return out;
  }
Пример #8
0
 /**
  * Get vertices inside a bbox.
  *
  * @return
  */
 @Secured({"ROLE_USER"})
 @GET
 @Path("/vertex")
 @Produces({MediaType.APPLICATION_JSON})
 public Object getVertex(@QueryParam("label") String label) {
   Graph graph = graphService.getGraph();
   Vertex vertex = graph.getVertex(label);
   if (vertex == null) {
     return null;
   }
   return new WrappedVertex(vertex).withGraph(graph);
 }
  private Vertex getVertexForPlace(String place, TraverseOptions options) {

    Matcher matcher = _latLonPattern.matcher(place);

    if (matcher.matches()) {
      double lat = Double.parseDouble(matcher.group(1));
      double lon = Double.parseDouble(matcher.group(4));
      Coordinate location = new Coordinate(lon, lat);
      return _indexService.getClosestVertex(location, options);
    }

    return _graphService.getContractionHierarchySet().getVertex(place);
  }
Пример #10
0
  /**
   * Get vertices connected to an edge
   *
   * @return
   */
  @Secured({"ROLE_USER"})
  @GET
  @Path("/verticesForEdge")
  @Produces({MediaType.APPLICATION_JSON})
  public Object getVerticesForEdge(@QueryParam("edge") int edgeId) {

    Graph graph = graphService.getGraph();
    Edge edge = graph.getEdgeById(edgeId);

    VertexSet out = new VertexSet();
    out.vertices = new ArrayList<Vertex>(2);
    out.vertices.add(edge.getFromVertex());
    out.vertices.add(edge.getToVertex());

    return out.withGraph(graph);
  }
  public List<DirectEdge> getOutgoingEdges(Vertex vertex) {
    Graph graph = _graphService.getGraph();
    GraphVertex gv = graph.getGraphVertex(vertex);
    if (gv == null) {
      return Collections.emptyList();
    }
    List<DirectEdge> result = new ArrayList<DirectEdge>();
    for (Edge out : gv.getOutgoing()) {

      if (!(out instanceof TurnEdge || out instanceof OutEdge || out instanceof PlainStreetEdge)) {
        continue;
      }
      result.add((StreetEdge) out);
    }
    return result;
  }
Пример #12
0
  /** Returns the first trip of the service day. */
  public TripPlan generateFirstTrip(RoutingRequest request) {
    Graph graph = graphService.getGraph(request.getRouterId());

    TransitIndexService transitIndex = graph.getService(TransitIndexService.class);
    transitIndexWithBreakRequired(transitIndex);

    request.setArriveBy(false);

    TimeZone tz = graph.getTimeZone();

    GregorianCalendar calendar = new GregorianCalendar(tz);
    calendar.setTimeInMillis(request.dateTime * 1000);
    calendar.set(Calendar.HOUR, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.AM_PM, 0);
    calendar.set(Calendar.SECOND, transitIndex.getOvernightBreak());

    request.dateTime = calendar.getTimeInMillis() / 1000;
    return generate(request);
  }
 public Coverage getGridCoverage() {
   if (coverage == null) {
     NEDDownloader downloader = new NEDDownloader();
     downloader.setGraph(graphService.getGraph());
     downloader.setCacheDirectory(cacheDirectory);
     List<File> paths = downloader.downloadNED();
     for (File path : paths) {
       GeotiffGridCoverageFactoryImpl factory = new GeotiffGridCoverageFactoryImpl();
       factory.setPath(path);
       GridCoverage2D regionCoverage =
           Interpolator2D.create(factory.getGridCoverage(), new InterpolationBilinear());
       if (coverage == null) {
         coverage = new UnifiedGridCoverage("unified", regionCoverage);
       } else {
         coverage.add(regionCoverage);
       }
     }
   }
   return coverage;
 }
Пример #14
0
  /**
   * Get vertices inside a bbox.
   *
   * @return
   */
  @Secured({"ROLE_USER"})
  @GET
  @Path("/vertices")
  @Produces({MediaType.APPLICATION_JSON})
  public Object getVertices(
      @QueryParam("lowerLeft") String lowerLeft,
      @QueryParam("upperRight") String upperRight,
      @QueryParam("pointsOnly") boolean pointsOnly,
      @QueryParam("exactClass") String className,
      @QueryParam("skipTransit") boolean skipTransit,
      @QueryParam("skipStreets") boolean skipStreets) {

    initIndexes();

    Envelope envelope = getEnvelope(lowerLeft, upperRight);

    @SuppressWarnings("unchecked")
    List<Vertex> query = vertexIndex.query(envelope);
    List<Vertex> filtered = new ArrayList<Vertex>();
    for (Vertex v : query) {
      if (skipTransit && v instanceof TransitVertex) continue;
      if (skipStreets && v instanceof StreetVertex) continue;
      if (className != null && !v.getClass().getName().endsWith("." + className)) continue;
      filtered.add(v);
    }
    if (pointsOnly) {
      SimpleVertexSet out = new SimpleVertexSet();
      out.vertices = new ArrayList<SimpleVertex>(filtered.size());
      for (Vertex v : filtered) {
        out.vertices.add(new SimpleVertex(v));
      }
      return out;
    } else {
      VertexSet out = new VertexSet();
      out.vertices = filtered;

      Graph graph = graphService.getGraph();
      return out.withGraph(graph);
    }
  }
Пример #15
0
  /**
   * Get edges connected to an vertex
   *
   * @return
   */
  @Secured({"ROLE_USER"})
  @GET
  @Path("/edgesForVertex")
  @Produces({MediaType.APPLICATION_JSON})
  public EdgesForVertex getEdgesForVertex(@QueryParam("vertex") String label) {

    Graph graph = graphService.getGraph();
    Vertex vertex = graph.getVertex(label);
    if (vertex == null) {
      return null;
    }
    EdgeSet incoming = new EdgeSet();
    incoming.addEdges(vertex.getIncoming(), graph);

    EdgeSet outgoing = new EdgeSet();
    outgoing.addEdges(vertex.getOutgoing(), graph);

    EdgesForVertex e4v = new EdgesForVertex();
    e4v.incoming = incoming.withGraph(graph);
    e4v.outgoing = outgoing.withGraph(graph);

    return e4v;
  }
 public boolean multipleOptionsBefore(Edge edge, State state) {
   Graph graph = _graphService.getGraph();
   boolean foundAlternatePaths = false;
   Vertex start = edge.getFromVertex();
   GraphVertex gv = graph.getGraphVertex(start);
   if (gv == null) {
     return false;
   }
   for (Edge out : gv.getOutgoing()) {
     if (out == edge) {
       continue;
     }
     if (!(out instanceof TurnEdge || out instanceof OutEdge || out instanceof PlainStreetEdge)) {
       continue;
     }
     if (state != null && out.traverse(state) == null) {
       continue;
     }
     // there were paths we didn't take.
     foundAlternatePaths = true;
     break;
   }
   return foundAlternatePaths;
 }
Пример #17
0
  @Override
  public List<GraphPath> getPaths(RoutingRequest options) {

    final Graph graph = graphService.getGraph(options.getRouterId());
    if (options.rctx == null) {
      options.setRoutingContext(graph);
      options.rctx.pathParsers =
          new PathParser[] {new BasicPathParser(), new NoThruTrafficPathParser()};
    }

    if (!options.getModes().isTransit()) {
      return sptService.getShortestPathTree(options).getPaths();
    }

    // also fall back to A* for short trips
    double distance =
        distanceLibrary.distance(
            options.rctx.origin.getCoordinate(), options.rctx.target.getCoordinate());
    if (distance < shortPathCutoff) {
      log.debug("Falling back to A* for very short path");
      return shortPathService.getPaths(options);
    }

    RaptorDataService service = graph.getService(RaptorDataService.class);
    if (service == null) {
      log.warn("No raptor data.  Rebuild with RaptorDataBuilder");
      return Collections.emptyList();
    }
    RaptorData data = service.getData();

    // we multiply the initial walk distance to account for epsilon dominance.
    double initialWalk = options.getMaxWalkDistance() * WALK_EPSILON;
    options.setMaxWalkDistance(initialWalk);

    // do not even bother with obviously impossible walks
    double minWalk =
        options.rctx.origin.getDistanceToNearestTransitStop()
            + options.rctx.target.getDistanceToNearestTransitStop();
    if (options.getMaxWalkDistance() < minWalk) {
      options.setMaxWalkDistance(minWalk);
    }

    RoutingRequest walkOptions = options.clone();
    walkOptions.rctx.pathParsers = new PathParser[0];
    TraverseModeSet modes = options.getModes().clone();
    modes.setTransit(false);
    walkOptions.setModes(modes);
    RaptorSearch search = new RaptorSearch(data, options);

    if (data.maxTransitRegions != null) {
      Calendar tripDate = Calendar.getInstance(graph.getTimeZone());
      tripDate.setTime(new Date(1000L * options.dateTime));

      Calendar maxTransitStart = Calendar.getInstance(graph.getTimeZone());
      maxTransitStart.set(Calendar.YEAR, data.maxTransitRegions.startYear);
      maxTransitStart.set(Calendar.MONTH, data.maxTransitRegions.startMonth);
      maxTransitStart.set(Calendar.DAY_OF_MONTH, data.maxTransitRegions.startDay);

      int day = 0;
      while (tripDate.after(maxTransitStart)) {
        day++;
        tripDate.add(Calendar.DAY_OF_MONTH, -1);
      }
      if (day > data.maxTransitRegions.maxTransit.length || options.isWheelchairAccessible()) {
        day = -1;
      }

      search.maxTimeDayIndex = day;
    }

    int rushAheadRound = preliminaryRaptorSearch(data, options, walkOptions, search);

    long searchBeginTime = System.currentTimeMillis();

    double expectedWorstTime =
        1.5
            * distanceLibrary.distance(
                options.rctx.origin.getCoordinate(), options.rctx.target.getCoordinate())
            / options.getWalkSpeed();

    int foundSoFar = 0;

    double firstWalkDistance = 0;
    List<RaptorState> targetStates = new ArrayList<RaptorState>();

    do {
      int bestElapsedTime = Integer.MAX_VALUE;
      RETRY:
      do {
        for (int round = 0; round < options.getMaxTransfers() + 2; ++round) {
          if (!round(data, options, walkOptions, search, round)) break;

          long elapsed = System.currentTimeMillis() - searchBeginTime;
          if (elapsed > multiPathTimeout * 1000 && multiPathTimeout > 0 && targetStates.size() > 0)
            break RETRY;

          ArrayList<RaptorState> toRemove = new ArrayList<RaptorState>();
          for (RaptorState state : search.getTargetStates()) {
            if (state.nBoardings == 0 && options.getMaxWalkDistance() > initialWalk) {
              toRemove.add(state);
            }
          }
          if (search.getTargetStates().size() > 0) {
            if (firstWalkDistance == 0) {
              firstWalkDistance = options.getMaxWalkDistance();
            }
            for (RaptorState state : toRemove) {
              search.removeTargetState(state.walkPath);
            }
          }
          if (targetStates.size() >= options.getNumItineraries() && round >= rushAheadRound) {
            int oldBest = bestElapsedTime;
            for (RaptorState state : search.getTargetStates()) {
              final int elapsedTime = (int) Math.abs(state.arrivalTime - options.dateTime);
              if (elapsedTime < bestElapsedTime) {
                bestElapsedTime = elapsedTime;
              }
            }

            int improvement = oldBest - bestElapsedTime;
            if (improvement < 600 && bestElapsedTime < expectedWorstTime) break RETRY;
          }
        }

        if (foundSoFar < search.getTargetStates().size()) {
          foundSoFar = search.getTargetStates().size();
        } else if (foundSoFar > 0) {
          // we didn't find anything new in this round, and we already have
          // some paths, so bail out
          break;
        }
        options = options.clone();
        walkOptions = walkOptions.clone();
        if (search.getTargetStates().size() > 0 && bestElapsedTime < expectedWorstTime) {
          // we have found some paths so we no longer want to expand the max walk distance
          break RETRY;
        } else {
          options.setMaxWalkDistance(options.getMaxWalkDistance() * 2);
          walkOptions.setMaxWalkDistance(options.getMaxWalkDistance());

          options.setWalkReluctance(options.getWalkReluctance() * 2);
          walkOptions.setWalkReluctance(options.getWalkReluctance());
        }
        search.reset(options);

      } while (options.getMaxWalkDistance() < initialWalk * MAX_WALK_MULTIPLE
          && initialWalk < Double.MAX_VALUE);

      options = options.clone();
      walkOptions = walkOptions.clone();
      for (RaptorState state : search.getTargetStates()) {
        for (AgencyAndId trip : state.getTrips()) {
          options.bannedTrips.add(trip);
        }
      }

      if (search.getTargetStates().size() == 0) break; // no paths found; searching more won't help

      options.setMaxWalkDistance(firstWalkDistance);
      walkOptions.setMaxWalkDistance(firstWalkDistance);

      targetStates.addAll(search.getTargetStates());
      search = new RaptorSearch(data, options);

    } while (targetStates.size() < options.getNumItineraries());

    collectRoutesUsed(data, options, targetStates);

    if (targetStates.isEmpty()) {
      log.info("RAPTOR found no paths");
    }
    Collections.sort(targetStates);

    if (targetStates.size() > options.getNumItineraries())
      targetStates = targetStates.subList(0, options.getNumItineraries());

    List<GraphPath> paths = new ArrayList<GraphPath>();
    for (RaptorState targetState : targetStates) {
      // reconstruct path
      ArrayList<RaptorState> states = new ArrayList<RaptorState>();
      RaptorState cur = targetState;
      while (cur != null) {
        states.add(cur);
        cur = cur.getParent();
      }
      // states is in reverse order of time
      State state = getState(targetState.getRequest(), data, states);
      paths.add(new GraphPath(state, true));
    }

    return paths;
  }
  @Override
  public List<GraphPath> plan(State origin, Vertex target, int nItineraries) {

    Date targetTime = new Date(origin.getTime() * 1000);
    TraverseOptions options = origin.getOptions();

    if (_graphService.getCalendarService() != null)
      options.setCalendarService(_graphService.getCalendarService());
    options.setTransferTable(_graphService.getGraph().getTransferTable());
    options.setServiceDays(targetTime.getTime() / 1000);
    if (options.getModes().getTransit()
        && !_graphService.getGraph().transitFeedCovers(targetTime)) {
      // user wants a path through the transit network,
      // but the date provided is outside those covered by the transit feed.
      throw new TransitTimesException();
    }
    // decide which A* heuristic to use
    options.remainingWeightHeuristic =
        _remainingWeightHeuristicFactory.getInstanceForSearch(options, target);
    LOG.debug("Applied A* heuristic: {}", options.remainingWeightHeuristic);

    // If transit is not to be used, disable walk limit and only search for one itinerary.
    if (!options.getModes().getTransit()) {
      nItineraries = 1;
      options.setMaxWalkDistance(Double.MAX_VALUE);
    }

    ArrayList<GraphPath> paths = new ArrayList<GraphPath>();

    // The list of options specifying various modes, banned routes, etc to try for multiple
    // itineraries
    Queue<TraverseOptions> optionQueue = new LinkedList<TraverseOptions>();
    optionQueue.add(options);

    /* if the user wants to travel by transit, create a bus-only set of options */
    if (options.getModes().getTrainish() && options.getModes().contains(TraverseMode.BUS)) {
      TraverseOptions busOnly = options.clone();
      busOnly.setModes(options.getModes().clone());
      busOnly.getModes().setTrainish(false);
      // Moved inside block to avoid double insertion in list ? (AMB)
      // optionQueue.add(busOnly);
    }

    double maxWeight = Double.MAX_VALUE;
    long maxTime = options.isArriveBy() ? 0 : Long.MAX_VALUE;
    while (paths.size() < nItineraries) {
      options = optionQueue.poll();
      if (options == null) {
        break;
      }
      StateEditor editor = new StateEditor(origin, null);
      editor.setTraverseOptions(options);
      origin = editor.makeState();

      // options.worstTime = maxTime;
      // options.maxWeight = maxWeight;
      long searchBeginTime = System.currentTimeMillis();
      LOG.debug("BEGIN SEARCH");
      List<GraphPath> somePaths = _routingService.route(origin, target);
      LOG.debug("END SEARCH {} msec", System.currentTimeMillis() - searchBeginTime);
      if (maxWeight == Double.MAX_VALUE) {
        /*
         * the worst trip we are willing to accept is at most twice as bad or twice as long.
         */
        if (somePaths.isEmpty()) {
          // if there is no first path, there won't be any other paths
          return null;
        }
        GraphPath path = somePaths.get(0);
        long duration = path.getDuration();
        LOG.debug("Setting max time and weight for subsequent searches.");
        LOG.debug("First path start time:  {}", path.getStartTime());
        maxTime =
            path.getStartTime() + MAX_TIME_FACTOR * (options.isArriveBy() ? -duration : duration);
        LOG.debug("First path duration:  {}", duration);
        LOG.debug("Max time set to:  {}", maxTime);
        maxWeight = path.getWeight() * MAX_WEIGHT_FACTOR;
        LOG.debug("Max weight set to:  {}", maxWeight);
      }
      if (somePaths.isEmpty()) {
        LOG.debug("NO PATHS FOUND");
        continue;
      }
      for (GraphPath path : somePaths) {
        if (!paths.contains(path)) {
          // DEBUG
          // path.dump();
          paths.add(path);
          // now, create a list of options, one with each trip in this journey banned.

          LOG.debug("New trips: {}", path.getTrips());
          TraverseOptions newOptions = options.clone();
          for (AgencyAndId trip : path.getTrips()) {
            newOptions.bannedTrips.add(trip);
          }

          if (!optionQueue.contains(newOptions)) {
            optionQueue.add(newOptions);
          }
          /*
           * // now, create a list of options, one with each route in this trip banned. //
           * the HashSet banned is not strictly necessary as the optionsQueue will //
           * already remove duplicate options, but it might be slightly faster as //
           * hashing TraverseOptions is slow. LOG.debug("New routespecs: {}",
           * path.getRouteSpecs()); for (RouteSpec spec : path.getRouteSpecs()) {
           * TraverseOptions newOptions = options.clone();
           * newOptions.bannedRoutes.add(spec); if (!optionQueue.contains(newOptions)) {
           * optionQueue.add(newOptions); } }
           */
        }
      }
      LOG.debug("{} / {} itineraries", paths.size(), nItineraries);
    }
    if (paths.size() == 0) {
      return null;
    }
    // We order the list of returned paths by the time of arrival or departure (not path duration)
    Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy()));
    return paths;
  }
Пример #19
0
  /**
   * Calculates walksheds for a given location, based on time given to walk and the walk speed.
   *
   * <p>Depending on the value for the "output" parameter (i.e. "POINTS", "SHED" or "EDGES"), a
   * different type of GeoJSON geometry is returned. If a SHED is requested, then a ConcaveHull of
   * the EDGES/roads is returned. If that fails, a ConvexHull will be returned.
   *
   * <p>The ConcaveHull parameter is set to 0.005 degrees. The offroad walkspeed is assumed to be
   * 0.83333 m/sec (= 3km/h) until a road is hit.
   *
   * <p>Note that the set of EDGES/roads returned as well as POINTS returned may contain duplicates.
   * If POINTS are requested, then not the end-points are returned at which the max time is reached,
   * but instead all the graph nodes/crossings that are within the time limits.
   *
   * <p>In case there is no road near by within the given time, then a circle for the walktime limit
   * is created and returned for the SHED parameter. Otherwise the edge with the direction towards
   * the closest road. Note that the circle is calculated in Euclidian 2D coordinates, and
   * distortions towards an ellipse will appear if it is transformed/projected to the user location.
   *
   * <p>An example request may look like this:
   * localhost:8080/otp-rest-servlet/ws/iso?layers=traveltime&styles=mask&batch=true&fromPlace=51.040193121307176
   * %2C-114.04471635818481&toPlace
   * =51.09098935%2C-113.95179705&time=2012-06-06T08%3A00%3A00&mode=WALK&maxWalkDistance=10000&walkSpeed=1.38&walkTime=10.7&output=EDGES
   * Though the first parameters (i) layer, (ii) styles and (iii) batch could be discarded.
   *
   * @param walkmins Maximum number of minutes to walk.
   * @param output Can be set to "POINTS", "SHED" or "EDGES" to return different types of GeoJSON
   *     geometry. SHED returns a ConcaveHull or ConvexHull of the edges/roads. POINTS returns all
   *     graph nodes that are within the time limit.
   * @return a JSON document containing geometries (either points, lineStrings or a polygon).
   * @throws Exception
   * @author sstein---geo.uzh.ch
   */
  @GET
  @Produces({MediaType.APPLICATION_JSON})
  public String getIsochrone(
      @QueryParam("walkTime") @DefaultValue("15") double walkmins,
      @QueryParam("output") @DefaultValue("POINTS") String output)
      throws Exception {

    this.debugGeoms = new ArrayList();
    this.tooFastTraversedEdgeGeoms = new ArrayList();

    RoutingRequest sptRequestA = buildRequest(0);
    String from = sptRequestA.getFrom().toString();
    int pos = 1;
    float lat = 0;
    float lon = 0;
    for (String s : from.split(",")) {
      if (s.isEmpty()) {
        // no location
        Response.status(Status.BAD_REQUEST).entity("no position").build();
        return null;
      }
      try {
        float num = Float.parseFloat(s);
        if (pos == 1) {
          lat = num;
        }
        if (pos == 2) {
          lon = num;
        }
      } catch (Exception e) {
        throw new WebApplicationException(
            Response.status(Status.BAD_REQUEST)
                .entity(
                    "Could not parse position string to number. Require numerical lat & long coords.")
                .build());
      }
      pos++;
    }

    GeometryFactory gf = new GeometryFactory();

    Coordinate dropPoint = new Coordinate(lon, lat);

    int walkInMin = (int) Math.floor(walkmins);
    double walkInSec = walkmins * 60;
    LOG.debug(
        "given travel time: " + walkInMin + " mins + " + (walkInSec - (60 * walkInMin)) + " sec");
    // restrict the evaluated SPT size to 30mins for requests with walking < 30min
    // if larger walking times are requested we adjust the evaluated
    // graph dynamically by 1.3 * min -> this should save processing time
    if (walkInMin < 30) {
      sptRequestA.worstTime = sptRequestA.dateTime + (30 * 60);
    } else {
      sptRequestA.worstTime = sptRequestA.dateTime + Math.round(walkInMin * 1.3 * 60);
    }
    // set the switch-time for shed/area calculation, i.e. to decide if the hull is calculated based
    // on points or on edges
    TraverseModeSet modes = sptRequestA.modes;
    LOG.debug("mode(s): " + modes);
    if ((modes.contains(TraverseMode.TRANSIT))
        || (modes.contains(TraverseMode.BUSISH))
        || (modes.contains(TraverseMode.TRAINISH))) {
      shedCalcMethodSwitchTimeInSec =
          60 * 20; // 20min (use 20min for transit, since buses may not come all the time)
    } else if (modes.contains(TraverseMode.CAR)) {
      shedCalcMethodSwitchTimeInSec = 60 * 10; // 10min
    } else if (modes.contains(TraverseMode.BICYCLE)) {
      shedCalcMethodSwitchTimeInSec = 60 * 10; // 10min
    } else {
      shedCalcMethodSwitchTimeInSec = 60 * 20; // 20min
    }
    // set the maxUserSpeed, which is used later to check for u-type streets/crescents when
    // calculating sub-edges;
    // Note, that the car speed depends on the edge itself, so this value may be replaced later
    this.usesCar = false;
    int numberOfModes = modes.getModes().size();
    if (numberOfModes == 1) {
      if (modes.getWalk()) {
        this.maxUserSpeed = sptRequestA.getWalkSpeed();
      } else if (modes.getBicycle()) {
        this.maxUserSpeed = sptRequestA.getBikeSpeed();
      } else if (modes.getDriving()) {
        this.maxUserSpeed = sptRequestA.getCarSpeed();
        this.usesCar = true;
      }
    } else { // for all other cases (multiple-modes)
      // sstein: I thought I may set it to 36.111 m/sec = 130 km/h,
      // but maybe it is better to assume walk speed for transit, i.e. treat it like if the
      // person gets off the bus on the last crossing and walks the "last mile".
      this.maxUserSpeed = sptRequestA.getWalkSpeed();
    }

    if (doSpeedTest) {
      LOG.debug("performing angle and speed based test to detect u-shapes");
    } else {
      LOG.debug("performing only angle based test to detect u-shapes");
    }

    // TODO: OTP prefers to snap to car-roads/ways, which is not so nice, when walking,
    // and a footpath is closer by. So far there is no option to switch that off

    // create the ShortestPathTree
    try {
      sptRequestA.setRoutingContext(graphService.getGraph());
    } catch (Exception e) {
      // if we get an exception here, and in particular a VertexNotFoundException,
      // then it is likely that we chose a (transit) mode without having that (transit) modes data
      LOG.debug("cannot set RoutingContext: " + e.toString());
      LOG.debug("cannot set RoutingContext: setting mode=WALK");
      sptRequestA.setMode(TraverseMode.WALK); // fall back to walk mode
      sptRequestA.setRoutingContext(graphService.getGraph());
    }
    ShortestPathTree sptA = sptService.getShortestPathTree(sptRequestA);
    StreetLocation origin = (StreetLocation) sptRequestA.rctx.fromVertex;
    sptRequestA.cleanup(); // remove inserted points

    // create a LineString for display
    Coordinate pathToStreetCoords[] = new Coordinate[2];
    pathToStreetCoords[0] = dropPoint;
    pathToStreetCoords[1] = origin.getCoordinate();
    LineString pathToStreet = gf.createLineString(pathToStreetCoords);

    // get distance between origin and drop point for time correction
    double distanceToRoad =
        this.distanceLibrary.distance(origin.getY(), origin.getX(), dropPoint.y, dropPoint.x);
    long offRoadTimeCorrection = (long) (distanceToRoad / this.offRoadWalkspeed);

    //
    // --- filter the states ---
    //
    Set<Coordinate> visitedCoords = new HashSet<Coordinate>();
    ArrayList<Edge> allConnectingEdges = new ArrayList<Edge>();
    Coordinate coords[] = null;
    long maxTime = (long) walkInSec - offRoadTimeCorrection;
    // System.out.println("Reducing walktime from: " + (int)(walkmins * 60) + "sec to " + maxTime +
    // "sec due to initial walk of " + distanceToRoad
    // + "m");

    // if the initial walk is already to long, there is no need to parse...
    if (maxTime <= 0) {
      noRoadNearBy = true;
      long timeToWalk = (long) walkInSec;
      long timeBetweenStates = offRoadTimeCorrection;
      long timeMissing = timeToWalk;
      double fraction = (double) timeMissing / (double) timeBetweenStates;
      pathToStreet = getSubLineString(pathToStreet, fraction);
      LOG.debug(
          "no street found within giving travel time (for off-road walkspeed: {} m/sec)",
          this.offRoadWalkspeed);
    } else {
      noRoadNearBy = false;
      Map<ReversibleLineStringWrapper, Edge> connectingEdgesMap = Maps.newHashMap();
      for (State state : sptA.getAllStates()) {
        long et = state.getElapsedTimeSeconds();
        if (et <= maxTime) {
          // -- filter points, as the same coordinate may be passed several times due to the graph
          // structure
          // in a Calgary suburb family homes neighborhood with a 15min walkshed it filtered about
          // 250 points away (while 145 were finally displayed)
          if (visitedCoords.contains(state.getVertex().getCoordinate())) {
            continue;
          } else {
            visitedCoords.add(state.getVertex().getCoordinate());
          }
          // -- get all Edges needed later for the edge representation
          // and to calculate an edge-based walkshed
          // Note, it can happen that we get a null geometry here, e.g. for hop-edges!
          Collection<Edge> vertexEdgesIn = state.getVertex().getIncoming();
          for (Iterator<Edge> iterator = vertexEdgesIn.iterator(); iterator.hasNext(); ) {
            Edge edge = (Edge) iterator.next();
            Geometry edgeGeom = edge.getGeometry();
            if (edgeGeom != null) { // make sure we get only real edges
              if (edgeGeom instanceof LineString) {
                // allConnectingEdges.add(edge); // instead of this, use a map now, so we don't have
                // similar edge many times
                connectingEdgesMap.put(
                    new ReversibleLineStringWrapper((LineString) edgeGeom), edge);
              }
            }
          }
          Collection<Edge> vertexEdgesOut = state.getVertex().getOutgoing();
          for (Iterator<Edge> iterator = vertexEdgesOut.iterator(); iterator.hasNext(); ) {
            Edge edge = (Edge) iterator.next();
            Geometry edgeGeom = edge.getGeometry();
            if (edgeGeom != null) {
              if (edgeGeom instanceof LineString) {
                // allConnectingEdges.add(edge); // instead of this, use a map now, so we don't
                // similar edge many times
                connectingEdgesMap.put(
                    new ReversibleLineStringWrapper((LineString) edgeGeom), edge);
              }
            }
          }
        } // end : if(et < maxTime)
      }
      // --
      // points from list to array, for later
      coords = new Coordinate[visitedCoords.size()];
      int i = 0;
      for (Coordinate c : visitedCoords) coords[i++] = c;

      // connection edges from Map to List
      allConnectingEdges.clear();
      for (Edge tedge : connectingEdgesMap.values()) allConnectingEdges.add(tedge);
    }
    StringWriter sw = new StringWriter();
    GeoJSONBuilder json = new GeoJSONBuilder(sw);
    //
    // -- create the different outputs ---
    //
    try {
      if (output.equals(IsoChrone.RESULT_TYPE_POINTS)) {
        // in case there was no road we create a circle and
        // and return those points
        if (noRoadNearBy) {
          Geometry circleShape = createCirle(dropPoint, pathToStreet);
          coords = circleShape.getCoordinates();
        }
        // -- the states/nodes with time elapsed <= X min.
        LOG.debug("write multipoint geom with {} points", coords.length);
        json.writeGeom(gf.createMultiPoint(coords));
        LOG.debug("done");
      } else if (output.equals(IsoChrone.RESULT_TYPE_SHED)) {

        Geometry geomsArray[] = null;
        // in case there was no road we create a circle
        if (noRoadNearBy) {
          Geometry circleShape = createCirle(dropPoint, pathToStreet);
          json.writeGeom(circleShape);
        } else {
          if (maxTime > shedCalcMethodSwitchTimeInSec) { // eg., walkshed > 20 min
            // -- create a point-based walkshed
            // less exact and should be used for large walksheds with many edges
            LOG.debug("create point-based shed (not from edges)");
            geomsArray = new Geometry[coords.length];
            for (int j = 0; j < geomsArray.length; j++) {
              geomsArray[j] = gf.createPoint(coords[j]);
            }
          } else {
            // -- create an edge-based walkshed
            // it is more exact and should be used for short walks
            LOG.debug("create edge-based shed (not from points)");
            Map<ReversibleLineStringWrapper, LineString> walkShedEdges = Maps.newHashMap();
            // add the walk from the pushpin to closest street point
            walkShedEdges.put(new ReversibleLineStringWrapper(pathToStreet), pathToStreet);
            // get the edges and edge parts within time limits
            ArrayList<LineString> withinTimeEdges =
                this.getLinesAndSubEdgesWithinMaxTime(
                    maxTime,
                    allConnectingEdges,
                    sptA,
                    angleLimitForUShapeDetection,
                    distanceToleranceForUShapeDetection,
                    maxUserSpeed,
                    usesCar,
                    doSpeedTest);
            for (LineString ls : withinTimeEdges) {
              walkShedEdges.put(new ReversibleLineStringWrapper(ls), ls);
            }
            geomsArray = new Geometry[walkShedEdges.size()];
            int k = 0;
            for (LineString ls : walkShedEdges.values()) geomsArray[k++] = ls;
          } // end if-else: maxTime condition
          GeometryCollection gc = gf.createGeometryCollection(geomsArray);
          // create the concave hull, but in case it fails we just return the convex hull
          Geometry outputHull = null;
          LOG.debug(
              "create concave hull from {} geoms with edge length limit of about {} m (distance on meridian)",
              geomsArray.length,
              concaveHullAlpha * 111132);
          // 1deg at Latitude phi = 45deg is about 111.132km
          // (see wikipedia:
          // http://en.wikipedia.org/wiki/Latitude#The_length_of_a_degree_of_latitude)
          try {
            ConcaveHull hull = new ConcaveHull(gc, concaveHullAlpha);
            outputHull = hull.getConcaveHull();
          } catch (Exception e) {
            outputHull = gc.convexHull();
            LOG.debug("Could not generate ConcaveHull for WalkShed, using ConvexHull instead.");
          }
          LOG.debug("write shed geom");
          json.writeGeom(outputHull);
          LOG.debug("done");
        }
      } else if (output.equals(IsoChrone.RESULT_TYPE_EDGES)) {
        // in case there was no road we return only the suggested path to the street
        if (noRoadNearBy) {
          json.writeGeom(pathToStreet);
        } else {
          // -- if we would use only the edges from the paths to the origin we will miss
          // some edges that will be never on the shortest path (e.g. loops/crescents).
          // However, we can retrieve all edges by checking the times for each
          // edge end-point
          Map<ReversibleLineStringWrapper, LineString> walkShedEdges = Maps.newHashMap();
          // add the walk from the pushpin to closest street point
          walkShedEdges.put(new ReversibleLineStringWrapper(pathToStreet), pathToStreet);
          // get the edges and edge parts within time limits
          ArrayList<LineString> withinTimeEdges =
              this.getLinesAndSubEdgesWithinMaxTime(
                  maxTime,
                  allConnectingEdges,
                  sptA,
                  angleLimitForUShapeDetection,
                  distanceToleranceForUShapeDetection,
                  maxUserSpeed,
                  usesCar,
                  doSpeedTest);
          for (LineString ls : withinTimeEdges) {
            walkShedEdges.put(new ReversibleLineStringWrapper(ls), ls);
          }
          Geometry mls = null;
          LineString edges[] = new LineString[walkShedEdges.size()];
          int k = 0;
          for (LineString ls : walkShedEdges.values()) edges[k++] = ls;
          LOG.debug("create multilinestring from {} geoms", edges.length);
          mls = gf.createMultiLineString(edges);
          LOG.debug("write geom");
          json.writeGeom(mls);
          LOG.debug("done");
        }
      } else if (output.equals("DEBUGEDGES")) {
        // -- for debugging, i.e. display of detected u-shapes/crescents
        ArrayList<LineString> withinTimeEdges =
            this.getLinesAndSubEdgesWithinMaxTime(
                maxTime,
                allConnectingEdges,
                sptA,
                angleLimitForUShapeDetection,
                distanceToleranceForUShapeDetection,
                maxUserSpeed,
                usesCar,
                doSpeedTest);
        if (this.showTooFastEdgesAsDebugGeomsANDnotUShapes) {
          LOG.debug("displaying edges that are traversed too fast");
          this.debugGeoms = this.tooFastTraversedEdgeGeoms;
        } else {
          LOG.debug("displaying detected u-shaped roads/crescents");
        }
        LineString edges[] = new LineString[this.debugGeoms.size()];
        int k = 0;
        for (Iterator iterator = debugGeoms.iterator(); iterator.hasNext(); ) {
          LineString ls = (LineString) iterator.next();
          edges[k] = ls;
          k++;
        }
        Geometry mls = gf.createMultiLineString(edges);
        LOG.debug("write debug geom");
        json.writeGeom(mls);
        LOG.debug("done");
      }
    } catch (org.codehaus.jettison.json.JSONException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    return sw.toString();
  }
  @Override
  public List<GraphPath> getPaths(RoutingRequest options) {

    if (options.rctx == null) {
      options.setRoutingContext(graphService.getGraph(options.getRouterId()));
      // move into setRoutingContext ?
      options.rctx.pathParsers =
          new PathParser[] {new BasicPathParser(), new NoThruTrafficPathParser()};
    }

    RemainingWeightHeuristic heuristic;
    if (options.getModes().isTransit()) {
      LOG.debug("Transit itinerary requested.");
      // always use the bidirectional heuristic because the others are not precise enough
      heuristic = new BidirectionalRemainingWeightHeuristic(options.rctx.graph);
    } else {
      LOG.debug("Non-transit itinerary requested.");
      heuristic = new DefaultRemainingWeightHeuristic();
    }

    // the states that will eventually be turned into paths and returned
    List<State> returnStates = new LinkedList<State>();

    BinHeap<State> pq = new BinHeap<State>();
    //        List<State> boundingStates = new ArrayList<State>();

    Vertex originVertex = options.rctx.origin;
    Vertex targetVertex = options.rctx.target;

    // increase maxWalk repeatedly in case hard limiting is in use
    WALK:
    for (double maxWalk = options.getMaxWalkDistance(); returnStates.isEmpty(); maxWalk *= 2) {
      if (maxWalk != Double.MAX_VALUE && maxWalk > MAX_WALK) {
        break;
      }
      LOG.debug("try search with max walk {}", maxWalk);
      // increase maxWalk if settings make trip impossible
      if (maxWalk
          < Math.min(
              distanceLibrary.distance(originVertex.getCoordinate(), targetVertex.getCoordinate()),
              originVertex.getDistanceToNearestTransitStop()
                  + targetVertex.getDistanceToNearestTransitStop())) continue WALK;
      options.setMaxWalkDistance(maxWalk);

      // cap search / heuristic weight
      final double AVG_TRANSIT_SPEED = 25; // m/sec
      double cutoff =
          (distanceLibrary.distance(originVertex.getCoordinate(), targetVertex.getCoordinate())
                  * 1.5)
              / AVG_TRANSIT_SPEED; // wait time is irrelevant in the heuristic
      cutoff += options.getMaxWalkDistance() * options.walkReluctance;
      options.maxWeight = cutoff;

      State origin = new State(options);
      // (used to) initialize heuristic outside loop so table can be reused
      heuristic.computeInitialWeight(origin, targetVertex);

      options.maxWeight = cutoff + 30 * 60 * options.waitReluctance;

      // reinitialize states for each retry
      HashMap<Vertex, List<State>> states = new HashMap<Vertex, List<State>>();
      pq.reset();
      pq.insert(origin, 0);
      long startTime = System.currentTimeMillis();
      long endTime = startTime + (int) (_timeouts[0] * 1000);
      LOG.debug("starttime {} endtime {}", startTime, endTime);
      QUEUE:
      while (!pq.empty()) {

        if (System.currentTimeMillis() > endTime) {
          LOG.debug("timeout at {} msec", System.currentTimeMillis() - startTime);
          if (returnStates.isEmpty()) break WALK; // disable walk distance increases
          else {
            storeMemory();
            break WALK;
          }
        }

        //                if (pq.peek_min_key() > options.maxWeight) {
        //                    LOG.debug("max weight {} exceeded", options.maxWeight);
        //                    break QUEUE;
        //                }

        State su = pq.extract_min();

        //                for (State bs : boundingStates) {
        //                    if (eDominates(bs, su)) {
        //                        continue QUEUE;
        //                    }
        //                }

        Vertex u = su.getVertex();

        if (traverseVisitor != null) {
          traverseVisitor.visitVertex(su);
        }

        if (u.equals(targetVertex)) {
          //                    boundingStates.add(su);
          returnStates.add(su);
          if (!options.getModes().isTransit()) break QUEUE;
          // options should contain max itineraries
          if (returnStates.size() >= _maxPaths) break QUEUE;
          if (returnStates.size() < _timeouts.length) {
            endTime = startTime + (int) (_timeouts[returnStates.size()] * 1000);
            LOG.debug(
                "{} path, set timeout to {}",
                returnStates.size(),
                _timeouts[returnStates.size()] * 1000);
          }
          continue QUEUE;
        }

        for (Edge e : options.isArriveBy() ? u.getIncoming() : u.getOutgoing()) {
          STATE:
          for (State new_sv = e.traverse(su); new_sv != null; new_sv = new_sv.getNextResult()) {
            if (traverseVisitor != null) {
              traverseVisitor.visitEdge(e, new_sv);
            }

            double h = heuristic.computeForwardWeight(new_sv, targetVertex);
            if (h == Double.MAX_VALUE) continue;
            //                    for (State bs : boundingStates) {
            //                        if (eDominates(bs, new_sv)) {
            //                            continue STATE;
            //                        }
            //                    }
            Vertex v = new_sv.getVertex();
            List<State> old_states = states.get(v);
            if (old_states == null) {
              old_states = new LinkedList<State>();
              states.put(v, old_states);
            } else {
              for (State old_sv : old_states) {
                if (eDominates(old_sv, new_sv)) {
                  continue STATE;
                }
              }
              Iterator<State> iter = old_states.iterator();
              while (iter.hasNext()) {
                State old_sv = iter.next();
                if (eDominates(new_sv, old_sv)) {
                  iter.remove();
                }
              }
            }
            if (traverseVisitor != null) traverseVisitor.visitEnqueue(new_sv);

            old_states.add(new_sv);
            pq.insert(new_sv, new_sv.getWeight() + h);
          }
        }
      }
    }
    storeMemory();

    // Make the states into paths and return them
    List<GraphPath> paths = new LinkedList<GraphPath>();
    for (State s : returnStates) {
      LOG.debug(s.toStringVerbose());
      paths.add(new GraphPath(s, true));
    }
    // sort by arrival time, though paths are already in order of increasing difficulty
    // Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy()));
    return paths;
  }