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

ongo.reactivemongo-bson-geo_2.13.0.20.0-noshaded.source-code.Geometry.scala Maven / Gradle / Ivy

package reactivemongo.api.bson

import scala.util.{ Failure, Success, Try }

import GeoPosition.safeWriter.{ safeWrite => writePos }

/**
 * GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#overview geometry object]]
 */
sealed trait GeoGeometry {
  /** The type of coordinates (depends on the type of geometry). */
  private[bson] type C

  def `type`: String

  /**
   * The coordinates
   */
  protected def coordinates: C
}

object GeoGeometry {
  implicit val handler: BSONDocumentHandler[GeoGeometry] = {
    implicit val cfg: MacroConfiguration = MacroConfiguration(
      discriminator = "type",
      typeNaming = TypeNaming(
        TypeNaming.SimpleName.andThen { (_: String).stripPrefix("Geo") }))

    Macros.handler[GeoGeometry]
  }

  // ---

  private[bson] def reader[G <: GeoGeometry](
    readCoordinates: BSONValue => Try[G]): BSONDocumentReader[G] =
    BSONDocumentReader.from[G] { doc =>
      doc.get("coordinates") match {
        case Some(coords) =>
          readCoordinates(coords)

        case _ =>
          Failure(exceptions.BSONValueNotFoundException("coordinates", doc))
      }
    }

  private[bson] def writer[G <: GeoGeometry](
    writeCoordinates: G => BSONValue): BSONDocumentWriter[G] with SafeBSONDocumentWriter[G] = BSONDocumentWriter.safe[G] { geometry =>
    BSONDocument(
      "type" -> geometry.`type`,
      "coordinates" -> writeCoordinates(geometry))
  }
}

/**
 * GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#point Point]]
 */
final case class GeoPoint(
  coordinates: GeoPosition) extends GeoGeometry {
  type C = GeoPosition

  @inline def `type` = GeoPoint.`type`
}

/** See [[GeoPoint]] */
object GeoPoint {
  val `type` = "Point"

  /**
   * Convenient factory (See [[GeoPosition]];
   * equivalent to `GeoPoint(GeoPosition(_1, _2))`).
   */
  @inline def apply(_1: Double, _2: Double): GeoPoint =
    GeoPoint(GeoPosition(_1, _2, None))

  /**
   * Convenient factory (See [[GeoPosition]];
   * equivalent to `GeoPoint(GeoPosition(_1, _2, elevation))`).
   */
  @inline def apply(_1: Double, _2: Double, elevation: Option[Double]): GeoPoint = GeoPoint(GeoPosition(_1, _2, elevation))

  implicit val reader: BSONDocumentReader[GeoPoint] =
    GeoGeometry.reader[GeoPoint] {
      case GeoPosition.BSON(pos) =>
        Success(GeoPoint(pos))

      case bson =>
        Failure(exceptions.TypeDoesNotMatchException(
          "", bson.getClass.getSimpleName))

    }

  implicit val writer: BSONDocumentWriter[GeoPoint] =
    GeoGeometry.writer[GeoPoint] { point =>
      writePos(point.coordinates)
    }
}

/**
 * GeoJSON [[https://tools.ietf.org/html/rfc7946#section-3.1.4 LineString]]
 */
final case class GeoLineString(
  _1: GeoPosition,
  _2: GeoPosition,
  more: Seq[GeoPosition]) extends GeoGeometry {
  type C = Tuple3[GeoPosition, GeoPosition, Seq[GeoPosition]]
  @inline def `type` = GeoLineString.`type`

  val coordinates = Tuple3(_1, _2, more)

  /**
   * Appends more positions
   *
   * {{{
   * import reactivemongo.api.bson.{ GeoLineString, GeoPosition }
   *
   * def equivalentTo(lineString: GeoLineString, positions: GeoPosition*) =
   *   lineString.copy(more = lineString.more ++ positions)
   * }}}
   */
  @inline def ++(positions: GeoPosition*): GeoLineString =
    copy(more = more ++ positions)
}

/** See [[GeoLineString]] */
object GeoLineString {
  val `type` = "LineString"

  /**
   * Convenient factory
   * (equivalent to `GeoLineString(_1, _2, Seq.empty)`).
   */
  @inline def apply(_1: GeoPosition, _2: GeoPosition): GeoLineString =
    GeoLineString(_1, _2, Seq.empty)

