Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add bike type #4354

Merged
merged 2 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions source/jormungandr/jormungandr/interfaces/v1/journey_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,18 @@ def __init__(self, output_type_serializer):
help='A journey containing a waiting section between TC and Zonal ODT with a duration greater to max_waiting_duration_odt '
'will be discarded. Units : seconds. Must be > 0. Default value : 30 minutes',
)
parser_get.add_argument(
"bike_type",
# wordings are from https://wiki.openstreetmap.org/wiki/Tag:amenity%3Dbicycle_rental#Types_of_bicycles_and_accessories
type=OptionValue(
[
'city_bike',
'ebike',
]
),
default='city_bike',
help="only available for Geovelo so far: whether to use electric bike.",
)

def parse_args(self, region=None, uri=None):
args = self.parsers['get'].parse_args()
Expand Down
21 changes: 14 additions & 7 deletions source/jormungandr/jormungandr/street_network/geovelo.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,14 @@ def _pt_object_summary_isochrone(cls, pt_object):
return [coord.lat, coord.lon, None]

@classmethod
def _make_request_arguments_bike_details(cls, bike_speed_mps):
def _make_request_arguments_bike_details(cls, bike_speed_mps, use_ebike):
bike_speed = mps_to_kmph(bike_speed_mps)

return {
'profile': 'MEDIAN', # can be BEGINNER, EXPERT
'bikeType': 'TRADITIONAL', # can be 'BSS'
'averageSpeed': bike_speed, # in km/h, BEGINNER sets it to 13
'eBike': use_ebike, # whether to use an electric bike or not
}

def sort_places_by_physical_mode_and_distance(self, points):
Expand All @@ -161,18 +162,18 @@ def priority_by_mode(point):
return sorted(points, key=lambda point: (priority_by_mode(point), point.distance))

@classmethod
def _make_request_arguments_isochrone(cls, origins, destinations, bike_speed_mps=3.33):
def _make_request_arguments_isochrone(cls, origins, destinations, bike_speed_mps=3.33, use_ebike=False):
origins_coord = [cls._pt_object_summary_isochrone(o) for o in origins]
destinations_coord = [cls._pt_object_summary_isochrone(o) for o in destinations]
return {
'starts': [o for o in origins_coord],
'ends': [o for o in destinations_coord],
'bikeDetails': cls._make_request_arguments_bike_details(bike_speed_mps),
'bikeDetails': cls._make_request_arguments_bike_details(bike_speed_mps, use_ebike),
'transportMode': 'BIKE',
}

@classmethod
def _make_request_arguments_direct_path(cls, origin, destination, bike_speed_mps=3.33):
def _make_request_arguments_direct_path(cls, origin, destination, bike_speed_mps=3.33, use_ebike=False):
coord_orig = get_pt_object_coord(origin)
coord_dest = get_pt_object_coord(destination)
return {
Expand All @@ -181,7 +182,7 @@ def _make_request_arguments_direct_path(cls, origin, destination, bike_speed_mps
{'latitude': coord_dest.lat, 'longitude': coord_dest.lon},
],
'transportModes': ['BIKE'],
'bikeDetails': cls._make_request_arguments_bike_details(bike_speed_mps),
'bikeDetails': cls._make_request_arguments_bike_details(bike_speed_mps, use_ebike),
}

def _call_geovelo(self, url, method=requests.post, data=None):
Expand Down Expand Up @@ -271,6 +272,10 @@ def inside_zone(self, point):
shapely_point = Point(coord.lon, coord.lat)
return self.polygon_zone.contains(shapely_point)

@staticmethod
def use_ebike(request):
return request.get('bike_type') == 'ebike'

def use_this_service_for_sn_matrix(self, origins, destinations):
# Use if shape is absent
if not self.polygon_zone:
Expand Down Expand Up @@ -306,7 +311,9 @@ def _get_street_network_routing_matrix(
)
)

