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"
+ )