Skip to content

Commit db3c526

Browse files
authored
Merge pull request #4229 from hove-io/handle_when_depart_is_in_an_excluded_zone
[Distributed Excluded Zone] Handle when depart is in an excluded zone
2 parents 7455a45 + 5ac9687 commit db3c526

File tree

10 files changed

+250
-43
lines changed

10 files changed

+250
-43
lines changed

source/jormungandr/jormungandr/excluded_zones_manager.py

+68-18
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,23 @@
2828
# www.navitia.io
2929

3030
import boto3
31+
import pytz
32+
import shapely.iterops
33+
from dateutil import parser
3134
from botocore.client import Config
3235
import logging
3336
import json
37+
import shapely
38+
import datetime
39+
from typing import Dict
3440

3541
from jormungandr import app, memory_cache, cache
3642
from jormungandr.resource_s3_object import ResourceS3Object
3743

3844

3945
class ExcludedZonesManager:
46+
excluded_shapes = dict() # type: Dict[str, shapely.geometry]
47+
4048
@staticmethod
4149
@cache.memoize(app.config[str('CACHE_CONFIGURATION')].get(str('ASGARD_S3_DATA_TIMEOUT'), 24 * 60))
4250
def get_object(resource_s3_object):
@@ -49,10 +57,22 @@ def get_object(resource_s3_object):
4957
return {}
5058

5159
@staticmethod
60+
def is_activated(activation_period, date):
61+
if activation_period is None:
62+
return False
63+
64+
def is_between(period, d):
65+
from_date = parser.parse(period['from']).date()
66+
to_date = parser.parse(period['to']).date()
67+
return from_date <= d < to_date
68+
69+
return any((is_between(period, date) for period in activation_period))
70+
71+
@classmethod
5272
@memory_cache.memoize(
53-
app.config[str('MEMORY_CACHE_CONFIGURATION')].get(str('ASGARD_S3_DATA_TIMEOUT'), 5 * 60)
73+
app.config[str('MEMORY_CACHE_CONFIGURATION')].get(str('ASGARD_S3_DATA_TIMEOUT'), 10 * 60)
5474
)
55-
def get_excluded_zones(instance_name=None, mode=None):
75+
def get_all_excluded_zones(cls):
5676
bucket_name = app.config.get(str("ASGARD_S3_BUCKET"))
5777
folder = "excluded_zones"
5878

@@ -67,28 +87,58 @@ def get_excluded_zones(instance_name=None, mode=None):
6787
continue
6888
try:
6989
json_content = ExcludedZonesManager.get_object(ResourceS3Object(obj, None))
70-
71-
if instance_name is not None and json_content.get('instance') != instance_name:
72-
continue
73-
if mode is not None and mode not in json_content.get("modes", []):
74-
continue
75-
7690
excluded_zones.append(json_content)
7791
except Exception:
78-
logger.exception(
79-
"Error on fetching excluded zones: bucket: {}, instance: {}, mode ={}",
80-
bucket_name,
81-
instance_name,
82-
mode,
83-
)
92+
logger.exception("Error on fetching excluded zones: bucket: {}", bucket_name)
8493
continue
85-
8694
except Exception:
8795
logger.exception(
88-
"Error on fetching excluded zones: bucket: {}, instance: {}, mode ={}",
96+
"Error on fetching excluded zones: bucket: {}",
8997
bucket_name,
90-
instance_name,
91-
mode,
9298
)
99+
excluded_shapes = dict()
100+
for zone in excluded_zones:
101+
# remove the DAMN MYPY to use walrus operator!!!!!
102+
shape_str = zone.get('shape')
103+
if shape_str:
104+
continue
105+
try:
106+
shape = shapely.wkt.loads(shape_str)
107+
except Exception as e:
108+
logger.error("error occurred when load shapes of excluded zones: " + str(e))
109+
continue
110+
excluded_shapes[zone.get("poi")] = shape
111+
112+
cls.excluded_shapes = excluded_shapes
93113

