diff --git a/src/client/images/agency_logo.png b/src/client/images/agency_logo.png old mode 100755 new mode 100644 diff --git a/src/main/java/org/opentripplanner/api/resource/UpdaterStatusResource.java b/src/main/java/org/opentripplanner/api/resource/UpdaterStatusResource.java index 0f798ca3699..c43dbd826b6 100644 --- a/src/main/java/org/opentripplanner/api/resource/UpdaterStatusResource.java +++ b/src/main/java/org/opentripplanner/api/resource/UpdaterStatusResource.java @@ -2,7 +2,6 @@ import org.opentripplanner.standalone.OTPServer; import org.opentripplanner.standalone.Router; -import org.opentripplanner.updater.GraphUpdater; import org.opentripplanner.updater.GraphUpdaterManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,40 +18,272 @@ @Produces(MediaType.APPLICATION_JSON) public class UpdaterStatusResource { + private static final Logger LOG = LoggerFactory.getLogger(UpdaterStatusResource.class); - /** Choose short or long form of results. */ - @QueryParam("detail") private boolean detail = false; + /** + * Choose short or long form of results. + */ + @QueryParam("detail") + private boolean detail = false; Router router; - public UpdaterStatusResource (@Context OTPServer otpServer, @PathParam("routerId") String routerId) { + /** + * + * @param otpServer + * @param routerId id of the router + */ + public UpdaterStatusResource(@Context OTPServer otpServer, @PathParam("routerId") String routerId) { router = otpServer.getRouter(routerId); } - /** Return a list of all agencies in the graph. */ + /** + * + * Returns view of the important data on real time status updates. + * + * @return general information about updates(errors of the updates, types + * of updates, received and applied updates) + * + */ @GET - public Response getUpdaters () { + public Response getDescription() { GraphUpdaterManager updaterManager = router.graph.updaterManager; if (updaterManager == null) { - return Response.status(Response.Status.NOT_FOUND).entity("No updaters running.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); } - return Response.status(Response.Status.OK).entity(updaterManager.getUpdaterDescriptions()).build(); + return Response.status(Response.Status.OK).entity(updaterManager.getDescription()).build(); } - /** Return status for a specific updater. */ + /** + * + * Returns all agencies for specific feed. + * @param feedId id of the feed + * @return a list of all agencies in the graph for the specific feed, + * if such exist otherwise return null + */ + @GET + @Path("/agency/{feedId}") + public Response getAgencies(@PathParam("feedId") String feedId) { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getAgency(feedId)).build(); + } + + /** + * + * Returns information on specific updater. + * @param updaterId id of the updater + * @return Description for the updater with updater id. + * If such does not exist "Updater does not exist." is reported. + */ @GET @Path("/{updaterId}") - public Response getUpdaters (@PathParam("updaterId") int updaterId) { + public Response getTypeOfUpdater(@PathParam("updaterId") int updaterId) { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getUpdater(updaterId)).build(); + } + + /** + * + * Returns information on stream addresses. + * @return set of all correct stream addresses and list of information on all + * stream addresses. + */ + @GET + @Path("/stream") + public Response getStreamAddresses() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getStreamAddresses()).build(); + } + + /** + * + * Returns types of updaters. + * @return a list of all types of updaters + */ + @GET + @Path("/types") + public Response getTypes() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getAllTypes()).build(); + } + + /** + * + * Returns short information on the updater. + * @param updaterId id of the updater + * @return type of updater for updater id, + * if such does not exist "No updater." is reported + */ + @GET + @Path("/types/{id}") + public Response getTypePerId(@PathParam("id") int updaterId) { GraphUpdaterManager updaterManager = router.graph.updaterManager; if (updaterManager == null) { - return Response.status(Response.Status.NOT_FOUND).entity("No updaters running.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); } - GraphUpdater updater = updaterManager.getUpdater(updaterId); - if (updater == null) { - return Response.status(Response.Status.NOT_FOUND).entity("No updater with that ID.").build(); + return Response.status(Response.Status.OK).entity(updaterManager.getType(updaterId)).build(); + } + + /** + * + * Returns a list of received updates. + * @return all updates grouped by trip id, + * exposing the number of times each trip was updated + */ + @GET + @Path("/updates") + public Response getUpdates() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getReceived()).build(); + } + + /** + * + * Returns the number of updates per type. + */ + @GET + @Path("/updates/types") + public Response getUpdatesTypes() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getUpdatesTypes()).build(); + } + + /** + * Returns the number of applied updates. + * @return the number of applied updates grouped by trip id + */ + @GET + @Path("/updates/applied") + public Response getApplied() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); } - return Response.status(Response.Status.OK).entity(updater.getClass()).build(); + return Response.status(Response.Status.OK).entity(updaterManager.getApplied()).build(); } + /** + * Returns errors that occurred for non-applied updates. + * @return errors for updates grouped by its string representation + * exposing the number of occurrences for each error + */ + @GET + @Path("/updates/errors") + public Response getErrors() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getErrors()).build(); + } + + /** + * Returns errors that occurred for last block of non-applied updates. + * @return errors for the last block of updates + */ + @GET + @Path("/updates/errors/last") + public Response getLastErrors() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getLastErrors()).build(); + } + + /** + * + * Returns information on the last updated trip. + * @return time when update occurred, id of the trip being updated for + * each last received and last applied update + */ + @GET + @Path("/updates/last") + public Response getLastAppliedReceived() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getLastAppliedReceived()).build(); + } + + /** + * + * Returns the ratio between received and applied updates. + * @return the number of received and applied updates + */ + @GET + @Path("/updates/ratio") + public Response getReceivedApplied() { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getReceivedApplied()).build(); + } + + /** + * Returns the number of applied updates for feed and trip. + * @param feedId id of the feed + * @param tripId id of the trip + * @return the number of updates for specific feed and trip, grouped by type + */ + @GET + @Path("/updates/applied/feed/{feedId}/trip/{tripId}") + public Response getUpdatesPerFeedPerTrip(@PathParam("feedId") String feedId, @PathParam("tripId") String tripId) { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getTypeAppliedPerFeedPerTrip(feedId, tripId)).build(); + } + + /** + * Returns the number of applied updates for provided feed. + * @param feedId id of the feed + * @return the number of applied updates for specific feed + */ + @GET + @Path("/updates/applied/feed/{feedId}") + public Response getAppliedPerFeed(@PathParam("feedId") String feedId) { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getAppliedPerFeed(feedId)).build(); + } + + /** + * Returns the information on applied updates not older than provided parameter. + * @param minutes the number of minutes (all updates older than 60 minutes are discarded) + * @return information about applied updates that occurred in the last number of minutes + */ + @GET + @Path("updates/applied/{lastMinutes}") + public Response getAppliedLastMinutes(@PathParam("lastMinutes") int minutes) { + GraphUpdaterManager updaterManager = router.graph.updaterManager; + if (updaterManager == null) { + return Response.status(Response.Status.NOT_FOUND).entity("There are no updaters running.").build(); + } + return Response.status(Response.Status.OK).entity(updaterManager.getAppliedLastMinutes(minutes)).build(); + } } diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 755d877a239..73f2066d280 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -116,6 +116,8 @@ public class Graph implements Serializable { public final Deduplicator deduplicator = new Deduplicator(); + private static Set correctStreamAddresses = new HashSet<>(); + /** * Map from GTFS ServiceIds to integers close to 0. Allows using BitSets instead of Set. * An empty Map is created before the Graph is built to allow registering IDs from multiple feeds. @@ -1033,7 +1035,7 @@ public SampleFactory getSampleFactory() { * * This speeds up calculation, but problem is that median needs to have all of latitudes/longitudes * in memory, this can become problematic in large installations. It works without a problem on New York State. - * @see GraphEnvelope + * //@see GraphEnvelope */ public void calculateTransitCenter() { if (hasTransit) { @@ -1070,4 +1072,12 @@ public long getTransitServiceStarts() { public long getTransitServiceEnds() { return transitServiceEnds; } -} + + public Set getCorrectStreamAddresses() { + return correctStreamAddresses; + } + + public void addCorrectStreamAddress(String correctStreamAddress) { + correctStreamAddresses.add(correctStreamAddress); + } +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java b/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java index ad583ac216c..c16a04b355a 100644 --- a/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java +++ b/src/main/java/org/opentripplanner/routing/graph/GraphIndex.java @@ -563,16 +563,20 @@ public Timetable currentUpdatedTimetableForTripPattern (TripPattern tripPattern) * stops -- no guessing is reasonable without that information. */ public void clusterStops() { - switch (graph.stopClusterMode) { - case "parentStation": - clusterByParentStation(); - break; - case "proximity": - clusterByProximity(); - break; - default: - clusterByProximity(); - } + if (graph.stopClusterMode != null) { + switch (graph.stopClusterMode) { + case "parentStation": + clusterByParentStation(); + break; + case "proximity": + clusterByProximity(); + break; + default: + clusterByProximity(); + } + } else { + clusterByProximity(); + } } private void clusterByProximity() { diff --git a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceFactory.java b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceFactory.java index 975c4a2759d..f8a10759177 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceFactory.java +++ b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceFactory.java @@ -35,9 +35,9 @@ the License, or (props, at your option) any later version. /** * Implements the default GTFS fare rules as described in * http://groups.google.com/group/gtfs-changes/msg/4f81b826cb732f3b - * + * * @author novalis - * + * */ public class DefaultFareServiceFactory implements FareServiceFactory { @@ -111,9 +111,9 @@ public void configure(JsonNode config) { /** * Build a specific FareServiceFactory given the config node, or fallback to the default if none * specified. - * + * * Accept different formats. Examples: - * + * *
      * { fares : "seattle" }
      * --------------------------
@@ -172,6 +172,9 @@ public static FareServiceFactory fromConfig(JsonNode config) {
         case "bike-rental-time-based":
             retval = new TimeBasedBikeRentalFareServiceFactory();
             break;
+        case "dutch":
+            retval = new DutchFareServiceFactory();
+            break;
         case "san-francisco":
             retval = new SFBayFareServiceFactory();
             break;
diff --git a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java
index 0436fb66045..d53a3888902 100644
--- a/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java
+++ b/src/main/java/org/opentripplanner/routing/impl/DefaultFareServiceImpl.java
@@ -217,13 +217,13 @@ public Fare getCost(GraphPath path) {
             FareType fareType = kv.getKey();
             Collection fareRules = kv.getValue();
 
-            // pick up a random currency from fareAttributes,
-            // we assume that all tickets use the same currency
+            // hardcode the fareAttribute currency, because the configuration
+            // parameter is ignored, and the assumption that all tickets are in the same currency
+            // doesn't hold for a feed with distance units
             Currency currency = null;
             WrappedCurrency wrappedCurrency = null;
             if (fareRules.size() > 0) {
-                currency = Currency.getInstance(fareRules.iterator().next().getFareAttribute()
-                        .getCurrencyType());
+                currency = Currency.getInstance("EUR");
                 wrappedCurrency = new WrappedCurrency(currency);
             }
             hasFare = populateFare(fare, currency, fareType, rides, fareRules);
diff --git a/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceFactory.java b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceFactory.java
new file mode 100644
index 00000000000..f7f2d815fef
--- /dev/null
+++ b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceFactory.java
@@ -0,0 +1,23 @@
+/* This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License
+ as published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see . */
+
+package org.opentripplanner.routing.impl;
+
+import org.opentripplanner.routing.services.FareService;
+
+public class DutchFareServiceFactory extends DefaultFareServiceFactory {
+    @Override
+    public FareService makeFareService() { 
+        return new DutchFareServiceImpl(regularFareRules.values());
+    }
+}
diff --git a/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java
new file mode 100644
index 00000000000..9659b128efe
--- /dev/null
+++ b/src/main/java/org/opentripplanner/routing/impl/DutchFareServiceImpl.java
@@ -0,0 +1,279 @@
+/* This program is free software: you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License
+ as published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see . */
+
+package org.opentripplanner.routing.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.geotools.xml.xsi.XSISimpleTypes;
+import org.onebusaway.gtfs.model.AgencyAndId;
+import org.opentripplanner.common.model.P2;
+import org.opentripplanner.routing.core.FareRuleSet;
+import org.opentripplanner.routing.core.Fare.FareType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DutchFareServiceImpl extends DefaultFareServiceImpl {
+
+    public DutchFareServiceImpl(Collection regularFareRules) {
+        addFareRules(FareType.regular, regularFareRules);
+    }
+
+    private static final long serialVersionUID = 1L;
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(DutchFareServiceImpl.class);
+
+    public static final int TRANSFER_DURATION = 60 * 35; /* tranfers within 35 min won't require a new base fare */
+
+    /* The Netherlands has an almost uniform system for electronic ticketing using a NFC-card, branded as OV-chipkaart.
+     *
+     * To travel through all modes in The Netherlands a uses has two products on their card:
+     * 1) Easy Trip, to travel with all operators except Dutch Railways
+     * 2) Reizen op Saldo, for Dutch Railways which should be explicitly
+     *    loaded on a card and requires the user to select a first or second class.
+     *
+     * Check-in and check-out is done using validators. For our calculation on transerfer time it matters if this
+     * validator is inside the vehicle - we have to wait for the validator to arrive - or we can already check in
+     * on a validator present on the stop, and wait for the vehicle to arrive.
+     *
+     * Reizen op Saldo is limited to Dutch Railways, and always allows to validate inside the stations.
+     * Additionally in the following cases validators are also on the platform or stations.
+     *  - Metro of Amsterdam operated by GVB
+     *  - Metro of Rotterdam operated by RET
+     *  - Lightrail of Utrecht operated by Qbuzz
+     *  - All heavy rail services operated by Arriva, Breng, Connexxion, NS, Syntus and Veolia.
+     *
+     * Leaving the platform or stations implies that the traveler must check-out. Thus a transfer will play a role.
+     *
+     * All other modes by these operators have validators inside the vehicle.
+     *
+     * TODO: It is an optimisation to be able to check-in early. And most likely only be visible by a trip which is
+     * artificially created to test for this implementation.
+     *
+     * Long-Distance-Discount for trains
+     * Between train operators in The Netherlands long distance discount applies under the following condition:
+     *  - a trip between two train operators takes places within 35 minutes
+     *
+     * First the route is maximised per operator.
+     *
+     * The price of the next operator consists of:
+     *     globally traveled units = 100 previous operator(s)
+     *     locally  traveled units = 10
+     *
+     *     (DutchRailwaysPrice(0 + 100) - DutchRailwaysPrice(0)) + (ArrivaPrice(100 + 10) - ArrivaPrice(100))
+     */
+
+    private class UnitsFareZone {
+        public int units;
+        public String fareZone;
+
+        public UnitsFareZone(int units, String fareZone) {
+            this.units = units;
+            this.fareZone = fareZone;
+        }
+    }
+
+    private UnitsFareZone getUnitsByZones(String agencyId, String startZone, String endZone, Collection fareRules) {
+        P2 od = new P2(startZone, endZone);
+
+        LOG.warn("Search " + startZone + " and " + endZone);
+
+        String fareIdStartsWith = agencyId + "::";
+
+        for (FareRuleSet ruleSet : fareRules) {
+            if (ruleSet.getFareAttribute().getId().getId().startsWith(fareIdStartsWith) &&
+                ruleSet.getOriginDestinations().contains(od)) {
+                String fareId = ruleSet.getFareAttribute().getId().getId();
+                String[] parts = fareId.split("::");
+                String fareZone = parts[1];
+
+                LOG.warn("Between " + startZone + " and " + endZone + ": " + (int) ruleSet.getFareAttribute().getPrice() + " (" + fareZone + ")");
+                return new UnitsFareZone((int) ruleSet.getFareAttribute().getPrice(), fareZone);
+            }
+        }
+
+        LOG.warn("Can't find units between " + startZone + " and " + endZone);
+
+        /* TODO: Raise Exception */
+
+        return null;
+    }
+    
+    private float getCostByUnits(String fareZone, int units, int prevSumUnits, Collection fareRules) {
+        if (units == 0) {
+            return 0f;
+        }
+
+        /* Train-units cannot exceed 250 units; http://wiki.ovinnederland.nl/wiki/Tariefeenheid#Tarieven_NS */
+        if (units > 250) {
+            units = 250;
+        }
+
+        float cost = 0f;
+
+        String fareId = fareZone + ":" + (units + prevSumUnits);
+        for (FareRuleSet ruleSet : fareRules) {
+            if (ruleSet.getFareAttribute().getId().getId().equals(fareId)) {
+                cost = ruleSet.getFareAttribute().getPrice();
+                break;
+            }
+        }
+
+        if (cost == 0f) {
+            LOG.warn("Can't find price for " + fareZone + " with " + units + " units");
+
+        } else if (prevSumUnits > 0) {
+
+            fareId = fareZone + ":" + prevSumUnits;
+            for (FareRuleSet ruleSet : fareRules) {
+                if (ruleSet.getFareAttribute().getId().getId().equals(fareId)) {
+                    cost -= ruleSet.getFareAttribute().getPrice();
+                    break;
+                }
+            }
+
+            LOG.warn("Can't find price for " + fareZone + " with " + prevSumUnits + " units");
+
+            return Float.POSITIVE_INFINITY;
+        }
+
+        return cost;
+    }
+
+    private float getEasyTripFareByLineFromTo(String route, String firstStop, String lastStop,
+                                              boolean entranceFee, Collection fareRules) {
+
+        float cost = Float.POSITIVE_INFINITY;
+
+        String fareId = route + ":" + firstStop + "-" + lastStop;
+
+        for (FareRuleSet ruleSet : fareRules) {
+            if (ruleSet.getFareAttribute().getId().getId().equals(fareId)) {
+                cost = ruleSet.getFareAttribute().getPrice();
+                break;
+            }
+        }
+
+        if (cost == Float.POSITIVE_INFINITY) {
+            LOG.warn("Can't find price for " + firstStop + " to " + lastStop + " operated on " + route);
+
+            return cost;
+        }
+
+        if (entranceFee) cost += 89f; /* TODO: Configurable? */
+
+        return cost;
+    }
+
+    @Override
+    protected float getLowestCost(FareType fareType, List rides,
+                                  Collection fareRules) {
+
+        float cost = 0f;
+
+	    int units = 0;
+        int prevSumUnits = 0;
+
+        boolean mustHaveCheckedOut = false;
+        String startTariefEenheden = null;
+        String endTariefEenheden = null;
+        String lastAgencyId = null;
+        String lastFareZone = null;
+
+        long alightedEasyTrip = 0;
+        long alightedTariefEenheden = 0;
+
+        for (Ride ride : rides) {
+            LOG.warn(String.format("%s %s %s %s %s %s", ride.startZone, ride.endZone, ride.firstStop, ride.lastStop, ride.route, ride.agency));
+
+            if (ride.agency.startsWith("IFF:")) {
+                LOG.warn("1. Trains");
+		        /* In Reizen op Saldo we will try to fares as long as possible. */
+
+                /* If our previous agency isn't this agency, then we must have checked out */
+                mustHaveCheckedOut |= !ride.agency.equals(lastAgencyId);
+
+                /* When a user has checked out, we first calculate the units made until then. */
+                if (mustHaveCheckedOut && lastAgencyId != null) {
+                    LOG.warn("2. Must have checked out from a station");
+                    UnitsFareZone unitsFareZone = getUnitsByZones(lastAgencyId, startTariefEenheden, endTariefEenheden, fareRules);
+                    if (unitsFareZone == null) return Float.POSITIVE_INFINITY;
+                    lastFareZone = unitsFareZone.fareZone;
+                    units += unitsFareZone.units;
+                    startTariefEenheden = ride.startZone;
+                    mustHaveCheckedOut = false;
+                }
+
+        		/* The entrance Fee applies if the transfer time ends before the new trip starts. */
+                if ((alightedTariefEenheden + TRANSFER_DURATION) < ride.startTime) {
+                    LOG.warn("3. Exceeded Transfer Time");
+                    cost += getCostByUnits(lastFareZone, units, prevSumUnits, fareRules);
+                    if (cost == Float.POSITIVE_INFINITY) return cost;
+
+                    startTariefEenheden = ride.startZone;
+                    units = 0;
+                    prevSumUnits = 0;
+                    mustHaveCheckedOut = false;
+
+                } else if (!ride.agency.equals(lastAgencyId)) {
+                    LOG.warn("4. Swiched Rail Agency");
+
+                    cost += getCostByUnits(lastFareZone, units, prevSumUnits, fareRules);
+                    if (cost == Float.POSITIVE_INFINITY) return cost;
+
+                    prevSumUnits += units;
+                    units = 0;
+                    startTariefEenheden = ride.startZone;
+                }
+
+                alightedTariefEenheden = ride.endTime;
+                endTariefEenheden = ride.endZone;
+                lastAgencyId = ride.agency;
+
+            } else {
+                LOG.warn("5. Easy Trip");
+
+                /* We are now on Easy Trip, so we must have checked-out from Reizen op Saldo, if we were on it */
+                mustHaveCheckedOut = (startTariefEenheden != null);
+
+                /* The entranceFee applies if the transfer time ends before the new trip starts. */
+                boolean entranceFee = ((alightedEasyTrip + TRANSFER_DURATION) < ride.startTime);
+
+                /* EasyTrip will always calculate its price per leg */
+                cost += getEasyTripFareByLineFromTo(ride.route.getId(), ride.startZone, ride.endZone, entranceFee, fareRules);
+                if (cost == Float.POSITIVE_INFINITY) return cost;
+
+                alightedEasyTrip = ride.endTime;
+            }
+        }
+
+        LOG.warn("6. Final");
+        if (lastAgencyId != null) {
+            UnitsFareZone unitsFareZone = getUnitsByZones(lastAgencyId, startTariefEenheden, endTariefEenheden, fareRules);
+            if (unitsFareZone == null) return Float.POSITIVE_INFINITY;
+
+            lastFareZone = unitsFareZone.fareZone;
+            units += unitsFareZone.units;
+            cost += getCostByUnits(lastFareZone, units, prevSumUnits, fareRules);
+        }
+
+        if (cost == Float.POSITIVE_INFINITY) return cost;
+
+        return cost / 100f;
+    }
+}
diff --git a/src/main/java/org/opentripplanner/standalone/OTPMain.java b/src/main/java/org/opentripplanner/standalone/OTPMain.java
index 35c595afeb9..73a223e1418 100644
--- a/src/main/java/org/opentripplanner/standalone/OTPMain.java
+++ b/src/main/java/org/opentripplanner/standalone/OTPMain.java
@@ -114,7 +114,7 @@ public void run() {
                     Graph graph = graphBuilder.getGraph();
                     graph.index(new DefaultStreetVertexIndexFactory());
                     // FIXME set true router IDs
-                    graphService.registerGraph("", new MemoryGraphSource("", graph));
+                    graphService.registerGraph("1", new MemoryGraphSource("1", graph));
                 }
             } else {
                 LOG.error("An error occurred while building the graph. Exiting.");
diff --git a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java
index 211e3a88304..f4bd7dfa280 100644
--- a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java
+++ b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java
@@ -13,12 +13,15 @@ the License, or (at your option) any later version.
 
 package org.opentripplanner.updater;
 
-import com.beust.jcommander.internal.Lists;
+import com.amazonaws.util.json.JSONException;
+import com.amazonaws.util.json.JSONObject;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+
+import java.util.*;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -28,34 +31,35 @@ the License, or (at your option) any later version.
 import java.util.concurrent.TimeUnit;
 
 import org.opentripplanner.routing.graph.Graph;
+import org.opentripplanner.updater.stoptime.TimetableSnapshotSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * This class is attached to the graph:
- * 
+ *
  * 
  * GraphUpdaterManager updaterManager = graph.getUpdaterManager();
  * 
- * + * * Each updater will run in its own thread. When changes to the graph have to be made by these * updaters, this should be done via the execute method of this manager to prevent race conditions * between graph write operations. - * + * */ public class GraphUpdaterManager { private static Logger LOG = LoggerFactory.getLogger(GraphUpdaterManager.class); - + /** * Text used for naming threads when the graph lacks a routerId. */ private static String DEFAULT_ROUTER_ID = "(default)"; - + /** * Thread factory used to create new threads. */ - + private ThreadFactory threadFactory; /** @@ -80,22 +84,25 @@ public class GraphUpdaterManager { * Parent graph of this manager */ Graph graph; + TimetableSnapshotSource snapshot; + ObjectMapper mapper = new ObjectMapper(); /** * Constructor - * + * * @param graph is parent graph of manager */ public GraphUpdaterManager(Graph graph) { this.graph = graph; - + String routerId = graph.routerId; if(routerId == null || routerId.isEmpty()) routerId = DEFAULT_ROUTER_ID; - + threadFactory = new ThreadFactoryBuilder().setNameFormat("GraphUpdater-" + routerId + "-%d").build(); scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory); updaterPool = Executors.newCachedThreadPool(threadFactory); + } public void stop() { @@ -134,7 +141,7 @@ public void stop() { /** * Adds an updater to the manager and runs it immediately in its own thread. - * + * * @param updater is the updater to add and run */ public void addUpdater(final GraphUpdater updater) { @@ -160,7 +167,7 @@ public void run() { * This is the method to use to modify the graph from the updaters. The runnables will be * scheduled after each other, guaranteeing that only one of these runnables will be active at * any time. - * + * * @param runnable is a graph writer runnable */ public void execute(GraphWriterRunnable runnable) { @@ -171,11 +178,11 @@ public void execute(GraphWriterRunnable runnable) { * This is another method to use to modify the graph from the updaters. It behaves like execute, * but blocks until the runnable has been executed. This might be particularly useful in the * setup method of an updater. - * + * * @param runnable is a graph writer runnable * @throws ExecutionException * @throws InterruptedException - * @see GraphUpdaterManager.execute + * //@see GraphUpdaterManager.execute */ public void executeBlocking(GraphWriterRunnable runnable) throws InterruptedException, ExecutionException { @@ -203,15 +210,16 @@ public void run() { public int size() { return updaterList.size(); + } /** - * Just an example of fetching status information from the graph updater manager to expose it in a web service. - * More useful stuff should be added later. + * @return All stream addresses */ - public Map getUpdaterDescriptions () { - Map ret = Maps.newTreeMap(); + private Map getAllStreamAddresses() { + Map ret = new HashMap<>(); int i = 0; + for (GraphUpdater updater : updaterList) { ret.put(i++, updater.toString()); } @@ -219,11 +227,163 @@ public Map getUpdaterDescriptions () { } /** - * Just an example of fetching status information from the graph updater manager to expose it in a web service. - * More useful stuff should be added later. + * @return correct and all stream addresses + */ + public String getStreamAddresses() { + Map combined = new HashMap<>(); + combined.put("Correct stream addresses", graph.getCorrectStreamAddresses()); + combined.put("All stream addresses", getAllStreamAddresses()); + return callMapper(combined); + } + + /** + * @return most of the important statistical data + */ + public String getDescription() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap); + } + + /** + * @param id + * @return Description of GraphUpdater for id + */ + public String getUpdater(int id) { + String ret = "Updater does not exist."; + if (id < updaterList.size()) + ret = updaterList.get(id).toString(); + return callMapper(ret); + } + + /** + * @param id of GraphUpdater whose type is required + * @return type of GraphUpdater + */ + public String getType(int id) { + GraphUpdater updater = null; + if (id < updaterList.size()) + updater = updaterList.get(id); + return callMapper(updater == null ? "No updater." : updater.getClass().toString()); + } + + /** + * @return all types of updaters + */ + public String getAllTypes() { + HashMap retVal = new HashMap<>(); + int i = 0; + for (GraphUpdater up : updaterList) + retVal.put(i++, up.getClass().getName()); + return callMapper(retVal); + } + + /** + * @return the number of all received updates per tripId + */ + public String getReceived() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getReceived()); + } + + /** + * @return the number of all applied updates per tripId + */ + public String getApplied() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getApplied()); + } + + /** + * @return error for non applied update + */ + public String getErrors() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getErrors()); + } + + /** + * @return errors for last update block + */ + public String getLastErrors() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getLastErrors()); + } + + /** + * @return the number of updates per each type + */ + public String getUpdatesTypes() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getTypes()); + } + + /** + * @return the timestamp in milliseconds for last applied and received updates and their trip id + */ + public String getLastAppliedReceived() { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getLastAppliedReceived()); + } + + /** + * @return the ratio of received and applied updates */ - public GraphUpdater getUpdater (int id) { - if (id >= updaterList.size()) return null; - return updaterList.get(id); + public String getReceivedApplied(){ + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getReceivedApplied()); } -} + + /** + * @param feedId + * @return the number of updates per provided feedId + * all updates older than @see TimetableSnapshotSource 60 minutes are discarded. + */ + public String getAppliedPerFeed(String feedId) { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getAppliedPerFeed(feedId)); + } + + /** + * @param feedId + * @param tripId + * @return the number of updates grouped by type per feedId and triId + * all updates older than @see TimetableSnapshotSource 60 minutes are discarded. + */ + public String getTypeAppliedPerFeedPerTrip(String feedId, String tripId) { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getAppliedTypePerFeedPerTrip(feedId, tripId)); + } + + /** + * @param feedId + * @return the description of agency for feedId + */ + public String getAgency(String feedId) { + return callMapper(graph.getAgencies(feedId)); + } + + /** + * @param minutes + * @return all updates that happened in the last number of minutes + * all updates older than @see TimetableSnapshotSource 60 minutes are discarded. + */ + public String getAppliedLastMinutes(int minutes) { + TimetableSnapshotSource snap = graph.timetableSnapshotSource; + return callMapper(snap.getAppliedLastMinutes(minutes)); + } + + /** + * @param o any object + * @return String representation of an Object in Json format + */ + private String callMapper(Object o) { + try { + return mapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return "Something went wrong with Json."; + } + } + + +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java b/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java index 9eed8dbb695..7c3e8f98ff3 100644 --- a/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java +++ b/src/main/java/org/opentripplanner/updater/stoptime/GtfsRealtimeHttpTripUpdateSource.java @@ -46,6 +46,7 @@ public class GtfsRealtimeHttpTripUpdateSource implements TripUpdateSource, JsonC private String feedId; private String url; + Graph graph; @Override public void configure(Graph graph, JsonNode config) throws Exception { @@ -53,6 +54,7 @@ public void configure(Graph graph, JsonNode config) throws Exception { if (url == null) { throw new IllegalArgumentException("Missing mandatory 'url' parameter"); } + this.graph = graph; this.url = url; this.feedId = config.path("feedId").asText(); } @@ -69,7 +71,7 @@ public List getUpdates() { // Decode message feedMessage = FeedMessage.PARSER.parseFrom(is); feedEntityList = feedMessage.getEntityList(); - + // Change fullDataset value if this is an incremental update if (feedMessage.hasHeader() && feedMessage.getHeader().hasIncrementality() @@ -77,7 +79,8 @@ public List getUpdates() { .equals(GtfsRealtime.FeedHeader.Incrementality.DIFFERENTIAL)) { fullDataset = false; } - + graph.addCorrectStreamAddress(url); + // Create List of TripUpdates updates = new ArrayList(feedEntityList.size()); for (FeedEntity feedEntity : feedEntityList) { @@ -86,6 +89,7 @@ public List getUpdates() { } } catch (Exception e) { LOG.warn("Failed to parse gtfs-rt feed from " + url + ":", e); + graph.timetableSnapshotSource.addError("Failed to parse gtfs-rt feed from " + url); } return updates; } diff --git a/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java index 196ee8eebff..5a2a987ed6f 100644 --- a/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/stoptime/TimetableSnapshotSource.java @@ -13,16 +13,30 @@ the License, or (at your option) any later version. package org.opentripplanner.updater.stoptime; +import java.sql.Timestamp; import java.text.ParseException; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.Calendar; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; - +import java.util.function.BiFunction; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import com.google.common.collect.ImmutableMap; +import org.apache.lucene.util.fst.PairOutputs; +import org.geotools.xml.xsi.XSISimpleTypes; +import org.joda.time.DateTime; import org.onebusaway.gtfs.model.Agency; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Route; @@ -43,11 +57,14 @@ the License, or (at your option) any later version. import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + /** * This class should be used to create snapshots of lookup tables of realtime data. This is * necessary to provide planning threads a consistent constant view of a graph with realtime data at @@ -66,6 +83,7 @@ public class TimetableSnapshotSource { */ private static final long MAX_ARRIVAL_DEPARTURE_TIME = 48 * 60 * 60; + @JsonIgnore public int logFrequency = 2000; private int appliedBlockCount = 0; @@ -75,6 +93,7 @@ public class TimetableSnapshotSource { * snapshot, just return the same one. Throttles the potentially resource-consuming task of * duplicating a TripPattern -> Timetable map and indexing the new Timetables. */ + @JsonIgnore public int maxSnapshotFrequency = 1000; // msec /** @@ -100,7 +119,10 @@ public class TimetableSnapshotSource { */ private final TripPatternCache tripPatternCache = new TripPatternCache(); - /** Should expired realtime data be purged from the graph. */ + /** + * Should expired realtime data be purged from the graph. + */ + @JsonIgnore public boolean purgeExpiredData = true; protected ServiceDate lastPurgeDate = null; @@ -113,8 +135,32 @@ public class TimetableSnapshotSource { private final Agency dummyAgency; + @JsonIgnore public GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher; + + private final List errorList = new ArrayList<>(); + private final List appliedList = new ArrayList<>(); + private final List receivedList = new ArrayList(); + private final List typeList = new ArrayList<>(); + private final List appliedSpecList = new ArrayList<>(); + + public static Map errorMap = new HashMap<>(); + public static Map appliedMap = new HashMap<>(); + public static Map receivedMap = new HashMap<>(); + public static Map typeMap = new HashMap<>(); + + private static Timestamp lastUpdate; + private static String lastUpdatedTrip; + private static Timestamp lastReceived; + private static String lastReceivedTrip; + private static int receivedNum = 0; + private static int appliedNum = 0; + private static Set lastErrors = new HashSet<>(); + + + private Runnable runnable; + public TimetableSnapshotSource(final Graph graph) { timeZone = graph.getTimeZone(); graphIndex = graph.index; @@ -123,14 +169,16 @@ public TimetableSnapshotSource(final Graph graph) { dummyAgency = new Agency(); dummyAgency.setId(""); dummyAgency.setName(""); + } /** * @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the - * timetable objects it references are guaranteed to never change, so the requesting - * thread is provided a consistent view of all TripTimes. The routing thread need only - * release its reference to the snapshot to release resources. + * timetable objects it references are guaranteed to never change, so the requesting + * thread is provided a consistent view of all TripTimes. The routing thread need only + * release its reference to the snapshot to release resources. */ + @JsonIgnore public TimetableSnapshot getTimetableSnapshot() { TimetableSnapshot snapshotToReturn; @@ -170,18 +218,19 @@ private TimetableSnapshot getTimetableSnapshot(final boolean force) { /** * Method to apply a trip update list to the most recent version of the timetable snapshot. A * GTFS-RT feed is always applied against a single static feed (indicated by feedId). -<<<<<<< HEAD - * -======= - * + * <<<<<<< HEAD + *

+ * ======= + *

* However, multi-feed support is not completed and we currently assume there is only one static * feed when matching IDs. + *

+ * >>>>>>> 7296be8ffd532a13afb0bec263a9f436ab787022 * ->>>>>>> 7296be8ffd532a13afb0bec263a9f436ab787022 - * @param graph graph to update (needed for adding/changing stop patterns) + * @param graph graph to update (needed for adding/changing stop patterns) * @param fullDataset true iff the list with updates represent all updates that are active right - * now, i.e. all previous updates should be disregarded - * @param updates GTFS-RT TripUpdate's that should be applied atomically + * now, i.e. all previous updates should be disregarded + * @param updates GTFS-RT TripUpdate's that should be applied atomically * @param feedId */ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final List updates, final String feedId) { @@ -192,7 +241,7 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final // Acquire lock on buffer bufferLock.lock(); - + lastErrors.clear(); try { if (fullDataset) { // Remove all updates from the buffer @@ -201,7 +250,12 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final LOG.debug("message contains {} trip updates", updates.size()); int uIndex = 0; + for (TripUpdate tripUpdate : updates) { + receivedNum++; + lastReceived = new Timestamp(System.currentTimeMillis()); + lastReceivedTrip = tripUpdate.getTrip().getTripId(); + receivedList.add(lastReceivedTrip); if (fuzzyTripMatcher != null && tripUpdate.hasTrip()) { final TripDescriptor trip = fuzzyTripMatcher.match(feedId, tripUpdate.getTrip()); tripUpdate = tripUpdate.toBuilder().setTrip(trip).build(); @@ -209,6 +263,8 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final if (!tripUpdate.hasTrip()) { LOG.warn("Missing TripDescriptor in gtfs-rt trip update: \n{}", tripUpdate); + errorList.add("Missing TripDescriptor in gtfs-rt trip update: " + tripUpdate); + lastErrors.add("Missing TripDescriptor in gtfs-rt trip update"); continue; } @@ -220,6 +276,8 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final serviceDate = ServiceDate.parseString(tripDescriptor.getStartDate()); } catch (final ParseException e) { LOG.warn("Failed to parse start date in gtfs-rt trip update: \n{}", tripUpdate); + errorList.add("Failed to parse start date in gtfs-rt trip update: " + tripUpdate); + lastErrors.add("Failed to parse start date in gtfs-rt trip update"); continue; } } else { @@ -256,14 +314,25 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final if (applied) { appliedBlockCount++; + appliedNum++; + lastUpdate = new Timestamp(System.currentTimeMillis()); + lastUpdatedTrip = tripUpdate.getTrip().getTripId(); + appliedList.add(lastUpdatedTrip); + typeList.add(tripScheduleRelationship.toString()); + appliedSpecList.add(new AppliedEntity(tripScheduleRelationship.toString(), feedId, lastUpdatedTrip)); + } else { LOG.warn("Failed to apply TripUpdate."); LOG.trace(" Contents: {}", tripUpdate); + errorList.add("Failed to apply TripUpdate. ");// + tripUpdate.getTrip().getTripId() + "."); + lastErrors.add("Failed to apply TripUpdate."); } if (appliedBlockCount % logFrequency == 0) { LOG.info("Applied {} trip updates.", appliedBlockCount); + } + } LOG.debug("end of update message"); @@ -276,12 +345,181 @@ public void applyTripUpdates(final Graph graph, final boolean fullDataset, final } else { getTimetableSnapshot(false); } + /** + * Every minute Maps for update statistics are updated + */ + if (runnable == null) { + runnable = new Runnable() { + public void run() { + LOG.info("Updating Maps."); + + errorMap = mergeMaps(toMap(errorList), errorMap); + appliedMap = mergeMaps(toMap(appliedList), appliedMap); + receivedMap = mergeMaps(toMap(receivedList), receivedMap); + typeMap = mergeMaps(toMap(typeList), typeMap); + + removeExpired(appliedSpecList, 60); + + errorList.clear(); + appliedList.clear(); + typeList.clear(); + receivedList.clear(); + + } + }; + ScheduledExecutorService service = Executors + .newSingleThreadScheduledExecutor(); + service.scheduleWithFixedDelay(runnable, 1, 1, TimeUnit.MINUTES); + } + + } finally { // Always release lock bufferLock.unlock(); } } + /** + * @param list that represents errors or updates + * @return Map which will group the same messages, and count the same occurrences + */ + static private Map toMap(List list) { + if (list == null || list.size() == 0) + return null; + return list.stream().collect(Collectors.groupingBy(s -> s, Collectors.counting())); + } + + /** + * + * @param list + * @return map grouped by occurrences of entry in the list + */ + static private Map toMapObject(List list) { + if (list == null || list.size() == 0) + return null; + return list.stream().collect(Collectors.groupingBy(s -> s, Collectors.counting())); + } + + /** + * + * @param list + * @return map of applied updates groupedby type, feedid and tripid + */ + static private Map>> toMap2(List list) { + if (list == null || list.size() == 0) + return null; + return list.stream().collect(Collectors.groupingBy(AppliedEntity::getType, Collectors.groupingBy(AppliedEntity::getFeedId, Collectors.groupingBy(AppliedEntity::getTripId, Collectors.counting())))); + + } + + /** + * + * @param list + * @param feedId + * @param tripId + * @return map formed from a list, grouped by occurrences of types per specific feedId and tripId + */ + static private Map toMap2(List list, String feedId, String tripId) { + if (feedId == null || tripId == null) { + return null; + } + FeedTrip ft = new FeedTrip(feedId, tripId); + BiFunction, List> byFeedTrip = + (feedtrip, l) -> l.stream() + .filter(a -> !(a.getFeedTrip().equals(feedtrip))) + .collect(Collectors.toList()); + return byFeedTrip.apply(ft, list).stream().collect(Collectors.groupingBy(AppliedEntity::getType, Collectors.counting())); + } + + /** + * + * @param list + * @param type + * @param id + * @return Object which is map formed from a list grouped by type if id is null, or + * if id not null the value of entry with key id + */ + static private Object toMap(List list, String type, String id) { + if (list == null || list.size() == 0) + return null; + Map ret; + switch (type) { + case "feedId": + ret = list.stream().collect(Collectors.groupingBy(AppliedEntity::getFeedId, Collectors.counting())); + break; + case "tripId": + ret = list.stream().collect(Collectors.groupingBy(AppliedEntity::getTripId, Collectors.counting())); + break; + case "type": + ret = list.stream().collect(Collectors.groupingBy(AppliedEntity::getType, Collectors.counting())); + break; + default: + LOG.error("Wrong parameter for Generating Map."); + return null; + } + if (id == null) + return ret; + return ret.get(id); + } + + /** + * @param firstMap + * @param secondMap + * @return Map that is concatenation of two maps grouped by the same messages - key, and count the same occurrences -value in the map + */ + static private Map mergeMaps(Map firstMap, Map secondMap) { + if (firstMap == null) + return secondMap; + else if (secondMap == null) + return firstMap; + Map result = Stream.concat(firstMap.entrySet().stream(), secondMap.entrySet().stream()) + .collect(Collectors.toMap( + entry -> entry.getKey(), // The key + entry -> entry.getValue(), // The value + // The "merger" as a method reference + (a, b) -> a + b + ) + ); + return result; + } + + /** + * removes all older than number of minutes data from list c + * @param c + * @param minutes + */ + static void removeExpired(List c, long minutes) { + if (c == null) + return; + Timestamp expirationDate = new Timestamp(System.currentTimeMillis() - minutes * 1000 * 60); + + for (Iterator it = c.iterator(); it.hasNext(); ) + if (((AppliedEntity) it.next()).getTimestamp().before(expirationDate)) + it.remove(); + + } + + /** + * + * @param minutes + * @return all applied updates that happened int he last number of minutes + */ + @JsonIgnore + private List getSelected(int minutes) { + if (appliedSpecList == null) + return null; + ArrayList c = new ArrayList(appliedSpecList); + + Timestamp expirationDate = new Timestamp(System.currentTimeMillis() - minutes * 1000 * 60); + + for (Iterator it = c.iterator(); it.hasNext(); ) + if (((AppliedEntity) it.next()).getTimestamp().before(expirationDate)) + it.remove(); + + return c; + } + + /** * Determine how the trip update should be handled. * @@ -306,7 +544,7 @@ private TripDescriptor.ScheduleRelationship determineTripScheduleRelationship(fi final StopTimeUpdate.ScheduleRelationship stopScheduleRelationship = stopTimeUpdate .getScheduleRelationship(); if (stopScheduleRelationship.equals(StopTimeUpdate.ScheduleRelationship.SKIPPED) - // TODO: uncomment next line when StopTimeUpdate.ScheduleRelationship.ADDED exists + // TODO: uncomment next line when StopTimeUpdate.ScheduleRelationship.ADDED exists // || stopScheduleRelationship.equals(StopTimeUpdate.ScheduleRelationship.ADDED) ) { hasModifiedStops = true; @@ -364,7 +602,7 @@ private boolean handleScheduledTrip(final TripUpdate tripUpdate, final String fe * @return true iff successful */ private boolean validateAndHandleAddedTrip(final Graph graph, final TripUpdate tripUpdate, - final String feedId, final ServiceDate serviceDate) { + final String feedId, final ServiceDate serviceDate) { // Preconditions Preconditions.checkNotNull(graph); Preconditions.checkNotNull(tripUpdate); @@ -552,7 +790,7 @@ private boolean isStopSkipped(final StopTimeUpdate stopTimeUpdate) { * @return true iff successful */ private boolean handleAddedTrip(final Graph graph, final TripUpdate tripUpdate, final List stops, - final String feedId, final ServiceDate serviceDate) { + final String feedId, final ServiceDate serviceDate) { // Preconditions Preconditions.checkNotNull(stops); Preconditions.checkArgument(tripUpdate.getStopTimeUpdateCount() == stops.size(), @@ -623,8 +861,8 @@ private boolean handleAddedTrip(final Graph graph, final TripUpdate tripUpdate, * @return true iff successful */ private boolean addTripToGraphAndBuffer(final String feedId, final Graph graph, final Trip trip, - final TripUpdate tripUpdate, final List stops, final ServiceDate serviceDate, - final RealTimeState realTimeState) { + final TripUpdate tripUpdate, final List stops, final ServiceDate serviceDate, + final RealTimeState realTimeState) { // Preconditions Preconditions.checkNotNull(stops); @@ -740,7 +978,7 @@ private boolean addTripToGraphAndBuffer(final String feedId, final Graph graph, */ private boolean cancelScheduledTrip(String feedId, String tripId, final ServiceDate serviceDate) { boolean success = false; - + final TripPattern pattern = getPatternForTripId(feedId, tripId); if (pattern != null) { @@ -879,7 +1117,7 @@ private boolean validateAndHandleModifiedTrip(final Graph graph, final TripUpdat * @return true iff successful */ private boolean handleModifiedTrip(final Graph graph, final Trip trip, final TripUpdate tripUpdate, final List stops, - final String feedId, final ServiceDate serviceDate) { + final String feedId, final ServiceDate serviceDate) { // Preconditions Preconditions.checkNotNull(stops); Preconditions.checkArgument(tripUpdate.getStopTimeUpdateCount() == stops.size(), @@ -938,7 +1176,7 @@ private boolean purgeExpiredData() { } /** - * Retrieve a trip pattern given a feed id and trid id. + * Retrieve a trip pattern given a feed id and trip id. * * @param feedId feed id for the trip id * @param tripId trip id without agency @@ -986,4 +1224,341 @@ private Stop getStopForStopId(String feedId, String stopId) { return stop; } + /** + * + * @return all errors, grouped by their type exposing the number of same occurrences + */ + public Map getErrors() { + + return errorMap; + } + + /** + * + * @return the set of all errors occurring in the last block of updates + */ + @JsonIgnore + public Set getLastErrors(){ + return lastErrors; + } + + /** + * + * @return the number of applied updates per tripId + */ + public Map getApplied() { + + return appliedMap; + } + + /** + * + * @return return the number of received updates er tripId + */ + public Map getReceived() { + + return receivedMap; + } + + /** + * + * @return the number of updates per type + */ + public Map getTypes() { + + return typeMap; + } + + /** + * + * @param feedId + * @return the number of applied updates for feedId + */ + @JsonIgnore + public Long getAppliedPerFeed(String feedId) { + Object ret = toMap(appliedSpecList, "feedId", feedId); + if (ret == null) + return null; + return (Long) ret; + } + + /** + * + * @return the number of applied per feedId + */ + public Map getAppliedPerFeed() { + Object ret = toMap(appliedSpecList, "feedId", null); + if (ret == null) + return null; + return (Map) ret; + } + + /** + * + * @return the number of applied per tripId + */ + public Map getAppliedPerTrip() { + Object ret = toMap(appliedSpecList, "tripId", null); + if (ret == null) + return null; + return (Map) ret; + } + + /** + * + * @return all the number of applied per type + */ + public Map getAppliedPerType() { + Object ret = toMap(appliedSpecList, "type", null); + if (ret == null) + return null; + return (Map) ret; + } + + /** + * + * @return all applied updates grouped by type, feedId and tripId + */ + public Map>> getAppliedTypePerFeedPerTrip() { + return toMap2(appliedSpecList); + } + + /** + * + * @param feedId + * @param tripId + * @return all applied updates per feedId and tripId, grouped by type exposing the number + */ + @JsonIgnore + public Map getAppliedTypePerFeedPerTrip(String feedId, String tripId) { + return toMap2(appliedSpecList, feedId, tripId); + + } + + /** + * + * @return timestamp last applied updates + */ + private Long getLastApplied() { + if (lastUpdate == null) + return null; + return lastUpdate.getTime(); + } + + /** + * + * @return timestamp last received updates + */ + private long getLastReceived() { + + if (lastReceived == null) + return 0L; + return lastReceived.getTime(); + } + + /** + * + * @return the tripId and timestamp of last applied and received update + */ + public Map getLastAppliedReceived(){ + Map ret = new HashMap<>(); + ret.put("lastUpdatedTrip", lastUpdatedTrip); + ret.put("lastUpdatedTime", getLastApplied()); + ret.put("lastReceivedTrip",lastReceivedTrip); + ret.put("lastReceivedTime",getLastReceived()); + return ret; + } + + /** + * + * @return the number of received and applied updates + */ + public Map getReceivedApplied(){ + Map ret = new HashMap<>(); + ret.put("receivedNum",receivedNum); + ret.put("appliedNum",appliedNum); + return ret; + } + + + /** + * Classes can addErrors if in relation to TripUpdate + * @param err + */ + public void addError(String err) { + errorList.add(err); + lastErrors.add(err); + } + + /** + * + * @param minutes + * @return information about all updates occurring in the last number of minutes + */ + @JsonIgnore + public List getAppliedLastMinutes(int minutes) { + + + Map c = toMapObject(getSelected(minutes)); + + if (c == null) + return null; + List ret = new ArrayList<>(); + int i = 0; + AppliedEntity ap; + for (Map.Entry entry : c.entrySet()) { + ap = (AppliedEntity) entry.getKey(); + ap.setOccurrence(entry.getValue()); + ret.add((AppliedEntity) entry.getKey()); + } + return ret; + + } + + + + +} + +/** + * Following classes are helpers while creating Statistics for updates + */ +class AppliedEntity implements Comparable { + @JsonIgnore + String type; + String feedId; + String tripId; + @JsonIgnore + FeedTrip feedTrip; + @JsonIgnore + Timestamp timestamp; + + public Long getOccurrence() { + return occurrence; + } + + public void setOccurrence(Long occurrence) { + this.occurrence = occurrence; + } + + Long occurrence; + + AppliedEntity(String type, + String feedId, + String tripId) { + this.type = type; + this.feedId = feedId; + this.tripId = tripId; + this.feedTrip = new FeedTrip(feedId, tripId); + this.timestamp = new Timestamp(System.currentTimeMillis()); + this.occurrence = 0L; + } + + public Timestamp getTimestamp() { + return timestamp; + } + + public String getTripId() { + return tripId; + } + + public String getType() { + return type; + } + + public String getFeedId() { + return feedId; + } + + public FeedTrip getFeedTrip() { + return feedTrip; + } + + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null) { + return false; + } + if (!(o instanceof AppliedEntity)) { + return false; + } + AppliedEntity ft = (AppliedEntity) o; + return ft.getFeedId().equals(this.getFeedId()) && ft.getTripId().equals(this.getTripId()) && ft.getType().equals(this.getType()); + } + + @Override + public int hashCode() { + return this.getFeedId().hashCode() * 13 + this.getTripId().hashCode() * 17 + this.getType().hashCode() * 31; + } + + + @Override + public int compareTo(Object o) { + if (o == this) { + return 0; + } + if (o == null) { + return -1; + } + if (!(o instanceof AppliedEntity)) { + return -1; + } + AppliedEntity ft = (AppliedEntity) o; + if (this.getFeedId().compareTo(ft.getFeedId()) == 0) + if (this.getTripId().compareTo(ft.getTripId()) == 0) + if (this.getType().compareTo(ft.getType()) == 0) + return 0; + else + return this.getType().compareTo(ft.getType()); + else return this.getTripId().compareTo(ft.getTripId()); + else return this.getFeedId().compareTo(ft.getFeedId()); + + } + +} + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") +class FeedTrip { + String feedId; + String tripId; + + public FeedTrip(String feedId, String tripId) { + this.feedId = feedId; + this.tripId = tripId; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null) { + return false; + } + if (!(o instanceof FeedTrip)) { + return false; + } + + FeedTrip ft = (FeedTrip) o; + return ft.getFeedId().equals(this.getFeedId()) && ft.getTripId().equals(this.getTripId()); + } + + @Override + public int hashCode() { + return this.getFeedId().hashCode() * 13 + this.getTripId().hashCode() * 17; + } + + + public String getFeedId() { + return feedId; + } + + public String getTripId() { + return tripId; + } + } diff --git a/src/main/java/org/opentripplanner/updater/stoptime/TripUpdateGraphWriterRunnable.java b/src/main/java/org/opentripplanner/updater/stoptime/TripUpdateGraphWriterRunnable.java index 5db3b4168fe..4a93c0e6879 100644 --- a/src/main/java/org/opentripplanner/updater/stoptime/TripUpdateGraphWriterRunnable.java +++ b/src/main/java/org/opentripplanner/updater/stoptime/TripUpdateGraphWriterRunnable.java @@ -13,6 +13,7 @@ the License, or (at your option) any later version. package org.opentripplanner.updater.stoptime; +import java.util.ArrayList; import java.util.List; import org.opentripplanner.routing.graph.Graph; @@ -40,6 +41,7 @@ public class TripUpdateGraphWriterRunnable implements GraphWriterRunnable { private final String feedId; + public TripUpdateGraphWriterRunnable(final boolean fullDataset, final List updates, final String feedId) { // Preconditions Preconditions.checkNotNull(updates); @@ -49,6 +51,7 @@ public TripUpdateGraphWriterRunnable(final boolean fullDataset, final List(feedEntityList.size()); for (FeedEntity feedEntity : feedEntityList) { diff --git a/src/scripts/otp b/src/scripts/otp old mode 100755 new mode 100644 diff --git a/src/scripts/otp-batch-analyst b/src/scripts/otp-batch-analyst old mode 100755 new mode 100644 diff --git a/src/test/resources/generateGtfs.py b/src/test/resources/generateGtfs.py old mode 100755 new mode 100644 diff --git a/src/test/resources/portland/Streets_pdx.shx b/src/test/resources/portland/Streets_pdx.shx old mode 100755 new mode 100644