diff --git a/src/main/java/org/opentripplanner/updater/bike_rental/BikeRentalUpdater.java b/src/main/java/org/opentripplanner/updater/bike_rental/BikeRentalUpdater.java
index f2fad57241a..9c4f174f1f1 100644
--- a/src/main/java/org/opentripplanner/updater/bike_rental/BikeRentalUpdater.java
+++ b/src/main/java/org/opentripplanner/updater/bike_rental/BikeRentalUpdater.java
@@ -97,6 +97,8 @@ protected void configurePolling (Graph graph, JsonNode config) throws Exception
source = new SmooveBikeRentalDataSource(networkName);
} else if (sourceType.equals("bicimad")) {
source = new BicimadBikeRentalDataSource();
+ } else if (sourceType.equals("samocat")) {
+ source = new SamocatScooterRentalDataSource(networkName);
}
}
diff --git a/src/main/java/org/opentripplanner/updater/bike_rental/SamocatScooterRentalDataSource.java b/src/main/java/org/opentripplanner/updater/bike_rental/SamocatScooterRentalDataSource.java
new file mode 100644
index 00000000000..795c7d981d7
--- /dev/null
+++ b/src/main/java/org/opentripplanner/updater/bike_rental/SamocatScooterRentalDataSource.java
@@ -0,0 +1,90 @@
+package org.opentripplanner.updater.bike_rental;
+
+import org.opentripplanner.routing.bike_rental.BikeRentalStation;
+import org.opentripplanner.util.NonLocalizedString;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Implementation of a BikeRentalDataSource for the Samocat scooter rental service used in Helsinki.
+ * Since scooters rental works similarly as bike rental, there is no point to create own data structures
+ * for them.
+ * @see BikeRentalDataSource
+ */
+public class SamocatScooterRentalDataSource extends GenericJsonBikeRentalDataSource {
+
+ private static final Logger log = LoggerFactory.getLogger(SamocatScooterRentalDataSource.class);
+
+ private String networkName;
+
+ public SamocatScooterRentalDataSource(String networkName) {
+ super("results");
+ this.networkName = defaultIfEmpty(networkName, "samocat");
+ }
+
+ private String defaultIfEmpty(String value, String defaultValue) {
+ if (value == null || value.isEmpty())
+ return defaultValue;
+
+ return value;
+ }
+
+ /**
+ *
+ * {
+ * "count": 10,
+ * "next": null,
+ * "previous": null,
+ * "results": [
+ * {
+ * "type": "Feature",
+ * "geometry": {
+ * "type": "Point",
+ * "coordinates": [
+ * 60.167913,
+ * 24.952269
+ * ]
+ * },
+ * "properties": {
+ * "station_id": "0309",
+ * "city": "Helsinki",
+ * "country": "Finland",
+ * "address": "some address",
+ * "rack_sum": 12,
+ * "free_racks": 2,
+ * "available_devices": 1
+ * }
+ * }
+ * ]
+ * }
+ *
+ */
+ public BikeRentalStation makeStation(JsonNode node) {
+ BikeRentalStation station = new BikeRentalStation();
+ JsonNode properties = node.path("properties");
+ JsonNode coordinates = node.path("geometry").path("coordinates");
+ station.id = properties.path("station_id").asText();
+ station.name = new NonLocalizedString(properties.path("address").asText());
+ station.state = "Station on";
+ station.networks = new HashSet();
+ station.networks.add(this.networkName);
+ try {
+ if (coordinates.get(0).isNull() || coordinates.isNull()) {
+ return null;
+ }
+ station.x = coordinates.get(0).asDouble();
+ station.y = coordinates.get(1).asDouble();
+ station.bikesAvailable = properties.path("available_devices").asInt();
+ station.spacesAvailable = properties.path("free_racks").asInt();
+ return station;
+ } catch (NumberFormatException e) {
+ // E.g. coordinates is empty
+ log.info("Error parsing bike rental station " + station.id, e);
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index db6325c60ba..1f0517813ed 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -54,6 +54,7 @@
+
diff --git a/src/test/java/org/opentripplanner/updater/bike_rental/TestBikeRentalStationSource.java b/src/test/java/org/opentripplanner/updater/bike_rental/TestBikeRentalStationSource.java
index 3fef663d5b2..e22dd8134d6 100644
--- a/src/test/java/org/opentripplanner/updater/bike_rental/TestBikeRentalStationSource.java
+++ b/src/test/java/org/opentripplanner/updater/bike_rental/TestBikeRentalStationSource.java
@@ -75,4 +75,53 @@ public void testSmoove() {
BikeRentalStation hamnWithCustomNetwork = rentalStationsWithCustomNetwork.get(0);
assertEquals("[Helsinki]", hamnWithCustomNetwork.networks.toString());
}
+
+ public void testSamocat() {
+ SamocatScooterRentalDataSource source = new SamocatScooterRentalDataSource(null);
+ source.setUrl("file:src/test/resources/bike/samocat.json");
+ assertTrue(source.update());
+ List rentalStations = source.getStations();
+
+ // Invalid station without coordinates should be ignored, so only 3
+ assertEquals(3, rentalStations.size());
+ for (BikeRentalStation rentalStation : rentalStations) {
+ System.out.println(rentalStation);
+ }
+
+ BikeRentalStation testikuja = rentalStations.get(0);
+ assertEquals("Testikuja 3", testikuja.name.toString());
+ assertEquals("0451", testikuja.id);
+ // Ignore whitespace in coordinates string
+ assertEquals(24.9355143, testikuja.x);
+ assertEquals(60.1637284, testikuja.y);
+ assertEquals(0, testikuja.spacesAvailable);
+ assertEquals(0, testikuja.bikesAvailable);
+ assertEquals("Station on", testikuja.state);
+ assertEquals("[samocat]", testikuja.networks.toString());
+
+ BikeRentalStation footie = rentalStations.get(1);
+ assertEquals("Footie 3", footie.name.toString());
+ assertEquals("0450", footie.id);
+ assertEquals(3, footie.spacesAvailable);
+ assertEquals(4, footie.bikesAvailable);
+ assertEquals(24.958877, footie.x);
+ assertEquals(60.194449, footie.y);
+
+ BikeRentalStation bartie = rentalStations.get(2);
+ assertEquals("Bartie 10", bartie.name.toString());
+ assertEquals("3451", bartie.id);
+ assertEquals(24.9537278, bartie.x);
+ assertEquals(60.2177349, bartie.y);
+ assertEquals(5, bartie.spacesAvailable);
+ assertEquals(1, bartie.bikesAvailable);
+ // Ignores mismatch with total_slots
+
+ // Test giving network name to data source
+ SamocatScooterRentalDataSource sourceWithCustomNetwork = new SamocatScooterRentalDataSource("vuosaari");
+ sourceWithCustomNetwork.setUrl("file:src/test/resources/bike/samocat.json");
+ assertTrue(sourceWithCustomNetwork.update());
+ List rentalStationsWithCustomNetwork = sourceWithCustomNetwork.getStations();
+ BikeRentalStation testitieWithCustomNetwork = rentalStationsWithCustomNetwork.get(0);
+ assertEquals("[vuosaari]", testitieWithCustomNetwork.networks.toString());
+ }
}
diff --git a/src/test/resources/bike/samocat.json b/src/test/resources/bike/samocat.json
new file mode 100644
index 00000000000..c337c8eb939
--- /dev/null
+++ b/src/test/resources/bike/samocat.json
@@ -0,0 +1,83 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 24.9355143,
+ 60.1637284
+ ]
+ },
+ "properties": {
+ "station_id": "0451",
+ "city": "Helsinki",
+ "country": "FI",
+ "address": "Testikuja 3",
+ "rack_sum": 12,
+ "free_racks": 0,
+ "available_devices": 0
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 24.958877,
+ 60.194449
+ ]
+ },
+ "properties": {
+ "station_id": "0450",
+ "city": "Helsinki",
+ "country": "FI",
+ "address": "Footie 3",
+ "rack_sum": 12,
+ "free_racks": 3,
+ "available_devices": 4
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 24.9537278,
+ 60.2177349
+ ]
+ },
+ "properties": {
+ "station_id": "3451",
+ "city": "Helsinki",
+ "country": "FI",
+ "address": "Bartie 10",
+ "rack_sum": 12,
+ "free_racks": 5,
+ "available_devices": 1
+ }
+ },
+ {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ null,
+ null
+ ]
+ },
+ "properties": {
+ "station_id": "3451",
+ "city": "Helsinki",
+ "country": "FI",
+ "address": "Nulltie 1",
+ "rack_sum": 12,
+ "free_racks": 5,
+ "available_devices": 1
+ }
+ }
+ ]
+}
\ No newline at end of file