@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +214 -29
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +21 -2
  8. package/ios/Types.swift +3 -0
  9. package/ios/extensions/UIImage+Toggle.swift +5 -2
  10. package/ios/hybrid/HybridAutoPlay.swift +28 -16
  11. package/ios/hybrid/HybridMapTemplate.swift +22 -5
  12. package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
  13. package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
  14. package/ios/templates/AutoPlayTemplate.swift +24 -2
  15. package/ios/templates/GridTemplate.swift +57 -57
  16. package/ios/templates/InformationTemplate.swift +84 -9
  17. package/ios/templates/ListTemplate.swift +66 -11
  18. package/ios/templates/MapTemplate.swift +83 -68
  19. package/ios/templates/MessageTemplate.swift +72 -6
  20. package/ios/templates/Parser.swift +564 -69
  21. package/ios/templates/SearchTemplate.swift +1 -1
  22. package/ios/templates/TemplateStore.swift +5 -15
  23. package/ios/utils/SymbolFont.swift +13 -28
  24. package/lib/Constants.d.ts +4 -0
  25. package/lib/Constants.js +6 -0
  26. package/lib/index.d.ts +1 -0
  27. package/lib/index.js +1 -0
  28. package/lib/specs/MapTemplate.nitro.d.ts +3 -1
  29. package/lib/templates/GridTemplate.d.ts +23 -3
  30. package/lib/templates/GridTemplate.js +8 -0
  31. package/lib/templates/InformationTemplate.d.ts +38 -12
  32. package/lib/templates/ListTemplate.d.ts +16 -47
  33. package/lib/templates/MapTemplate.d.ts +26 -2
  34. package/lib/templates/MapTemplate.js +16 -2
  35. package/lib/templates/MessageTemplate.d.ts +52 -11
  36. package/lib/templates/MessageTemplate.js +1 -1
  37. package/lib/utils/NitroOptionsPanel.d.ts +103 -0
  38. package/lib/utils/NitroOptionsPanel.js +63 -0
  39. package/lib/utils/NitroSection.d.ts +89 -2
  40. package/lib/utils/NitroSection.js +49 -2
  41. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
  42. package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
  43. package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
  44. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
  45. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
  46. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
  47. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
  48. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
  49. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
  50. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
  51. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
  52. package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
  53. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
  54. package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
  55. package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
  56. package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
  57. package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
  58. package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
  59. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
  60. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
  61. package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
  62. package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
  63. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
  64. package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
  79. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
  80. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  81. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
  82. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
  83. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
  84. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
  85. package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
  86. package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
  87. package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
  88. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
  89. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
  90. package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
  91. package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
  92. package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
  93. package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
  94. package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
  95. package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
  96. package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
  97. package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
  98. package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
  99. package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
  100. package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
  101. package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
  102. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
  103. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
  104. package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
  105. package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
  106. package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
  107. package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
  108. package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
  109. package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
  110. package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
  111. package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
  112. package/package.json +1 -1
  113. package/src/Constants.ts +9 -0
  114. package/src/index.ts +1 -0
  115. package/src/specs/MapTemplate.nitro.ts +3 -1
  116. package/src/templates/GridTemplate.ts +38 -3
  117. package/src/templates/InformationTemplate.ts +60 -29
  118. package/src/templates/ListTemplate.ts +27 -55
  119. package/src/templates/MapTemplate.ts +60 -2
  120. package/src/templates/MessageTemplate.ts +71 -24
  121. package/src/utils/NitroOptionsPanel.ts +218 -0
  122. package/src/utils/NitroSection.ts +160 -5
@@ -6,6 +6,7 @@
6
6
  //
7
7
 
8
8
  import CarPlay
9
+ import CoreLocation
9
10
  import ImageIO
10
11
  import UIKit
11
12
 
