Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jormungandr] Add connector Forseti for car parking #4183

Merged
merged 2 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions source/jormungandr/jormungandr/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions source/jormungandr/jormungandr/instance_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions source/jormungandr/jormungandr/interfaces/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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())
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
#
# 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'))
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -79,6 +81,7 @@ def __eq__(self, other):
"available_electric_vehicle",
"occupied_electric_vehicle",
"state",
"availability",
"total_places",
]:
if hasattr(other, item):
Expand Down
Loading