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',