66import asyncio ,math ,time
77from pyproj import Geod
88GEO = Geod (ellps = 'WGS84' )
9- REGIONAL_POLICY = 'buffered-water-v19- coastal-400m '
10- COMPATIBLE_POLICIES = {REGIONAL_POLICY , 'buffered-water-v18-coastal-350m' }
9+ REGIONAL_POLICY = 'buffered-water-v21-shallow-200m- coastal-320m '
10+ COMPATIBLE_POLICIES = {REGIONAL_POLICY }
1111PLANNING_BUDGET_SECONDS = 660.
12- REGIONAL_BUFFER_M = 40 .
12+ REGIONAL_BUFFER_M = 200 .
1313REGIONAL_DEPTH_M = 2.
1414
1515def distance (a ,b ):return GEO .inv (* a ,* b )[2 ]
@@ -69,6 +69,7 @@ def start_task(self,work):
6969 # its own bounded provider deadline. One budget includes all retries.
7070 self .task_started = time .monotonic ()
7171 work_task = asyncio .create_task (work )
72+ work_task .add_done_callback (lambda task : task .exception () if not task .cancelled () else None )
7273 self .task = asyncio .create_task (asyncio .wait_for (work_task ,PLANNING_BUDGET_SECONDS ))
7374 # Cancellation before the wrapper starts must still close its work.
7475 self .task .add_done_callback (lambda _ : work_task .cancel () if not work_task .done () else None )
@@ -110,7 +111,7 @@ def active_point(self,state,key):
110111 from .course import passed_vertex
111112 while plan ['index' ]< len (plan ['points' ]):
112113 index = plan ['index' ];point = plan ['points' ][index ]
113- reached = distance (state ['position' ],point )< (5 if plan .get ('mode' )== 'port_departure' else 12 if plan .get ('mode' )== 'port_approach' else 80 )
114+ reached = distance (state ['position' ],point )< (5 if plan .get ('mode' ) in ( 'port_departure' , 'shallow_exit' ) else 12 if plan .get ('mode' )== 'port_approach' else 80 )
114115 previous = plan ['points' ][index - 1 ] if index else plan .get ('probe_position' )
115116 passed = (plan .get ('mode' ) in ('coastal' ,'coastal_transfer' ,'coastal_approach' ) and previous is not None
116117 and index < len (plan ['points' ])- 1 and passed_vertex (previous ,point ,plan ['points' ][index + 1 ],
@@ -220,7 +221,7 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
220221 # cannot fall back to the original straight line across that land.
221222 guide_exhausted = (plan .get ('route_key' )== key and plan .get ('mapped_guidance' )
222223 and plan .get ('points' ) and plan ['index' ]>= len (plan ['points' ]))
223- needed = needed or bool (guide_exhausted )
224+ needed = needed or bool (guide_exhausted ) or ( plan . get ( 'mode' ) == 'shallow_exit' and plan . get ( 'points' ) and plan [ 'index' ] >= len ( plan [ 'points' ]))
224225 approach_runway = (plan .get ('mode' )== 'port_approach_lookahead' and point is not None
225226 and distance (state ['position' ],plan ['probe_position' ])< 500. )
226227 prefer_port = terminal and (point is None or (plan .get ('mode' )!= 'port_approach' and not approach_runway ))
@@ -245,10 +246,28 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
245246 # local segment did not itself need a detour or coast discovery.
246247 expired = state .get ('regional_planning' ) or {}
247248 needed = needed or (expired .get ('status' )== 'expired' and expired .get ('reason' ) in
248- ('regional_clearance_policy_changed' ,'regional_goal_progress_stalled' ,'suez_fine_grid' ,'predicted_stopping_corridor' ,'checked_route_wait_stalled' ))
249+ ('regional_clearance_policy_changed' ,'regional_goal_progress_stalled' ,'suez_fine_grid' ,'predicted_stopping_corridor' ,'checked_route_wait_stalled' , 'shallow_water_clearance' ))
249250 if self .task and self .task .done ():
250251 try :
251252 value = self .task .result ()
253+ connection = value .pop ('continuation_of' ,None )
254+ if connection :
255+ # Renew from the checked runway's END, retaining the
256+ # remaining forward bends. Replanning from the moving hull
257+ # can choose an already passed bay as a new coast entry.
258+ if (plan .get ('job' )!= connection ['job' ] or plan .get ('route_key' )!= key
259+ or connection ['endpoint' ] not in plan .get ('points' ,[])):
260+ raise RuntimeError ('coastal_runway_changed' )
261+ end = plan ['points' ].index (connection ['endpoint' ])
262+ if plan ['index' ]> end :raise RuntimeError ('coastal_runway_already_passed' )
263+ retained = plan ['points' ][plan ['index' ]:end + 1 ]
264+ value ['points' ]= retained + value ['points' ];value ['index' ]= 0
265+ value ['retained_checked_points' ]= len (retained )
266+ value ['renewed_from' ]= connection
267+ value ['probe_position' ]= list (state ['position' ])
268+ for metric in ('minimum_regional_clearance_m' ,):
269+ if plan .get (metric ) is not None and value .get (metric ) is not None :
270+ value [metric ]= min (plan [metric ],value [metric ])
252271 # A failed optional preference must preserve a previously
253272 # checked detour while waiting for a better coastal region.
254273 coast_plan = (plan .get ('route_key' )== key and plan .get ('mode' ) in ('coastal' ,'coastal_transfer' ,'coastal_approach' )
@@ -280,7 +299,13 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
280299 coast_needed = nearest is not None and (self .probed is None or distance (self .probed ,state ['position' ])>= 3000 )
281300 if (prefer_port or needed or coast_needed or renew_coast ) and not self .task and time .monotonic ()>= self .retry :
282301 state ['regional_planning' ]= dict (status = 'background' ,mode = 'port_approach' if terminal else 'coastal' if coast_needed else 'detour' )
283- self .start_task (self .prepare_port (state ,target ,key ) if terminal else self .prepare (state ,target ,key ,nearest ,needed ,extent = self .recovery_extent ,progress_target = progress_target ,coastal_target = coastal_target ))
302+ if terminal :work = self .prepare_port (state ,target ,key )
303+ elif renew_coast and point is not None :
304+ work = self .prepare_continuation (state ,target ,key ,plan ,extent = self .recovery_extent ,
305+ progress_target = progress_target ,coastal_target = coastal_target )
306+ else :work = self .prepare (state ,target ,key ,nearest ,needed ,extent = self .recovery_extent ,
307+ progress_target = progress_target ,coastal_target = coastal_target )
308+ self .start_task (work )
284309 self .recovery_extent = None
285310 if point is not None :return point
286311 plan = state .get ('regional_navigation' ) or {}
@@ -326,6 +351,23 @@ def departure_update(self,state,target,key):
326351 state ['regional_waiting' ]= dict (reason = 'resident_departure_corridor_pending' )
327352 return state ['position' ]
328353
354+ async def prepare_continuation (self ,state ,target ,key ,plan ,extent = None ,progress_target = None ,coastal_target = None ):
355+ endpoint = list (plan ['points' ][- 1 ]);connection = dict (job = plan ['job' ],endpoint = endpoint )
356+ sample = dict (state ,position = endpoint )
357+ nearest = None
358+ if self .coast_index is not None :
359+ from .coastal import COAST_DISCOVERY_M ,MAXIMUM_COASTAL_DISTANCE_M
360+ separation ,shore = self .coast_index .nearest (endpoint )
361+ if separation < COAST_DISCOVERY_M :
362+ nearest = (separation ,shore )
363+ if separation > MAXIMUM_COASTAL_DISTANCE_M and hasattr (self .coast_index ,'progressive_shore' ):
364+ nearest = self .coast_index .progressive_shore (endpoint ,coastal_target or target )
365+ value = await self .prepare (sample ,target ,key ,nearest ,True ,extent = extent ,
366+ progress_target = progress_target ,coastal_target = coastal_target )
367+ if not value .get ('points' ):raise RuntimeError ('coastal_continuation_unavailable' )
368+ value ['continuation_of' ]= connection
369+ return value
370+
329371 async def prepare_departure (self ,state ,target ,key ):
330372 from .port_approach import corridor
331373 from .land import guard_from_spec
@@ -466,6 +508,9 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
466508 coastal_goal = coastal_target if nearest is not None and coastal_target is not None else target
467509 value = None ;coast_error = None
468510 if not canal and (nearest or needed ):
511+ from .shallow import exit_corridor
512+ value = await asyncio .to_thread (exit_corridor ,grid ,job ['spec' ],current ,progress_target ,guard = guard )
513+ if value is None and not canal and (nearest or needed ):
469514 from .coastal import coastal_corridor
470515 try :value = await asyncio .to_thread (coastal_corridor ,grid ,job ['spec' ],current ,coastal_goal ,guard = guard ,progress_target = progress_target )
471516 except RuntimeError as error :coast_error = str (error )[:160 ]
@@ -539,6 +584,9 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
539584 if not value ['points' ]:value ['coastal_preference_error' ]= coast_error or 'no_verified_progressive_coastal_corridor'
540585 value .update (route_key = key ,job = job ['id' ],source = job ['manifest' ]['source' ],policy = REGIONAL_POLICY ,
541586 mapped_land_checked = guard is not None )
587+ if not canal and value ['points' ]:
588+ value .setdefault ('shallow_water_buffer_m' ,REGIONAL_BUFFER_M )
589+ value .setdefault ('shallow_boundary_depth_m' ,REGIONAL_DEPTH_M )
542590 value ['probe_position' ]= current
543591 value ['destination_progress_target' ]= list (progress_target )
544592 if coastal_target is not None and value .get ('mode' ) in ('coastal' ,'coastal_transfer' ,'coastal_approach' ):
0 commit comments