@@ -25,9 +26,13 @@ class Parser {
25
26
  var actions: [CPAlertAction] = []
26
27
 
27
28
  if let alertActions = alertActions {
29
+ // Image-only actions (from the panel-only PanelActionsIos shape) have no title and
30
+ // can't be represented as a CPAlertAction below iOS 27 where panels aren't used.
28
31
  alertActions.forEach { alertAction in
32
+ guard let title = alertAction.title else { return }
33
+
29
34
  let action = CPAlertAction(
30
- title: alertAction.title!,
35
+ title: title,
31
36
  style: parseActionAlertStyle(style: alertAction.style),
32
37
  handler: { actionHandler in
33
38
  alertAction.onPress()
@@ -64,8 +69,6 @@ class Parser {
64
69
  if let glypImage = action.image?.glyphImage {
65
70
  image = SymbolFont.imageFromNitroImage(
66
71
  image: glypImage,
67
- // this icon is not scaled properly when used as image asset, so we use the plain image, as CP does the correct coloring anyways
68
- noImageAsset: true,
69
72
  traitCollection: traitCollection
70
73
  )!
71
74
  }
@@ -220,16 +223,18 @@ class Parser {
220
223
  {
221
224
  guard let actions else { return [] }
222
225
 
223
- return actions.map { action in
224
- let button = CPTextButton(
225
- title: action.title!,
226
+ // Image-only actions (from the panel-only PanelActionsIos shape) have no title and
227
+ // can't be represented as a CPTextButton below iOS 27 where panels aren't used.
228
+ return actions.compactMap { action in
229
+ guard let title = action.title else { return nil }
230
+
231
+ return CPTextButton(
232
+ title: title,
226
233
  textStyle: parseTextButtonStyle(style: action.style),
227
234
  handler: { void in
228
235
  action.onPress()
229
236
  }
230
237
  )
231
-
232
- return button
233
238
  }
234
239
  }
235
240
 
@@ -244,6 +249,63 @@ class Parser {
244
249
  }
245
250
  }
246
251
 
252
+ /// Read-only equivalent of `parseInformationItems` for the map-panel case, since `CPMapPanelItem` only wraps a `CPListItem`, not a `CPInformationItem`.
253
+ @available(iOS 27.0, *)
254
+ static func parseInformationPanelItems(section: NitroSection) -> [CPMapPanelItem] {
255
+ return section.items.map { item in
256
+ CPMapPanelItem(
257
+ listItem: CPListItem(
258
+ text: parseText(text: item.title),
259
+ detailText: parseText(text: item.detailedText)
260
+ )
261
+ )
262
+ }
263
+ }
264
+
265
+ /// `actions[0]` becomes the primary text button; `actions[1]`, if present, becomes the icon-only symbol button — `CPMapPanelButtonConfiguration` supports nothing beyond that.
266
+ @available(iOS 27.0, *)
267
+ static func parsePanelButtonConfiguration(
268
+ actions: [NitroAction]?,
269
+ traitCollection: UITraitCollection
270
+ ) -> CPMapPanelButtonConfiguration? {
271
+ guard let actions, let primaryAction = actions.first else { return nil }
272
+
273
+ let primaryButton = CPTextButton(
274
+ title: primaryAction.title ?? "",
275
+ textStyle: parseTextButtonStyle(style: primaryAction.style),
276
+ handler: { _ in
277
+ primaryAction.onPress()
278
+ }
279
+ )
280
+
281
+ var symbolButton: CPButton?
282
+ if actions.count > 1,
283
+ let image = parseNitroImage(image: actions[1].image, traitCollection: traitCollection)
284
+ {
285
+ let secondaryAction = actions[1]
286
+ symbolButton = CPButton(
287
+ image: image,
288
+ handler: { _ in
289
+ secondaryAction.onPress()
290
+ }
291
+ )
292
+ }
293
+
294
+ /// according to the docs travelEstimates is optional, the intializer just does not know it....
295
+ /// set it and nil it on next line....
296
+ let buttonConfiguration = CPMapPanelButtonConfiguration(
297
+ primaryAction: primaryButton,
298
+ secondaryButton: symbolButton,
299
+ travelEstimates: CPTravelEstimates(
300
+ distanceRemaining: Measurement(value: 0, unit: .astronomicalUnits),
301
+ timeRemaining: 0
302
+ )
303
+ )
304
+ buttonConfiguration.travelEstimates = nil
305
+
306
+ return buttonConfiguration
307
+ }
308
+
247
309
  static func parseSearchResults(
248
310
  section: NitroSection?,
249
311
  traitCollection: UITraitCollection
@@ -272,6 +334,110 @@ class Parser {
272
334
  }
273
335
  }
274
336
 
337
+ static func parseListItems(
338
+ section: NitroSection,
339
+ sectionIndex: Int,
340
+ updateSection: @escaping (NitroSection, Int) -> Void,
341
+ traitCollection: UITraitCollection
342
+ ) -> [CPListItem] {
343
+ let selectedIndex = section.items.firstIndex { item in
344
+ item.selected == true
345
+ }
346
+
347
+ return section.items.enumerated().map { (itemIndex, item) in
348
+ parseListItem(
349
+ item: item,
350
+ itemIndex: itemIndex,
351
+ selectedIndex: selectedIndex,
352
+ section: section,
353
+ sectionIndex: sectionIndex,
354
+ updateSection: updateSection,
355
+ traitCollection: traitCollection
356
+ )
357
+ }
358
+ }
359
+
360
+ private static func parseListItem(
361
+ item: NitroRow,
362
+ itemIndex: Int,
363
+ selectedIndex: Int?,
364
+ section: NitroSection,
365
+ sectionIndex: Int,
366
+ updateSection: @escaping (NitroSection, Int) -> Void,
367
+ traitCollection: UITraitCollection
368
+ ) -> CPListItem {
369
+ let isSelected =
370
+ section.type == .radio
371
+ && Int(selectedIndex ?? -1) == itemIndex
372
+
373
+ let toggleImage = item.checked.map { checked in
374
+ UIImage.makeToggleImage(
375
+ enabled: checked,
376
+ maximumImageSize: CPListItem.maximumImageSize,
377
+ traitCollection: traitCollection
378
+ )
379
+ }
380
+
381
+ let listItem = CPListItem(
382
+ text: parseText(text: item.title),
383
+ detailText: parseText(text: item.detailedText),
384
+ image: Parser.parseNitroImage(
385
+ image: item.image,
386
+ traitCollection: traitCollection
387
+ ),
388
+ accessoryImage: isSelected
389
+ ? UIImage.checkmark : toggleImage,
390
+ accessoryType: item.browsable == true
391
+ ? .disclosureIndicator : .none
392
+ )
393
+
394
+ listItem.isEnabled = item.enabled
395
+
396
+ listItem.handler = { _item, completion in
397
+ let updatedItems = section.items.enumerated().map { (rowIndex, row) in
398
+ let checked: Bool? =
399
+ if rowIndex == itemIndex, let checked = row.checked {
400
+ !checked
401
+ }
402
+ else { row.checked }
403
+
404
+ let selected: Bool? =
405
+ if section.type == .radio {
406
+ rowIndex == itemIndex
407
+ }
408
+ else {
409
+ nil
410
+ }
411
+
412
+ return NitroRow(
413
+ title: row.title,
414
+ detailedText: row.detailedText,
415
+ browsable: row.browsable,
416
+ enabled: row.enabled,
417
+ image: row.image,
418
+ imageType: row.imageType,
419
+ checked: checked,
420
+ onPress: row.onPress,
421
+ selected: selected,
422
+ coordinate: row.coordinate,
423
+ distance: row.distance,
424
+ duration: row.duration,
425
+ travelEstimatesVisible: row.travelEstimatesVisible,
426
+ address: row.address
427
+ )
428
+ }
429
+
430
+ let updatedSection = NitroSection(title: section.title, items: updatedItems, type: section.type)
431
+
432
+ updateSection(updatedSection, sectionIndex)
433
+
434
+ item.onPress?(item.checked.map { checked in !checked })
435
+ completion()
436
+ }
437
+
438
+ return listItem
439
+ }
440
+
275
441
  static func parseSections(
276
442
  sections: [NitroSection]?,
277
443
  updateSection: @escaping (NitroSection, Int) -> Void,
@@ -280,81 +446,410 @@ class Parser {
280
446
  guard let sections else { return [] }
281
447
 
282
448
  return sections.enumerated().map { (sectionIndex, section) in
283
- let selectedIndex = section.items.firstIndex { item in
284
- item.selected == true
449
+ let items = parseListItems(
450
+ section: section,
451
+ sectionIndex: sectionIndex,
452
+ updateSection: updateSection,
453
+ traitCollection: traitCollection
454
+ )
455
+
456
+ return CPListSection(
457
+ items: items,
458
+ header: section.title,
459
+ sectionIndexTitle: nil
460
+ )
461
+ }
462
+ }
463
+
464
+ /// Builds the `CPMapPanelItem`s for a list-type section in panel context. Mirrors `parseListItems`, except a row carrying
465
+ /// waypoint data (`coordinate`) becomes a `CPMapTemplateWaypoint`-backed item instead of a `CPListItem`-backed one — used
466
+ /// both by `ListTemplate` panels and the options panel's list sections.
467
+ @available(iOS 27.0, *)
468
+ static func parsePanelItems(
469
+ section: NitroSection,
470
+ sectionIndex: Int,
471
+ updateSection: @escaping (NitroSection, Int) -> Void,
472
+ traitCollection: UITraitCollection
473
+ ) -> [CPMapPanelItem] {
474
+ let selectedIndex = section.items.firstIndex { item in
475
+ item.selected == true
476
+ }
477
+
478
+ return section.items.enumerated().flatMap { (itemIndex, item) -> [CPMapPanelItem] in
479
+ if let coordinate = item.coordinate {
480
+ let image = parseWaypointImage(item.image, traitCollection: traitCollection)
481
+ let onPress = item.onPress
482
+ let waypointItem = parseWaypointPanelItem(
483
+ name: parseText(text: item.title),
484
+ address: item.address,
485
+ coordinate: coordinate,
486
+ distance: item.distance,
487
+ duration: item.duration,
488
+ image: image,
489
+ onPress: { onPress?(nil) }
490
+ )
491
+
492
+ guard item.travelEstimatesVisible == true, let distance = item.distance,
493
+ let duration = item.duration
494
+ else {
495
+ return [waypointItem]
496
+ }
497
+
498
+ let travelEstimates = CPTravelEstimates(
499
+ distanceRemaining: parseDistance(distance: distance),
500
+ timeRemaining: duration.seconds
501
+ )
502
+ let travelEstimatesItem = CPMapPanelItem(travelEstimates: travelEstimates) {
503
+ _,
504
+ completion in
505
+ completion()
506
+ }
507
+
508
+ return [waypointItem, travelEstimatesItem]
285
509
  }
286
- let items = section.items.enumerated().map { (itemIndex, item) in
287
- let isSelected =
288
- section.type == .radio
289
- && Int(selectedIndex ?? -1) == itemIndex
290
-
291
- let toggleImage = item.checked.map { checked in
292
- UIImage.makeToggleImage(
293
- enabled: checked,
294
- maximumImageSize: CPListItem.maximumImageSize
510
+
511
+ let listItem = parseListItem(
512
+ item: item,
513
+ itemIndex: itemIndex,
514
+ selectedIndex: selectedIndex,
515
+ section: section,
516
+ sectionIndex: sectionIndex,
517
+ updateSection: updateSection,
518
+ traitCollection: traitCollection
519
+ )
520
+
521
+ return [CPMapPanelItem(listItem: listItem)]
522
+ }
523
+ }
524
+
525
+ @available(iOS 27.0, *)
526
+ static func parseMapPanelSections(
527
+ sections: [NitroSection]?,
528
+ updateSection: @escaping (NitroSection, Int) -> Void,
529
+ traitCollection: UITraitCollection
530
+ ) -> [CPMapPanelSection] {
531
+ guard let sections else { return [] }
532
+
533
+ return sections.enumerated().map { (sectionIndex, section) in
534
+ let items = parsePanelItems(
535
+ section: section,
536
+ sectionIndex: sectionIndex,
537
+ updateSection: updateSection,
538
+ traitCollection: traitCollection
539
+ )
540
+
541
+ return CPMapPanelSection(
542
+ title: section.title,
543
+ items: items
544
+ )
545
+ }
546
+ }
547
+
548
+ /// Builds sections for `CPNavigationSession.optionsPanel`. Each section is independently a full list (via `parsePanelItems`,
549
+ /// radio included — a list row carrying waypoint data becomes a `CPMapTemplateWaypoint` item), a full grid, or a charging
550
+ /// station's outlets — `sectionIndex` addresses this array; only list sections call `updateSection`.
551
+ ///
552
+ /// The case order below (`.first` = list, `.second` = grid, `.third` = charger) matches whatever nitrogen assigned the last
553
+ /// time `yarn specs` ran, not necessarily the declaration order of `NitroOptionsPanelSection` in TS — check
554
+ /// `NitroOptionsPanelSection.swift` after regenerating.
555
+ @available(iOS 27.0, *)
556
+ static func parseOptionsPanelSections(
557
+ sections: [NitroOptionsPanelSection]?,
558
+ updateSection: @escaping (NitroSection, Int) -> Void,
559
+ traitCollection: UITraitCollection
560
+ ) -> [CPMapPanelSection] {
561
+ guard let sections else { return [] }
562
+
563
+ return sections.enumerated().map { (sectionIndex, section) in
564
+ switch section {
565
+ case .first(let listSection):
566
+ let items = parsePanelItems(
567
+ section: listSection,
568
+ sectionIndex: sectionIndex,
569
+ updateSection: updateSection,
570
+ traitCollection: traitCollection
571
+ )
572
+
573
+ return CPMapPanelSection(
574
+ title: listSection.title,
575
+ items: items
576
+ )
577
+
578
+ case .second(let gridSection):
579
+ let buttons = parseGridButtons(
580
+ buttons: gridSection.buttons,
581
+ traitCollection: traitCollection
582
+ )
583
+
584
+ return CPMapPanelSection(
585
+ title: gridSection.title,
586
+ items: [CPMapPanelItem(gridButtons: buttons)]
587
+ )
588
+
589
+ case .third(let chargerSection):
590
+ var items = chargerSection.outlets.map { outlet -> CPMapPanelItem in
591
+ let connection = CPChargingStationConnection(
592
+ connector: parseChargingConnector(outlet.connector),
593
+ voltage: Measurement(value: outlet.voltage, unit: .volts),
594
+ power: parsePower(kilowatts: outlet.powerKw)
295
595
  )
596
+
597
+ let onPress = outlet.onPress
598
+ return CPMapPanelItem(chargingStationConnection: connection) { _, completion in
599
+ onPress?()
600
+ completion()
601
+ }
296
602
  }
297
603
 
298
- let listItem = CPListItem(
299
- text: parseText(text: item.title),
300
- detailText: parseText(text: item.detailedText),
301
- image: Parser.parseNitroImage(
302
- image: item.image,
303
- traitCollection: traitCollection
304
- ),
305
- accessoryImage: isSelected
306
- ? UIImage.checkmark : toggleImage,
307
- accessoryType: item.browsable == true
308
- ? .disclosureIndicator : .none
309
- )
604
+ if let location = chargerSection.location {
605
+ let onPress = location.onPress
606
+ let image = parseWaypointImage(location.image, traitCollection: traitCollection)
607
+ items.insert(
608
+ parseWaypointPanelItem(
609
+ name: location.name,
610
+ address: location.address,
611
+ coordinate: location.coordinate,
612
+ distance: location.distance,
613
+ duration: location.duration,
614
+ image: image,
615
+ onPress: { onPress?() }
616
+ ),
617
+ at: 0
618
+ )
310
619
 
311
- listItem.isEnabled = item.enabled
312
-
313
- listItem.handler = { _item, completion in
314
-
315
- let updatedItems = section.items.enumerated().map { (rowIndex, row) in
316
- let checked: Bool? =
317
- if rowIndex == itemIndex, let checked = row.checked {
318
- !checked
319
- }
320
- else { row.checked }
321
-
322
- let selected: Bool? =
323
- if section.type == .radio {
324
- rowIndex == itemIndex
325
- }
326
- else {
327
- nil
328
- }
329
-
330
- return NitroRow(
331
- title: row.title,
332
- detailedText: row.detailedText,
333
- browsable: row.browsable,
334
- enabled: row.enabled,
335
- image: row.image,
336
- checked: checked,
337
- onPress: row.onPress,
338
- selected: selected
620
+ if location.visible == true {
621
+ let travelEstimates = CPTravelEstimates(
622
+ distanceRemaining: parseDistance(distance: location.distance),
623
+ timeRemaining: location.duration.seconds
624
+ )
625
+ items.insert(
626
+ CPMapPanelItem(travelEstimates: travelEstimates) { _, completion in
627
+ completion()
628
+ },
629
+ at: 1
339
630
  )
340
631
  }
632
+ }
633
+
634
+ return CPMapPanelSection(
635
+ title: chargerSection.title,
636
+ items: items
637
+ )
638
+ }
639
+ }
640
+ }
341
641
 
342
- let updatedSection = NitroSection(title: section.title, items: updatedItems, type: section.type)
642
+ /// Shared image handling for a waypoint panel item (`WaypointRow`, `ChargerLocation`) — a glyph image is rendered
643
+ /// directly at `CPNavigationAlert.maximumAvatarImageSize`, since `CPMapPanelItem`'s waypoint image has no documented
644
+ /// size of its own; anything else falls back to the regular image conversion.
645
+ @available(iOS 27.0, *)
646
+ private static func parseWaypointImage(
647
+ _ image: ImageProtocol?,
648
+ traitCollection: UITraitCollection
649
+ ) -> UIImage? {
650
+ if let glyphImage = image?.glyphImage {
651
+ return SymbolFont.imageFromNitroImage(
652
+ image: glyphImage,
653
+ // iOS 27 makes the image overflow
654
+ // adjusting it with displayScale gives it the proper size but causes some blur
655
+ size: CPNavigationAlert.maximumAvatarImageSize.width / traitCollection.displayScale,
656
+ traitCollection: traitCollection
657
+ )
658
+ }
343
659
 
344
- updateSection(updatedSection, sectionIndex)
660
+ return parseNitroImage(image: image, traitCollection: traitCollection)
661
+ }
345
662
 
346
- item.onPress?(item.checked.map { checked in !checked })
347
- completion()
663
+ /// Builds a `CPMapPanelItem` wrapping a `CPMapTemplateWaypoint` — used both for a list row carrying waypoint data
664
+ /// (`WaypointRow`) and for a charging station's own location (`ChargerLocation`).
665
+ @available(iOS 27.0, *)
666
+ private static func parseWaypointPanelItem(
667
+ name: String?,
668
+ address: String?,
669
+ coordinate: WaypointCoordinate,
670
+ distance: Distance?,
671
+ duration: DurationWithTimeZone?,
672
+ image: UIImage?,
673
+ onPress: @escaping () -> Void
674
+ ) -> CPMapPanelItem {
675
+ let nameVariants = name.map { [$0] } ?? []
676
+
677
+ let navigationWaypoint = CPNavigationWaypoint(
678
+ centerPoint: CPLocationCoordinate3D(
679
+ latitude: coordinate.latitude,
680
+ longitude: coordinate.longitude,
681
+ altitude: coordinate.altitude ?? CLLocationDistanceMax
682
+ ),
683
+ locationThreshold: nil,
684
+ nameVariants: nameVariants,
685
+ addressVariants: address.map { [$0] } ?? [],
686
+ entryPoints: [],
687
+ timeZone: duration.flatMap { TimeZone(identifier: $0.timezone) }
688
+ )
689
+
690
+ // `CPMapTemplateWaypoint.travelEstimates` isn't optional and isn't displayed by the
691
+ // waypoint item itself (the visible estimate is the separate sibling `CPMapPanelItem`
692
+ // above, added only when the caller opts in) — a negative value is Apple's documented
693
+ // way to say "unavailable" (renders as "--") for whenever this happens to be read.
694
+ let travelEstimates =
695
+ if let distance, let duration {
696
+ CPTravelEstimates(
697
+ distanceRemaining: parseDistance(distance: distance),
698
+ timeRemaining: duration.seconds
699
+ )
700
+ }
701
+ else {
702
+ CPTravelEstimates(
703
+ distanceRemaining: Measurement(value: -1, unit: .meters),
704
+ timeRemaining: -1
705
+ )
706
+ }
707
+
708
+ let mapTemplateWaypoint = CPMapTemplateWaypoint(
709
+ waypoint: navigationWaypoint,
710
+ travelEstimates: travelEstimates
711
+ )
712
+
713
+ return CPMapPanelItem(mapTemplateWaypoint: mapTemplateWaypoint, image: image) { _, completion in
714
+ onPress()
715
+ completion()
716
+ }
717
+ }
718
+
719
+ @available(iOS 27.0, *)
720
+ private static func parseChargingConnector(
721
+ _ connector: ChargingConnector
722
+ ) -> CPChargingStationConnection.Connector {
723
+ switch connector {
724
+ case .ccs1: return .ccs1
725
+ case .ccs2: return .ccs2
726
+ case .j1772: return .j1772
727
+ case .chademo: return .chaDeMo
728
+ case .mennekes: return .mennekes
729
+ case .gbtdc: return .gbtDC
730
+ case .gbtac: return .gbtAC
731
+ case .nacsdc: return .nacsDC
732
+ case .nacsac: return .nacsAC
733
+ default: return .ccs2
734
+ }
735
+ }
736
+
737
+ /// values above 1000 kW are shown in megawatts instead, since nobody wants to read "1500 kW" on a charger card.
738
+ private static func parsePower(kilowatts: Double) -> Measurement<UnitPower> {
739
+ let measurement = Measurement(value: kilowatts, unit: UnitPower.kilowatts)
740
+ return kilowatts >= 1000 ? measurement.converted(to: .megawatts) : measurement
741
+ }
742
+
743
+ /// `onPanButtonPress` is called instead of `button.onPress` for `.pan`-typed buttons, since panning belongs to the `CPMapTemplate`, not the button.
744
+ static func parseMapButtons(
745
+ mapButtons: [NitroMapButton],
746
+ onPanButtonPress: @escaping () -> Void
747
+ ) -> [CPMapButton] {
748
+ guard let traitCollection = SceneStore.getRootTraitCollection() else {
749
+ return []
750
+ }
751
+
752
+ return mapButtons.map { button in
753
+ if let glyphImage = button.image.glyphImage,
754
+ let icon = SymbolFont.imageFromNitroImage(
755
+ image: glyphImage,
756
+ traitCollection: traitCollection
757
+ )
758
+ {
759
+ return CPMapButton(image: icon) { _ in
760
+ if button.type == .pan {
761
+ onPanButtonPress()
762
+ return
763
+ }
764
+ button.onPress?()
765
+ }
766
+ }
767
+ if let assetImage = button.image.assetImage,
768
+ let icon = Parser.parseAssetImage(
769
+ assetImage: assetImage,
770
+ traitCollection: traitCollection
771
+ )
772
+ {
773
+ return CPMapButton(image: icon) { _ in
774
+ if button.type == .pan {
775
+ onPanButtonPress()
776
+ return
777
+ }
778
+ button.onPress?()
779
+ }
780
+ }
781
+ if let remoteImage = button.image.remoteImage,
782
+ let icon = Parser.parseRemoteImage(
783
+ remoteImage: remoteImage,
784
+ traitCollection: traitCollection
785
+ )
786
+ {
787
+ return CPMapButton(image: icon) { _ in
788
+ if button.type == .pan {
789
+ onPanButtonPress()
790
+ return
791
+ }
792
+ button.onPress?()
793
+ }
794
+ }
795
+
796
+ return CPMapButton { _ in
797
+ if button.type == .pan {
798
+ onPanButtonPress()
799
+ return
348
800
  }
801
+ button.onPress?()
802
+ }
803
+ }
804
+ }
349
805
 
350
- return listItem
806
+ static func parseGridButtons(
807
+ buttons: [NitroGridButton],
808
+ traitCollection: UITraitCollection
809
+ ) -> [CPGridButton] {
810
+ let gridButtonHeight: CGFloat
811
+
812
+ if #available(iOS 26.0, *) {
813
+ gridButtonHeight = CPGridTemplate.maximumGridButtonImageSize.height
814
+ }
815
+ else {
816
+ gridButtonHeight = 44
817
+ }
818
+
819
+ return buttons.compactMap { button in
820
+ var image: UIImage?
821
+
822
+ if let glyphImage = button.image.glyphImage {
823
+ image = SymbolFont.imageFromNitroImage(
824
+ image: glyphImage,
825
+ size: gridButtonHeight,
826
+ traitCollection: traitCollection
827
+ )
351
828
  }
352
829
 
353
- return CPListSection(
354
- items: items,
355
- header: section.title,
356
- sectionIndexTitle: nil
357
- )
830
+ if let assetImage = button.image.assetImage {
831
+ image = Parser.parseAssetImage(
832
+ assetImage: assetImage,
833
+ traitCollection: traitCollection
834
+ )
835
+ }
836
+
837
+ if let remoteImage = button.image.remoteImage {
838
+ image = Parser.parseRemoteImage(
839
+ remoteImage: remoteImage,
840
+ traitCollection: traitCollection
841
+ )
842
+ }
843
+
844
+ guard let image = image else { return nil }
845
+ guard let title = Parser.parseText(text: button.title) else { return nil }
846
+
847
+ return CPGridButton(
848
+ titleVariants: [title],
849
+ image: image
850
+ ) { _ in
851
+ button.onPress()
852
+ }
358
853
  }
359
854
  }
360
855