data = self._make_request_arguments_isochrone(origins, destinations, request['bike_speed'])
data = self._make_request_arguments_isochrone(
origins, destinations, request['bike_speed'], self.use_ebike(request)
)
r = self._call_geovelo(
'{}/{}'.format(self.service_url, 'api/v2/routes_m2m'), requests.post, ujson.dumps(data)
)
Expand Down Expand Up @@ -466,7 +473,7 @@ def _direct_path(
raise InvalidArguments('Geovelo, mode {} not implemented'.format(mode))

data = self._make_request_arguments_direct_path(
pt_object_origin, pt_object_destination, request['bike_speed']
pt_object_origin, pt_object_destination, request['bike_speed'], self.use_ebike(request)
)
single_result = True
if (
Expand Down
123 changes: 76 additions & 47 deletions source/jormungandr/jormungandr/street_network/tests/geovelo_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@
}


def direct_path_response_valid():
def direct_path_response_valid(ebike):
"""
A mock of a valid response from geovelo.
Reply to POST of {"starts":[[48.803064,2.443385, "refStart1"]],
"ends":[[48.802049,2.426482, "refEnd1"]]}
Modify with caution as it will affect every tests using these start and end uris.
"""
duration = 2822 if ebike else 3155
return [
{
"distances": {
Expand All @@ -65,7 +66,7 @@ def direct_path_response_valid():
"recommendedRoads": 7759.0,
"total": 11393.0,
},
"duration": 3155,
"duration": duration,
"estimatedDatetimeOfArrival": "2017-02-24T16:52:08.711",
"estimatedDatetimeOfDeparture": "2017-02-24T15:59:33.711",
"id": "bG9jPTQ4Ljg4Nzk0LDIuMzE0MzM4JmxvYz00OC44Mjk5MjcsMi4zNzY3NDcjQkVHSU5ORVIjRmFsc2UjQkVHSU5ORVIjMTMjRmFsc2UjRmFsc2UjMjAxNy0wMi0yNCAxNTo1OTozMy43MTEwNjgjVFJBRElUSU9OQUwjMCMwI1JFQ09NTUVOREVEI0ZhbHNl",
Expand Down Expand Up @@ -116,7 +117,7 @@ def direct_path_response_valid():
"profile": "BEGINNER",
"verticalGain": 51,
},
"duration": 3155,
"duration": duration,
"estimatedDatetimeOfArrival": "2017-02-24T16:52:08.711",
"estimatedDatetimeOfDeparture": "2017-02-24T15:59:33.711",
"geometry": "_yzf|AszglClL`ShClEzCrHj@nNnBfD~AoHfDeD`nBmeC|AqBvAuAhWyWjVqUbJsJdd@uf@uPwh@sBgG{JoYmEkMeEwLxv@iu@}Hwj@k@}DcCqQ{Ims@m@yEbBcDbCyEhEiIhSm`@rPy\\bI}OfBwD|H}CaNehA}MwiAzA_LiBsPa@sDoBaSeAoKiCcHqBQmG{Rqk@meAa@oHRwSDuDv^yfChAmAnAkHrFqZnA{Gb@wDxc@m{Btd@a|Bz@cErHum@xLobBfBqMd@aHAkGmA{LaAkCaUwoBo@sFiR{aB_@wJuFoe@u@sDm@_CyTonB`@yDy@gIdA{EyBsTNoG_OqzBe@uDuAaLjD~DzAmAzNoLxCcBtDqEj_A}y@`_@i\\vFcF~c@w^~GuFbJgJ`JaHnCmDri@kd@nQcOxCiC~\\wYfCuBlC_CpB_BtCeCxDoEdx@}q@pG_Fpi@yd@lDuBhO_NjD}CxKyJ~IcIlIsHp^{\\dRwN~DuDjBqBxEkEzh@md@xHwG~AqAhGgFzMeLrM_Lp{@_t@lEeDfKsJvQ_PxDuCnCoCzO_NlIcHhSyQzFcF~CoCdsA{hAfDoCtSqStDyCjAmEn@uJ`Ywk@xEeJxHePwWabAaB{GkKqc@wL}n@sAuG`@qFxB}A~vBc|AfUkPjE}C|b@uZ`w@oj@~j@ca@tl@mb@zMgIzIoEvNiIlHaJjIcFfNwFhH_ChMuBpMoAlK_@rLH~Lp@|OjCfFjAtD|FfCz@lBp@nHe@xkCl~@vaAf]b{@pZzMhFnCb@nFpBtGkWlBuBbBcC|NgTzWu_@fB_CtCcC|@_Cvb@qgAtw@usBdRnMtJnGb\\vTy@hEvCtAfFdC`Bv@nEtBtExBhW`Q|HtE|ChBrAjAdL_Nb^qc@zUkYxCqDlCmDxRkTrAwAlEyDdH{GhZmm@lCgFfDkIvA|@lv@xe@pCfBpDlBpC_I|DuKr[gy@xAwDhKgX`FyMzBkFdCsG|JoWdk@c{AdLsZzEcObFgMbMa\\fR}f@nBzF`c@d`BjAdF`Mra@hGfSzw@f}BxF~LfCzDzA@lCzPnZdu@p^x{@rFzNzCfIpLl[fAvCvxA}`BPaDhFkGnHkJvF_IrKoIfCiDrz@nkBvKzFdEoDfGkEbO{MbFsErDzAz^_\\t_@e]lE_Gpc@ql@r_@wg@fk@gv@fa@{h@rNcRvLvWrWrk@fB`ElD~H`Pv]pEdKhAbCbN|[fBnElBtE~AlF`Mlu@rZoXjE{Dt^e\\|AnDzAhDzJwK]eAg@CTpBrGeBzBkBk@eBHa@J_AeAY}@}ClCiCw@wJ[cAeB?gScp@YtCxBjH~DvM",
Expand Down Expand Up @@ -272,54 +273,76 @@ def get_matrix_test():
def direct_path_geovelo_test():
instance = MagicMock()
geovelo = Geovelo(instance=instance, service_url=MOCKED_SERVICE_URL)
resp_json = direct_path_response_valid()

origin = make_pt_object(type_pb2.ADDRESS, lon=2, lat=48.2, uri='refStart1')
destination = make_pt_object(type_pb2.ADDRESS, lon=3, lat=48.3, uri='refEnd1')
fallback_extremity = PeriodExtremity(str_to_time_stamp('20161010T152000'), False)
with requests_mock.Mocker() as req:

def json_matcher(request, _):
req_data = request.json()
return direct_path_response_valid(req_data.get("bikeDetails", {}).get("eBike"))

req.post(
'{}/api/v2/computedroutes?instructions=true&elevations=true&geometry=true'
'&single_result=true&bike_stations=false&objects_as_ids=true&'.format(MOCKED_SERVICE_URL),
json=resp_json,
)
geovelo_resp = geovelo.direct_path_with_fp(
instance, 'bike', origin, destination, fallback_extremity, MOCKED_REQUEST, None, None
json=json_matcher,
)
assert geovelo_resp.status_code == 200
assert geovelo_resp.response_type == response_pb2.ITINERARY_FOUND
assert len(geovelo_resp.journeys) == 1
assert geovelo_resp.journeys[0].duration == 3155 # 52min35s
assert geovelo_resp.journeys[0].requested_date_time == 0 # parameter datetime absent in MOCKED_REQUEST
assert len(geovelo_resp.journeys[0].sections) == 1
assert geovelo_resp.journeys[0].arrival_date_time == str_to_time_stamp('20161010T152000')
assert geovelo_resp.journeys[0].departure_date_time == str_to_time_stamp('20161010T142725')
assert geovelo_resp.journeys[0].sections[0].type == response_pb2.STREET_NETWORK
assert geovelo_resp.journeys[0].sections[0].type == response_pb2.STREET_NETWORK
assert geovelo_resp.journeys[0].sections[0].duration == 3155
assert geovelo_resp.journeys[0].sections[0].length == 11393
assert geovelo_resp.journeys[0].sections[0].street_network.coordinates[2].lon == 2.314258
assert geovelo_resp.journeys[0].sections[0].street_network.coordinates[2].lat == 48.887428
assert geovelo_resp.journeys[0].sections[0].origin == origin
assert geovelo_resp.journeys[0].sections[0].destination == destination
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].name == "Rue Jouffroy d'Abbans"
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].direction == 0
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].length == 40
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].duration == 11
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[0].distance_from_start == 0
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[0].elevation == 45.5
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[1].distance_from_start == 128
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[1].elevation == 44
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[2].distance_from_start == 274
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[2].elevation == 50
assert geovelo_resp.journeys[0].sections[0].cycle_lane_length == 98
assert len(geovelo_resp.journeys[0].sections[0].street_network.street_information) == 3
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[0].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[0].length == 58.0
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[1].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[1].length == 40.0
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[2].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[2].length == 0.0