  private[bson] val readCoordinates: BSONValue => Try[GeoLineString] = {
    case BSONArray(Seq(
      GeoPosition.BSON(a),
      GeoPosition.BSON(b),
      more @ _*)) =>
      GeoPosition.readSeq(more).map { morePos =>
        GeoLineString(a, b, morePos)
      }

    case bson =>
      Failure(exceptions.TypeDoesNotMatchException(
        "[ , , ... ]",
        bson.getClass.getSimpleName))
  }

  implicit val reader: BSONDocumentReader[GeoLineString] =
    GeoGeometry.reader[GeoLineString](readCoordinates)

  private[bson] val writeCoordinates: GeoLineString => BSONArray = { lineStr =>
    writePos(lineStr._1) +: writePos(lineStr._2) +: BSONArray(
      lineStr.more.map(writePos))
  }

  implicit def writer: BSONDocumentWriter[GeoLineString] =
    GeoGeometry.writer[GeoLineString](writeCoordinates)
}

/**
 * GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#polygon Polygon]]
 */
final class GeoPolygon private[bson] (
  val exterior: GeoLinearRing,
  val interior: Seq[GeoLinearRing]) extends GeoGeometry {
  type C = Tuple2[GeoLinearRing, Seq[GeoLinearRing]]
  @inline def `type`: String = GeoPolygon.`type`

  private[bson] lazy val tupled = Tuple2(exterior, interior)

  @inline def coordinates = tupled

  /**
   * {{{
   * import reactivemongo.api.bson.{ GeoLinearRing, GeoPolygon }
   *
   * def equivalentTo(poly: GeoPolygon, interior: Seq[GeoLinearRing]) =
   *   GeoPolygon(poly.exterior, (poly.interior ++ interior): _*)
   * }}}
   */
  def ++(interior: GeoLinearRing*): GeoPolygon =
    new GeoPolygon(exterior, this.interior ++ interior)

  override def equals(that: Any): Boolean = that match {
    case other: GeoPolygon => tupled == other.tupled
    case _ => false
  }

  override lazy val hashCode: Int = tupled.hashCode

  override def toString: String =
    s"""GeoPolygon(${exterior}${interior mkString (", ", ", ", "")})"""
}

/** See [[GeoPolygon]] */
object GeoPolygon {
  val `type`: String = "Polygon"

  /**
   * Creates a [[https://docs.mongodb.com/manual/reference/geojson/#polygons-with-a-single-ring single ring]] polygon.
   *
   * @param exterior the single (exterior) ring (cannot self-intersect)
   */
  def apply(exterior: GeoLinearRing): GeoPolygon =
    new GeoPolygon(exterior, Seq.empty)

  /**
   * Creates a [[https://docs.mongodb.com/manual/reference/geojson/#polygons-with-multiple-rings multiple rings]] polygon.
   *
   * Any interior ring must be entirely contained by the outer ring.
   * Interior rings cannot intersect or overlap each other.
   * Interior rings cannot share an edge.
   *
   * @param exterior the exterior (mandatory) ring (cannot self-intersect)
   * @param interior some optional interior rings
   */
  def apply(exterior: GeoLinearRing, interior: GeoLinearRing*): GeoPolygon =
    new GeoPolygon(exterior, interior)

  @inline def unapply(polygon: GeoPolygon): Option[(GeoLinearRing, Seq[GeoLinearRing])] = Option(polygon).map(_.tupled)

  private[bson] val readCoordinates: BSONValue => Try[GeoPolygon] = {
    case BSONArray(Seq(exterior @ BSONArray(_), interior @ _*)) =>
      for {
        ex <- GeoLinearRing.reader.readTry(exterior)
        in <- GeoLinearRing.readSeq(interior)
      } yield new GeoPolygon(ex, in)

    case bson =>
      Failure(exceptions.TypeDoesNotMatchException(
        "[ , ... ]", bson.getClass.getSimpleName))
  }

  implicit val reader: BSONDocumentReader[GeoPolygon] =
    GeoGeometry.reader[GeoPolygon](readCoordinates)

  private[bson] val writeCoordinates: GeoPolygon => BSONArray = { polygon =>
    import GeoLinearRing.safeWriter.safeWrite

    BSONArray(safeWrite(polygon.exterior) +: polygon.interior.map(safeWrite))
  }

