/* 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.updater.bike_rental;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import com.fasterxml.jackson.databind.JsonNode;
import org.opentripplanner.graph_builder.linking.SimpleStreetSplitter;
import org.opentripplanner.routing.bike_rental.BikeRentalStation;
import org.opentripplanner.routing.bike_rental.BikeRentalStationService;
import org.opentripplanner.routing.edgetype.RentABikeOffEdge;
import org.opentripplanner.routing.edgetype.RentABikeOnEdge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.vertextype.BikeRentalStationVertex;
import org.opentripplanner.updater.GraphUpdaterManager;
import org.opentripplanner.updater.GraphWriterRunnable;
import org.opentripplanner.updater.JsonConfigurable;
import org.opentripplanner.updater.PollingGraphUpdater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
/**
* Dynamic bike-rental station updater which encapsulate one BikeRentalDataSource.
*
* Usage example ('bike1' name is an example) in the file 'Graph.properties':
*
* <pre>
* bike1.type = bike-rental
* bike1.frequencySec = 60
* bike1.networks = V3,V3N
* bike1.sourceType = jcdecaux
* bike1.url = https://api.jcdecaux.com/vls/v1/stations?contract=Xxx?apiKey=Zzz
* </pre>
*/
public class BikeRentalUpdater extends PollingGraphUpdater {
private static final Logger LOG = LoggerFactory.getLogger(BikeRentalUpdater.class);
private GraphUpdaterManager updaterManager;
private static final String DEFAULT_NETWORK_LIST = "default";
Map<BikeRentalStation, BikeRentalStationVertex> verticesByStation = new HashMap<BikeRentalStation, BikeRentalStationVertex>();
private BikeRentalDataSource source;
private Graph graph;
private SimpleStreetSplitter linker;
private BikeRentalStationService service;
private String network = "default";
@Override
public void setGraphUpdaterManager(GraphUpdaterManager updaterManager) {
this.updaterManager = updaterManager;
}
@Override
protected void configurePolling (Graph graph, JsonNode config) throws Exception {
// Set data source type from config JSON
String sourceType = config.path("sourceType").asText();
String apiKey = config.path("apiKey").asText();
String networkName = config.path("network").asText();
BikeRentalDataSource source = null;
if (sourceType != null) {
if (sourceType.equals("jcdecaux")) {
source = new JCDecauxBikeRentalDataSource();
} else if (sourceType.equals("b-cycle")) {
source = new BCycleBikeRentalDataSource(apiKey, networkName);
} else if (sourceType.equals("bixi")) {
source = new BixiBikeRentalDataSource();
} else if (sourceType.equals("keolis-rennes")) {
source = new KeolisRennesBikeRentalDataSource();
} else if (sourceType.equals("ov-fiets")) {
source = new OVFietsKMLDataSource();
} else if (sourceType.equals("city-bikes")) {
source = new CityBikesBikeRentalDataSource();
} else if (sourceType.equals("vcub")) {
source = new VCubDataSource();
} else if (sourceType.equals("citi-bike-nyc")) {
source = new CitiBikeNycBikeRentalDataSource(networkName);
} else if (sourceType.equals("next-bike")) {
source = new NextBikeRentalDataSource(networkName);
} else if (sourceType.equals("kml")) {
source = new GenericKmlBikeRentalDataSource();
} else if (sourceType.equals("sf-bay-area")) {
source = new SanFranciscoBayAreaBikeRentalDataSource(networkName);
} else if (sourceType.equals("share-bike")) {
source = new ShareBikeRentalDataSource();
}
}
if (source == null) {
throw new IllegalArgumentException("Unknown bike rental source type: " + sourceType);
} else if (source instanceof JsonConfigurable) {
((JsonConfigurable) source).configure(graph, config);
}
// Configure updater
LOG.info("Setting up bike rental updater.");
this.graph = graph;
this.source = source;
this.network = config.path("networks").asText(DEFAULT_NETWORK_LIST);
LOG.info("Creating bike-rental updater running every {} seconds : {}", frequencySec, source);
}
@Override
public void setup() throws InterruptedException, ExecutionException {
// Creation of network linker library will not modify the graph
linker = new SimpleStreetSplitter(graph);
// Adding a bike rental station service needs a graph writer runnable
updaterManager.executeBlocking(new GraphWriterRunnable() {
@Override
public void run(Graph graph) {
service = graph.getService(BikeRentalStationService.class, true);
}
});
}
@Override
protected void runPolling() throws Exception {
LOG.debug("Updating bike rental stations from " + source);
if (!source.update()) {
LOG.debug("No updates");
return;
}
List<BikeRentalStation> stations = source.getStations();
// Create graph writer runnable to apply these stations to the graph
BikeRentalGraphWriterRunnable graphWriterRunnable = new BikeRentalGraphWriterRunnable(stations);
updaterManager.execute(graphWriterRunnable);
}
@Override
public void teardown() {
}
private class BikeRentalGraphWriterRunnable implements GraphWriterRunnable {
private List<BikeRentalStation> stations;
public BikeRentalGraphWriterRunnable(List<BikeRentalStation> stations) {
this.stations = stations;
}
@Override
public void run(Graph graph) {
// Apply stations to graph
Set<BikeRentalStation> stationSet = new HashSet<BikeRentalStation>();
Set<String> defaultNetworks = new HashSet<String>(Arrays.asList(network));
/* add any new stations and update bike counts for existing stations */
for (BikeRentalStation station : stations) {
if (station.networks == null) {
/* API did not provide a network list, use default */
station.networks = defaultNetworks;
}
service.addBikeRentalStation(station);
stationSet.add(station);
BikeRentalStationVertex vertex = verticesByStation.get(station);
if (vertex == null) {
vertex = new BikeRentalStationVertex(graph, station);
if (!linker.link(vertex)) {
// the toString includes the text "Bike rental station"
LOG.warn("{} not near any streets; it will not be usable.", station);
}
verticesByStation.put(station, vertex);
new RentABikeOnEdge(vertex, vertex, station.networks);
if (station.allowDropoff)
new RentABikeOffEdge(vertex, vertex, station.networks);
} else {
vertex.setBikesAvailable(station.bikesAvailable);
vertex.setSpacesAvailable(station.spacesAvailable);
}
}
/* remove existing stations that were not present in the update */
List<BikeRentalStation> toRemove = new ArrayList<BikeRentalStation>();
for (Entry<BikeRentalStation, BikeRentalStationVertex> entry : verticesByStation.entrySet()) {
BikeRentalStation station = entry.getKey();
if (stationSet.contains(station))
continue;
BikeRentalStationVertex vertex = entry.getValue();
if (graph.containsVertex(vertex)) {
graph.removeVertexAndEdges(vertex);
}
toRemove.add(station);
service.removeBikeRentalStation(station);
// TODO: need to unsplit any streets that were split
}
for (BikeRentalStation station : toRemove) {
// post-iteration removal to avoid concurrent modification
verticesByStation.remove(station);
}
}
}
}