def _test(request):

use_ebike = Geovelo.use_ebike(request)

geovelo_resp = geovelo.direct_path_with_fp(
instance, 'bike', origin, destination, fallback_extremity, request, None, None
)
assert geovelo_resp.status_code == 200
assert geovelo_resp.response_type == response_pb2.ITINERARY_FOUND
assert len(geovelo_resp.journeys) == 1
assert geovelo_resp.journeys[0].duration == 2822 if use_ebike else 3155
assert geovelo_resp.journeys[0].arrival_date_time == str_to_time_stamp('20161010T152000')
assert geovelo_resp.journeys[0].departure_date_time == str_to_time_stamp(
'20161010T143258' if use_ebike else '20161010T142725'
)
assert (
geovelo_resp.journeys[0].requested_date_time == 0
) # parameter datetime absent in MOCKED_REQUEST
assert len(geovelo_resp.journeys[0].sections) == 1
assert geovelo_resp.journeys[0].sections[0].type == response_pb2.STREET_NETWORK
assert geovelo_resp.journeys[0].sections[0].type == response_pb2.STREET_NETWORK
assert geovelo_resp.journeys[0].sections[0].duration == 2822 if use_ebike else 3155
assert geovelo_resp.journeys[0].sections[0].length == 11393
assert geovelo_resp.journeys[0].sections[0].street_network.coordinates[2].lon == 2.314258
assert geovelo_resp.journeys[0].sections[0].street_network.coordinates[2].lat == 48.887428
assert geovelo_resp.journeys[0].sections[0].origin == origin
assert geovelo_resp.journeys[0].sections[0].destination == destination
assert (
geovelo_resp.journeys[0].sections[0].street_network.path_items[1].name == "Rue Jouffroy d'Abbans"
)
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].direction == 0
assert geovelo_resp.journeys[0].sections[0].street_network.path_items[1].length == 40
assert (
geovelo_resp.journeys[0].sections[0].street_network.path_items[1].duration == 10
if use_ebike
else 11
)
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[0].distance_from_start == 0
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[0].elevation == 45.5
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[1].distance_from_start == 128
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[1].elevation == 44
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[2].distance_from_start == 274
assert geovelo_resp.journeys[0].sections[0].street_network.elevations[2].elevation == 50
assert geovelo_resp.journeys[0].sections[0].cycle_lane_length == 98
assert len(geovelo_resp.journeys[0].sections[0].street_network.street_information) == 3
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[0].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[0].length == 58.0
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[1].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[1].length == 40.0
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[2].cycle_path_type == 2
assert geovelo_resp.journeys[0].sections[0].street_network.street_information[2].length == 0.0

_test(MOCKED_REQUEST)
_test(dict({"bike_type": "ebike"}, **MOCKED_REQUEST))


def direct_path_geovelo_zero_test():
Expand Down Expand Up @@ -392,7 +415,7 @@ def distances_durations_test():
"""
instance = MagicMock()
geovelo = Geovelo(instance=instance, service_url=MOCKED_SERVICE_URL)
resp_json = direct_path_response_valid()
resp_json = direct_path_response_valid(False)

origin = make_pt_object(type_pb2.ADDRESS, lon=2, lat=48.2, uri='refStart1')
destination = make_pt_object(type_pb2.ADDRESS, lon=3, lat=48.3, uri='refEnd1')
Expand All @@ -417,16 +440,22 @@ def make_request_arguments_bike_details_test():
"""
instance = MagicMock()
geovelo = Geovelo(instance=instance, service_url=MOCKED_SERVICE_URL)
data = geovelo._make_request_arguments_bike_details(bike_speed_mps=3.33)
data = geovelo._make_request_arguments_bike_details(bike_speed_mps=3.33, use_ebike=False)
assert ujson.loads(ujson.dumps(data)) == ujson.loads(
'''{"profile": "MEDIAN", "averageSpeed": 12,
"bikeType": "TRADITIONAL"}'''
"bikeType": "TRADITIONAL","eBike": false}'''
)

data = geovelo._make_request_arguments_bike_details(bike_speed_mps=4.1, use_ebike=False)
assert ujson.loads(ujson.dumps(data)) == ujson.loads(
'''{"profile": "MEDIAN", "averageSpeed": 15,
"bikeType": "TRADITIONAL","eBike": false}'''
)

data = geovelo._make_request_arguments_bike_details(bike_speed_mps=4.1)
data = geovelo._make_request_arguments_bike_details(bike_speed_mps=4.1, use_ebike=True)
assert ujson.loads(ujson.dumps(data)) == ujson.loads(
'''{"profile": "MEDIAN", "averageSpeed": 15,
"bikeType": "TRADITIONAL"}'''
"bikeType": "TRADITIONAL","eBike": true}'''
)


Expand Down
2 changes: 1 addition & 1 deletion source/jormungandr/requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pytest==8.3.3 ; python_version >= "3.9"
pytest-mock==3.14.0 ; python_version >= "3.9"
pytest-cov==4.1.0 ; python_version >= "3.9"

requests-mock==1.0.0
requests-mock==1.12.1
flex==6.10.0
jsonschema==2.6.0
pytest-timeout==1.3.3
Expand Down
Loading