Move SSD group guessing helper to typed Kotlin

ober

59fa7b56bd6ee09641f35b89c0f2960cad5c373d

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index c2e02c1..5c8db68 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -480,6 +480,90 @@
            (boxType : String)
            (reason : String)))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/GuessingHelpers.kt"
+      (typed-library (com sfb ssdreview)
+        (export guessForGroup)
+        (type Int32)
+        (type Float32)
+        (type FloatArray)
+        (def (guess-safe-float (value : Float32)) : Float32
+          (if (> value (float32 1.0)) value (float32 1.0)))
+        (def (guessForGroup (group : SsdGroup)
+                            (imageWidth : Int32)
+                            (imageHeight : Int32)
+                            (race : String)) : Guess
+          (let ((bbox (SsdGroup-bbox group)))
+            (let ((x1 (float-array-ref bbox (int32 0)))
+                  (y1 (float-array-ref bbox (int32 1)))
+                  (x2 (float-array-ref bbox (int32 2)))
+                  (y2 (float-array-ref bbox (int32 3))))
+              (let ((width (guess-safe-float (- x2 x1)))
+                    (height (guess-safe-float (- y2 y1)))
+                    (count (SsdGroup-count group))
+                    (cx (/ (+ x1 x2) (float32 2.0)))
+                    (cy (/ (+ y1 y2) (float32 2.0)))
+                    (imageWidthF (float32 imageWidth))
+                    (imageHeightF (float32 imageHeight)))
+                (let ((safeImageWidth (guess-safe-float imageWidthF))
+                      (safeImageHeight (guess-safe-float imageHeightF))
+                      (nearEdge
+                        (or (< y1 (* imageHeightF (float32 0.22)))
+                            (or (> y2 (* imageHeightF (float32 0.78)))
+                                (or (< x1 (* imageWidthF (float32 0.16)))
+                                    (> x2 (* imageWidthF (float32 0.84)))))))
+                      (lineLike
+                        (or (>= width (* height (float32 1.8)))
+                            (>= height (* width (float32 1.8)))))
+                      (compactBank
+                        (< (* width height)
+                           (* (* imageWidthF imageHeightF) (float32 0.035)))))
+                  (if (and (>= count (int32 3))
+                           (and nearEdge
+                                (or lineLike
+                                    (or compactBank
+                                        (>= count (int32 5))))))
+                    (if (equal? race "andromedan")
+                      (make-Guess "pa panel" "PA Panel" "Andromedan outer-edge bank geometry")
+                      (make-Guess
+                        (shieldLabel (/ cx safeImageWidth) (/ cy safeImageHeight))
+                        "Shield"
+                        "outer-edge bank geometry"))
+                    (if (and (>= count (int32 10))
+                             (and (< cy (* imageHeightF (float32 0.34)))
+                                  (>= width (* height (float32 1.4)))))
+                      (if (equal? race "andromedan")
+                        (make-Guess "pa panel" "PA Panel" "upper bank geometry")
+                        (make-Guess "shield-1" "Shield" "upper horizontal bank geometry"))
+                      (if (and (<= count (int32 4))
+                               (< (* width height)
+                                  (* (* imageWidthF imageHeightF) (float32 0.006))))
+                        (if (and nearEdge (>= count (int32 2)))
+                          (if (equal? race "andromedan")
+                            (make-Guess "pa panel" "PA Panel" "small Andromedan outer-edge bank geometry")
+                            (make-Guess
+                              (shieldLabel (/ cx safeImageWidth) (/ cy safeImageHeight))
+                              "Shield"
+                              "small outer-edge bank geometry"))
+                          (make-Guess
+                            (if (equal? race "andromedan") "phaser-2" "phaser")
+                            "Phaser"
+                            "compact weapon-size group"))
+                        (if (and (and (>= count (int32 1))
+                                      (<= count (int32 3)))
+                                 (and (< cy (* imageHeightF (float32 0.34)))
+                                      (and (> cx (* imageWidthF (float32 0.35)))
+                                           (< cx (* imageWidthF (float32 0.65))))))
+                          (make-Guess "bridge" "Bridge" "upper center command position")
+                          (if (and (and (>= count (int32 3))
+                                        (<= count (int32 12)))
+                                   (and (> cy (* imageHeightF (float32 0.32)))
+                                        (< cy (* imageHeightF (float32 0.72)))))
+                            (make-Guess "hull" "Forward Hull" "central internal bank")
+                            (make-Guess
+                              (string-append (int32->string count) " boxes")
+                              ""
+                              "unclassified detected group")))))))))))))
+
     (typed-kotlin-file "com/sfb/ssdreview/OcrModels.kt"
       (typed-library (com sfb ssdreview)
         (export make-OcrPattern OcrPattern? OcrPattern-tokens OcrPattern-label
@@ -1259,63 +1343,6 @@
        "        }"
        "    }"
        ""
-       "    private fun guessForGroup(group: SsdGroup, imageWidth: Int, imageHeight: Int, race: String): Guess {"
-       "        val x1 = group.bbox[0]"
-       "        val y1 = group.bbox[1]"
-       "        val x2 = group.bbox[2]"
-       "        val y2 = group.bbox[3]"
-       "        val width = (x2 - x1).coerceAtLeast(1f)"
-       "        val height = (y2 - y1).coerceAtLeast(1f)"
-       "        val count = group.count"
-       "        val cx = (x1 + x2) / 2f"
-       "        val cy = (y1 + y2) / 2f"
-       "        val nearEdge = y1 < imageHeight * 0.22f ||"
-       "            y2 > imageHeight * 0.78f ||"
-       "            x1 < imageWidth * 0.16f ||"
-       "            x2 > imageWidth * 0.84f"
-       "        val lineLike = width >= height * 1.8f || height >= width * 1.8f"
-       "        val compactBank = width * height < imageWidth * imageHeight * 0.035f"
-       ""
-       "        if (count >= 3 && nearEdge && (lineLike || compactBank || count >= 5)) {"
-       "            return if (race == \"andromedan\") {"
-       "                Guess(\"pa panel\", \"PA Panel\", \"Andromedan outer-edge bank geometry\")"
-       "            } else {"
-       "                Guess("
-       "                    shieldLabel(cx / imageWidth.coerceAtLeast(1), cy / imageHeight.coerceAtLeast(1)),"
-       "                    \"Shield\","
-       "                    \"outer-edge bank geometry\""
-       "                )"
-       "            }"
-       "        }"
-       "        if (count >= 10 && cy < imageHeight * 0.34f && width >= height * 1.4f) {"
-       "            return if (race == \"andromedan\") {"
-       "                Guess(\"pa panel\", \"PA Panel\", \"upper bank geometry\")"
-       "            } else {"
-       "                Guess(\"shield-1\", \"Shield\", \"upper horizontal bank geometry\")"
-       "            }"
-       "        }"
-       "        if (count <= 4 && width * height < imageWidth * imageHeight * 0.006f) {"
-       "            if (nearEdge && count >= 2) {"
-       "                return if (race == \"andromedan\") {"
-       "                    Guess(\"pa panel\", \"PA Panel\", \"small Andromedan outer-edge bank geometry\")"
-       "                } else {"
-       "                    Guess("
-       "                        shieldLabel(cx / imageWidth.coerceAtLeast(1), cy / imageHeight.coerceAtLeast(1)),"
-       "                        \"Shield\","
-       "                        \"small outer-edge bank geometry\""
-       "                    )"
-       "                }"
-       "            }"
-       "            return Guess(if (race == \"andromedan\") \"phaser-2\" else \"phaser\", \"Phaser\", \"compact weapon-size group\")"
-       "        }"
-       "        if (count in 1..3 && cy < imageHeight * 0.34f && cx > imageWidth * 0.35f && cx < imageWidth * 0.65f) {"
-       "            return Guess(\"bridge\", \"Bridge\", \"upper center command position\")"
-       "        }"
-       "        if (count in 3..12 && cy > imageHeight * 0.32f && cy < imageHeight * 0.72f) {"
-       "            return Guess(\"hull\", \"Forward Hull\", \"central internal bank\")"
-       "        }"
-       "        return Guess(\"${count} boxes\", \"\", \"unclassified detected group\")"
-       "    }"
        "}"
        ""
        ))