/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.reader.gtfs;
import com.conveyal.gtfs.GTFSFeed;
import com.conveyal.gtfs.model.Fare;
import com.conveyal.gtfs.model.FareRule;
import com.google.transit.realtime.GtfsRealtime;
import com.graphhopper.gtfs.fare.FixedFareAttributeLoader;
import com.graphhopper.storage.Directory;
import com.graphhopper.storage.Graph;
import com.graphhopper.storage.GraphExtension;
import org.mapdb.*;
import java.io.*;
import java.time.LocalDate;
import java.time.ZoneId;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.zip.ZipFile;
public class GtfsStorage implements GraphExtension {
public static class Validity implements Serializable {
final BitSet validity;
final ZoneId zoneId;
final LocalDate start;
Validity(BitSet validity, ZoneId zoneId, LocalDate start) {
this.validity = validity;
this.zoneId = zoneId;
this.start = start;
}
@Override
public boolean equals(Object other) {
if (! (other instanceof Validity)) return false;
Validity v = (Validity) other;
return validity.equals(v.validity) && zoneId.equals(v.zoneId) && start.equals(v.start);
}
@Override
public int hashCode() {
return Objects.hash(validity, zoneId, start);
}
}
static class FeedIdWithTimezone implements Serializable {
final String feedId;
final ZoneId zoneId;
FeedIdWithTimezone(String feedId, ZoneId zoneId) {
this.feedId = feedId;
this.zoneId = zoneId;
}
@Override
public boolean equals(Object other) {
if (! (other instanceof FeedIdWithTimezone)) return false;
FeedIdWithTimezone v = (FeedIdWithTimezone) other;
return feedId.equals(v.feedId) && zoneId.equals(v.zoneId);
}
@Override
public int hashCode() {
return Objects.hash(feedId, zoneId);
}
}
private boolean isClosed = false;
private Directory dir;
private Set<String> gtfsFeedIds;
private Map<String, GTFSFeed> gtfsFeeds = new HashMap<>();
private HTreeMap<Validity, Integer> operatingDayPatterns;
private Map<Integer, Validity> validities;
private Bind.MapWithModificationListener<FeedIdWithTimezone, Integer> timeZones;
private Map<Integer, FeedIdWithTimezone> readableTimeZones;
private Map<Integer, String> extra;
private Map<Integer, Integer> stopSequences;
private Map<String, Fare> fares;
private Map<GtfsRealtime.TripDescriptor, int[]> boardEdgesForTrip;
private Map<GtfsRealtime.TripDescriptor, int[]> leaveEdgesForTrip;
enum EdgeType {
HIGHWAY, ENTER_TIME_EXPANDED_NETWORK, LEAVE_TIME_EXPANDED_NETWORK, ENTER_PT, EXIT_PT, HOP, DWELL, BOARD, ALIGHT, OVERNIGHT, TRANSFER, WAIT
}
private DB data;
@Override
public boolean isRequireNodeField() {
return true;
}
@Override
public boolean isRequireEdgeField() {
return false;
}
@Override
public int getDefaultNodeFieldValue() {
return 0;
}
@Override
public int getDefaultEdgeFieldValue() {
return EdgeType.HIGHWAY.ordinal();
}
@Override
public void init(Graph graph, Directory dir) {
this.dir = dir;
}
@Override
public void setSegmentSize(int bytes) {
}
@Override
public GraphExtension copyTo(GraphExtension extStorage) {
throw new UnsupportedOperationException("copyTo not yet supported");
}
@Override
public boolean loadExisting() {
this.data = DBMaker.newFileDB(new File(dir.getLocation() + "/transit_schedule")).transactionDisable().mmapFileEnable().readOnly().make();
init();
for (String gtfsFeedId : this.gtfsFeedIds) {
try {
GTFSFeed feed = new GTFSFeed(dir.getLocation() + "/" + gtfsFeedId);
this.gtfsFeeds.put(gtfsFeedId, feed);
} catch (IOException | ExecutionException e) {
throw new RuntimeException(e);
}
}
return true;
}
@Override
public GraphExtension create(long byteCount) {
final File file = new File(dir.getLocation() + "/transit_schedule");
try {
Files.deleteIfExists(file.toPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
this.data = DBMaker.newFileDB(file).transactionDisable().mmapFileEnable().asyncWriteEnable().make();
init();
return this;
}
private void init() {
this.gtfsFeedIds = data.getHashSet("gtfsFeeds");
this.operatingDayPatterns = data.getHashMap("validities");
Map<Integer, Validity> reverseOperatingDayPatterns = new HashMap<>();
for (Map.Entry<Validity, Integer> entry : this.operatingDayPatterns.entrySet()) {
reverseOperatingDayPatterns.put(entry.getValue(), entry.getKey());
}
Bind.mapInverse(this.operatingDayPatterns, reverseOperatingDayPatterns);
this.timeZones = data.getHashMap("timeZones");
Map<Integer, FeedIdWithTimezone> readableTimeZones = new HashMap<>();
for (Map.Entry<FeedIdWithTimezone, Integer> entry : this.timeZones.entrySet()) {
readableTimeZones.put(entry.getValue(), entry.getKey());
}
Bind.mapInverse(this.timeZones, readableTimeZones);
this.readableTimeZones = Collections.unmodifiableMap(readableTimeZones);
this.validities = Collections.unmodifiableMap(reverseOperatingDayPatterns);
this.extra = data.getTreeMap("extra");
this.stopSequences = data.getTreeMap("stopSequences");
this.fares = data.getTreeMap("fares");
this.boardEdgesForTrip = data.getHashMap("boardEdgesForTrip");
this.leaveEdgesForTrip = data.getHashMap("leaveEdgesForTrip");
}
void loadGtfsFromFile(String id, ZipFile zip) {
try {
GTFSFeed feed = new GTFSFeed(dir.getLocation() + "/" + id);
feed.loadFromFile(zip);
fixFares(feed, zip);
this.gtfsFeeds.put(id, feed);
} catch (Exception e) {
throw new RuntimeException(e);
}
this.gtfsFeedIds.add(id);
}
private void fixFares(GTFSFeed feed, ZipFile zip) {
feed.fares.clear();
Map<String, Fare> fares = new HashMap<>();
try {
new FixedFareAttributeLoader(feed, fares).loadTable(zip);
new FareRule.Loader(feed, fares).loadTable(zip);
} catch (IOException e) {
throw new RuntimeException(e);
}
feed.fares.putAll(fares);
}
@Override
public void flush() {
}
@Override
public void close() {
if (!isClosed) {
isClosed = true;
data.close();
for (GTFSFeed feed : gtfsFeeds.values()) {
feed.close();
}
}
}
@Override
public boolean isClosed() {
return isClosed;
}
@Override
public long getCapacity() {
return 0;
}
Map<Validity, Integer> getOperatingDayPatterns() {
return operatingDayPatterns;
}
Map<Integer, Validity> getValidities() {
return validities;
}
Map<Integer, FeedIdWithTimezone> getTimeZones() {
return readableTimeZones;
}
Map<FeedIdWithTimezone, Integer> getWritableTimeZones() {
return timeZones;
}
Map<Integer, String> getExtraStrings() {
return extra;
}
Map<Integer, Integer> getStopSequences() {
return stopSequences;
}
Map<GtfsRealtime.TripDescriptor, int[]> getBoardEdgesForTrip() {
return boardEdgesForTrip;
}
Map<GtfsRealtime.TripDescriptor, int[]> getAlightEdgesForTrip() {
return leaveEdgesForTrip;
}
Map<String, Fare> getFares() {
return fares;
}
Map<String, GTFSFeed> getGtfsFeeds() {
return Collections.unmodifiableMap(gtfsFeeds);
}
}