  implicit val writer: BSONDocumentWriter[GeoPolygon] =
    GeoGeometry.writer[GeoPolygon](writeCoordinates)
}

/** GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#multipoint MultiPoint]] (collection of [[GeoPosition]]) */
case class GeoMultiPoint(
  coordinates: Seq[GeoPosition]) extends GeoGeometry {
  type C = Seq[GeoPosition]

  val `type`: String = GeoMultiPoint.`type`
}

/** See [[GeoMultiPoint]] */
object GeoMultiPoint {
  val `type`: String = "MultiPoint"

  implicit val reader: BSONDocumentReader[GeoMultiPoint] =
    GeoGeometry.reader[GeoMultiPoint] {
      case BSONArray(values) =>
        GeoPosition.readSeq(values).map(GeoMultiPoint(_))

    }

  implicit val writer: BSONDocumentWriter[GeoMultiPoint] =
    GeoGeometry.writer[GeoMultiPoint] { multi =>
      BSONArray(multi.coordinates.map(GeoPosition.safeWriter.safeWrite))
    }

}

/** GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#multilinestring MultiLineString]] (collection of [[GeoLineString]]) */
case class GeoMultiLineString(coordinates: Seq[GeoLineString]) extends GeoGeometry {
  type C = Seq[GeoLineString]
  @inline def `type`: String = GeoMultiLineString.`type`
}

/** See [[GeoMultiLineString]] */
object GeoMultiLineString {
  val `type`: String = "MultiLineString"

  implicit val reader: BSONDocumentReader[GeoMultiLineString] = {
    @annotation.tailrec
    def go(
      values: Seq[BSONValue],
      out: List[GeoLineString]): Try[Seq[GeoLineString]] =
      values.headOption match {
        case Some(v) => GeoLineString.readCoordinates(v) match {
          case Success(lineString) =>
            go(values.tail, lineString :: out)

          case Failure(cause) =>
            Failure(cause)
        }

        case _ =>
          Success(out.reverse)
      }

    GeoGeometry.reader[GeoMultiLineString] {
      case BSONArray(values) =>
        go(values, List.empty).map(GeoMultiLineString(_))

      case bson =>
        Failure(exceptions.TypeDoesNotMatchException(
          "[ , ... ]", bson.getClass.getSimpleName))
    }
  }

  implicit val writer: BSONDocumentWriter[GeoMultiLineString] =
    GeoGeometry.writer[GeoMultiLineString] { multiLineString =>
      BSONArray(multiLineString.coordinates.map(GeoLineString.writeCoordinates))
    }

}

/** GeoJSON [[https://docs.mongodb.com/manual/reference/geojson/#multipolygon MultiPolygon]] (collection of [[GeoPolygon]]) */
case class GeoMultiPolygon(coordinates: Seq[GeoPolygon]) extends GeoGeometry {
  type C = Seq[GeoPolygon]
  @inline def `type`: String = GeoMultiPolygon.`type`
}

/** See [[GeoMultiPolygon]] */
object GeoMultiPolygon {
  val `type`: String = "MultiPolygon"

  implicit val reader: BSONDocumentReader[GeoMultiPolygon] = {
    @annotation.tailrec
    def go(
      values: Seq[BSONValue],
      out: List[GeoPolygon]): Try[Seq[GeoPolygon]] = values.headOption match {
      case Some(bson) => GeoPolygon.readCoordinates(bson) match {
        case Success(polygon) =>
          go(values.tail, polygon :: out)

        case Failure(cause) =>
          Failure(cause)
      }

      case _ =>
        Success(out.reverse)
    }

    GeoGeometry.reader[GeoMultiPolygon] {
      case BSONArray(values) =>
        go(values, List.empty).map(GeoMultiPolygon(_))

      case bson =>
        Failure(exceptions.TypeDoesNotMatchException(
          "[ , ... ]", bson.getClass.getSimpleName))
    }
  }

  implicit val writer: BSONDocumentWriter[GeoMultiPolygon] =
    GeoGeometry.writer[GeoMultiPolygon] { multiPolygon =>
      BSONArray(multiPolygon.coordinates.map(GeoPolygon.writeCoordinates))
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy