Skip to content

Commit

Permalink
Add Tile Info Endpoints (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Brown authored Apr 15, 2020
1 parent 382675b commit c43d248
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 31 deletions.
18 changes: 11 additions & 7 deletions application/src/main/scala/com/azavea/franklin/api/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@ $$$$
connectionEc,
Blocker.liftExecutionContext(transactionEc)
)
collectionItemEndpoints = new CollectionItemEndpoints(apiConfig.enableTransactions)
allEndpoints = LandingPageEndpoints.endpoints ++ CollectionEndpoints.endpoints ++ collectionItemEndpoints.endpoints ++ SearchEndpoints.endpoints
docs = allEndpoints.toOpenAPI("Franklin", "0.0.1")
docRoutes = new SwaggerHttp4s(docs.toYaml, "open-api", "spec.yaml").routes[IO]
landingPageRoutes = new LandingPageService[IO](apiConfig).routes
searchRoutes = new SearchService[IO](xa).routes
collectionItemEndpoints = new CollectionItemEndpoints(
apiConfig.enableTransactions,
apiConfig.enableTiles
)
allEndpoints = LandingPageEndpoints.endpoints ++ CollectionEndpoints.endpoints ++ collectionItemEndpoints.endpoints ++ SearchEndpoints.endpoints
docs = allEndpoints.toOpenAPI("Franklin", "0.0.1")
docRoutes = new SwaggerHttp4s(docs.toYaml, "open-api", "spec.yaml").routes[IO]
landingPageRoutes = new LandingPageService[IO](apiConfig).routes
searchRoutes = new SearchService[IO](apiConfig.apiHost, apiConfig.enableTiles, xa).routes
collectionRoutes = new CollectionsService[IO](xa).routes <+> new CollectionItemsService[IO](
xa,
apiConfig.apiHost,
apiConfig.enableTransactions
apiConfig.enableTransactions,
apiConfig.enableTiles
).routes
router = CORS(
Router(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import sttp.model.StatusCode.{NotFound => NF, BadRequest, PreconditionFailed}
import sttp.tapir._
import sttp.tapir.json.circe._

class CollectionItemEndpoints(enableTransactions: Boolean) {
class CollectionItemEndpoints(enableTransactions: Boolean, enableTiles: Boolean) {

val base = endpoint.in("collections")

Expand All @@ -36,6 +36,15 @@ class CollectionItemEndpoints(enableTransactions: Boolean) {
.description("A single feature")
.name("collectionItemUnique")

val collectionItemTiles: Endpoint[(String, String), NotFound, (Json, String), Nothing] =
base.get
.in(path[String] / "items" / path[String] / "tiles")
.out(jsonBody[Json])
.errorOut(oneOf(statusMapping(NF, jsonBody[NotFound].description("not found"))))
.out(header[String]("ETag"))
.description("An item's tile endpoints")
.name("collectionItemUnique")

val postItem: Endpoint[(String, StacItem), ValidationError, (Json, String), Nothing] =
base.post
.in(path[String] / "items")
Expand Down Expand Up @@ -120,5 +129,7 @@ class CollectionItemEndpoints(enableTransactions: Boolean) {
)

val endpoints = List(collectionItemsList, collectionItemsUnique) ++
(if (enableTransactions) transactionEndpoints else Nil)
(if (enableTransactions) transactionEndpoints else Nil) ++ (if (enableTiles)
List(collectionItemTiles)
else Nil)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.azavea.franklin.api

import com.azavea.stac4s._
import eu.timepit.refined.types.string.NonEmptyString

import java.net.URLEncoder
import java.nio.charset.StandardCharsets

package object implicits {

implicit class combineNonEmptyString(s: NonEmptyString) {
Expand All @@ -11,4 +15,31 @@ package object implicits {
NonEmptyString.unsafeFrom(s.value.concat(otherString))
}

implicit class StacItemWithCog(item: StacItem) {

def addTilesLink(apiHost: String, collectionId: String, itemId: String) = {
val cogAsset = item.assets.values.exists { asset =>
asset._type match {
case Some(`image/cog`) => true
case _ => false
}
}
val updatedLinks = cogAsset match {
case true => {
val encodedItemId = URLEncoder.encode(itemId, StandardCharsets.UTF_8.toString)
val tileLink: StacLink = StacLink(
s"$apiHost/collections/$collectionId/items/$encodedItemId/tiles",
StacLinkType.VendorLinkType("tiles"),
Some(`application/json`),
Some("Tile URLs for Item"),
List.empty
)
tileLink :: item.links
}
case _ => item.links
}
(item.copy(links = updatedLinks))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.azavea.franklin.api

import cats.implicits._
import com.azavea.franklin.database.{temporalExtentFromString, temporalExtentToString}
import com.azavea.stac4s.{Bbox, StacItem, TemporalExtent, ThreeDimBbox, TwoDimBbox}
import com.azavea.stac4s._
import geotrellis.vector.Geometry
import io.circe.{Encoder, Json}
import sttp.tapir.Codec.PlainCodec
Expand Down Expand Up @@ -71,4 +71,5 @@ package object schemas {

implicit val codecStacItem: Codec.JsonCodec[StacItem] =
jsonCodec.mapDecode(decStacItem)(encStacItem)

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import cats.data.NonEmptyList
import cats.effect._
import cats.implicits._
import com.azavea.franklin.api.endpoints.CollectionItemEndpoints
import com.azavea.franklin.api.implicits._
import com.azavea.franklin.database.Filterables._
import com.azavea.franklin.database.StacItemDao
import com.azavea.franklin.datamodel._
import com.azavea.franklin.error.{
CrudError,
InvalidPatch,
Expand All @@ -15,6 +17,7 @@ import com.azavea.franklin.error.{
ValidationError
}
import com.azavea.stac4s.StacLinkType
import com.azavea.stac4s._
import com.azavea.stac4s.{`application/json`, StacItem, StacLink}
import doobie._
import doobie.implicits._
Expand All @@ -27,10 +30,14 @@ import org.http4s.HttpRoutes
import org.http4s.dsl.Http4sDsl
import sttp.tapir.server.http4s._

import java.net.URLDecoder
import java.nio.charset.StandardCharsets

class CollectionItemsService[F[_]: Sync](
xa: Transactor[F],
apiHost: NonEmptyString,
enableTransactions: Boolean
enableTransactions: Boolean,
enableTiles: Boolean
)(
implicit contextShift: ContextShift[F]
) extends Http4sDsl[F] {
Expand All @@ -52,19 +59,51 @@ class CollectionItemsService[F[_]: Sync](
}

def getCollectionItemUnique(
collectionId: String,
itemId: String
rawCollectionId: String,
rawItemId: String
): F[Either[NF, (Json, String)]] = {
val itemId = URLDecoder.decode(rawItemId, StandardCharsets.UTF_8.toString)
val collectionId = URLDecoder.decode(rawCollectionId, StandardCharsets.UTF_8.toString)

for {
itemOption <- StacItemDao.getCollectionItem(collectionId, itemId).transact(xa)
} yield {
Either.fromOption(
itemOption map { item => (item.asJson, item.##.toString) },
itemOption map { item =>
val updatedItem = (if (enableTiles) { item.addTilesLink(apiHost, collectionId, itemId) }
else { item })
(updatedItem.asJson, item.##.toString)
},
NF(s"Item $itemId in collection $collectionId not found")
)
}
}

def getCollectionItemTileInfo(
rawCollectionId: String,
rawItemId: String
): F[Either[NF, (Json, String)]] = {
val itemId = URLDecoder.decode(rawItemId, StandardCharsets.UTF_8.toString)
val collectionId = URLDecoder.decode(rawCollectionId, StandardCharsets.UTF_8.toString)

for {
itemOption <- StacItemDao.getCollectionItem(collectionId, itemId).transact(xa)
} yield {
itemOption match {
case Some(item) =>
Either.fromOption(
TileInfo
.fromStacItem(apiHost, collectionId, item)
.map(info => (info.asJson, info.##.toString)),
NF(
s"Unable to construct tile info object for item $itemId in collection $collectionId. Is there at least one COG asset?"
)
)
case None => Either.left(NF(s"Item $itemId in collection $collectionId not found"))
}
}
}

def postItem(collectionId: String, item: StacItem): F[Either[ValidationError, (Json, String)]] = {
val fallbackCollectionLink = StacLink(
s"$apiHost/api/collections/$collectionId",
Expand Down Expand Up @@ -156,7 +195,11 @@ class CollectionItemsService[F[_]: Sync](
Right((updated.asJson, updated.##.toString))
}

val collectionItemEndpoints = new CollectionItemEndpoints(enableTransactions)
val collectionItemEndpoints = new CollectionItemEndpoints(enableTransactions, enableTiles)

val collectionItemTileRoutes = collectionItemEndpoints.collectionItemTiles.toRoutes {
case (collectionId, itemId) => getCollectionItemTileInfo(collectionId, itemId)
}

val transactionRoutes: List[HttpRoutes[F]] = List(
collectionItemEndpoints.postItem.toRoutes {
Expand Down Expand Up @@ -185,7 +228,9 @@ class CollectionItemsService[F[_]: Sync](
transactionRoutes
} else {
List.empty
})
}) ++ (if (enableTiles) {
List(collectionItemTileRoutes)
} else { List.empty })

val routes: HttpRoutes[F] = routesList.foldK
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import org.http4s._
import org.http4s.dsl.Http4sDsl
import sttp.tapir.server.http4s._

import java.net.URLDecoder
import java.nio.charset.StandardCharsets

class CollectionsService[F[_]: Sync](xa: Transactor[F])(implicit contextShift: ContextShift[F])
extends Http4sDsl[F] {

Expand All @@ -28,7 +31,8 @@ class CollectionsService[F[_]: Sync](xa: Transactor[F])(implicit contextShift: C

}

def getCollectionUnique(collectionId: String): F[Either[NF, Json]] = {
def getCollectionUnique(rawCollectionId: String): F[Either[NF, Json]] = {
val collectionId = URLDecoder.decode(rawCollectionId, StandardCharsets.UTF_8.toString)
for {
collectionOption <- StacCollectionDao
.getCollectionUnique(collectionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@ package com.azavea.franklin.api.services
import cats.effect._
import cats.implicits._
import com.azavea.franklin.api.endpoints.SearchEndpoints
import com.azavea.franklin.api.implicits._
import com.azavea.franklin.database.{SearchFilters, StacItemDao}
import doobie.implicits._
import doobie.util.transactor.Transactor
import eu.timepit.refined.types.string.NonEmptyString
import io.circe._
import io.circe.syntax._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import sttp.tapir.server.http4s._

class SearchService[F[_]: Sync](xa: Transactor[F])(
class SearchService[F[_]: Sync](apiHost: NonEmptyString, enableTiles: Boolean, xa: Transactor[F])(
implicit contextShift: ContextShift[F]
) extends Http4sDsl[F] {

def search(searchFilters: SearchFilters): F[Either[Unit, Json]] = {
for {
searchResult <- StacItemDao.getSearchResult(searchFilters).transact(xa)
} yield {
Either.right(searchResult.asJson)
val updatedFeatures = searchResult.features.map { item =>
(item.collection, enableTiles) match {
case (Some(collectionId), true) => item.addTilesLink(apiHost.value, collectionId, item.id)
case _ => item
}
}
Either.right(searchResult.copy(features = updatedFeatures).asJson)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ case class ApiConfig(
internalPort: PosInt,
host: String,
scheme: String,
enableTransactions: Boolean
enableTransactions: Boolean,
enableTiles: Boolean
) {

val apiHost: NonEmptyString = getHost(publicPort, host, scheme)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ trait ApiOptions {
)
.orFalse

private val enableTiles = Opts.flag("with-tiles", "Whether to include tile endpoints").orFalse

val apiConfig: Opts[ApiConfig] = (
externalPort,
internalPort,
apiHost,
apiScheme,
enableTransactions
enableTransactions,
enableTiles
) mapN ApiConfig
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.azavea.franklin.crawler
import com.azavea.stac4s._
import eu.timepit.refined.types.string.NonEmptyString

import java.net.URLEncoder
import java.nio.charset.StandardCharsets

// A wrapper that stores a collection, an optional parent, its children, and its items
case class CollectionWrapper(
value: StacCollection,
Expand All @@ -17,17 +20,18 @@ case class CollectionWrapper(
serverHost: NonEmptyString,
rootLink: StacLink
) = {

val encodedItemId = URLEncoder.encode(item.id, StandardCharsets.UTF_8.toString)
val encodedCollectionId = URLEncoder.encode(collection.id, StandardCharsets.UTF_8.toString)
val selfLink = StacLink(
s"${serverHost.value}/collections/${collection.id}/items/${item.id}",
s"${serverHost.value}/collections/$encodedCollectionId/items/$encodedItemId",
StacLinkType.Self,
Some(`application/geo+json`),
Some(item.id),
List.empty
)

val parentLink = StacLink(
s"${serverHost.value}/collections/${collection.id}/",
s"${serverHost.value}/collections/$encodedCollectionId/",
StacLinkType.Parent,
Some(`application/json`),
collection.title,
Expand All @@ -53,11 +57,13 @@ case class CollectionWrapper(
List.empty
)

val collectionId = value.id
val collectionId = URLEncoder.encode(value.id, StandardCharsets.UTF_8.toString)

val itemLinks = items.map { item =>
val itemId = URLEncoder.encode(item.id, StandardCharsets.UTF_8.toString)

StacLink(
s"${serverHost.value}/collections/$collectionId/items/${item.id}",
s"${serverHost.value}/collections/$collectionId/items/$itemId",
StacLinkType.Item,
Some(`application/geo+json`),
Some(item.id),
Expand All @@ -66,8 +72,9 @@ case class CollectionWrapper(
}

val childrenLinks = children.map { child =>
val encodedChildId = URLEncoder.encode(child.value.id, StandardCharsets.UTF_8.toString)
StacLink(
s"${serverHost.value}/collections/${child.value.id}/",
s"${serverHost.value}/collections/$encodedChildId/",
StacLinkType.Child,
Some(`application/json`),
child.value.title,
Expand All @@ -76,9 +83,10 @@ case class CollectionWrapper(
}

val parentLink = parent.map { p =>
val encodedParentId = URLEncoder.encode(p.value.id, StandardCharsets.UTF_8.toString)
List(
StacLink(
s"${serverHost.value}/collections/${p.value.id}/",
s"${serverHost.value}/collections/$encodedParentId/",
StacLinkType.Parent,
Some(`application/json`),
p.value.title,
Expand Down
Loading

0 comments on commit c43d248

Please sign in to comment.