Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OGCSources enhanced temporal support #352

Merged
merged 7 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- StacCollectionRasterSource implementation [#340](https://github.com/geotrellis/geotrellis-server/issues/340)
- WCS Rendering formats support [#195](https://github.com/geotrellis/geotrellis-server/issues/195)
- GeoTrellis Server STAC Package [#350](https://github.com/geotrellis/geotrellis-server/issues/350)
- STAC Collection temporal extents support [#347](https://github.com/geotrellis/geotrellis-server/issues/347)

### Changed
- Update GT Server STAC4S dependency [#319](https://github.com/geotrellis/geotrellis-server/issues/319)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import geotrellis.raster.resample._
import geotrellis.server.ogc._
import geotrellis.store.GeoTrellisPath
import com.azavea.maml.ast._
import cats.syntax.option._

// This sumtype corresponds to the in-config representation of a source
sealed trait OgcSourceConf {
Expand All @@ -38,12 +39,36 @@ case class RasterSourceConf(
defaultStyle: Option[String],
styles: List[StyleConf],
resampleMethod: ResampleMethod = ResampleMethod.DEFAULT,
overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT
overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT,
datetimeField: String = SimpleSource.TimeFieldDefault,
timeFormat: OgcTimeFormat = OgcTimeFormat.Self
) extends OgcSourceConf {
def toLayer: RasterOgcSource = {
GeoTrellisPath.parseOption(source) match {
case Some(_) => GeoTrellisOgcSource(name, title, source, defaultStyle, styles.map(_.toStyle), resampleMethod, overviewStrategy)
case None => SimpleSource(name, title, RasterSource(source), defaultStyle, styles.map(_.toStyle), resampleMethod, overviewStrategy, None)
case Some(_) =>
GeoTrellisOgcSource(
name,
title,
source,
defaultStyle,
styles.map(_.toStyle),
resampleMethod,
overviewStrategy,
datetimeField.some,
timeFormat
)
case None =>
SimpleSource(
name,
title,
RasterSource(source),
defaultStyle,
styles.map(_.toStyle),
resampleMethod,
overviewStrategy,
datetimeField.some,
timeFormat
)
pomadchin marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -55,7 +80,8 @@ case class MapAlgebraSourceConf(
defaultStyle: Option[String],
styles: List[StyleConf],
resampleMethod: ResampleMethod = ResampleMethod.DEFAULT,
overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT
overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT,
timeFormat: OgcTimeFormat = OgcTimeFormat.Self
) extends OgcSourceConf {
private def listParams(expr: Expression): List[String] = {
def eval(subExpr: Expression): List[String] =
Expand All @@ -73,26 +99,25 @@ case class MapAlgebraSourceConf(
* in the algebra field
*/
def model(possibleSources: List[RasterOgcSource]): MapAlgebraSource = {
val layerNames = listParams(algebra)
val sourceList = layerNames.map { name =>
val layerNames = listParams(algebra)
val sourceList = layerNames.map { name =>
val layerSrc = possibleSources.find(_.name == name).getOrElse {
throw new Exception(
s"MAML Layer expected but was unable to find the simple layer '$name', make sure all required layers are in the server configuration and are correctly spelled there and in all provided MAML"
)
}
(layerSrc.timeMetadataKey, name -> layerSrc.source)
name -> layerSrc
}
val timeMetadataKey = sourceList.flatMap(_._1).headOption
MapAlgebraSource(
name,
title,
sourceList.map(_._2).toMap,
sourceList.toMap,
algebra,
defaultStyle,
styles.map(_.toStyle),
resampleMethod,
overviewStrategy,
timeMetadataKey
timeFormat
)
}
}
48 changes: 29 additions & 19 deletions ogc/src/main/scala/geotrellis/server/ogc/OgcSource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,19 @@ case class SimpleSource(
styles: List[OgcStyle],
resampleMethod: ResampleMethod,
overviewStrategy: OverviewStrategy,
timeMetadataKey: Option[String]
timeMetadataKey: Option[String],
timeFormat: OgcTimeFormat
) extends RasterOgcSource {
lazy val time: OgcTime = source.time(timeMetadataKey)
lazy val time: OgcTime = source.time(timeMetadataKey).format(timeFormat)

def toLayer(crs: CRS, style: Option[OgcStyle], temporalSequence: List[OgcTime]): SimpleOgcLayer =
SimpleOgcLayer(name, title, crs, source, style, resampleMethod, overviewStrategy)
}

object SimpleSource {
val TimeFieldDefault: String = "times"
}

case class GeoTrellisOgcSource(
name: String,
title: String,
Expand All @@ -106,7 +111,8 @@ case class GeoTrellisOgcSource(
styles: List[OgcStyle],
resampleMethod: ResampleMethod,
overviewStrategy: OverviewStrategy,
timeMetadataKey: Option[String] = "times".some
timeMetadataKey: Option[String],
timeFormat: OgcTimeFormat
) extends RasterOgcSource {

def toLayer(crs: CRS, style: Option[OgcStyle], temporalSequence: List[OgcTime]): SimpleOgcLayer = {
Expand Down Expand Up @@ -134,13 +140,13 @@ case class GeoTrellisOgcSource(
),
None,
None,
timeMetadataKey.getOrElse("times")
timeMetadataKey.getOrElse(SimpleSource.TimeFieldDefault)
)
}

lazy val time: OgcTime =
if (!source.isTemporal) OgcTimeEmpty
else OgcTimePositions(source.times)
else OgcTimePositions(source.times).format(timeFormat)

/** If temporal, try to match in the following order:
*
Expand Down Expand Up @@ -212,16 +218,23 @@ case class MapAlgebraSourceMetadata(
case class MapAlgebraSource(
name: String,
title: String,
sources: Map[String, RasterSource],
ogcSources: Map[String, RasterOgcSource],
algebra: Expression,
defaultStyle: Option[String],
styles: List[OgcStyle],
resampleMethod: ResampleMethod,
overviewStrategy: OverviewStrategy,
timeMetadataKey: Option[String]
timeFormat: OgcTimeFormat
) extends OgcSource {
// each of the underlying ogcSources uses it's own timeMetadataKey
val timeMetadataKey: Option[String] = None

lazy val sources: Map[String, RasterSource] = ogcSources.mapValues(_.source)
lazy val sourcesList: List[RasterSource] = sources.values.toList
lazy val ogcSourcesList: List[RasterOgcSource] = ogcSources.values.toList

def extentIn(crs: CRS): Extent = {
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sources.values.map(_.reproject(crs)).toList)
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sourcesList.map(_.reproject(crs)))
jisantuc marked this conversation as resolved.
Show resolved Hide resolved
val extents = reprojectedSources.map(_.extent)

SampleUtils.intersectExtents(extents).getOrElse {
Expand All @@ -230,7 +243,7 @@ case class MapAlgebraSource(
}

def bboxIn(crs: CRS): BoundingBox = {
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sources.values.map(_.reproject(crs)).toList)
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sourcesList.map(_.reproject(crs)))
val extents = reprojectedSources.map(_.extent)
val extentIntersection = SampleUtils.intersectExtents(extents)
val cellSize = SampleUtils.chooseLargestCellSize(reprojectedSources.map(_.cellSize))
Expand All @@ -253,7 +266,7 @@ case class MapAlgebraSource(
)

lazy val nativeExtent: Extent = {
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sources.values.map(_.reproject(nativeCrs.head)).toList)
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sourcesList.map(_.reproject(nativeCrs.head)))
val extents = reprojectedSources.map(_.extent)
val extentIntersection = SampleUtils.intersectExtents(extents)

Expand All @@ -264,20 +277,17 @@ case class MapAlgebraSource(
}

lazy val nativeRE: GridExtent[Long] = {
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sources.values.map(_.reproject(nativeCrs.head)).toList)
val reprojectedSources: NEL[RasterSource] = NEL.fromListUnsafe(sourcesList.map(_.reproject(nativeCrs.head)))
val cellSize = SampleUtils.chooseSmallestCellSize(reprojectedSources.map(_.cellSize))

new GridExtent[Long](nativeExtent, cellSize)
}

val time: OgcTime =
timeMetadataKey.toList
.flatMap { key => sources.values.toList.map(_.time(key.some)) }
.foldLeft[OgcTime](OgcTimeEmpty)(_ |+| _)
val time: OgcTime = ogcSources.values.toList.map(_.time).foldLeft[OgcTime](OgcTimeEmpty)(_ |+| _).format(timeFormat)

val attributes: Map[String, String] = Map.empty
lazy val nativeCrs: Set[CRS] = sources.values.map(_.crs).toSet
lazy val minBandCount: Int = sources.values.map(_.bandCount).min
lazy val cellTypes: Set[CellType] = sources.values.map(_.cellType).toSet
lazy val resolutions: List[CellSize] = sources.values.flatMap(_.resolutions).toList.distinct
lazy val nativeCrs: Set[CRS] = ogcSourcesList.flatMap(_.nativeCrs).toSet
lazy val minBandCount: Int = sourcesList.map(_.bandCount).min
lazy val cellTypes: Set[CellType] = sourcesList.map(_.cellType).toSet
lazy val resolutions: List[CellSize] = sourcesList.flatMap(_.resolutions).distinct
}
61 changes: 58 additions & 3 deletions ogc/src/main/scala/geotrellis/server/ogc/OgcTime.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import cats.syntax.semigroup._
import jp.ne.opt.chronoscala.Imports._

import java.time.ZonedDateTime
import java.time.Duration
import scala.util.Try

sealed trait OgcTime {
Expand All @@ -37,6 +38,8 @@ object OgcTime {
(l, r) match {
case (l: OgcTimePositions, r: OgcTimePositions) => l |+| r
case (l: OgcTimeInterval, r: OgcTimeInterval) => l |+| r
case (l: OgcTimePositions, r: OgcTimeInterval) => l.toOgcTimeInterval |+| r
case (l: OgcTimeInterval, r: OgcTimePositions) => l |+| r.toOgcTimeInterval
case (l: OgcTimePositions, _: OgcTimeEmpty.type) => l
case (l: OgcTimeInterval, _: OgcTimeEmpty.type) => l
case (_: OgcTimeEmpty.type, r: OgcTimePositions) => r
Expand All @@ -54,6 +57,28 @@ object OgcTime {

def fromString(str: String): OgcTime =
Try(OgcTimeInterval.fromString(str)).getOrElse(OgcTimePositions(str.split(",").toList))

implicit class OgcTimeOps(val self: OgcTime) extends AnyVal {

/** Reformat OgcTime if possible. */
def format(format: OgcTimeFormat): OgcTime = {
format match {
case OgcTimeFormat.Interval =>
self match {
case interval: OgcTimeInterval => interval
case positions: OgcTimePositions => positions.toOgcTimeInterval
case _ => self
}
case OgcTimeFormat.Positions =>
self match {
case interval: OgcTimeInterval => interval.toTimePositions.getOrElse(interval)
case positions: OgcTimePositions => positions
case _ => self
}
case OgcTimeFormat.Self => self
}
}
}
}

case object OgcTimeEmpty extends OgcTime {
Expand All @@ -67,11 +92,26 @@ case object OgcTimeEmpty extends OgcTime {
final case class OgcTimePositions(list: NonEmptyList[ZonedDateTime]) extends OgcTime {
import OgcTimePositions._

/** Compute (if possible) the period of the [[ZonedDateTime]] lists. */
def computeIntervalPeriod: Option[Duration] = {
val periods =
list.toList
.sliding(2)
.map { case Seq(l, r, _*) => Duration.ofMillis((l - r.toEpochMilli).toEpochMilli) }
.toList
.distinct

if (periods.length < 2) periods.headOption
else None
}

def toOgcTimeInterval: OgcTimeInterval = {
val times = list.sorted
OgcTimeInterval(times.head, times.last, None)
OgcTimeInterval(times.head, times.last, computeIntervalPeriod.map(_.toString))
}
override def toString: String = list.toList.map(_.toInstant.toString).mkString(", ")

def toList: List[String] = list.toList.map(_.toInstant.toString)
override def toString: String = toList.mkString(", ")
}

object OgcTimePositions {
Expand Down Expand Up @@ -110,6 +150,19 @@ object OgcTimePositions {
* encoded directly
*/
final case class OgcTimeInterval(start: ZonedDateTime, end: ZonedDateTime, interval: Option[String]) extends OgcTime {
def duration: Option[Duration] = interval.flatMap(p => Try(Duration.parse(p)).toOption)
pomadchin marked this conversation as resolved.
Show resolved Hide resolved

def toTimePositions: Option[OgcTimePositions] =
duration.flatMap { d =>
val positions =
(start.toEpochMilli to end.toEpochMilli by d.toMillis)
.map(Instant.ofEpochMilli)
.map(ZonedDateTime.ofInstant(_, start.getZone))
.toList

NonEmptyList.fromList(positions).map(OgcTimePositions.apply)
}

override def toString: String =
if (start != end) s"${start.toInstant.toString}/${end.toInstant.toString}${interval.map("/" + _).getOrElse("")}"
else start.toInstant.toString
Expand All @@ -126,7 +179,9 @@ object OgcTimeInterval {
OgcTimeInterval(times.head, times.last, None)
}

def apply(timePeriod: ZonedDateTime): OgcTimeInterval = OgcTimeInterval(timePeriod, timePeriod, None)
def apply(start: ZonedDateTime): OgcTimeInterval = OgcTimeInterval(start, start, None)

def apply(start: ZonedDateTime, end: ZonedDateTime): OgcTimeInterval = OgcTimeInterval(start, end, None)

def apply(timeString: String): OgcTimeInterval = fromString(timeString)

Expand Down
39 changes: 39 additions & 0 deletions ogc/src/main/scala/geotrellis/server/ogc/OgcTimeFormat.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 Azavea
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package geotrellis.server.ogc

import io.circe.Codec
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveEnumerationCodec

/** ADT to change [[OgcTime]] internal representation */
sealed trait OgcTimeFormat

object OgcTimeFormat {

/** Represent [[OgcTime]] as [[OgcTimePositions]]. */
case object Positions extends OgcTimeFormat

/** Represent [[OgcTime]] as [[OgcTimeInterval]]. */
case object Interval extends OgcTimeFormat

/** Don't change the internal [[OgcTime]] representation. */
case object Self extends OgcTimeFormat

private implicit val config: Configuration = Configuration.default.copy(transformConstructorNames = _.toLowerCase)
implicit val modeCodec: Codec[OgcTimeFormat] = deriveEnumerationCodec
}
Loading