94114
return excluded_zones
115+
116+
@staticmethod
117+
@memory_cache.memoize(
118+
app.config[str('MEMORY_CACHE_CONFIGURATION')].get(str('ASGARD_S3_DATA_TIMEOUT'), 10 * 60)
119+
)
120+
def get_excluded_zones(instance_name=None, mode=None, date=None):
121+
excluded_zones = []
122+
for json_content in ExcludedZonesManager.get_all_excluded_zones():
123+
if instance_name is not None and json_content.get('instance') != instance_name:
124+
continue
125+
if mode is not None and mode not in json_content.get("modes", []):
126+
continue
127+
if date is not None and not ExcludedZonesManager.is_activated(
128+
json_content.get('activation_periods'), date
129+
):
130+
continue
131+
excluded_zones.append(json_content)
132+
133+
return excluded_zones
134+
135+
@classmethod
136+
@cache.memoize(app.config[str('CACHE_CONFIGURATION')].get(str('ASGARD_S3_DATA_TIMEOUT'), 10 * 60))
137+
def is_excluded(cls, obj, mode, timestamp):
138+
date = datetime.datetime.fromtimestamp(timestamp, tz=pytz.timezone("UTC")).date()
139+
# update excluded zones
140+
excluded_zones = ExcludedZonesManager.get_excluded_zones(instance_name=None, mode=mode, date=date)
141+
poi_ids = set((zone.get("poi") for zone in excluded_zones))
142+
shapes = (cls.excluded_shapes.get(poi_id) for poi_id in poi_ids if cls.excluded_shapes.get(poi_id))
143+
p = shapely.geometry.Point(obj.lon, obj.lat)
144+
return any((shape.contains(p) for shape in shapes))

source/jormungandr/jormungandr/georef.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def inner(stop_area_uri, instance_publication_date):
179179
logging.getLogger(__name__).info(
180180
'PtRef, Unable to find stop_point with filter {}'.format(stop_area_uri)
181181
)
182-
return {sp.uri for sp in result.stop_points}
182+
return {(sp.uri, sp.coord.lon, sp.coord.lat) for sp in result.stop_points}
183183

184184
return inner(uri, self.instance.publication_date)
185185

source/jormungandr/jormungandr/scenarios/helper_classes/fallback_durations.py

+31-12
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from math import sqrt
3636
from .helper_utils import get_max_fallback_duration
3737
from jormungandr.street_network.street_network import StreetNetworkPathType
38-
from jormungandr import new_relic
38+
from jormungandr import new_relic, excluded_zones_manager
3939
from jormungandr.fallback_modes import FallbackModes
4040
import logging
4141
from .helper_utils import timed_logger
@@ -44,6 +44,9 @@
4444
from jormungandr.exceptions import GeoveloTechnicalError
4545
from .helper_exceptions import StreetNetworkException
4646
from jormungandr.scenarios.utils import include_poi_access_points
47+
from jormungandr.scenarios.helper_classes.places_free_access import FreeAccessObject
48+
import functools
49+
import itertools
4750

4851
# The basic element stored in fallback_durations.
4952
# in DurationElement. can be found:
@@ -189,16 +192,27 @@ def _update_free_access_with_free_radius(self, free_access, proximities_by_crowf
189192
free_radius_distance = self._request.free_radius_to
190193
if free_radius_distance is not None:
191194
free_access.free_radius.update(
192-
p.uri for p in proximities_by_crowfly if p.distance < free_radius_distance
195+
FreeAccessObject(p.uri, p.stop_point.coord.lon, p.stop_point.coord.lat)
196+
for p in proximities_by_crowfly
197+
if p.distance < free_radius_distance
193198
)
194199

195200
def _get_all_free_access(self, proximities_by_crowfly):
196201
free_access = self._places_free_access.wait_and_get()
197202
self._update_free_access_with_free_radius(free_access, proximities_by_crowfly)
198203
all_free_access = free_access.crowfly | free_access.odt | free_access.free_radius
204+
if self._request['_use_excluded_zones'] and all_free_access:
205+
# the mode is hardcoded to walking because we consider that we access to all free_access places
206+
# by walking
207+
is_excluded = functools.partial(
208+
excluded_zones_manager.ExcludedZonesManager.is_excluded,
209+
mode='walking',
210+
timestamp=self._request['datetime'],
211+
)
212+
all_free_access = set(itertools.filterfalse(is_excluded, all_free_access))
199213
return all_free_access
200214

201-
def _build_places_isochrone(self, proximities_by_crowfly, all_free_access):
215+
def _build_places_isochrone(self, proximities_by_crowfly, all_free_access_uris):
202216
places_isochrone = []
203217
stop_points = []
204218
# in this map, we store all the information that will be useful where we update the final result
@@ -207,16 +221,17 @@ def _build_places_isochrone(self, proximities_by_crowfly, all_free_access):
207221
# - stop_point_uri: to which stop point the access point is attached
208222
# - access_point: the actual access_point, of type pt_object
209223
access_points_map = defaultdict(list)
224+
210225
if self._mode == FallbackModes.car.name or self._request['_access_points'] is False:
211226
# if a place is freely accessible, there is no need to compute it's access duration in isochrone
212-
places_isochrone.extend(p for p in proximities_by_crowfly if p.uri not in all_free_access)
213-
stop_points.extend(p for p in proximities_by_crowfly if p.uri not in all_free_access)
227+
places_isochrone.extend(p for p in proximities_by_crowfly if p.uri not in all_free_access_uris)
228+
stop_points.extend(p for p in proximities_by_crowfly if p.uri not in all_free_access_uris)
214229
places_isochrone = self._streetnetwork_service.filter_places_isochrone(places_isochrone)
215230
else:
216231
proximities_by_crowfly = self._streetnetwork_service.filter_places_isochrone(proximities_by_crowfly)
217232
for p in proximities_by_crowfly:
218233
# if a place is freely accessible, there is no need to compute it's access duration in isochrone
219-
if p.uri in all_free_access:
234+
if p.uri in all_free_access_uris:
220235
continue
221236
# what we are looking to compute, is not the stop_point, but the entrance and exit of a stop_point
222237
# if any of them are existent
@@ -231,14 +246,14 @@ def _build_places_isochrone(self, proximities_by_crowfly, all_free_access):
231246

232247
return places_isochrone, access_points_map, stop_points
233248

234-
def _fill_fallback_durations_with_free_access(self, fallback_durations, all_free_access):
249+
def _fill_fallback_durations_with_free_access(self, fallback_durations, all_free_access_uris):
235250
# Since we have already places that have free access, we add them into the result
236251
from collections import deque
237252

238253
deque(
239254
(
240255
fallback_durations.update({uri: DurationElement(0, response_pb2.reached, None, 0, None, None)})
241-
for uri in all_free_access
256+
for uri in all_free_access_uris
242257
),
243258
maxlen=1,
244259
)
@@ -343,6 +358,8 @@ def _do_request(self):
343358

344359
all_free_access = self._get_all_free_access(proximities_by_crowfly)
345360

361+
all_free_access_uris = set((free_access.uri for free_access in all_free_access))
362+
346363
# places_isochrone: a list of pt_objects selected from proximities_by_crowfly that will be sent to street
347364
# network service to compute the routing matrix
348365
# access_points_map: a map of access_point.uri vs a list of tuple whose elements are stop_point.uri, length and
@@ -353,15 +370,15 @@ def _do_request(self):
353370
# "stop_point:2", by walking (42 meters, 41 sec) and (43 meters, 44sec) respectively
354371
# it is a temporary storage that will be used later to update fallback_durations
355372
places_isochrone, access_points_map, stop_points = self._build_places_isochrone(
356-
proximities_by_crowfly, all_free_access
373+
proximities_by_crowfly, all_free_access_uris
357374
)
358375

359376
centers_isochrone = self._determine_centers_isochrone()
360377
result = []
361378
for center_isochrone in centers_isochrone:
362379
result.append(
363380
self.build_fallback_duration(
364-
center_isochrone, all_free_access, places_isochrone, access_points_map
381+
center_isochrone, all_free_access_uris, places_isochrone, access_points_map
365382
)
366383
)
367384
if len(result) == 1:
@@ -393,14 +410,16 @@ def _async_request(self):
393410
def wait_and_get(self):
394411
return self._value.wait_and_get() if self._value else None
395412

396-
def build_fallback_duration(self, center_isochrone, all_free_access, places_isochrone, access_points_map):
413+
def build_fallback_duration(
414+
self, center_isochrone, all_free_access_uris, places_isochrone, access_points_map
415+
):
397416
logger = logging.getLogger(__name__)
398417

399418
# the final result to be returned, which is a map of stop_points.uri vs DurationElement
400419
fallback_durations = defaultdict(lambda: DurationElement(float('inf'), None, None, 0, None, None))
401420

402421
# Since we have already places that have free access, we add them into the fallback_durations
403-
self._fill_fallback_durations_with_free_access(fallback_durations, all_free_access)
422+
self._fill_fallback_durations_with_free_access(fallback_durations, all_free_access_uris)
404423

405424
# There are two cases that places_isochrone maybe empty:
406425
# 1. The duration of direct_path is very small that we cannot find any proximities by crowfly

source/jormungandr/jormungandr/scenarios/helper_classes/helper_utils.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _is_crowfly_needed(uri, fallback_durations, crowfly_sps, fallback_direct_pat
8282
f = fallback_durations.get(uri, None)
8383
is_unknown_projection = f.status == response_pb2.unknown if f else False
8484

85-
is_crowfly_sp = uri in crowfly_sps
85+
is_crowfly_sp = uri in set((sp.uri for sp in crowfly_sps))
8686

8787
# At this point, theoretically, fallback_dp should be found since the isochrone has already given a
8888
# valid value BUT, in some cases(due to the bad projection, etc), fallback_dp may not exist even
@@ -538,7 +538,7 @@ def _build_crowfly(pt_journey, entry_point, mode, places_free_access, fallback_d
538538
# No need for a crowfly if the pt section starts from the requested object
539539
return None
540540

541-
if pt_obj.uri in places_free_access.odt:
541+
if pt_obj.uri in [o.uri for o in places_free_access.odt]:
542542
pt_obj.CopyFrom(entry_point)
543543
# Update first or last coord in the shape
544544
fallback_logic.update_shape_coord(pt_journey, get_pt_object_coord(pt_obj))
@@ -595,7 +595,7 @@ def _build_fallback(
595595
_, _, _, _, via_pt_access, via_poi_access = fallback_durations[pt_obj.uri]
596596

597597
if requested_obj.uri != pt_obj.uri:
598-
if pt_obj.uri in accessibles_by_crowfly.odt:
598+
if pt_obj.uri in [o.uri for o in accessibles_by_crowfly.odt]:
599599
pt_obj.CopyFrom(requested_obj)
600600
else:
601601
# extend the journey with the fallback routing path
@@ -731,7 +731,7 @@ def compute_fallback(
731731
pt_departure = fallback.get_pt_section_datetime(journey)
732732
fallback_extremity_dep = PeriodExtremity(pt_departure, False)
733733
from_sub_request_id = "{}_{}_from".format(request_id, i)
734-
if from_obj.uri != pt_orig.uri and pt_orig.uri not in orig_all_free_access:
734+
if from_obj.uri != pt_orig.uri and pt_orig.uri not in set((p.uri for p in orig_all_free_access)):
735735
# here, if the mode is car, we have to find from which car park the stop_point is accessed
736736
if dep_mode == 'car':
737737
orig_obj = orig_fallback_durations_pool.wait_and_get(dep_mode)[pt_orig.uri].car_park
@@ -763,7 +763,7 @@ def compute_fallback(
763763
pt_arrival = fallback.get_pt_section_datetime(journey)
764764
fallback_extremity_arr = PeriodExtremity(pt_arrival, True)
765765
to_sub_request_id = "{}_{}_to".format(request_id, i)
766-
if to_obj.uri != pt_dest.uri and pt_dest.uri not in dest_all_free_access:
766+
if to_obj.uri != pt_dest.uri and pt_dest.uri not in set((p.uri for p in dest_all_free_access)):
767767
if arr_mode == 'car':
768768
dest_obj = dest_fallback_durations_pool.wait_and_get(arr_mode)[pt_dest.uri].car_park
769769
real_mode = dest_fallback_durations_pool.get_real_mode(arr_mode, dest_obj.uri)

source/jormungandr/jormungandr/scenarios/helper_classes/places_free_access.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@
2727
# https://groups.google.com/d/forum/navitia
2828
# www.navitia.io
2929
from __future__ import absolute_import
30+
31+
import collections
3032
from navitiacommon import type_pb2
3133
from jormungandr import utils, new_relic
3234
from collections import namedtuple
3335
import logging
3436
from .helper_utils import timed_logger
3537

38+
FreeAccessObject = namedtuple('FreeAccessObject', ['uri', 'lon', 'lat'])
3639
PlaceFreeAccessResult = namedtuple('PlaceFreeAccessResult', ['crowfly', 'odt', 'free_radius'])
3740

3841

@@ -73,18 +76,30 @@ def _do_request(self):
7376
place = self._requested_place_obj
7477

7578
if place.embedded_type == type_pb2.STOP_AREA:
76-
crowfly = self._get_stop_points_for_stop_area(self._instance.georef, place.uri)
79+
crowfly = {
80+
FreeAccessObject(sp[0], sp[1], sp[2])
81+
for sp in self._get_stop_points_for_stop_area(self._instance.georef, place.uri)
82+
}
7783
elif place.embedded_type == type_pb2.ADMINISTRATIVE_REGION:
78-
crowfly = {sp.uri for sa in place.administrative_region.main_stop_areas for sp in sa.stop_points}
84+
crowfly = {
85+
FreeAccessObject(sp.uri, sp.coord.lon, sp.coord.lat)
86+
for sa in place.administrative_region.main_stop_areas
87+
for sp in sa.stop_points
88+
}
7989
elif place.embedded_type == type_pb2.STOP_POINT:
80-
crowfly = {place.stop_point.uri}
90+
crowfly = {
91+
FreeAccessObject(place.stop_point.uri, place.stop_point.coord.lon, place.stop_point.coord.lat)
92+
}
8193

8294
coord = utils.get_pt_object_coord(place)
8395
odt = set()
8496

8597
if coord:
8698
odt_sps = self._get_odt_stop_points(self._pt_planner, coord)
87-
[odt.add(stop_point.uri) for stop_point in odt_sps]
99+
collections.deque(
100+
(odt.add(FreeAccessObject(sp.uri, sp.coord.lon, sp.coord.lat)) for sp in odt_sps),
101+
maxlen=1,
102+
)
88103

89104
self._logger.debug("finish places with free access from %s", self._requested_place_obj.uri)
90105

source/jormungandr/jormungandr/street_network/asgard.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,21 @@ def _get_street_network_routing_matrix(
242242
req.sn_routing_matrix.mode = FallbackModes.car.name
243243

244244
res = self._call_asgard(req, request_id)
245+
246+
# to handle the case where all origins or all destinations happen to be located in excluded zones
247+
# Asgard could have returned a matrix filled with Unreached status, which is kind of waste of the bandwidth
248+
# So instead, asgard return with an error_id(all_excluded), we fill the matrix with just on element
249+
# to make jormun believe that Asgard has actually responded without errors,
250+
# so no crow fly is about to be created
251+
if res is not None and res.HasField('error') and res.error.id == response_pb2.Error.all_excluded:
252+
row = res.sn_routing_matrix.rows.add()
253+
r = row.routing_response.add()
254+
r.routing_status = response_pb2.unreached
255+
r.duration = -1
256+
245257
self._check_for_error_and_raise(res)
246-
return res.sn_routing_matrix
258+
259+
return res.sn_routing_matrix if res else None
247260

248261
@staticmethod
249262
def handle_car_no_park_modes(mode):

source/jormungandr/jormungandr/street_network/kraken.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def _create_sn_routing_matrix_request(
248248
)
249249

250250
def _check_for_error_and_raise(self, res):
251-
if res is None or res.HasField('error'):
251+
if res is None or res.HasField('error') and res.error.id != response_pb2.Error.all_excluded:
252252
logging.getLogger(__name__).error(
253253
'routing matrix query error {}'.format(res.error if res else "Unknown")
254254
)

0 commit comments

Comments
 (0)