diff --git a/source/jormungandr/jormungandr/instance.py b/source/jormungandr/jormungandr/instance.py
index 4f418570b7..8a3c551dbc 100644
--- a/source/jormungandr/jormungandr/instance.py
+++ b/source/jormungandr/jormungandr/instance.py
@@ -820,6 +820,8 @@ def additional_parameters(self):
additional_time_after_first_section_taxi = _make_property_getter('additional_time_after_first_section_taxi')
additional_time_before_last_section_taxi = _make_property_getter('additional_time_before_last_section_taxi')
+ on_street_bike_parking_duration = _make_property_getter('on_street_bike_parking_duration')
+
max_walking_direct_path_duration = _make_property_getter('max_walking_direct_path_duration')
max_bike_direct_path_duration = _make_property_getter('max_bike_direct_path_duration')
max_bss_direct_path_duration = _make_property_getter('max_bss_direct_path_duration')
diff --git a/source/jormungandr/jormungandr/interfaces/v1/Journeys.py b/source/jormungandr/jormungandr/interfaces/v1/Journeys.py
index c1ad3f657f..077a1000df 100644
--- a/source/jormungandr/jormungandr/interfaces/v1/Journeys.py
+++ b/source/jormungandr/jormungandr/interfaces/v1/Journeys.py
@@ -826,6 +826,9 @@ def _set_specific_params(mod):
if args.get('additional_time_before_last_section_taxi') is None:
args['additional_time_before_last_section_taxi'] = mod.additional_time_before_last_section_taxi
+ if args.get("on_street_bike_parking_duration") is None:
+ args["on_street_bike_parking_duration"] = mod.on_street_bike_parking_duration
+
if args.get('_stop_points_nearby_duration') is None:
args['_stop_points_nearby_duration'] = mod.stop_points_nearby_duration
diff --git a/source/jormungandr/jormungandr/interfaces/v1/journey_common.py b/source/jormungandr/jormungandr/interfaces/v1/journey_common.py
index e5f9bd866b..910eb7554b 100644
--- a/source/jormungandr/jormungandr/interfaces/v1/journey_common.py
+++ b/source/jormungandr/jormungandr/interfaces/v1/journey_common.py
@@ -30,7 +30,7 @@
# www.navitia.io
from __future__ import absolute_import, print_function, unicode_literals, division
-from jormungandr import i_manager, fallback_modes, partner_services, app
+from jormungandr import i_manager, fallback_modes, park_modes, partner_services, app
from jormungandr.interfaces.v1.ResourceUri import ResourceUri
from datetime import datetime
from jormungandr.resources_utils import ResourceUtc
@@ -239,6 +239,20 @@ def __init__(self, output_type_serializer):
action="append",
help='Same as first_section_mode but for the last section.',
)
+
+ parser_get.add_argument(
+ "park_mode[]",
+ type=OptionValue(park_modes.all_park_modes),
+ dest="park_mode",
+ action="append",
+ help='Force the park mode for the first or last section of a journey\n'
+ 'Need to be set with one of the first_section_mode[] or last_section_mode[] corresponding to vehicles that could be parked\n'
+ 'Note: Only work with the first or last section mode being a bike for the moment\n'
+ 'Eg: If you want to park a bike at the departure, you need:\n'
+ '`first_section_mode[]=bike&park_mode[]=on_street`'
+ 'Eg: If you want to park a bike at the arrival, you need:\n'
+ '`last_section_mode[]=bike&park_mode[]=on_street`',
+ )
# for retrocompatibility purpose, we duplicate (without []):
parser_get.add_argument(
"first_section_mode",
@@ -506,6 +520,13 @@ def __init__(self, output_type_serializer):
type=int,
help="the additional time added to the taxi section, right before riding the taxi but after hopping off the public transit",
)
+
+ parser_get.add_argument(
+ "on_street_bike_parking_duration",
+ type=int,
+ help="the additional time added to the bike section before and after parking the bike",
+ )
+
parser_get.add_argument(
"_pt_planner",
type=OptionValue(['kraken', 'loki']),
diff --git a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py
index cc7bcc410c..f9151b0913 100644
--- a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py
+++ b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py
@@ -74,6 +74,7 @@ class ParametersSerializer(serpy.Serializer):
max_extra_second_pass = Field(schema_type=int)
additional_time_after_first_section_taxi = Field(schema_type=int)
additional_time_before_last_section_taxi = Field(schema_type=int)
+ on_street_bike_parking_duration = Field(schema_type=int)
max_walking_direct_path_duration = Field(schema_type=int)
max_bike_direct_path_duration = Field(schema_type=int)
max_bss_direct_path_duration = Field(schema_type=int)
diff --git a/source/jormungandr/jormungandr/park_modes.py b/source/jormungandr/jormungandr/park_modes.py
new file mode 100644
index 0000000000..4e57f7084f
--- /dev/null
+++ b/source/jormungandr/jormungandr/park_modes.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2001-2024, Hove and/or its affiliates. All rights reserved.
+#
+# This file is part of Navitia,
+# the software to build cool stuff with public transport.
+#
+# Hope you'll enjoy and contribute to this project,
+# powered by Hove (www.hove.com).
+# Help us simplify mobility and open public transport:
+# a non ending quest to the responsive locomotion way of traveling!
+#
+# LICENCE: This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# Stay tuned using
+# twitter @navitia
+# channel `#navitia` on riot https://riot.im/app/#/room/#navitia:matrix.org
+# https://groups.google.com/d/forum/navitia
+# www.navitia.io
+#
+#
+
+
+from enum import Enum
+from navitiacommon import request_pb2
+
+
+class ParkMode(Enum):
+ none = request_pb2.NONE
+ on_street = request_pb2.OnStreet
+ park_and_ride = request_pb2.ParkAndRide
+
+ @classmethod
+ def modes_str(cls):
+
+ return {e.name for e in cls}
+
+
+all_park_modes = ParkMode.modes_str()
diff --git a/source/jormungandr/jormungandr/scenarios/distributed.py b/source/jormungandr/jormungandr/scenarios/distributed.py
index addabb3469..d35242809f 100644
--- a/source/jormungandr/jormungandr/scenarios/distributed.py
+++ b/source/jormungandr/jormungandr/scenarios/distributed.py
@@ -53,7 +53,7 @@
from jormungandr.new_relic import record_custom_parameter
from navitiacommon import response_pb2, type_pb2
from flask_restful import abort
-from .helper_classes.helper_utils import timed_logger
+from .helper_classes.timer_logger_helper import timed_logger
from .helper_classes.helper_exceptions import (
NoGraphicalIsochroneFoundException,
PtException,
@@ -355,6 +355,9 @@ def finalise_journeys(self, future_manager, request, responses, context, instanc
request=request,
journeys=journeys_to_complete,
request_id="{}_complete_pt_journey".format(request_id),
+ instance=instance,
+ future_manager=future_manager,
+ _request_id=request_id,
)
if request['_loki_compute_pt_journey_fare'] is True and request['_pt_planner'] == "loki":
wait_and_complete_pt_journey_fare(
@@ -589,6 +592,9 @@ def graphical_isochrones(self, request, instance):
'additional_time_before_last_section_taxi'
] = instance.additional_time_after_first_section_taxi
+ if request.get('on_street_bike_parking_duration') is None:
+ request['on_street_bike_parking_duration'] = instance.on_street_bike_parking_duration
+
krakens_call = set({(request["origin_mode"][0], request["destination_mode"][0], "indifferent")})
pt_object_origin = None
pt_object_destination = None
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/complete_pt_journey.py b/source/jormungandr/jormungandr/scenarios/helper_classes/complete_pt_journey.py
index 38c95fa7fc..4df5bfef5d 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/complete_pt_journey.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/complete_pt_journey.py
@@ -32,9 +32,9 @@
complete_pt_journey,
compute_fallback,
_build_crowflies,
- timed_logger,
complete_transfer,
)
+from .timer_logger_helper import timed_logger
from .helper_exceptions import InvalidDateBoundException
from jormungandr.street_network.street_network import StreetNetworkPathType
from collections import namedtuple
@@ -166,6 +166,7 @@ def wait_and_complete_pt_journey(
request,
journeys,
request_id,
+ **kwargs
):
"""
In this function, we compute all fallback path once the pt journey is finished, then we build the
@@ -206,6 +207,7 @@ def wait_and_complete_pt_journey(
orig_fallback_durations_pool=orig_fallback_durations_pool,
dest_fallback_durations_pool=dest_fallback_durations_pool,
request=request,
+ **kwargs
)
if {pt_element.dep_mode, pt_element.arr_mode} & {'car', 'car_no_park'}:
tag_LEZ(pt_element.pt_journeys)
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/fallback_durations.py b/source/jormungandr/jormungandr/scenarios/helper_classes/fallback_durations.py
index 158089d60e..9b7b5581ef 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/fallback_durations.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/fallback_durations.py
@@ -38,7 +38,7 @@
from jormungandr import new_relic, excluded_zones_manager
from jormungandr.fallback_modes import FallbackModes
import logging
-from .helper_utils import timed_logger
+from .timer_logger_helper import timed_logger
import six
from navitiacommon import type_pb2
from jormungandr.exceptions import GeoveloTechnicalError
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/helper_utils.py b/source/jormungandr/jormungandr/scenarios/helper_classes/helper_utils.py
index 773e5558d9..c7960183de 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/helper_utils.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/helper_utils.py
@@ -31,6 +31,7 @@
from __future__ import absolute_import, unicode_literals
+from jormungandr.park_modes import ParkMode
from jormungandr.street_network.street_network import StreetNetworkPathType
from jormungandr.utils import (
PeriodExtremity,
@@ -38,8 +39,10 @@
get_pt_object_coord,
generate_id,
)
-from jormungandr.street_network.utils import crowfly_distance_between
+from jormungandr.street_network.utils import crowfly_distance_between, get_manhattan_duration
from jormungandr.fallback_modes import FallbackModes, all_fallback_modes
+from jormungandr.scenarios.helper_classes.place_by_uri import PlaceByUri
+import math
from .helper_exceptions import *
from navitiacommon import response_pb2, type_pb2
import copy
@@ -192,6 +195,29 @@ def _make_ending_car_park_sections(
car_park_to_sp_section.duration = car_park_crowfly_duration
+def _make_bike_park(begin_date_time, duration):
+ bike_park_section = response_pb2.Section()
+ bike_park_section.id = "section_bike_park"
+ bike_park_section.duration = duration
+ bike_park_section.type = response_pb2.PARK
+ bike_park_section.begin_date_time = begin_date_time
+ bike_park_section.end_date_time = bike_park_section.begin_date_time + duration
+ return bike_park_section
+
+
+def _make_bike_park_street_network(origin, begin_date_time, destination, end_date_time, duration):
+ bike_park_to_sp_section = response_pb2.Section()
+ bike_park_to_sp_section.id = "Street_network_section_2"
+ bike_park_to_sp_section.origin.CopyFrom(origin)
+ bike_park_to_sp_section.destination.CopyFrom(destination)
+ bike_park_to_sp_section.type = response_pb2.STREET_NETWORK
+ bike_park_to_sp_section.street_network.mode = response_pb2.Walking
+ bike_park_to_sp_section.begin_date_time = begin_date_time
+ bike_park_to_sp_section.end_date_time = end_date_time + duration
+ bike_park_to_sp_section.duration = duration
+ return bike_park_to_sp_section
+
+
def _extend_with_car_park(
fallback_dp, pt_journey, fallback_type, walking_speed, car_park, car_park_duration, car_park_crowfly_duration
):
@@ -391,6 +417,162 @@ def add_poi_access_point_in_sections(fallback_type, via_poi_access, sections):
poi_access.is_entrance = True
+def _update_journey(journey, park_section, street_mode_section, to_replace, new_fallbacks):
+ journey.duration += park_section.duration + street_mode_section.duration
+ journey.durations.total += park_section.duration + street_mode_section.duration
+ journey.arrival_date_time += park_section.duration + street_mode_section.duration
+ journey.sections.remove(to_replace)
+ journey.sections.extend([street_mode_section, park_section])
+ journey.sections.extend(new_fallbacks)
+ journey.nb_sections += 2
+
+
+def walking_time(cord1, cord2, walking_speed):
+ """
+ Calculate the walking time between two coordinates.
+
+ Args:
+ cord1 (tuple): The (latitude, longitude) of the starting point.
+ cord2 (tuple): The (latitude, longitude) of the destination point.
+ walking_speed (float, optional): Walking speed in meter per seconds.
+
+ Returns:
+ float: The walking time in secondes.
+ """
+ distance = crowfly_distance_between(cord1, cord2)
+ return get_manhattan_duration(distance, walking_speed)
+
+
+def _get_place(kwargs, uri):
+ """
+ Retrieve a place instance based on the provided URI.
+
+ Args:
+ kwargs (dict): A dictionary containing the following keys:
+ - future_manager: The future manager instance.
+ - instance: The instance to be used.
+ - request_id: The ID of the request.
+ uri (str): The URI of the place to retrieve.
+
+ Returns:
+ PlaceByUri: An instance of PlaceByUri after waiting for the result.
+ """
+ place_by_uri_instance = PlaceByUri(kwargs["future_manager"], kwargs["instance"], uri, kwargs["request_id"])
+ return place_by_uri_instance.wait_and_get()
+
+
+def _update_fallback_with_bike_mode(
+ journey, fallback_dp, fallback_period_extremity, fallback_type, via_pt_access, via_poi_access, **kwargs
+):
+ """
+ Updates the journey with bike mode fallback sections.
+
+ This function updates the journey sections with bike mode fallback sections based on the fallback type
+ (BEGINNING_FALLBACK or ENDING_FALLBACK). It aligns the fallback direct path datetime, updates the section IDs,
+ and creates the necessary links between the fallback and public transport parts. It also handles the addition
+ of POI access points in the sections.
+
+ Args:
+ journey (Journey): The journey object to be updated.
+ fallback_dp (DirectPath): The direct path object for the fallback.
+ fallback_period_extremity (datetime): The extremity datetime for the fallback period.
+ fallback_type (StreetNetworkPathType): The type of fallback (BEGINNING_FALLBACK or ENDING_FALLBACK).
+ via_pt_access (PtObject): The public transport access point object.
+ via_poi_access (POIObject): The point of interest access point object.
+ **kwargs: Additional keyword arguments, including:
+ - origin_mode (list): The mode of origin (e.g., ["bike"]).
+ - destination_mode (list): The mode of destination (e.g., ["bike"]).
+ - future_manager (FutureManager): The future manager instance.
+ - instance (Instance): The instance object.
+ - request_id (str): The request ID.
+ - additional_time (timedelta): The additional time to be added to the sections.
+
+ Returns:
+ None
+ """
+ # Validate required arguments
+ if not all(kwargs.get(key) for key in ("future_manager", "instance", "request_id")):
+ raise EntryPointException(
+ "Future manager, instance, and request ID are required.", response_pb2.Error.internal_error
+ )
+
+ aligned_fallback = _align_fallback_direct_path_datetime(fallback_dp, fallback_period_extremity)
+ fallback_sections = aligned_fallback.journeys[0].sections
+ _rename_fallback_sections_ids(fallback_sections)
+
+ # We have to create the link between the fallback and the pt part manually here
+ if fallback_type == StreetNetworkPathType.BEGINNING_FALLBACK and "bike" in kwargs["origin_mode"]:
+ address = _get_place(kwargs, fallback_sections[-1].destination.uri)
+ walktime = walking_time(
+ address.address.coord,
+ journey.sections[0].destination.stop_point.coord,
+ kwargs["instance"].walking_speed,
+ )
+ fallback_sections[-1].destination.CopyFrom(address)
+ for s in journey.sections:
+ s.begin_date_time += kwargs["additional_time"] + walktime
+ s.end_date_time += kwargs["additional_time"] + walktime
+ park_section = _make_bike_park(fallback_sections[-1].end_date_time, kwargs["additional_time"])
+ street_mode_section = _make_bike_park_street_network(
+ fallback_sections[-1].destination,
+ park_section.end_date_time,
+ journey.sections[0].destination,
+ (fallback_sections[-1].end_date_time + park_section.duration) - journey.sections[0].end_date_time,
+ walktime,
+ )
+ street_mode_section.street_network.coordinates.extend(
+ [journey.sections[0].destination.stop_point.coord, fallback_sections[-1].destination.address.coord]
+ )
+ _update_journey(journey, park_section, street_mode_section, journey.sections[0], fallback_sections)
+ if fallback_type == StreetNetworkPathType.BEGINNING_FALLBACK and "bike" not in kwargs["origin_mode"]:
+ section_to_replace = journey.sections[0]
+ journey.sections.remove(section_to_replace)
+ fallback_sections[-1].destination.CopyFrom(journey.sections[0].origin)
+ journey.sections.extend(fallback_sections)
+ elif fallback_type == StreetNetworkPathType.ENDING_FALLBACK and "bike" in kwargs["destination_mode"]:
+ walktime = walking_time(
+ journey.sections[-1].origin.stop_point.coord,
+ fallback_sections[0].origin.address.coord,
+ kwargs["instance"].walking_speed,
+ )
+ address = _get_place(kwargs, fallback_sections[0].origin.uri)
+ fallback_sections[0].origin.CopyFrom(address)
+ street_mode_section = _make_bike_park_street_network(
+ journey.sections[-1].origin,
+ fallback_sections[0].begin_date_time,
+ fallback_sections[0].origin,
+ (fallback_sections[0].begin_date_time + kwargs["additional_time"])
+ - journey.sections[-1].begin_date_time,
+ walktime,
+ )
+ park_section = _make_bike_park(street_mode_section.begin_date_time, kwargs["additional_time"])
+ fallback_sections[0].begin_date_time += kwargs["additional_time"]
+ street_mode_section.street_network.coordinates.extend(
+ [journey.sections[-1].origin.stop_point.coord, fallback_sections[0].origin.address.coord]
+ )
+ _update_journey(journey, park_section, street_mode_section, journey.sections[-1], fallback_sections)
+ elif fallback_type == StreetNetworkPathType.ENDING_FALLBACK and "bike" not in kwargs["destination_mode"]:
+ section_to_replace = journey.sections[-1]
+ journey.sections.remove(section_to_replace)
+ fallback_sections[0].origin.CopyFrom(journey.sections[-1].destination)
+ journey.sections.extend(fallback_sections)
+
+ add_poi_access_point_in_sections(fallback_type, via_poi_access, fallback_sections)
+
+ if isinstance(via_pt_access, type_pb2.PtObject) and via_pt_access.embedded_type == type_pb2.ACCESS_POINT:
+ if fallback_type == StreetNetworkPathType.BEGINNING_FALLBACK:
+ journey.sections[-1].vias.add().CopyFrom(via_pt_access.access_point)
+ target_section = next((s for s in journey.sections if s.id == "Street_network_section_2"), None)
+ if target_section:
+ target_section.vias.extend(journey.sections[-1].vias)
+ target_section.street_network.path_items.extend(
+ [journey.sections[-1].street_network.path_items[-1]]
+ )
+ else:
+ journey.sections[0].vias.add().CopyFrom(via_pt_access.access_point)
+ journey.sections.sort(key=cmp_to_key(SectionSorter()))
+
+
def _update_fallback_sections(
journey, fallback_dp, fallback_period_extremity, fallback_type, via_pt_access, via_poi_access
):
@@ -575,6 +757,7 @@ def _build_fallback(
fallback_durations_pool,
request,
fallback_type,
+ **kwargs
):
accessibles_by_crowfly = obj_accessible_by_crowfly.wait_and_get()
fallback_durations = fallback_durations_pool.wait_and_get(mode)
@@ -638,14 +821,34 @@ def _build_fallback(
fallback_dp_copy, fallback_type, requested_obj, via_poi_access, language
)
- _update_fallback_sections(
- pt_journey,
- fallback_dp_copy,
- fallback_period_extremity,
- fallback_type,
- via_pt_access,
- via_poi_access,
- )
+ if request["park_mode"] == [ParkMode.on_street.name] and (
+ request["origin_mode"] == ["bike"] or request["destination_mode"] == ["bike"]
+ ):
+ _update_fallback_with_bike_mode(
+ pt_journey,
+ fallback_dp_copy,
+ fallback_period_extremity,
+ fallback_type,
+ via_pt_access,
+ via_poi_access,
+ park_mode=request["park_mode"],
+ origin_mode=request["origin_mode"],
+ destination_mode=request["destination_mode"],
+ additional_time=request["on_street_bike_parking_duration"],
+ instance=kwargs["instance"],
+ future_manager=kwargs["future_manager"],
+ request_id=kwargs["_request_id"],
+ request=request,
+ )
+ else:
+ _update_fallback_sections(
+ pt_journey,
+ fallback_dp_copy,
+ fallback_period_extremity,
+ fallback_type,
+ via_pt_access,
+ via_poi_access,
+ )
# update distances and durations by mode if it's a proper computed streetnetwork fallback
if fallback_dp_copy and fallback_dp_copy.journeys:
@@ -794,6 +997,7 @@ def complete_pt_journey(
orig_fallback_durations_pool,
dest_fallback_durations_pool,
request,
+ **kwargs
):
"""
We complete the pt_journey by adding the beginning fallback and the ending fallback
@@ -812,6 +1016,7 @@ def complete_pt_journey(
orig_fallback_durations_pool,
request=request,
fallback_type=StreetNetworkPathType.BEGINNING_FALLBACK,
+ **kwargs
)
pt_journey = _build_fallback(
@@ -823,6 +1028,7 @@ def complete_pt_journey(
dest_fallback_durations_pool,
request=request,
fallback_type=StreetNetworkPathType.ENDING_FALLBACK,
+ **kwargs
)
logger.debug("finish building pt journey starts with %s and ends with %s", dep_mode, arr_mode)
@@ -861,24 +1067,6 @@ def check_final_results_or_raise(final_results, orig_fallback_durations_pool, de
)
-@contextmanager
-def timed_logger(logger, task_name, request_id):
- start = time.time()
- try:
- yield logger
- finally:
- end = time.time()
- elapsed_time = (end - start) * 1000
- start_in_ms = int(start * 1000)
- end_in_ms = int(end * 1000)
-
- logger.info(
- "Task : {}, request : {}, start : {}, end : {}, elapsed time: {} ms".format(
- task_name, request_id, start_in_ms, end_in_ms, '%.2e' % elapsed_time
- )
- )
-
-
def complete_transfer(pt_journey, transfer_pool):
"""
We complete the pt_journey by adding to transfer section :
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/place_by_uri.py b/source/jormungandr/jormungandr/scenarios/helper_classes/place_by_uri.py
index 224a0965cb..2aa267bcd9 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/place_by_uri.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/place_by_uri.py
@@ -29,7 +29,7 @@
from __future__ import absolute_import
from jormungandr import new_relic
import logging
-from .helper_utils import timed_logger
+from .timer_logger_helper import timed_logger
class PlaceByUri:
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/places_free_access.py b/source/jormungandr/jormungandr/scenarios/helper_classes/places_free_access.py
index e6f975e4ad..cd76e3d063 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/places_free_access.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/places_free_access.py
@@ -33,7 +33,7 @@
from jormungandr import utils, new_relic
from collections import namedtuple
import logging
-from .helper_utils import timed_logger
+from .timer_logger_helper import timed_logger
from jormungandr.street_network.utils import crowfly_distance_between
FreeAccessObject = namedtuple('FreeAccessObject', ['uri', 'lon', 'lat'])
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py b/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py
index 675d27f103..03d3159ac5 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py
@@ -29,7 +29,8 @@
from __future__ import absolute_import
import jormungandr.street_network.utils
-from .helper_utils import get_max_fallback_duration, timed_logger
+from .helper_utils import get_max_fallback_duration
+from .timer_logger_helper import timed_logger
from jormungandr import utils, new_relic, fallback_modes as fm
import logging
from navitiacommon import type_pb2
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/pt_journey.py b/source/jormungandr/jormungandr/scenarios/helper_classes/pt_journey.py
index de40d334fa..d1100aa92f 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/pt_journey.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/pt_journey.py
@@ -37,7 +37,7 @@
import copy
import logging
from functools import cmp_to_key
-from .helper_utils import timed_logger
+from .timer_logger_helper import timed_logger
PtPoolElement = namedtuple('PtPoolElement', ['dep_mode', 'arr_mode', 'pt_journey'])
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/streetnetwork_path.py b/source/jormungandr/jormungandr/scenarios/helper_classes/streetnetwork_path.py
index 16e87444bb..9833f891d1 100644
--- a/source/jormungandr/jormungandr/scenarios/helper_classes/streetnetwork_path.py
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/streetnetwork_path.py
@@ -32,7 +32,6 @@
import logging
import gevent
from .helper_utils import (
- timed_logger,
prepend_first_coord,
append_last_coord,
extend_path_with_via_poi_access,
@@ -40,6 +39,7 @@
is_valid_direct_path_streetwork,
is_valid_direct_path,
)
+from .timer_logger_helper import timed_logger
from navitiacommon import type_pb2, response_pb2
from jormungandr.exceptions import GeoveloTechnicalError
from .helper_exceptions import StreetNetworkException
diff --git a/source/jormungandr/jormungandr/scenarios/helper_classes/timer_logger_helper.py b/source/jormungandr/jormungandr/scenarios/helper_classes/timer_logger_helper.py
new file mode 100644
index 0000000000..56af1e9f29
--- /dev/null
+++ b/source/jormungandr/jormungandr/scenarios/helper_classes/timer_logger_helper.py
@@ -0,0 +1,50 @@
+# encoding: utf-8
+# Copyright (c) 2001-2024, Hove and/or its affiliates. All rights reserved.
+#
+# This file is part of Navitia,
+# the software to build cool stuff with public transport.
+#
+# Hope you'll enjoy and contribute to this project,
+# powered by Hove (www.hove.com).
+# Help us simplify mobility and open public transport:
+# a non ending quest to the responsive locomotion way of traveling!
+#
+# LICENCE: This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# Stay tuned using
+# twitter @navitia
+# channel `#navitia` on riot https://riot.im/app/#/room/#navitia:matrix.org
+# https://groups.google.com/d/forum/navitia
+# www.navitia.io
+
+from contextlib import contextmanager
+import time
+
+
+@contextmanager
+def timed_logger(logger, task_name, request_id):
+ start = time.time()
+ try:
+ yield logger
+ finally:
+ end = time.time()
+ elapsed_time = (end - start) * 1000
+ start_in_ms = int(start * 1000)
+ end_in_ms = int(end * 1000)
+
+ logger.info(
+ "Task : {}, request : {}, start : {}, end : {}, elapsed time: {} ms".format(
+ task_name, request_id, start_in_ms, end_in_ms, '%.2e' % elapsed_time
+ )
+ )
diff --git a/source/jormungandr/tests/routing_tests_experimental.py b/source/jormungandr/tests/routing_tests_experimental.py
index 6e4e66c6c8..182cbceaa5 100644
--- a/source/jormungandr/tests/routing_tests_experimental.py
+++ b/source/jormungandr/tests/routing_tests_experimental.py
@@ -1489,3 +1489,77 @@ def test_same_journey_schedules_link(self):
assert href_value is not None
# Before correction: assert href_value.count('allowed_id') == 11
assert href_value.count('allowed_id') == 2
+
+
+@dataset({"main_routing_test": {"scenario": "distributed"}})
+class TestBikeWithParkingPenalty(NewDefaultScenarioAbstractTestFixture):
+ def test_bike_with_parking_penalty_first_section(self):
+ query = (
+ sub_query
+ + "&datetime=20120614T075000"
+ + "&from=2.36893;48.88413"
+ + "&to=2.28928;48.84710"
+ + "&first_section_mode[]=bike"
+ + "&bike_speed=0.1"
+ + "&park_mode[]=on_street"
+ )
+
+ response = self.query_region(query)
+ check_best(response)
+
+ journeys = get_not_null(response, 'journeys')
+ assert len(journeys) > 0
+ for journey in journeys:
+ if len(journey['sections']) > 1:
+ assert journey['sections'][0]['mode'] == 'bike'
+ assert journey['sections'][1]['type'] == 'park'
+ assert journey['sections'][2]['type'] == 'street_network'
+ assert journey['sections'][2]['mode'] == 'walking'
+ assert len(journey['sections'][2]['geojson']['coordinates']) == 2
+
+ def test_bike_with_parking_penalty_last_section(self):
+ query = (
+ sub_query
+ + "&datetime=20120614T075000"
+ + "&from=2.36893;48.88413"
+ + "&to=2.28928;48.84710"
+ + "&last_section_mode[]=bike"
+ + "&bike_speed=0.1"
+ + "&park_mode[]=on_street"
+ )
+
+ response = self.query_region(query)
+ check_best(response)
+
+ journeys = get_not_null(response, 'journeys')
+ assert len(journeys) > 0
+ for journey in journeys:
+ if len(journey['sections']) > 1:
+ assert journey['sections'][-1]['mode'] == 'bike'
+ assert journey['sections'][-2]['type'] == 'park'
+ assert journey['sections'][-3]['type'] == 'street_network'
+
+ def test_bike_with_parking_penalty_first_section_and_access_points(self):
+ query = (
+ sub_query
+ + "&datetime=20120614T075000"
+ + "&from=2.36893;48.88413"
+ + "&to=2.28928;48.84710"
+ + "&first_section_mode[]=bike"
+ + "&bike_speed=0.1"
+ + "&park_mode[]=on_street"
+ + "&_access_points=true"
+ )
+
+ response = self.query_region(query)
+ check_best(response)
+
+ journeys = get_not_null(response, 'journeys')
+ assert len(journeys) > 0
+ for journey in journeys:
+ if len(journey['sections']) > 1:
+ assert journey['sections'][0]['mode'] == 'bike'
+ assert journey['sections'][1]['type'] == 'park'
+ assert journey['sections'][2]['type'] == 'street_network'
+ assert journey['sections'][2]['mode'] == 'walking'
+ assert len(journey['sections'][2]['vias']) >= 1
diff --git a/source/navitia-proto b/source/navitia-proto
index 1b261a831c..6a2edc1d04 160000
--- a/source/navitia-proto
+++ b/source/navitia-proto
@@ -1 +1 @@
-Subproject commit 1b261a831c2be711d07d5682de8b05afd0764a87
+Subproject commit 6a2edc1d043b6c8025707948e14029380a4a2a36
diff --git a/source/navitiacommon/navitiacommon/default_values.py b/source/navitiacommon/navitiacommon/default_values.py
index 9b86a338d2..64da1f7b40 100644
--- a/source/navitiacommon/navitiacommon/default_values.py
+++ b/source/navitiacommon/navitiacommon/default_values.py
@@ -151,6 +151,10 @@
# Additionnal time in second before the taxi section when used as last section mode
additional_time_before_last_section_taxi = 5 * 60
+# Additionnal time in second after the parking section when used as first section mode
+on_street_bike_parking_duration = 5 * 60
+
+
max_walking_direct_path_duration = 24 * 60 * 60
max_bike_direct_path_duration = 24 * 60 * 60
diff --git a/source/navitiacommon/navitiacommon/models/__init__.py b/source/navitiacommon/navitiacommon/models/__init__.py
index cb1bda3f7e..26f59b3732 100644
--- a/source/navitiacommon/navitiacommon/models/__init__.py
+++ b/source/navitiacommon/navitiacommon/models/__init__.py
@@ -516,6 +516,10 @@ class Instance(db.Model): # type: ignore
db.Integer, default=default_values.additional_time_before_last_section_taxi, nullable=False
)
+ on_street_bike_parking_duration = db.Column(
+ db.Integer, default=default_values.on_street_bike_parking_duration, nullable=False
+ )
+
max_walking_direct_path_duration = db.Column(
db.Integer, default=default_values.max_walking_direct_path_duration, nullable=False
)
diff --git a/source/tyr/migrations/versions/c3ba234a3b9c_add_on_street_bike_parking_duration.py b/source/tyr/migrations/versions/c3ba234a3b9c_add_on_street_bike_parking_duration.py
new file mode 100644
index 0000000000..2f7ec2c334
--- /dev/null
+++ b/source/tyr/migrations/versions/c3ba234a3b9c_add_on_street_bike_parking_duration.py
@@ -0,0 +1,25 @@
+"""empty message
+
+Revision ID: c3ba234a3b9c
+Revises: fd13bb348665
+Create Date: 2024-11-29 11:45:56.806718
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'c3ba234a3b9c'
+down_revision = 'fd13bb348665'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column(
+ 'instance',
+ sa.Column('on_street_bike_parking_duration', sa.Integer(), nullable=False, server_default='300'),
+ )
+
+
+def downgrade():
+ op.drop_column('instance', 'on_street_bike_parking_duration')
diff --git a/source/tyr/tyr/fields.py b/source/tyr/tyr/fields.py
index c2438b4157..18a2b8b3f9 100644
--- a/source/tyr/tyr/fields.py
+++ b/source/tyr/tyr/fields.py
@@ -198,6 +198,7 @@ def format(self, value):
'autocomplete_backend': fields.Raw,
'additional_time_after_first_section_taxi': fields.Raw,
'additional_time_before_last_section_taxi': fields.Raw,
+ 'on_street_bike_parking_duration': fields.Raw,
'max_additional_connections': fields.Raw,
'successive_physical_mode_to_limit_id': fields.Raw,
'car_park_provider': fields.Raw,
diff --git a/source/tyr/tyr/resources.py b/source/tyr/tyr/resources.py
index e0e6881e9b..08ddf6cdf4 100644
--- a/source/tyr/tyr/resources.py
+++ b/source/tyr/tyr/resources.py
@@ -711,6 +711,14 @@ def put(self, version=0, id=None, name=None):
default=instance.additional_time_before_last_section_taxi,
)
+ parser.add_argument(
+ "on_street_bike_parking_duration",
+ type=int,
+ help="additionnal time after the bike section when used as first section mode",
+ location=("json", "values"),
+ default=instance.on_street_bike_parking_duration,
+ )
+
parser.add_argument(
'max_additional_connections',
type=int,
@@ -1050,6 +1058,7 @@ def map_args_to_instance(attr_name):
'autocomplete_backend',
'additional_time_after_first_section_taxi',
'additional_time_before_last_section_taxi',
+ 'on_street_bike_parking_duration',
'max_additional_connections',
'car_park_provider',
'street_network_car',