@@ -78,7 +78,12 @@ def active_point(self,state,key):
7878 state ['position' ],maximum_offset = 200. ,minimum_leg = 0. ,corner_margin = 40. ))
7979 if not reached and not passed :break
8080 plan ['index' ]+= 1
81- if plan ['index' ]>= len (plan ['points' ]):return None
81+ if plan ['index' ]>= len (plan ['points' ]):
82+ # Only consume an explicitly validated finite onward route. This
83+ # supplies runway during renewal without extrapolating a tangent.
84+ if plan .get ('onward_verified' ) and plan .get ('onward_points' ):
85+ plan ['points' ].extend (plan .pop ('onward_points' ));plan ['onward_used' ]= True
86+ else :return None
8287 point = plan ['points' ][plan ['index' ]]
8388 # A local detour may bypass the coarse frontier without entering its
8489 # 80 m arrival circle. Reusing that persisted goal hundreds of km
@@ -101,16 +106,26 @@ def active_point(self,state,key):
101106 self .discard (state ,'regional_goal_progress_stalled' )
102107 return None
103108 return point
104- def update (self ,state ,target ,needed = False ,terminal = False ):
109+ def update (self ,state ,target ,needed = False ,terminal = False , retain_coast = False ):
105110 from posim_terrain .canal_override import in_region
106111 canal = in_region (state ['position' ])
107112 key = [state ['lap' ],state ['leg' ],state ['waypoint' ]]
108113 from .port_approach import APPROACH_DISTANCE_M
109114 terminal = terminal and distance (state ['position' ],target )< APPROACH_DISTANCE_M
115+ state .pop ('regional_waiting' ,None )
110116 if self .key != key :
111117 if self .task :self .task .cancel ()
112118 self .task = None ;self .key = key ;self .retry = 0
113119 self .probed = None
120+ plan = state .get ('regional_navigation' ) or {};old = plan .get ('route_key' )
121+ if (retain_coast and not terminal and not canal and old and len (old )== 3
122+ and old [:2 ]== key [:2 ] and old [2 ]< key [2 ]
123+ and plan .get ('policy' )== REGIONAL_POLICY
124+ and plan .get ('mode' ) in ('coastal' ,'coastal_approach' )):
125+ plan ['route_key' ]= list (key );plan .pop ('continuation' ,None )
126+ state ['regional_handoff' ]= dict (previous_key = old ,route_key = list (key ),
127+ position = list (state ['position' ]),reason = 'measured_coastal_shape_advance' )
128+ self .progress = None
114129 nearest = None
115130 if self .coastal and not canal :
116131 if self .index_task is None and self .coast_index is None :
@@ -145,6 +160,8 @@ def update(self,state,target,needed=False,terminal=False):
145160 and distance (state ['position' ],plan ['points' ][- 1 ])< 2500
146161 and distance (state ['position' ],plan .get ('probe_position' ,state ['position' ]))>= 1000
147162 and (self .probed is None or distance (self .probed ,state ['position' ])>= 1000 ))
163+ renew_coast = renew_coast or (point is not None and nearest is not None
164+ and plan .get ('mode' )== 'coastal' and not plan .get ('onward_verified' ))
148165 coast_needed = nearest is not None and (self .probed is None or distance (self .probed ,state ['position' ])>= 3000 )
149166 prefer_coast = (point is not None and coast_needed and not needed and
150167 (state .get ('regional_navigation' ) or {}).get ('mode' ) not in ('coastal' ,'coastal_approach' ))
@@ -160,10 +177,17 @@ def update(self,state,target,needed=False,terminal=False):
160177 value = self .task .result ()
161178 # A failed optional preference must preserve a previously
162179 # checked detour while waiting for a better coastal region.
163- if not (point is not None and not value ['points' ]):state ['regional_navigation' ]= value
180+ coast_plan = (plan .get ('route_key' )== key and plan .get ('mode' ) in ('coastal' ,'coastal_approach' )
181+ and plan .get ('policy' )== REGIONAL_POLICY )
182+ preserve_empty = (not value ['points' ] and (point is not None or
183+ (coast_plan and nearest is not None and not terminal and not needed )))
184+ if not preserve_empty :state ['regional_navigation' ]= value
164185 self .probed = list (value ['probe_position' ]) if value .get ('mode' ) not in ('coastal' ,'coastal_approach' ) else None
165186 state ['regional_planning' ]= dict (status = 'ready' if value ['points' ] else 'course_retained' ,
166187 job = value ['job' ],mode = value .get ('mode' ,'detour' ),coastal_preference_error = value .get ('coastal_preference_error' ))
188+ if preserve_empty and coast_plan :
189+ self .probed = None ;self .retry = time .monotonic ()+ 60
190+ state ['regional_planning' ]['status' ]= 'retrying'
167191 except (Exception ,asyncio .CancelledError ) as e :
168192 state ['regional_planning' ]= dict (status = 'retrying' ,reason = str (e )[:160 ]);self .retry = time .monotonic ()+ 60
169193 self .task = None
@@ -179,18 +203,19 @@ def update(self,state,target,needed=False,terminal=False):
179203 state ['regional_planning' ]= dict (status = 'background' ,mode = 'port_approach' if terminal else 'coastal' if coast_needed else 'detour' )
180204 self .task = asyncio .create_task (self .prepare_port (state ,target ,key ) if terminal else self .prepare (state ,target ,key ,nearest ,needed ))
181205 if point is not None :return point
182- if self .task and plan .get ('route_key' )== key and plan .get ('mode' ) in ('coastal' ,'coastal_approach' ):
183- points = plan .get ('points' ,[])
184- if len (points )>= 2 and distance (state ['position' ],points [- 1 ])< 1500 :
185- continuation = plan .get ('continuation' )
186- if continuation is None or distance (state ['position' ],continuation )< 80 :
187- heading = GEO .inv (* points [- 2 ],* points [- 1 ])[0 ]
188- continuation = list (GEO .fwd (* state ['position' ],heading ,750. )[:2 ])
189- plan ['continuation' ]= continuation
190- if distance (continuation ,target )< distance (state ['position' ],target ):
191- # Only a goal proposal: the native resident-terrain planner
192- # must approve it before thrust is commanded.
193- return continuation
206+ plan = state .get ('regional_navigation' ) or {}
207+ if (plan .get ('route_key' )== key and plan .get ('policy' )== REGIONAL_POLICY
208+ and plan .get ('mode' ) in ('coastal' ,'coastal_approach' ) and plan .get ('points' )
209+ and plan ['index' ]>= len (plan ['points' ])
210+ and distance (state ['position' ],plan ['points' ][- 1 ])< 1500
211+ and (nearest is not None or self .task or self .index_task )):
212+ # An exhausted checked path is not permission to steer offshore.
213+ # The caller holds thrust, keeping physics and sensors running.
214+ # Retry backoff and optional empty results retain this boundary.
215+ plan .pop ('continuation' ,None )
216+ state ['regional_waiting' ]= dict (reason = 'coastal_renewal' ,job = plan ['job' ],
217+ endpoint = plan ['points' ][- 1 ])
218+ return plan ['points' ][- 1 ]
194219 return target
195220 async def prepare_port (self ,state ,target ,key ):
196221 # A port's narrow entrance is resolved by the same triangles already
0 commit comments