All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.id.tmm.fetch.aws.textract.model.Geometry.scala Maven / Gradle / Ivy

package au.id.tmm.fetch.aws.textract.model

import au.id.tmm.fetch.aws.textract.model.Geometry.Polygon.Point
import au.id.tmm.utilities.errors.{ExceptionOr, GenericException}
import io.circe.{Codec, Decoder, Encoder}

import scala.collection.immutable.ArraySeq

final case class Geometry(
  boundingBox: Geometry.BoundingBox,
  polygon: Geometry.Polygon,
)

object Geometry {

  private def requireNonNegative(double: Float): ExceptionOr[Float] =
    if (double >= 0) {
      Right(double)
    } else {
      Left(GenericException(s"Expected positive but was $double"))
    }

  final case class BoundingBox private (
    left: Float,
    top: Float,
    height: Float,
    width: Float,
  ) {
    def bottom: Float = top + height
    def right: Float  = left + width
    def centre: Point =
      Point(top + (height / 2f), left + (width / 2)) match {
        case Right(p) => p
        case Left(e)  => throw new AssertionError(e)
      }
  }

  object BoundingBox {
    def apply(
      left: Float,
      top: Float,
      height: Float,
      width: Float,
    ): ExceptionOr[BoundingBox] =
      for {
        left   <- requireNonNegative(left)
        top    <- requireNonNegative(top)
        height <- requireNonNegative(height)
        width  <- requireNonNegative(width)
      } yield new BoundingBox(left, top, height, width)

    implicit val codec: Codec[BoundingBox] = Codec.from(
      Decoder.forProduct4("left", "top", "height", "width")(BoundingBox.apply).emap(_.left.map(_.getMessage)),
      Encoder.forProduct4("left", "top", "height", "width")(b => (b.left, b.top, b.height, b.width)),
    )
  }

  final case class Polygon(
    points: ArraySeq[Polygon.Point],
  ) extends AnyVal

  object Polygon {

    final case class Point private (x: Float, y: Float)

    object Point {
      def apply(
        x: Float,
        y: Float,
      ): ExceptionOr[Point] =
        for {
          x <- requireNonNegative(x)
          y <- requireNonNegative(y)
        } yield new Point(x, y)

      implicit val codec: Codec[Point] = Codec.from(
        Decoder.forProduct2("x", "y")(Point.apply).emap(_.left.map(_.toString)),
        Encoder.forProduct2("x", "y")(p => (p.x, p.y)),
      )
    }

    implicit val codec: Codec[Polygon] = Codec.from(
      Decoder[ArraySeq[Point]].map(Polygon.apply),
      Encoder[ArraySeq[Point]].contramap(_.points),
    )
  }

  implicit val codec: Codec[Geometry] = Codec.from(
    Decoder.forProduct2("boundingBox", "polygon")(Geometry.apply),
    Encoder.forProduct2("boundingBox", "polygon")(g => (g.boundingBox, g.polygon)),
  )

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy