diff --git a/source/jormungandr/jormungandr/interfaces/v1/Places.py b/source/jormungandr/jormungandr/interfaces/v1/Places.py index ea96010d0c..3e35b74d76 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/Places.py +++ b/source/jormungandr/jormungandr/interfaces/v1/Places.py @@ -396,7 +396,7 @@ def get(self, region=None, lon=None, lat=None, uri=None): uris = uri.split("/") if len(uris) >= 2: args["uri"] = transform_id(uris[-1]) - # for coherence we check the type of the object + # for coherence, we check the type of the object obj_type = uris[-2] if obj_type not in places_types: abort(404, message='places_nearby api not available for {}'.format(obj_type)) diff --git a/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py b/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py index 53d1db434b..4dc2cfe5a2 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py +++ b/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py @@ -121,6 +121,7 @@ class PtObjectSerializer(PbGenericSerializer): quality = jsonschema.Field(schema_type=int, required=False, display_none=True, deprecated=True) stop_area = jsonschema.MethodField(schema_type=lambda: StopAreaSerializer()) stop_point = jsonschema.MethodField(schema_type=lambda: StopPointSerializer()) + poi = jsonschema.MethodField(schema_type=lambda: PoiSerializer()) line = jsonschema.MethodField(schema_type=lambda: LineSerializer()) network = jsonschema.MethodField(schema_type=lambda: NetworkSerializer()) route = jsonschema.MethodField(schema_type=lambda: RouteSerializer()) @@ -170,6 +171,12 @@ def get_stop_point(self, obj): else: return None + def get_poi(self, obj): + if obj.HasField(str('poi')): + return PoiSerializer(obj.poi, display_none=False).data + else: + return None + class TripSerializer(PbGenericSerializer): pass @@ -357,6 +364,7 @@ class AddressSerializer(PbGenericSerializer): class PoiSerializer(PbGenericSerializer): coord = CoordSerializer(required=False) + links = DisruptionLinkSerializer(attr='impact_uris', display_none=False) label = jsonschema.Field(schema_type=str) administrative_regions = AdminSerializer(many=True, display_none=False) poi_type = PoiTypeSerializer(display_none=False) diff --git a/source/jormungandr/jormungandr/scenarios/new_default.py b/source/jormungandr/jormungandr/scenarios/new_default.py index c052607f17..489c31e197 100644 --- a/source/jormungandr/jormungandr/scenarios/new_default.py +++ b/source/jormungandr/jormungandr/scenarios/new_default.py @@ -47,6 +47,9 @@ switch_back_to_ridesharing, nCr, updated_common_journey_request_with_default, + get_disruptions_on_poi, + add_disruptions, + get_impact_uris_for_poi, ) from navitiacommon import type_pb2, response_pb2, request_pb2 from jormungandr.scenarios.qualifier import ( @@ -99,6 +102,7 @@ from six.moves import range from six.moves import zip from functools import cmp_to_key +from datetime import datetime SECTION_TYPES_TO_RETAIN = {response_pb2.PUBLIC_TRANSPORT, response_pb2.STREET_NETWORK} JOURNEY_TAGS_TO_RETAIN = ['best_olympics'] @@ -491,6 +495,50 @@ def update_total_co2_emission(pb_resp): j.co2_emission.unit = 'gEC' +def update_disruptions_on_pois(instance, pb_resp): + """ + Maintain a set of uri from all journey.section.origin and journey.section.destination of type Poi, + call loki with api api_disruptions&pois[]... + For each disruption on poi, add disruption id in the attribute links and add disruptions in the response + """ + if not pb_resp.journeys: + return + # Add uri of all the pois in a set + poi_uris = set() + poi_objets = [] + since_datetime = date_to_timestamp(datetime.utcnow()) + until_datetime = date_to_timestamp(datetime.utcnow()) + for j in pb_resp.journeys: + for s in j.sections: + if s.origin.embedded_type == type_pb2.POI: + poi_uris.add(s.origin.uri) + poi_objets.append(s.origin.poi) + since_datetime = min(since_datetime, s.begin_date_time) + + if s.destination.embedded_type == type_pb2.POI: + poi_uris.add(s.destination.uri) + poi_objets.append(s.destination.poi) + until_datetime = max(until_datetime, s.end_date_time) + + if since_datetime >= until_datetime: + since_datetime = until_datetime - 1 + # Get disruptions for poi_uris calling loki with api poi_disruptions and poi_uris in param + poi_disruptions = get_disruptions_on_poi(instance, poi_uris, since_datetime, until_datetime) + if poi_disruptions is None: + return + + # For each poi in pt_objects: + # add impact_uris from resp_poi and + # copy object poi in impact.impacted_objects + for pt_object in poi_objets: + impact_uris = get_impact_uris_for_poi(poi_disruptions, pt_object) + for impact_uri in impact_uris: + pt_object.impact_uris.append(impact_uri) + + # Add all impacts from resp_poi to the response + add_disruptions(pb_resp, poi_disruptions) + + def update_total_air_pollutants(pb_resp): """ update journey.air_pollutants @@ -1434,6 +1482,9 @@ def fill_journeys(self, request_type, api_request, instance): # need to clean extra terminus after culling journeys journey_filter.remove_excess_terminus(pb_resp) + # Update disruptions on pois + update_disruptions_on_pois(instance, pb_resp) + self._compute_pagination_links(pb_resp, instance, api_request['clockwise']) return pb_resp diff --git a/source/jormungandr/jormungandr/scenarios/simple.py b/source/jormungandr/jormungandr/scenarios/simple.py index 5edf42230f..2bb6a11db5 100644 --- a/source/jormungandr/jormungandr/scenarios/simple.py +++ b/source/jormungandr/jormungandr/scenarios/simple.py @@ -38,7 +38,11 @@ from navitiacommon.type_pb2 import ActiveStatus, Severity from jormungandr.interfaces.common import pb_odt_level from jormungandr.scenarios.utils import places_type, pt_object_type, add_link -from jormungandr.scenarios.utils import build_pagination +from jormungandr.scenarios.utils import ( + build_pagination, + fill_disruptions_on_pois, + fill_disruptions_on_places_nearby, +) from jormungandr.exceptions import UnknownObject @@ -301,6 +305,9 @@ def places_nearby(self, request, instance): req.places_nearby.filter = request["filter"] req.disable_disruption = request["disable_disruption"] if request.get("disable_disruption") else False resp = instance.send_and_receive(req) + # For pois, ws should also call loki to get disruptions on pois + if not req.disable_disruption: + fill_disruptions_on_places_nearby(instance, resp) build_pagination(request, resp) return resp @@ -335,6 +342,9 @@ def __on_ptref(self, resource_name, requested_type, request, instance): else: resp = instance.send_and_receive(req) + # For api = pois, ws should also call loki to get disruptions on pois + if (not req.disable_disruption) and req.ptref.requested_type == type_pb2.POI: + fill_disruptions_on_pois(instance, resp) build_pagination(request, resp) return resp diff --git a/source/jormungandr/jormungandr/scenarios/tests/helpers_tests.py b/source/jormungandr/jormungandr/scenarios/tests/helpers_tests.py index 7cf17df7b5..abc2d37a0f 100644 --- a/source/jormungandr/jormungandr/scenarios/tests/helpers_tests.py +++ b/source/jormungandr/jormungandr/scenarios/tests/helpers_tests.py @@ -32,6 +32,8 @@ is_car_direct_path, fill_best_boarding_position, ) +from jormungandr.street_network.tests.streetnetwork_test_utils import make_pt_object +from jormungandr import utils BEST_BOARDING_POSITIONS = [response_pb2.FRONT, response_pb2.MIDDLE] @@ -516,3 +518,120 @@ def fill_best_boarding_position_test(): assert response_pb2.BoardingPosition.FRONT in journey.sections[0].best_boarding_positions assert response_pb2.BoardingPosition.MIDDLE in journey.sections[0].best_boarding_positions assert response_pb2.BoardingPosition.BACK not in journey.sections[0].best_boarding_positions + + +def get_response_with_a_disruption_on_poi(): + start_period = "20240712T165200" + end_period = "20240812T165200" + response = response_pb2.Response() + impact = response.impacts.add() + impact.uri = "test_impact_uri" + impact.disruption_uri = "test_disruption_uri" + impacted_object = impact.impacted_objects.add() + + # poi = make_pt_object(type_pb2.POI, lon=1, lat=2, uri='poi:test_uri') + # impacted_object.pt_object.CopyFrom(poi) + impacted_object.pt_object.name = "poi_name_from_loki" + impacted_object.pt_object.uri = "poi_uri" + impacted_object.pt_object.embedded_type = type_pb2.POI + impact.updated_at = utils.str_to_time_stamp(u'20240712T205200') + application_period = impact.application_periods.add() + application_period.begin = utils.str_to_time_stamp(start_period) + application_period.end = utils.str_to_time_stamp(end_period) + + # Add a message + message = impact.messages.add() + message.text = "This is the message sms" + message.channel.id = "sms" + message.channel.name = "sms" + message.channel.content_type = "text" + + # Add a severity + impact.severity.effect = type_pb2.Severity.UNKNOWN_EFFECT + impact.severity.name = ' not blocking' + impact.severity.priority = 1 + impact.contributor = "shortterm.test_poi" + + return response + + +def get_object_pois_in_ptref_response(): + response = response_pb2.Response() + poi = response.pois.add() + poi.uri = "poi_uri" + poi.name = "poi_name_from_kraken" + poi.coord.lat = 2 + poi.coord.lon = 1 + poi.poi_type.uri = "poi_type:amenity:parking" + poi.poi_type.name = "Parking P+R" + return response + + +def get_object_pois_in_places_nearby_response(): + response = response_pb2.Response() + place_nearby = response.places_nearby.add() + place_nearby.uri = "poi_uri" + place_nearby.name = "poi_name_from_kraken" + place_nearby.embedded_type = type_pb2.POI + place_nearby.poi.uri = "poi_uri" + place_nearby.poi.name = "poi_name_from_kraken" + place_nearby.poi.coord.lat = 2 + place_nearby.poi.coord.lon = 1 + place_nearby.poi.poi_type.uri = "poi_type:amenity:parking" + place_nearby.poi.poi_type.name = "Parking P+R" + return response + + +def get_journey_with_pois(): + response = response_pb2.Response() + journey = response.journeys.add() + + # Walking section from 'stop_a' to 'poi:test_uri' + section = journey.sections.add() + section.type = response_pb2.STREET_NETWORK + section.street_network.mode = response_pb2.Walking + section.origin.uri = 'stop_a' + section.origin.embedded_type = type_pb2.STOP_POINT + section.destination.uri = 'poi_uri' + section.destination.embedded_type = type_pb2.POI + section.destination.poi.uri = 'poi_uri' + section.destination.poi.name = 'poi_name_from_kraken' + section.destination.poi.coord.lon = 1.0 + section.destination.poi.coord.lat = 2.0 + + # Bss section from 'poi:test_uri' to 'poi_b' + section = journey.sections.add() + section.street_network.mode = response_pb2.Bss + section.type = response_pb2.STREET_NETWORK + section.origin.uri = 'poi_uri' + section.origin.embedded_type = type_pb2.POI + section.origin.poi.uri = 'poi_uri' + section.origin.poi.name = 'poi_name_from_kraken' + section.origin.poi.coord.lon = 1.0 + section.origin.poi.coord.lat = 2.0 + section.destination.uri = 'poi_b' + section.destination.embedded_type = type_pb2.POI + + # Walking section from 'poi_b' to 'stop_b' + section = journey.sections.add() + section.type = response_pb2.STREET_NETWORK + section.street_network.mode = response_pb2.Walking + section.origin.uri = 'poi_b' + section.origin.embedded_type = type_pb2.POI + section.destination.uri = 'stop_b' + section.destination.embedded_type = type_pb2.STOP_POINT + return response + + +def verify_poi_in_impacted_objects(object, poi_empty=True): + assert object.name == "poi_name_from_loki" + assert object.uri == "poi_uri" + assert object.embedded_type == type_pb2.POI + if poi_empty: + assert object.poi.uri == '' + assert object.poi.name == '' + else: + assert object.poi.uri == 'poi_uri' + assert object.poi.name == 'poi_name_from_kraken' + assert object.poi.coord.lon == 1.0 + assert object.poi.coord.lat == 2.0 diff --git a/source/jormungandr/jormungandr/scenarios/tests/new_default_tests.py b/source/jormungandr/jormungandr/scenarios/tests/new_default_tests.py index f34f1ff87a..975494e199 100644 --- a/source/jormungandr/jormungandr/scenarios/tests/new_default_tests.py +++ b/source/jormungandr/jormungandr/scenarios/tests/new_default_tests.py @@ -37,12 +37,14 @@ _tag_journey_by_mode, get_kraken_calls, update_best_boarding_positions, + update_disruptions_on_pois, ) from jormungandr.instance import Instance from jormungandr.scenarios.utils import switch_back_to_ridesharing from jormungandr.utils import make_origin_destination_key, str_to_time_stamp from werkzeug.exceptions import HTTPException import pytest +from pytest_mock import mocker from collections import defaultdict """ @@ -798,10 +800,34 @@ def __init__(self, name="fake_instance", olympics_forbidden_uris=None): } -def make_pt_object_poi(property_type="olympic", property_value="1234"): - pt_object_poi = type_pb2.PtObject() - pt_object_poi.embedded_type = type_pb2.POI - property = pt_object_poi.poi.properties.add() - property.type = property_type - property.value = property_value - return pt_object_poi +def journey_with_disruptions_on_poi_test(mocker): + instance = lambda: None + # As in navitia, object poi in the response of places_nearby doesn't have any impact + response_journey_with_pois = helpers_tests.get_journey_with_pois() + assert len(response_journey_with_pois.impacts) == 0 + assert len(response_journey_with_pois.journeys) == 1 + journey = response_journey_with_pois.journeys[0] + assert len(journey.sections) == 3 + + # Prepare disruptions on poi as response of end point poi_disruptions of loki + # pt_object poi as impacted object is absent in the response of poi_disruptions + disruptions_with_poi = helpers_tests.get_response_with_a_disruption_on_poi() + assert len(disruptions_with_poi.impacts) == 1 + assert disruptions_with_poi.impacts[0].uri == "test_impact_uri" + assert len(disruptions_with_poi.impacts[0].impacted_objects) == 1 + object = disruptions_with_poi.impacts[0].impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=True) + + mock = mocker.patch( + 'jormungandr.scenarios.new_default.get_disruptions_on_poi', return_value=disruptions_with_poi + ) + update_disruptions_on_pois(instance, response_journey_with_pois) + + assert len(response_journey_with_pois.impacts) == 1 + impact = response_journey_with_pois.impacts[0] + assert len(impact.impacted_objects) == 1 + object = impact.impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=False) + + mock.assert_called_once() + return diff --git a/source/jormungandr/jormungandr/scenarios/tests/utils_tests.py b/source/jormungandr/jormungandr/scenarios/tests/utils_tests.py new file mode 100644 index 0000000000..477ba2f8b0 --- /dev/null +++ b/source/jormungandr/jormungandr/scenarios/tests/utils_tests.py @@ -0,0 +1,96 @@ +# Copyright (c) 2001-2022, 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 navitiacommon import type_pb2, response_pb2 +import jormungandr.scenarios.tests.helpers_tests as helpers_tests +from jormungandr.scenarios.utils import fill_disruptions_on_pois, fill_disruptions_on_places_nearby +import pytest +from pytest_mock import mocker + + +def update_disruptions_on_pois_for_ptref_test(mocker): + instance = lambda: None + # As in navitia, object poi in the response of ptref doesn't have any impact + response_pois = helpers_tests.get_object_pois_in_ptref_response() + assert len(response_pois.impacts) == 0 + assert response_pois.pois[0].uri == "poi_uri" + assert response_pois.pois[0].name == "poi_name_from_kraken" + + # Prepare disruptions on poi as response of end point poi_disruptions of loki + # pt_object poi as impacted object is absent in the response of poi_disruptions + disruptions_with_poi = helpers_tests.get_response_with_a_disruption_on_poi() + assert len(disruptions_with_poi.impacts) == 1 + assert disruptions_with_poi.impacts[0].uri == "test_impact_uri" + assert len(disruptions_with_poi.impacts[0].impacted_objects) == 1 + pt_object = disruptions_with_poi.impacts[0].impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=pt_object, poi_empty=True) + + mock = mocker.patch('jormungandr.scenarios.utils.get_disruptions_on_poi', return_value=disruptions_with_poi) + fill_disruptions_on_pois(instance, response_pois) + + # In the final response, we should have a disruption as well as object poi in disruption. + assert len(response_pois.impacts) == 1 + assert response_pois.pois[0].uri == "poi_uri" + assert response_pois.pois[0].name == "poi_name_from_kraken" + assert len(response_pois.impacts[0].impacted_objects) == 1 + object = response_pois.impacts[0].impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=pt_object, poi_empty=False) + mock.assert_called_once() + return + + +def update_disruptions_on_pois_for_places_nearby_test(mocker): + instance = lambda: None + # As in navitia, object poi in the response of places_nearby doesn't have any impact + response_places_nearby = helpers_tests.get_object_pois_in_places_nearby_response() + assert len(response_places_nearby.impacts) == 0 + assert len(response_places_nearby.places_nearby) == 1 + assert response_places_nearby.places_nearby[0].uri == "poi_uri" + assert response_places_nearby.places_nearby[0].name == "poi_name_from_kraken" + + # As above Prepare disruptions on poi as response of end point poi_disruptions of loki + disruptions_with_poi = helpers_tests.get_response_with_a_disruption_on_poi() + assert len(disruptions_with_poi.impacts) == 1 + assert disruptions_with_poi.impacts[0].uri == "test_impact_uri" + object = disruptions_with_poi.impacts[0].impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=True) + + mock = mocker.patch('jormungandr.scenarios.utils.get_disruptions_on_poi', return_value=disruptions_with_poi) + fill_disruptions_on_places_nearby(instance, response_places_nearby) + + # In the final response, we should have a disruption as well as object poi in disruption. + assert len(response_places_nearby.impacts) == 1 + assert response_places_nearby.places_nearby[0].uri == "poi_uri" + assert response_places_nearby.places_nearby[0].name == "poi_name_from_kraken" + assert len(response_places_nearby.impacts[0].impacted_objects) == 1 + object = response_places_nearby.impacts[0].impacted_objects[0].pt_object + helpers_tests.verify_poi_in_impacted_objects(object=object, poi_empty=False) + + mock.assert_called_once() + return diff --git a/source/jormungandr/jormungandr/scenarios/utils.py b/source/jormungandr/jormungandr/scenarios/utils.py index d199e908a5..3572412a38 100644 --- a/source/jormungandr/jormungandr/scenarios/utils.py +++ b/source/jormungandr/jormungandr/scenarios/utils.py @@ -30,8 +30,10 @@ from __future__ import absolute_import, print_function, unicode_literals, division import navitiacommon.type_pb2 as type_pb2 import navitiacommon.response_pb2 as response_pb2 +import navitiacommon.request_pb2 as request_pb2 from future.moves.itertools import zip_longest from jormungandr.fallback_modes import FallbackModes +from copy import deepcopy import six places_type = { @@ -461,3 +463,91 @@ def include_poi_access_points(request, pt_object, mode): ] and pt_object.poi.children ) + + +def get_impact_uris_for_poi(response, poi): + impact_uris = set() + if response is None: + return impact_uris + for impact in response.impacts: + for object in impact.impacted_objects: + if object.pt_object.embedded_type == type_pb2.POI and object.pt_object.uri == poi.uri: + impact_uris.add(impact.uri) + object.pt_object.poi.CopyFrom(poi) + + return impact_uris + + +def fill_disruptions_on_pois(instance, response): + if not response.pois: + return + + # add all poi_ids as parameters + poi_uris = set() + for poi in response.pois: + poi_uris.add(poi.uri) + + # calling loki with api poi_disruptions + resp_poi = get_disruptions_on_poi(instance, poi_uris) + + # For each poi in the response add impact_uris from resp_poi + # and copy object poi in impact.impacted_objects + for poi in response.pois: + impact_uris = get_impact_uris_for_poi(resp_poi, poi) + for impact_uri in impact_uris: + poi.impact_uris.append(impact_uri) + + # Add all impacts from resp_poi to the response + add_disruptions(response, resp_poi) + + +def fill_disruptions_on_places_nearby(instance, response): + if not response.places_nearby: + return + + # Add all the poi uris in a list + poi_uris = set() + for place_nearby in response.places_nearby: + if place_nearby.embedded_type == type_pb2.POI: + poi_uris.add(place_nearby.uri) + + # calling loki with api poi_disruptions + resp_poi = get_disruptions_on_poi(instance, poi_uris) + + # For each poi in the response add impact_uris from resp_poi + # and copy object poi in impact.impacted_objects + for place_nearby in response.places_nearby: + if place_nearby.embedded_type == type_pb2.POI: + impact_uris = get_impact_uris_for_poi(resp_poi, place_nearby.poi) + for impact_uri in impact_uris: + place_nearby.poi.impact_uris.append(impact_uri) + + # Add all impacts from resp_poi to the response + add_disruptions(response, resp_poi) + + +def get_disruptions_on_poi(instance, uris, since_datetime=None, until_datetime=None): + if not uris: + return None + try: + pt_planner = instance.get_pt_planner("loki") + req = request_pb2.Request() + req.requested_api = type_pb2.poi_disruptions + + req.poi_disruptions.pois.extend(uris) + if since_datetime: + req.poi_disruptions.since_datetime = since_datetime + if until_datetime: + req.poi_disruptions.until_datetime = until_datetime + + # calling loki with api api_disruptions + resp_poi = pt_planner.send_and_receive(req) + except Exception: + return None + return resp_poi + + +def add_disruptions(pb_resp, pb_disruptions): + if pb_disruptions is None: + return + pb_resp.impacts.extend(pb_disruptions.impacts) diff --git a/source/navitia-proto b/source/navitia-proto index b5bb482a31..adc0e7b201 160000 --- a/source/navitia-proto +++ b/source/navitia-proto @@ -1 +1 @@ -Subproject commit b5bb482a31cf7c63e51c73b5c1acb0691b2ea91e +Subproject commit adc0e7b20156c9ba3e17a50f3aebd8f79295c943