private FeatureCollection convexHhull(TaskMonitor monitor, FeatureCollection fc) { monitor.allowCancellationRequests(); monitor.report(I18N.get("ui.plugin.analysis.ConvexHullPlugIn.Computing-Convex-Hull") + "..."); int size = fc.size(); GeometryFactory geomFact = null; if (size == 0) { return null; } int count = 0; Geometry[] geoms = new Geometry[size]; for (Iterator i = fc.iterator(); i.hasNext(); ) { Feature f = (Feature) i.next(); Geometry geom = f.getGeometry(); if (geom == null) { continue; } if (geomFact == null) { geomFact = geom.getFactory(); } geoms[count++] = geom; } GeometryCollection gc = geomFact.createGeometryCollection(geoms); Geometry hull = gc.convexHull(); List hullList = new ArrayList(); hullList.add(hull); return FeatureDatasetFactory.createFromGeometry(hullList); }
/** * 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(); }