Move SSD learned example JSON to typed Kotlin

ober

90eaea7dfd8d47fc0e78a25535381f02dac7e834

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index efcf532..61c8477 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1629,12 +1629,14 @@
     (typed-kotlin-file "com/sfb/ssdreview/LearnedFeatures.kt"
       (kotlin-imports (org json JSONArray) (org json JSONObject))
       (typed-library (com sfb ssdreview)
-        (export groupFeaturesFromTruthGroup groupFeaturesFromGroup featureDistance)
+        (export groupFeaturesFromTruthGroup groupFeaturesFromGroup
+                learnedExampleFromTruthGroup featureDistance)
         (type JSONArray)
         (type JSONObject)
         (type Float32)
         (type FloatArray)
         (type Int32)
+        (type String)
         (def (featureJsonArrayOrEmpty (raw : (Nullable JSONArray))) : JSONArray
           (if (nullable-null? raw)
               (json-array-empty)
@@ -1699,6 +1701,29 @@
               (float-array-ref bbox (int32 3))
               imageWidth
               imageHeight)))
+        (def (learnedExampleFromTruthGroup (truth : JSONObject)
+                                           (truthPath : String)
+                                           (group : JSONObject)) : JSONObject
+          (let ((out (json-object-empty)))
+            (begin
+              (json-object-put-string! out "source_key" (json-object-opt-string truth "source_key"))
+              (json-object-put-string! out "truth_path" truthPath)
+              (json-object-put-string! out "label" (json-object-opt-string group "label"))
+              (json-object-put-string! out "box_type_id" (json-object-opt-string group "box_type_id"))
+              (json-object-put-string! out "firing_arc"
+                (json-object-opt-string-default
+                  group
+                  "firing_arc"
+                  (json-object-opt-string group "arc")))
+              (json-object-put-int32! out "count" (json-object-opt-int32-default group "count" (int32 0)))
+              (json-object-put-json-array! out "bbox"
+                (featureJsonArrayOrEmpty (json-object-opt-json-array group "bbox")))
+              (json-object-put-json-object! out "features"
+                (groupFeaturesFromTruthGroup
+                  group
+                  (imageWidth truth)
+                  (imageHeight truth)))
+              out)))
         (def (featureDistance (a : JSONObject) (b : JSONObject)) : Float32
           (let ((countA (featureField a "count"))
                 (countB (featureField b "count")))
@@ -6889,15 +6914,7 @@
        "                } catch (_: Exception) {"
        "                    continue"
        "                }"
-       "                examples.put(JSONObject()"
-       "                    .put(\"source_key\", truth.optString(\"source_key\"))"
-       "                    .put(\"truth_path\", file.name)"
-       "                    .put(\"label\", group.optString(\"label\"))"
-       "                    .put(\"box_type_id\", group.optString(\"box_type_id\"))"
-       "                    .put(\"firing_arc\", group.optString(\"firing_arc\", group.optString(\"arc\")))"
-       "                    .put(\"count\", group.optInt(\"count\"))"
-       "                    .put(\"bbox\", group.optJSONArray(\"bbox\") ?: JSONArray())"
-       "                    .put(\"features\", groupFeaturesFromTruthGroup(group, imageWidth(truth), imageHeight(truth))))"
+       "                examples.put(learnedExampleFromTruthGroup(truth, file.name, group))"
        "            }"
        "        }"
        "        val learned = JSONObject()"