/* 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 <http://www.gnu.org/licenses/>. */
package org.opentripplanner.routing.edgetype;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.opentripplanner.routing.trippattern.UpdateBlock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// this is only currently in edgetype because that's where Trippattern is. move these classes elsewhere.
/**
* Part of concurrency control for stoptime updates.
*
* All updates should be performed on a snapshot before it is handed off to any searches.
* A single snapshot should be used for an entire search, and should remain unchanged
* for that duration to provide a consistent view not only of trips that have been boarded,
* but of relative arrival and departure times of other trips that have not necessarily been boarded.
*
* At this point, only one writing thread at a time is supported.
*/
public class TimetableResolver {
private static final Logger LOG = LoggerFactory.getLogger(TimetableResolver.class);
// Use HashMap not Map so we can clone.
// if this turns out to be slow/spacious we can use an array with integer pattern indexes
private HashMap<TableTripPattern, Timetable> timetables = new HashMap<TableTripPattern, Timetable>();
/** A set of all timetables which have been modified and are waiting to be indexed. */
private Set<Timetable> dirty = new HashSet<Timetable>();
/**
* Returns an updated timetable for the specified pattern if one is available in this snapshot,
* or the originally scheduled timetable if there are no updates in this snapshot.
*/
public Timetable resolve(TableTripPattern pattern) {
Timetable timetable = timetables.get(pattern);
if (timetable == null) {
return pattern.scheduledTimetable;
} else {
LOG.trace("returning modified timetable");
return timetable;
}
}
/**
* @return whether or not the update was actually applied
*/
public boolean update(TableTripPattern pattern, UpdateBlock block) {
// synchronization prevents commits/snapshots while update is in progress
synchronized(this) {
if (dirty == null)
throw new ConcurrentModificationException("This TimetableResolver is read-only.");
Timetable tt = resolve(pattern);
// we need to perform the copy of Timetable here rather than in Timetable.update()
// to avoid repeatedly copying in case several updates are applied to the same timetable
if ( ! dirty.contains(tt)) {
tt = tt.copy();
timetables.put(pattern, tt);
dirty.add(tt);
}
return tt.update(block);
}
}
/**
* This produces a small delay of typically around 50ms, which is almost entirely due to
* the indexing step. Cloning the map is much faster (2ms).
* It is perhaps better to index timetables as they are changed to avoid experiencing all
* this lag at once, but we want to avoid re-indexing when receiving multiple updates for
* the same timetable in rapid succession. This compromise is expressed by the
* maxSnapshotFrequency property of StoptimeUpdater. The indexing could be made much more
* efficient as well.
* @return an immutable copy of this TimetableResolver with all updates applied
*/
@SuppressWarnings("unchecked")
public TimetableResolver commit() {
TimetableResolver ret = new TimetableResolver();
// synchronization prevents updates while commit/snapshot in progress
synchronized(this) {
if (! this.isDirty())
return null;
for (Timetable tt : dirty)
tt.finish(); // summarize, index, etc. the new timetables
ret.timetables = (HashMap<TableTripPattern, Timetable>) this.timetables.clone();
this.dirty.clear();
}
ret.dirty = null; // mark the snapshot as henceforth immutable
return ret;
}
public String toString() {
String d = dirty == null ? "committed" : String.format("%d dirty", dirty.size());
return String.format("Timetable snapshot: %d timetables (%s)", timetables.size(), d);
}
public boolean isDirty() {
if (dirty == null)
return false;
return dirty.size() > 0;
}
}