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}