route_update.go
1package waypoint
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7
8 "git.kilimanjaro.io/ygo"
9)
10
11// RouteUpdate holds the route data and optional point update to apply to a specific waypoint
12type RouteUpdate struct {
13 WaypointID string
14 RouteKey string
15 Route Route
16 NewPoint []float64 // Optional: new point to snap to, nil if no snapping needed
17 ErrorCodes []string // Error codes to set on waypoint (e.g., ["RouteTooFar"])
18}
19
20// UpdateWaypointRoutes applies calculated route data and optional point updates to waypoints in the document.
21// It traverses the XML tree, finds waypoints by ID, and updates their "from" attribute
22// with the computed route data. If NewPoint is set in a RouteUpdate, the waypoint's point attribute is also updated.
23// Multiple routes per waypoint are supported - all routes are merged into the "from" map and errors are accumulated.
24func UpdateWaypointRoutes(doc *ygo.Doc, updates []RouteUpdate) error {
25 if len(updates) == 0 {
26 return nil
27 }
28
29 // Build lookup map for quick access - supports multiple routes per waypoint
30 updateMap := make(map[string][]RouteUpdate)
31 for _, u := range updates {
32 updateMap[u.WaypointID] = append(updateMap[u.WaypointID], u)
33 }
34
35 // Get the fragment
36 frag, err := doc.GetXmlFragment("main")
37 if err != nil {
38 return fmt.Errorf("failed to get xml fragment 'main': %w", err)
39 }
40 defer frag.Destroy()
41
42 // Update waypoints in a write transaction
43 return doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
44 childIter, err := frag.ChildIter(txn)
45 if err != nil {
46 return fmt.Errorf("failed to get child iterator: %w", err)
47 }
48
49 for {
50 child, err := childIter.Next()
51 if err != nil {
52 return fmt.Errorf("failed to get next child: %w", err)
53 }
54 if child == nil {
55 break
56 }
57
58 if child.Tag() == ygo.TagXmlElem {
59 elem, err := child.ToXmlElement()
60 child.Destroy()
61 if err != nil {
62 return fmt.Errorf("failed to convert to XmlElement: %w", err)
63 }
64
65 err = dfsUpdateRouteData(elem, txn, updateMap)
66 elem.Destroy()
67 if err != nil {
68 return err
69 }
70 } else {
71 child.Destroy()
72 }
73 }
74
75 return nil
76 })
77}
78
79// dfsUpdateRouteData recursively traverses XML elements and updates waypoint route data.
80// It processes ALL routes for a waypoint, accumulating errors from all route attempts.
81func dfsUpdateRouteData(elem *ygo.XmlElement, txn *ygo.Transaction, updateMap map[string][]RouteUpdate) error {
82 // Check if this element is a waypoint
83 tag, err := elem.Tag()
84 if err != nil {
85 return fmt.Errorf("failed to get element tag: %w", err)
86 }
87
88 if tag == "waypoint" {
89 // Get waypoint ID
90 id, err := getWaypointID(elem, txn)
91 if err != nil {
92 return fmt.Errorf("failed to get waypoint ID: %w", err)
93 }
94
95 // Check if we have updates for this waypoint
96 if updates, ok := updateMap[id]; ok && len(updates) > 0 {
97 if err := applyRouteUpdates(elem, txn, updates); err != nil {
98 return fmt.Errorf("failed to apply route updates to waypoint %s: %w", id, err)
99 }
100 }
101 }
102
103 // Recurse into children
104 childIter, err := elem.ChildIter(txn)
105 if err != nil {
106 return fmt.Errorf("failed to get child iterator: %w", err)
107 }
108
109 for {
110 child, err := childIter.Next()
111 if err != nil {
112 return fmt.Errorf("failed to get next child: %w", err)
113 }
114 if child == nil {
115 break
116 }
117
118 if child.Tag() != ygo.TagXmlElem {
119 child.Destroy()
120 continue
121 }
122
123 childElem, err := child.ToXmlElement()
124 child.Destroy()
125 if err != nil {
126 return fmt.Errorf("failed to convert child to XmlElement: %w", err)
127 }
128
129 if err := dfsUpdateRouteData(childElem, txn, updateMap); err != nil {
130 childElem.Destroy()
131 return err
132 }
133 childElem.Destroy()
134 }
135
136 return nil
137}
138
139// getWaypointID extracts the ID attribute from a waypoint element
140func getWaypointID(elem *ygo.XmlElement, txn *ygo.Transaction) (string, error) {
141 attrIter, err := elem.AttrIter(txn)
142 if err != nil {
143 return "", err
144 }
145 defer attrIter.Destroy()
146
147 for {
148 attr, err := attrIter.Next()
149 if err != nil {
150 return "", err
151 }
152 if attr == nil {
153 break
154 }
155
156 if attr.Name == "id" {
157 val, err := attr.Value.ToInterface()
158 if err != nil {
159 attr.Destroy()
160 return "", err
161 }
162 if str, ok := val.(string); ok {
163 attr.Destroy()
164 return str, nil
165 }
166 }
167 attr.Destroy()
168 }
169
170 return "", nil
171}
172
173// UpdateSourceWaypointPoints applies point snapping to source waypoints.
174// This is separate from UpdateWaypointRoutes because source waypoints are different
175// from the destination waypoints being updated with route data.
176func UpdateSourceWaypointPoints(doc *ygo.Doc, pointUpdates map[string][]float64) error {
177 if len(pointUpdates) == 0 {
178 return nil
179 }
180
181 // Get the fragment
182 frag, err := doc.GetXmlFragment("main")
183 if err != nil {
184 return fmt.Errorf("failed to get xml fragment 'main': %w", err)
185 }
186 defer frag.Destroy()
187
188 // Update waypoints in a write transaction
189 return doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
190 childIter, err := frag.ChildIter(txn)
191 if err != nil {
192 return fmt.Errorf("failed to get child iterator: %w", err)
193 }
194
195 for {
196 child, err := childIter.Next()
197 if err != nil {
198 return fmt.Errorf("failed to get next child: %w", err)
199 }
200 if child == nil {
201 break
202 }
203
204 if child.Tag() == ygo.TagXmlElem {
205 elem, err := child.ToXmlElement()
206 child.Destroy()
207 if err != nil {
208 return fmt.Errorf("failed to convert to XmlElement: %w", err)
209 }
210
211 err = dfsUpdateSourcePoints(elem, txn, pointUpdates)
212 elem.Destroy()
213 if err != nil {
214 return err
215 }
216 } else {
217 child.Destroy()
218 }
219 }
220
221 return nil
222 })
223}
224
225// dfsUpdateSourcePoints recursively traverses XML elements and updates source waypoint points
226func dfsUpdateSourcePoints(elem *ygo.XmlElement, txn *ygo.Transaction, pointUpdates map[string][]float64) error {
227 tag, err := elem.Tag()
228 if err != nil {
229 return fmt.Errorf("failed to get element tag: %w", err)
230 }
231
232 if tag == "waypoint" {
233 id, err := getWaypointID(elem, txn)
234 if err != nil {
235 return fmt.Errorf("failed to get waypoint ID: %w", err)
236 }
237
238 if newPoint, ok := pointUpdates[id]; ok && len(newPoint) >= 2 {
239 pointInput, err := ygo.JSON(newPoint)
240 if err != nil {
241 return fmt.Errorf("failed to marshal point: %w", err)
242 }
243 if err := elem.SetAttribute(txn, "point", pointInput); err != nil {
244 return fmt.Errorf("failed to set point attribute: %w", err)
245 }
246 slog.Debug("snapped source waypoint point",
247 "waypoint", id,
248 "newPoint", newPoint)
249 }
250 }
251
252 // Recurse into children
253 childIter, err := elem.ChildIter(txn)
254 if err != nil {
255 return fmt.Errorf("failed to get child iterator: %w", err)
256 }
257
258 for {
259 child, err := childIter.Next()
260 if err != nil {
261 return fmt.Errorf("failed to get next child: %w", err)
262 }
263 if child == nil {
264 break
265 }
266
267 if child.Tag() != ygo.TagXmlElem {
268 child.Destroy()
269 continue
270 }
271
272 childElem, err := child.ToXmlElement()
273 child.Destroy()
274 if err != nil {
275 return fmt.Errorf("failed to convert child to XmlElement: %w", err)
276 }
277
278 if err := dfsUpdateSourcePoints(childElem, txn, pointUpdates); err != nil {
279 childElem.Destroy()
280 return err
281 }
282 childElem.Destroy()
283 }
284
285 return nil
286}
287
288// applyRouteUpdates applies multiple route updates to a waypoint's "from" attribute.
289// It accumulates all routes into the "from" map and collects all error codes.
290// The point is updated only once (all routes go to the same destination).
291func applyRouteUpdates(elem *ygo.XmlElement, txn *ygo.Transaction, updates []RouteUpdate) error {
292 if len(updates) == 0 {
293 return nil
294 }
295
296 // Get current "from" attribute
297 var currentFrom map[string]Route
298
299 attrIter, err := elem.AttrIter(txn)
300 if err != nil {
301 return fmt.Errorf("failed to get attribute iterator: %w", err)
302 }
303
304 for {
305 attr, err := attrIter.Next()
306 if err != nil {
307 attrIter.Destroy()
308 return fmt.Errorf("failed to get next attribute: %w", err)
309 }
310 if attr == nil {
311 break
312 }
313
314 if attr.Name == "from" {
315 // Read raw JSON bytes and unmarshal directly into typed map
316 if jsonBytes, ok := attr.Value.JSONBytes(); ok {
317 var fromMap map[string]Route
318 if err := json.Unmarshal(jsonBytes, &fromMap); err == nil {
319 currentFrom = fromMap
320 }
321 }
322 attr.Destroy()
323 } else {
324 attr.Destroy()
325 }
326 }
327 attrIter.Destroy()
328
329 // If no current "from", initialize empty map
330 if currentFrom == nil {
331 currentFrom = make(map[string]Route)
332 }
333
334 // Accumulate error codes from all updates
335 allErrorCodes := make([]string, 0)
336 var pointToSnap []float64
337 waypointID := updates[0].WaypointID
338
339 // Apply ALL route updates and collect errors
340 for _, update := range updates {
341 // Apply route to "from" map
342 currentFrom[update.RouteKey] = update.Route
343 slog.Debug("applied route update",
344 "waypoint", update.WaypointID,
345 "routeKey", update.RouteKey,
346 "routeID", update.Route.ID)
347
348 // Collect error codes
349 for _, code := range update.ErrorCodes {
350 // Avoid duplicates
351 found := false
352 for _, existing := range allErrorCodes {
353 if existing == code {
354 found = true
355 break
356 }
357 }
358 if !found {
359 allErrorCodes = append(allErrorCodes, code)
360 }
361 }
362
363 // Use the first valid point update (all routes go to same destination)
364 if pointToSnap == nil && len(update.NewPoint) >= 2 {
365 pointToSnap = update.NewPoint
366 }
367 }
368
369 // Write updated "from" attribute
370 input, err := ygo.JSON(currentFrom)
371 if err != nil {
372 return fmt.Errorf("failed to marshal updated from map: %w", err)
373 }
374
375 if err := elem.SetAttribute(txn, "from", input); err != nil {
376 return fmt.Errorf("failed to set from attribute: %w", err)
377 }
378
379 // Set error codes ONCE with all accumulated errors
380 if len(allErrorCodes) > 0 {
381 errorInput, err := ygo.JSON(allErrorCodes)
382 if err != nil {
383 return fmt.Errorf("failed to marshal error codes: %w", err)
384 }
385 if err := elem.SetAttribute(txn, "error", errorInput); err != nil {
386 return fmt.Errorf("failed to set error attribute: %w", err)
387 }
388 slog.Debug("set waypoint error codes",
389 "waypoint", waypointID,
390 "codes", allErrorCodes)
391 } else {
392 // Clear error attribute if no errors
393 emptyErrInput, _ := ygo.JSON([]string{})
394 if err := elem.SetAttribute(txn, "error", emptyErrInput); err != nil {
395 return fmt.Errorf("failed to clear error attribute: %w", err)
396 }
397 }
398
399 // Apply point update if present (only once for all routes to this waypoint)
400 if pointToSnap != nil {
401 pointInput, err := ygo.JSON(pointToSnap)
402 if err != nil {
403 return fmt.Errorf("failed to marshal point: %w", err)
404 }
405 if err := elem.SetAttribute(txn, "point", pointInput); err != nil {
406 return fmt.Errorf("failed to set point attribute: %w", err)
407 }
408 slog.Debug("snapped waypoint point",
409 "waypoint", waypointID,
410 "newPoint", pointToSnap)
411 }
412
413 return nil
414}