diff --git a/source/jormungandr/jormungandr/instance.py b/source/jormungandr/jormungandr/instance.py index 009bec2ee6..a6c7b7e930 100644 --- a/source/jormungandr/jormungandr/instance.py +++ b/source/jormungandr/jormungandr/instance.py @@ -64,6 +64,7 @@ from jormungandr.equipments import EquipmentProviderManager from jormungandr.external_services import ExternalServiceManager from jormungandr.parking_space_availability.bss.bss_provider_manager import BssProviderManager +from jormungandr.parking_space_availability.car.car_park_provider_manager import CarParkingProviderManager from jormungandr.utils import ( can_connect_to_database, make_origin_destination_key, @@ -171,6 +172,7 @@ def __init__( resp_content_limit_bytes=None, resp_content_limit_endpoints_whitelist=None, individual_bss_provider=[], + individual_car_parking_provider=[], ): super(Instance, self).__init__( name=name, @@ -268,6 +270,14 @@ def __init__( individual_bss_provider, self.get_bss_stations_services_from_db ) + # Init CAR provider manager from config from external services in bdd + if disable_database: + self.car_parking_provider_manager = CarParkingProviderManager(individual_car_parking_provider) + else: + self.car_parking_provider_manager = CarParkingProviderManager( + individual_car_parking_provider, self.get_car_parking_services_from_db + ) + self.external_service_provider_manager.init_external_services() self.instance_db = instance_db self._ghost_words = ghost_words or [] @@ -335,6 +345,14 @@ def get_bss_stations_services_from_db(self): result = models.external_services if models else [] return [res for res in result if res.navitia_service == 'bss_stations'] + def get_car_parking_services_from_db(self): + """ + :return: a callable query of external services associated to the current instance in db + """ + models = self._get_models() + result = models.external_services if models else [] + return [res for res in result if res.navitia_service == 'car_parkings'] + @property def autocomplete(self): if self._autocomplete_type: @@ -1015,6 +1033,9 @@ def get_all_ridesharing_services(self): def get_all_bss_providers(self): return self.bss_provider_manager.get_providers() + def get_all_car_parking_providers(self): + return self.car_parking_provider_manager.get_providers() + def get_autocomplete(self, requested_autocomplete): if not requested_autocomplete: return self.autocomplete diff --git a/source/jormungandr/jormungandr/instance_manager.py b/source/jormungandr/jormungandr/instance_manager.py index 66ac63b074..cf20dad1e6 100644 --- a/source/jormungandr/jormungandr/instance_manager.py +++ b/source/jormungandr/jormungandr/instance_manager.py @@ -130,6 +130,7 @@ def register_instance(self, config): resp_content_limit_bytes=config.get('resp_content_limit_bytes', None), resp_content_limit_endpoints_whitelist=config.get('resp_content_limit_endpoints_whitelist', None), individual_bss_provider=config.get('individual_bss_provider', []), + individual_car_parking_provider=config.get('individual_car_parking_provider', []), ) self.instances[instance.name] = instance diff --git a/source/jormungandr/jormungandr/interfaces/v1/__init__.py b/source/jormungandr/jormungandr/interfaces/v1/__init__.py index b6968c8ded..c387887e03 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/__init__.py +++ b/source/jormungandr/jormungandr/interfaces/v1/__init__.py @@ -60,6 +60,10 @@ def add_common_status(response, instance): for bp in instance.get_all_bss_providers(): response['status']['bss_providers'].append(bp.status()) + response['status']['car_parking_providers'] = [] + for cpp in instance.get_all_car_parking_providers(): + response['status']['car_parking_providers'].append(cpp.status()) + response['status']['equipment_providers_services'] = {} response['status']['equipment_providers_services'][ 'equipment_providers_keys' diff --git a/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py b/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py index 3dbf7009af..53d1db434b 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py +++ b/source/jormungandr/jormungandr/interfaces/v1/serializer/pt.py @@ -334,6 +334,7 @@ class CarParkSerializer(PbNestedSerializer): available_electric_vehicle = jsonschema.IntField() occupied_electric_vehicle = jsonschema.IntField() state = jsonschema.Field(schema_type=str) + availability = jsonschema.Field(schema_type=bool, display_none=True) class AdminSerializer(SortedGenericSerializer, PbGenericSerializer): diff --git a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py index ca9745e0b3..cc7bcc410c 100644 --- a/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py +++ b/source/jormungandr/jormungandr/interfaces/v1/serializer/status.py @@ -210,6 +210,12 @@ class BSSStationsServiceSerializer(OutsideServiceCommon): class_ = Field(schema_type=str, label='class', attr='class') +class CarParkingServiceSerializer(OutsideServiceCommon): + id = Field(display_none=True) + url = Field(display_none=True) + class_ = Field(schema_type=str, label='class', attr='class') + + class EquipmentProvidersSerializer(NullableDictSerializer): key = Field(schema_type=str, display_none=False) codes_types = Field(schema_type=str, many=True, display_none=True) @@ -262,6 +268,7 @@ class CommonStatusSerializer(NullableDictSerializer): street_networks = StreetNetworkSerializer(many=True, display_none=False) ridesharing_services = RidesharingServicesSerializer(many=True, display_none=False) bss_providers = BSSStationsServiceSerializer(many=True, display_none=False) + car_parking_providers = CarParkingServiceSerializer(many=True, display_none=False) equipment_providers_services = EquipmentProvidersServicesSerializer(display_none=False) external_providers_services = ExternalServiceProvidersServicesSerializer(display_none=False) start_production_date = Field(schema_type=str, display_none=False) diff --git a/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py b/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py index d1f9a36bbc..da9315f290 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py +++ b/source/jormungandr/jormungandr/parking_space_availability/bss/forseti.py @@ -110,7 +110,6 @@ def support_poi(self, poi): return True def status(self): - # return {'network': self.network, 'operators': self.operators} return { 'id': six.text_type(self.network), 'url': self.service_url, diff --git a/source/jormungandr/jormungandr/parking_space_availability/car/car_park_provider_manager.py b/source/jormungandr/jormungandr/parking_space_availability/car/car_park_provider_manager.py index 52bbad434e..174809e324 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/car/car_park_provider_manager.py +++ b/source/jormungandr/jormungandr/parking_space_availability/car/car_park_provider_manager.py @@ -29,18 +29,94 @@ from __future__ import absolute_import, print_function, unicode_literals, division from jormungandr.parking_space_availability.abstract_provider_manager import AbstractProviderManager +from jormungandr.utils import can_connect_to_database +import logging +import datetime POI_TYPE_ID = 'poi_type:amenity:parking' class CarParkingProviderManager(AbstractProviderManager): - def __init__(self, car_park_providers_configurations): + def __init__(self, car_park_providers_configurations, providers_getter=None, update_interval=60): super(CarParkingProviderManager, self).__init__() self.car_park_providers = [] + self._db_car_park_providers = {} + self._db_car_park_providers_last_update = {} + self._update_interval = update_interval + self._db_providers_getter = providers_getter + self._last_update = datetime.datetime(1970, 1, 1) for configuration in car_park_providers_configurations: arguments = configuration.get('args', {}) self.car_park_providers.append(self._init_class(configuration['class'], arguments)) + def update_config(self): + if ( + self._last_update + datetime.timedelta(seconds=self._update_interval) > datetime.datetime.utcnow() + or not self._db_providers_getter + ): + return + + logger = logging.getLogger(__name__) + + # If database is not accessible we update the value of self._last_update and exit + if not can_connect_to_database(): + logger.debug('Database is not accessible') + self._last_update = datetime.datetime.utcnow() + return + + logger.debug('updating car parking providers from database') + self._last_update = datetime.datetime.utcnow() + + try: + # car parking provider list from the database (external_service) + db_providers = self._db_providers_getter() + except Exception as e: + logger.exception('No access to table external_service (error: {})'.format(e)) + # database is not accessible, so let's use the values already present in self.car_park_providers and + # avoid sending query to the database for another update_interval + self._last_update = datetime.datetime.utcnow() + return + + if not db_providers: + logger.debug('No provider active in the database') + self._db_car_park_providers = {} + self._db_car_park_providers_last_update = {} + return + + if not self._need_update(db_providers): + return + + for provider in db_providers: + # it's a new car parking provider or it has been updated, we add it + if ( + provider.id not in self._db_car_park_providers_last_update + or provider.last_update() > self._db_car_park_providers_last_update[provider.id] + ): + self.update_provider(provider) + + # remove deleted car parking providers in the database + for to_delete in set(self._db_car_park_providers.keys()) - {p.id for p in db_providers}: + del self._db_car_park_providers[to_delete] + del self._db_car_park_providers_last_update[to_delete] + logger.info('deleting par parking provider %s', to_delete) + + def _need_update(self, providers): + for provider in providers: + if ( + provider.id not in self._db_car_park_providers_last_update + or provider.last_update() > self._db_car_park_providers_last_update[provider.id] + ): + return True + return False + + def update_provider(self, provider): + logger = logging.getLogger(__name__) + try: + self._db_car_park_providers[provider.id] = self._init_class(provider.klass, provider.full_args()) + self._db_car_park_providers_last_update[provider.id] = provider.last_update() + except Exception: + logger.exception('impossible to initialize car parking provider from the database') + def _handle_poi(self, item): if 'poi_type' in item and item['poi_type']['id'] == POI_TYPE_ID: provider = self._find_provider(item) @@ -50,4 +126,12 @@ def _handle_poi(self, item): return None def _get_providers(self): - return self.car_park_providers + self.update_config() + return self.car_park_providers + list(self._db_car_park_providers.values()) + + def get_providers(self): + return self._get_providers() + + def exist_provider(self): + self.update_config() + return any(self.get_providers()) diff --git a/source/jormungandr/jormungandr/parking_space_availability/car/forseti.py b/source/jormungandr/jormungandr/parking_space_availability/car/forseti.py new file mode 100644 index 0000000000..47a5ebfcc0 --- /dev/null +++ b/source/jormungandr/jormungandr/parking_space_availability/car/forseti.py @@ -0,0 +1,82 @@ +# coding: utf-8 + +# 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 __future__ import absolute_import, print_function, unicode_literals, division +import jmespath + +from jormungandr.parking_space_availability.car.parking_places import ParkingPlaces +from jormungandr.parking_space_availability.car.common_car_park_provider import CommonCarParkProvider +from jormungandr.street_network.utils import crowfly_distance_between +from jormungandr.utils import Coords +import six + +DEFAULT_FORSETI_FEED_PUBLISHER = None + + +class ForsetiProvider(CommonCarParkProvider): + """ + class managing calls to Forseti external service providing real-time car parking availability + + """ + + def __init__( + self, service_url, distance=50, timeout=2, feed_publisher=DEFAULT_FORSETI_FEED_PUBLISHER, **kwargs + ): + self.provider_name = "FORSETI" + self.service_url = service_url + self.distance = distance + super(ForsetiProvider, self).__init__(service_url, [], [], timeout, feed_publisher, **kwargs) + + def status(self): + return { + 'id': six.text_type(self.provider_name), + 'url': self.service_url, + 'class': self.__class__.__name__, + } + + def support_poi(self, poi): + return True + + def get_informations(self, poi): + if not poi.get('properties', {}).get('amenity'): + return None + + data = self._call_webservice(self.ws_service_template.format(self.dataset)) + + if data: + return self.process_data(data, poi) + + def process_data(self, data, poi): + poi_coord = Coords(poi.get('coord').get('lat'), poi.get('coord').get('lon')) + for parking in data.get('parkings', []): + parking_coord = Coords(parking.get('coord').get('lat'), parking.get('coord').get('lon')) + distance = crowfly_distance_between(poi_coord, parking_coord) + if distance < self.distance: + return ParkingPlaces(availability=parking.get('availability')) diff --git a/source/jormungandr/jormungandr/parking_space_availability/car/parking_places.py b/source/jormungandr/jormungandr/parking_space_availability/car/parking_places.py index 3d15979581..bfd83377e0 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/car/parking_places.py +++ b/source/jormungandr/jormungandr/parking_space_availability/car/parking_places.py @@ -42,6 +42,7 @@ def __init__( available_electric_vehicle=None, occupied_electric_vehicle=None, state=None, + availability=None, ): if available is not None: self.available = available @@ -63,7 +64,8 @@ def __init__( self.occupied_electric_vehicle = occupied_electric_vehicle if state is not None: self.state = state - + if availability is not None: + self.availability = availability if not total_places and any(n is not None for n in [available, occupied, available_PRM, occupied_PRM]): self.total_places = (available or 0) + (occupied or 0) + (available_PRM or 0) + (occupied_PRM or 0) @@ -79,6 +81,7 @@ def __eq__(self, other): "available_electric_vehicle", "occupied_electric_vehicle", "state", + "availability", "total_places", ]: if hasattr(other, item): diff --git a/source/jormungandr/jormungandr/parking_space_availability/car/tests/forseti_test.py b/source/jormungandr/jormungandr/parking_space_availability/car/tests/forseti_test.py new file mode 100644 index 0000000000..d1c6d4316f --- /dev/null +++ b/source/jormungandr/jormungandr/parking_space_availability/car/tests/forseti_test.py @@ -0,0 +1,72 @@ +# encoding: utf-8 +# 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 __future__ import absolute_import, print_function, unicode_literals, division +from copy import deepcopy +from jormungandr.parking_space_availability.car.forseti import ForsetiProvider +from jormungandr.parking_space_availability.car.parking_places import ParkingPlaces +from mock import MagicMock + +poi = { + 'properties': {'amenity': 'parking'}, + 'poi_type': {'name': 'Parking', 'id': 'poi_type:public_parking'}, + 'coord': {'lat': '42.3682265', 'lon': '-83.07793565'}, +} + + +def parking_space_availability_forseti_support_poi_test(): + """ + ForsetiProvider car parking provider support + Since we search car parking in forseti with coordinate, it is always True + """ + provider = ForsetiProvider('http://forseti') + poi_copy = deepcopy(poi) + assert provider.support_poi(poi_copy) + + +def parking_space_availability_forseti_get_informations_test(): + webservice_response = { + "parkings": [ + { + "parkcode": "48200", + "name": "786 Parking", + "coord": {"lat": 42.368227, "lon": -83.0779357}, + "availability": True, + } + ], + "pagination": {"start_page": 0, "items_on_page": 25, "items_per_page": 25, "total_result": 1}, + } + + parking_places = ParkingPlaces(availability=True) + provider = ForsetiProvider('http://forseti') + provider._call_webservice = MagicMock(return_value=webservice_response) + parking = provider.get_informations(poi) + assert parking == parking_places + print(parking) diff --git a/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py b/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py index 73a2baa668..0024d75c11 100644 --- a/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py +++ b/source/jormungandr/jormungandr/parking_space_availability/parking_places_manager.py @@ -108,6 +108,22 @@ def wrapper(*args, **kwargs): 'Error while handling global car park realtime availability', ) + if ( + show_car_park + and instance + and instance.car_parking_provider_manager + and instance.car_parking_provider_manager.exist_provider() + ): + _handle( + response, + instance.car_parking_provider_manager, + self.attribute, + self.logger, + 'Error while handling individual car park realtime availability with configuration for instance: {}'.format( + instance + ), + ) + return response, status, h return wrapper 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 72faed6846..22ddfc56c1 100644 --- a/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py +++ b/source/jormungandr/jormungandr/scenarios/helper_classes/proximities_by_crowfly.py @@ -114,7 +114,7 @@ def _do_request(self): crow_fly = self._get_crow_fly(self._instance.georef) if self._mode == fm.FallbackModes.car.name: - # pick up only parkings with park_ride = yes + # pick up only sytral_parkings with park_ride = yes crow_fly = jormungandr.street_network.utils.pick_up_park_ride_car_park(crow_fly) logger.debug( diff --git a/source/navitiacommon/navitiacommon/constants.py b/source/navitiacommon/navitiacommon/constants.py index 2ce4d0171f..7ddf6f4fee 100644 --- a/source/navitiacommon/navitiacommon/constants.py +++ b/source/navitiacommon/navitiacommon/constants.py @@ -41,4 +41,5 @@ 'vehicle_positions', 'bss_stations', 'obstacles', + 'car_parkings', ) diff --git a/source/tyr/migrations/versions/3f42a8407b93_add_car_parkings_in_navitia_service_type.py b/source/tyr/migrations/versions/3f42a8407b93_add_car_parkings_in_navitia_service_type.py new file mode 100644 index 0000000000..0c28ce093c --- /dev/null +++ b/source/tyr/migrations/versions/3f42a8407b93_add_car_parkings_in_navitia_service_type.py @@ -0,0 +1,35 @@ +"""Add car_parkings in navitia_service_type + +Revision ID: 3f42a8407b93 +Revises: 1094ff8c5975 +Create Date: 2023-12-06 15:43:34.661720 + +""" + +# revision identifiers, used by Alembic. +revision = '3f42a8407b93' +down_revision = '1094ff8c5975' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +def upgrade(): + op.execute("COMMIT") # See https://bitbucket.org/zzzeek/alembic/issue/123 + op.execute("ALTER TYPE navitia_service_type ADD VALUE 'car_parkings'") + + +def downgrade(): + op.execute( + "DELETE FROM associate_instance_external_service WHERE external_service_id in (SELECT ID FROM external_service WHERE navitia_service = 'car_parkings')" + ) + op.execute("DELETE FROM external_service WHERE navitia_service = 'car_parkings'") + op.execute("ALTER TABLE external_service ALTER COLUMN navitia_service TYPE text") + op.execute("DROP TYPE navitia_service_type CASCADE") + op.execute( + "CREATE TYPE navitia_service_type AS ENUM ('free_floatings', 'vehicle_occupancies', 'realtime_proxies', 'vehicle_positions', 'bss_stations', 'obstacles')" + ) + op.execute( + "ALTER TABLE external_service ALTER COLUMN navitia_service TYPE navitia_service_type USING navitia_service::navitia_service_type" + )