├── .gitignore ├── LICENSE.md ├── README.md ├── build.sbt ├── project ├── Dependencies.scala ├── Version.scala └── plugins.sbt ├── sbt ├── scripts ├── build-serverless └── publish ├── serverless.yml.tpl ├── src └── main │ └── scala │ ├── ColorRamps.scala │ ├── Main.scala │ └── requests │ ├── RgbRequest.scala │ └── package.scala └── templates └── passthrough.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Serverless directories 16 | .serverless 17 | 18 | # ensime files 19 | /.ensime 20 | /.ensime_cache/* 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2017 James Santucci 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lambda Geotrellis Tile Server 2 | ====== 3 | 4 | This repository creates a geotrellis tile server on AWS's API Gateway service, backed by AWS Lambda. 5 | The resulting url pattern is: 6 | 7 | `https:///tile/{b}/{p}/{l}/{v}/{z}/{x}/{y}/` 8 | 9 | ### Getting data 10 | 11 | `b`, `p`, and `l` specify an s3 path to a `RasterRDD`. 12 | 13 | - `b` is the bucket 14 | - `p` is the url-encoded prefix 15 | - `l` is the url-encoded name of the layer 16 | 17 | `{z}/{x}/{y}` is just normal TMS stuff. 18 | 19 | ### Visualizing your layer 20 | 21 | The `v` parameter specifies what type of visualization you'd like. All of the ColorRamps 22 | from `geotrellis.raster.render.ColorRamps` are available with shortened case-insensitive 23 | string names. Those names and the corresponding `ColorRamps` are: 24 | 25 | - `viridis`: `Viridis` 26 | - `magma`: `Magma` 27 | - `inferno`: `Inferno` 28 | - `plasma`: `Plasma` 29 | - `blor`: `BlueToOrange` 30 | - `ylor`: `LightYellowToOrange` 31 | - `blrd`: `BlueToRed` 32 | - `gror`: `GreenToRedOrange` 33 | - `sunsetdark`: `LightToDarkSunset` 34 | - `greens`: `LightToDarkGreen` 35 | - `ylrd`: `HeatmapYellowToRed` 36 | - `blylrd`: `HeatmapBlueToYellowToRedSpectrum` 37 | - `rdylwt`: `HeatmapDarkRedToYellowWhite` 38 | - `prwt`: `HeatmapLightPurpleToDarkPurpleToWhite` 39 | - `landuse`: `ClassificationBoldLandUse` 40 | - `terrain`: `ClassificationMutedTerrain` 41 | 42 | To return an RGB png, pass `rgb` as the visualization type. No promises about 43 | what will happen if you ask for an `rgb` visualization of a single band tiff. 44 | 45 | If you pass in any other string name, you'll get `Viridis`. You probably wanted 46 | `Viridis` anyway, since it's so pretty. 47 | 48 | There's currently no way to specify class breaks, so `landuse` and `terrain` currently 49 | look pretty dumb. 50 | 51 | Deployment 52 | ------ 53 | 54 | *tl;dr*: `./scripts/publish` 55 | 56 | ### Longer version: 57 | 58 | You'll need the following AWS permissions: 59 | - cloudformation:* 60 | - lambda:* 61 | - apigateway:* 62 | - iam:* 63 | - logs:* 64 | - s3:* 65 | 66 | A more minimal set of permissions is possible, but I haven't mapped out the extent of permissions 67 | requried in each domain. The above will _definitely_ work. 68 | 69 | Once that's set up for your user: 70 | 71 | - Configure your AWS profile (`aws configure --profile `) 72 | - `npm install -g serverless` 73 | - `./scripts/publish` 74 | - Configure your API gateway endpoint from the AWS console: 75 | - Set binary media types to `image/png` in the options for your API 76 | - Navigate to the `GET` endpoint and set "Content handling" in its integration response to "Convert to binary (if needed)" for your resource 77 | - Deploy the API using the `Actions` dropdown 78 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | import sbtrelease.{Version => SbtVersion} 4 | 5 | name := "lambda-geotrellis-tile-server" 6 | 7 | resolvers += Resolver.sonatypeRepo("public") 8 | scalaVersion := "2.11.8" 9 | releaseNextVersion := { ver => SbtVersion(ver).map(_.bumpMinor.string).getOrElse("Error") } 10 | 11 | lazy val commonSettings = Seq( 12 | organization := "com.jisantuc", 13 | version := "0.0.1", 14 | cancelable in Global := true, 15 | scalaVersion := Version.scala, 16 | scalacOptions := Seq( 17 | "-deprecation", 18 | "-unchecked", 19 | "-feature", 20 | "-language:implicitConversions", 21 | "-language:reflectiveCalls", 22 | "-language:higherKinds", 23 | "-language:postfixOps", 24 | "-language:existentials", 25 | "-language:experimental.macros", 26 | "-feature" 27 | ), 28 | shellPrompt := { s => Project.extract(s).currentProject.id + " > " }, 29 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 30 | ) 31 | 32 | 33 | libraryDependencies ++= Seq( 34 | Dependencies.awsJavaCore, 35 | Dependencies.awsJavaEvents, 36 | Dependencies.awsJavaLog4j, 37 | Dependencies.commonsIo, 38 | Dependencies.geotrellisS3, 39 | Dependencies.geotrellisRaster, 40 | Dependencies.geotrellisSpark, 41 | Dependencies.circeCore, 42 | Dependencies.circeGeneric, 43 | Dependencies.circeParser 44 | ) 45 | 46 | lazy val root = Project("root", file(".")) 47 | .settings(resolvers += "LocationTech GeoTrellis Releases" at "https://repo.locationtech.org/content/repositories/geotrellis-releases") 48 | .settings(commonSettings:_*) 49 | 50 | assemblyMergeStrategy in assembly := { 51 | case "reference.conf" => MergeStrategy.concat 52 | case "application.conf" => MergeStrategy.concat 53 | case n if n.endsWith(".SF") || n.endsWith(".RSA") || n.endsWith(".DSA") => MergeStrategy.discard 54 | case "META-INF/MANIFEST.MF" => MergeStrategy.discard 55 | case _ => MergeStrategy.first 56 | } 57 | assemblyJarName in assembly := s"lambda-geotrellis-tile-server.jar" 58 | 59 | import S3._ 60 | s3Settings 61 | mappings in upload := Seq((file(s"target/scala-2.11/${name.value}.jar"), s"${name.value}.jar")) 62 | host in upload := "lambda-geotrellis-tile-server-jar.s3.amazonaws.com" 63 | progress in upload := true 64 | upload <<= upload dependsOn assembly 65 | 66 | initialCommands in console := """ 67 | |import io.circe.parser._ 68 | |import io.circe.syntax._ 69 | |import geotrellis.spark.io._ 70 | |import geotrellis.spark.io.s3._ 71 | """.trim.stripMargin 72 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" 5 | lazy val geotrellisSparkEtl = "org.locationtech.geotrellis" %% "geotrellis-spark-etl" % Version.geotrellis 6 | lazy val geotrellisSpark = "org.locationtech.geotrellis" %% "geotrellis-spark" % Version.geotrellis 7 | lazy val geotrellisS3 = "org.locationtech.geotrellis" %% "geotrellis-s3" % Version.geotrellis 8 | lazy val geotrellisRaster = "org.locationtech.geotrellis" %% "geotrellis-raster" % Version.geotrellis 9 | lazy val geotrellisVector = "org.locationtech.geotrellis" %% "geotrellis-vector" % Version.geotrellis 10 | val geotrellisUtil = "org.locationtech.geotrellis" %% "geotrellis-util" % Version.geotrellis 11 | val awsJavaCore = "com.amazonaws" % "aws-lambda-java-core" % Version.awsJavaCore % "provided" 12 | val awsJavaEvents = "com.amazonaws" % "aws-lambda-java-events" % Version.awsJavaEvents % "provided" 13 | val awsJavaLog4j = "com.amazonaws" % "aws-lambda-java-log4j" % Version.awsJavaLog4j % "provided" 14 | val commonsIo = "commons-io" % "commons-io" % Version.commonsIo 15 | val circeCore = "io.circe" %% "circe-core" % Version.circe 16 | val circeGeneric = "io.circe" %% "circe-generic" % Version.circe 17 | val circeParser = "io.circe" %% "circe-parser" % Version.circe 18 | } 19 | -------------------------------------------------------------------------------- /project/Version.scala: -------------------------------------------------------------------------------- 1 | object Version { 2 | val geotrellis = "1.1.0-RC6" 3 | val scala = "2.11.8" 4 | val awsJavaCore = "1.1.0" 5 | val awsJavaEvents = "1.3.0" 6 | val awsJavaLog4j = "1.0.0" 7 | val commonsIo = "2.4" 8 | val circe = "0.7.0" 9 | } 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.sbtPluginReleases 2 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3") 4 | addSbtPlugin("com.typesafe.sbt" % "sbt-s3" % "0.9") 5 | -------------------------------------------------------------------------------- /sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A more capable sbt runner, coincidentally also called sbt. 4 | # Author: Paul Phillips 5 | 6 | set -o pipefail 7 | 8 | declare -r sbt_release_version="0.13.13" 9 | declare -r sbt_unreleased_version="0.13.13" 10 | 11 | declare -r latest_212="2.12.1" 12 | declare -r latest_211="2.11.8" 13 | declare -r latest_210="2.10.6" 14 | declare -r latest_29="2.9.3" 15 | declare -r latest_28="2.8.2" 16 | 17 | declare -r buildProps="project/build.properties" 18 | 19 | declare -r sbt_launch_ivy_release_repo="http://repo.typesafe.com/typesafe/ivy-releases" 20 | declare -r sbt_launch_ivy_snapshot_repo="https://repo.scala-sbt.org/scalasbt/ivy-snapshots" 21 | declare -r sbt_launch_mvn_release_repo="http://repo.scala-sbt.org/scalasbt/maven-releases" 22 | declare -r sbt_launch_mvn_snapshot_repo="http://repo.scala-sbt.org/scalasbt/maven-snapshots" 23 | 24 | declare -r default_jvm_opts_common="-Xms512m -Xmx1536m -Xss2m" 25 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 26 | 27 | declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new 28 | declare sbt_explicit_version 29 | declare verbose noshare batch trace_level 30 | declare sbt_saved_stty debugUs 31 | 32 | declare java_cmd="java" 33 | declare sbt_launch_dir="$HOME/.sbt/launchers" 34 | declare sbt_launch_repo 35 | 36 | # pull -J and -D options to give to java. 37 | declare -a java_args scalac_args sbt_commands residual_args 38 | 39 | # args to jvm/sbt via files or environment variables 40 | declare -a extra_jvm_opts extra_sbt_opts 41 | 42 | echoerr () { echo >&2 "$@"; } 43 | vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } 44 | die () { echo "Aborting: $@" ; exit 1; } 45 | 46 | # restore stty settings (echo in particular) 47 | onSbtRunnerExit() { 48 | [[ -n "$sbt_saved_stty" ]] || return 49 | vlog "" 50 | vlog "restoring stty: $sbt_saved_stty" 51 | stty "$sbt_saved_stty" 52 | unset sbt_saved_stty 53 | } 54 | 55 | # save stty and trap exit, to ensure echo is re-enabled if we are interrupted. 56 | trap onSbtRunnerExit EXIT 57 | sbt_saved_stty="$(stty -g 2>/dev/null)" 58 | vlog "Saved stty: $sbt_saved_stty" 59 | 60 | # this seems to cover the bases on OSX, and someone will 61 | # have to tell me about the others. 62 | get_script_path () { 63 | local path="$1" 64 | [[ -L "$path" ]] || { echo "$path" ; return; } 65 | 66 | local target="$(readlink "$path")" 67 | if [[ "${target:0:1}" == "/" ]]; then 68 | echo "$target" 69 | else 70 | echo "${path%/*}/$target" 71 | fi 72 | } 73 | 74 | declare -r script_path="$(get_script_path "$BASH_SOURCE")" 75 | declare -r script_name="${script_path##*/}" 76 | 77 | init_default_option_file () { 78 | local overriding_var="${!1}" 79 | local default_file="$2" 80 | if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then 81 | local envvar_file="${BASH_REMATCH[1]}" 82 | if [[ -r "$envvar_file" ]]; then 83 | default_file="$envvar_file" 84 | fi 85 | fi 86 | echo "$default_file" 87 | } 88 | 89 | declare sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" 90 | declare jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" 91 | 92 | build_props_sbt () { 93 | [[ -r "$buildProps" ]] && \ 94 | grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' 95 | } 96 | 97 | update_build_props_sbt () { 98 | local ver="$1" 99 | local old="$(build_props_sbt)" 100 | 101 | [[ -r "$buildProps" ]] && [[ "$ver" != "$old" ]] && { 102 | perl -pi -e "s/^sbt\.version\b.*\$/sbt.version=${ver}/" "$buildProps" 103 | grep -q '^sbt.version[ =]' "$buildProps" || printf "\nsbt.version=%s\n" "$ver" >> "$buildProps" 104 | 105 | vlog "!!!" 106 | vlog "!!! Updated file $buildProps setting sbt.version to: $ver" 107 | vlog "!!! Previous value was: $old" 108 | vlog "!!!" 109 | } 110 | } 111 | 112 | set_sbt_version () { 113 | sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" 114 | [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version 115 | export sbt_version 116 | } 117 | 118 | url_base () { 119 | local version="$1" 120 | 121 | case "$version" in 122 | 0.7.*) echo "http://simple-build-tool.googlecode.com" ;; 123 | 0.10.* ) echo "$sbt_launch_ivy_release_repo" ;; 124 | 0.11.[12]) echo "$sbt_launch_ivy_release_repo" ;; 125 | 0.*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" 126 | echo "$sbt_launch_ivy_snapshot_repo" ;; 127 | 0.*) echo "$sbt_launch_ivy_release_repo" ;; 128 | *-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" 129 | echo "$sbt_launch_mvn_snapshot_repo" ;; 130 | *) echo "$sbt_launch_mvn_release_repo" ;; 131 | esac 132 | } 133 | 134 | make_url () { 135 | local version="$1" 136 | 137 | local base="${sbt_launch_repo:-$(url_base "$version")}" 138 | 139 | case "$version" in 140 | 0.7.*) echo "$base/files/sbt-launch-0.7.7.jar" ;; 141 | 0.10.* ) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 142 | 0.11.[12]) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 143 | 0.*) echo "$base/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; 144 | *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; 145 | esac 146 | } 147 | 148 | addJava () { vlog "[addJava] arg = '$1'" ; java_args+=("$1"); } 149 | addSbt () { vlog "[addSbt] arg = '$1'" ; sbt_commands+=("$1"); } 150 | addScalac () { vlog "[addScalac] arg = '$1'" ; scalac_args+=("$1"); } 151 | addResidual () { vlog "[residual] arg = '$1'" ; residual_args+=("$1"); } 152 | 153 | addResolver () { addSbt "set resolvers += $1"; } 154 | addDebugger () { addJava "-Xdebug" ; addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"; } 155 | setThisBuild () { 156 | vlog "[addBuild] args = '$@'" 157 | local key="$1" && shift 158 | addSbt "set $key in ThisBuild := $@" 159 | } 160 | setScalaVersion () { 161 | [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' 162 | addSbt "++ $1" 163 | } 164 | setJavaHome () { 165 | java_cmd="$1/bin/java" 166 | setThisBuild javaHome "_root_.scala.Some(file(\"$1\"))" 167 | export JAVA_HOME="$1" 168 | export JDK_HOME="$1" 169 | export PATH="$JAVA_HOME/bin:$PATH" 170 | } 171 | 172 | getJavaVersion() { "$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d \"; } 173 | 174 | checkJava() { 175 | # Warn if there is a Java version mismatch between PATH and JAVA_HOME/JDK_HOME 176 | 177 | [[ -n "$JAVA_HOME" && -e "$JAVA_HOME/bin/java" ]] && java="$JAVA_HOME/bin/java" 178 | [[ -n "$JDK_HOME" && -e "$JDK_HOME/lib/tools.jar" ]] && java="$JDK_HOME/bin/java" 179 | 180 | if [[ -n "$java" ]]; then 181 | pathJavaVersion=$(getJavaVersion java) 182 | homeJavaVersion=$(getJavaVersion "$java") 183 | if [[ "$pathJavaVersion" != "$homeJavaVersion" ]]; then 184 | echoerr "Warning: Java version mismatch between PATH and JAVA_HOME/JDK_HOME, sbt will use the one in PATH" 185 | echoerr " Either: fix your PATH, remove JAVA_HOME/JDK_HOME or use -java-home" 186 | echoerr " java version from PATH: $pathJavaVersion" 187 | echoerr " java version from JAVA_HOME/JDK_HOME: $homeJavaVersion" 188 | fi 189 | fi 190 | } 191 | 192 | java_version () { 193 | local version=$(getJavaVersion "$java_cmd") 194 | vlog "Detected Java version: $version" 195 | echo "${version:2:1}" 196 | } 197 | 198 | # MaxPermSize critical on pre-8 JVMs but incurs noisy warning on 8+ 199 | default_jvm_opts () { 200 | local v="$(java_version)" 201 | if [[ $v -ge 8 ]]; then 202 | echo "$default_jvm_opts_common" 203 | else 204 | echo "-XX:MaxPermSize=384m $default_jvm_opts_common" 205 | fi 206 | } 207 | 208 | build_props_scala () { 209 | if [[ -r "$buildProps" ]]; then 210 | versionLine="$(grep '^build.scala.versions' "$buildProps")" 211 | versionString="${versionLine##build.scala.versions=}" 212 | echo "${versionString%% .*}" 213 | fi 214 | } 215 | 216 | execRunner () { 217 | # print the arguments one to a line, quoting any containing spaces 218 | vlog "# Executing command line:" && { 219 | for arg; do 220 | if [[ -n "$arg" ]]; then 221 | if printf "%s\n" "$arg" | grep -q ' '; then 222 | printf >&2 "\"%s\"\n" "$arg" 223 | else 224 | printf >&2 "%s\n" "$arg" 225 | fi 226 | fi 227 | done 228 | vlog "" 229 | } 230 | 231 | [[ -n "$batch" ]] && exec /dev/null; then 255 | curl --fail --silent --location "$url" --output "$jar" 256 | elif which wget >/dev/null; then 257 | wget -q -O "$jar" "$url" 258 | fi 259 | } && [[ -r "$jar" ]] 260 | } 261 | 262 | acquire_sbt_jar () { 263 | { 264 | sbt_jar="$(jar_file "$sbt_version")" 265 | [[ -r "$sbt_jar" ]] 266 | } || { 267 | sbt_jar="$HOME/.ivy2/local/org.scala-sbt/sbt-launch/$sbt_version/jars/sbt-launch.jar" 268 | [[ -r "$sbt_jar" ]] 269 | } || { 270 | sbt_jar="$(jar_file "$sbt_version")" 271 | download_url "$(make_url "$sbt_version")" "$sbt_jar" 272 | } 273 | } 274 | 275 | usage () { 276 | set_sbt_version 277 | cat < display stack traces with a max of frames (default: -1, traces suppressed) 296 | -debug-inc enable debugging log for the incremental compiler 297 | -no-colors disable ANSI color codes 298 | -sbt-create start sbt even if current directory contains no sbt project 299 | -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) 300 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) 301 | -ivy path to local Ivy repository (default: ~/.ivy2) 302 | -no-share use all local caches; no sharing 303 | -offline put sbt in offline mode 304 | -jvm-debug Turn on JVM debugging, open at the given port. 305 | -batch Disable interactive mode 306 | -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted 307 | -script Run the specified file as a scala script 308 | 309 | # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) 310 | -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version 311 | -sbt-version use the specified version of sbt (default: $sbt_release_version) 312 | -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version 313 | -sbt-jar use the specified jar as the sbt launcher 314 | -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) 315 | -sbt-launch-repo repo url for downloading sbt launcher jar (default: $(url_base "$sbt_version")) 316 | 317 | # scala version (default: as chosen by sbt) 318 | -28 use $latest_28 319 | -29 use $latest_29 320 | -210 use $latest_210 321 | -211 use $latest_211 322 | -212 use $latest_212 323 | -scala-home use the scala build at the specified directory 324 | -scala-version use the specified version of scala 325 | -binary-version use the specified scala version when searching for dependencies 326 | 327 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 328 | -java-home alternate JAVA_HOME 329 | 330 | # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution 331 | # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found 332 | $(default_jvm_opts) 333 | JVM_OPTS environment variable holding either the jvm args directly, or 334 | the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') 335 | Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. 336 | -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) 337 | -Dkey=val pass -Dkey=val directly to the jvm 338 | -J-X pass option -X directly to the jvm (-J is stripped) 339 | 340 | # passing options to sbt, OR to this runner 341 | SBT_OPTS environment variable holding either the sbt args directly, or 342 | the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') 343 | Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. 344 | -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) 345 | -S-X add -X to sbt's scalacOptions (-S is stripped) 346 | EOM 347 | } 348 | 349 | process_args () { 350 | require_arg () { 351 | local type="$1" 352 | local opt="$2" 353 | local arg="$3" 354 | 355 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 356 | die "$opt requires <$type> argument" 357 | fi 358 | } 359 | while [[ $# -gt 0 ]]; do 360 | case "$1" in 361 | -h|-help) usage; exit 1 ;; 362 | -v) verbose=true && shift ;; 363 | -d) addSbt "--debug" && shift ;; 364 | -w) addSbt "--warn" && shift ;; 365 | -q) addSbt "--error" && shift ;; 366 | -x) debugUs=true && shift ;; 367 | -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; 368 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 369 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 370 | -no-share) noshare=true && shift ;; 371 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 372 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; 373 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 374 | -offline) addSbt "set offline in Global := true" && shift ;; 375 | -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; 376 | -batch) batch=true && shift ;; 377 | -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; 378 | -script) require_arg file "$1" "$2" && sbt_script="$2" && addJava "-Dsbt.main.class=sbt.ScriptMain" && shift 2 ;; 379 | 380 | -sbt-create) sbt_create=true && shift ;; 381 | -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; 382 | -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; 383 | -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; 384 | -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; 385 | -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; 386 | -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; 387 | -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; 388 | -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; 389 | -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "_root_.scala.Some(file(\"$2\"))" && shift 2 ;; 390 | -java-home) require_arg path "$1" "$2" && setJavaHome "$2" && shift 2 ;; 391 | -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; 392 | -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; 393 | 394 | -D*) addJava "$1" && shift ;; 395 | -J*) addJava "${1:2}" && shift ;; 396 | -S*) addScalac "${1:2}" && shift ;; 397 | -28) setScalaVersion "$latest_28" && shift ;; 398 | -29) setScalaVersion "$latest_29" && shift ;; 399 | -210) setScalaVersion "$latest_210" && shift ;; 400 | -211) setScalaVersion "$latest_211" && shift ;; 401 | -212) setScalaVersion "$latest_212" && shift ;; 402 | new) sbt_new=true && sbt_explicit_version="$sbt_release_version" && addResidual "$1" && shift ;; 403 | *) addResidual "$1" && shift ;; 404 | esac 405 | done 406 | } 407 | 408 | # process the direct command line arguments 409 | process_args "$@" 410 | 411 | # skip #-styled comments and blank lines 412 | readConfigFile() { 413 | local end=false 414 | until $end; do 415 | read || end=true 416 | [[ $REPLY =~ ^# ]] || [[ -z $REPLY ]] || echo "$REPLY" 417 | done < "$1" 418 | } 419 | 420 | # if there are file/environment sbt_opts, process again so we 421 | # can supply args to this runner 422 | if [[ -r "$sbt_opts_file" ]]; then 423 | vlog "Using sbt options defined in file $sbt_opts_file" 424 | while read opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") 425 | elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then 426 | vlog "Using sbt options defined in variable \$SBT_OPTS" 427 | extra_sbt_opts=( $SBT_OPTS ) 428 | else 429 | vlog "No extra sbt options have been defined" 430 | fi 431 | 432 | [[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" 433 | 434 | # reset "$@" to the residual args 435 | set -- "${residual_args[@]}" 436 | argumentCount=$# 437 | 438 | # set sbt version 439 | set_sbt_version 440 | 441 | checkJava 442 | 443 | # only exists in 0.12+ 444 | setTraceLevel() { 445 | case "$sbt_version" in 446 | "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; 447 | *) setThisBuild traceLevel $trace_level ;; 448 | esac 449 | } 450 | 451 | # set scalacOptions if we were given any -S opts 452 | [[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" 453 | 454 | # Update build.properties on disk to set explicit version - sbt gives us no choice 455 | [[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && update_build_props_sbt "$sbt_explicit_version" 456 | vlog "Detected sbt version $sbt_version" 457 | 458 | if [[ -n "$sbt_script" ]]; then 459 | residual_args=( $sbt_script ${residual_args[@]} ) 460 | else 461 | # no args - alert them there's stuff in here 462 | (( argumentCount > 0 )) || { 463 | vlog "Starting $script_name: invoke with -help for other options" 464 | residual_args=( shell ) 465 | } 466 | fi 467 | 468 | # verify this is an sbt dir, -create was given or user attempts to run a scala script 469 | [[ -r ./build.sbt || -d ./project || -n "$sbt_create" || -n "$sbt_script" || -n "$sbt_new" ]] || { 470 | cat < Some(Viridis) 12 | case "magma" => Some(Magma) 13 | case "inferno" => Some(Inferno) 14 | case "plasma" => Some(Plasma) 15 | case "blor" => Some(BlueToOrange) 16 | case "ylor" => Some(LightYellowToOrange) 17 | case "blrd" => Some(BlueToRed) 18 | case "gror" => Some(GreenToRedOrange) 19 | case "sunsetdark" => Some(LightToDarkSunset) 20 | case "greens" => Some(LightToDarkGreen) 21 | case "ylrd" => Some(HeatmapYellowToRed) 22 | case "blylrd" => Some(HeatmapBlueToYellowToRedSpectrum) 23 | case "rdylwt" => Some(HeatmapDarkRedToYellowWhite) 24 | case "prwt" => Some(HeatmapLightPurpleToDarkPurpleToWhite) 25 | case "landuse" => Some(ClassificationBoldLandUse) 26 | case "terrain" => Some(ClassificationMutedTerrain) 27 | case "rgb" => None 28 | case _ => Some(default) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package io.github.jisantuc.gtlambda.lambda 2 | 3 | import io.github.jisantuc.gtlambda.tile._ 4 | 5 | import com.amazonaws.services.lambda.runtime.{Context, RequestStreamHandler, LambdaLogger} 6 | import geotrellis.raster.render.ColorMap 7 | import io.circe.Json 8 | import io.circe.parser.{parse, decode} 9 | 10 | import java.io.{InputStream, ByteArrayOutputStream, OutputStream} 11 | import java.util.Base64 12 | import java.nio.charset.StandardCharsets.UTF_8 13 | 14 | class TileRequestHandler extends RequestStreamHandler { 15 | def handleRequest(input: InputStream, out: OutputStream, context: Context): Unit = { 16 | val encoder = Base64.getEncoder() 17 | val payload = scala.io.Source.fromInputStream(input).mkString("") 18 | val payloadJson: Json = parse(payload) match { 19 | case Right(js) => js 20 | case Left(e) => throw e 21 | } 22 | implicit val logger = context.getLogger() 23 | logger.log(s"$payload") 24 | val decoded = decode[DefaultRequest](payload) 25 | val encodedBytes = decoded match { 26 | case Right(req) => 27 | logger.log(s"Decoded class was ${req.getClass.toString}") 28 | req.vizType match { 29 | case "rgb" => 30 | val tile = req.toMultibandTile 31 | logger.log(s"Class of returned tile was ${tile.getClass.toString}") 32 | encoder.encode(tile.renderPng.bytes) 33 | case "empty" => 34 | val emptyRequest = EmptyRequest(req.x, req.y, req.z) 35 | val tile = req.toTile 36 | encoder.encode(tile.renderPng.bytes) 37 | case s => 38 | val cm = ColorMap((0 to 3500 by 100).toArray, 39 | ColorOptions.fromString(s).getOrElse(ColorOptions.default)) 40 | val tile = req.toTile 41 | logger.log(s"Class of returned tile was ${tile.getClass.toString}") 42 | encoder.encode(tile.renderPng(cm).bytes) 43 | } 44 | case Left(_) => throw new Exception(payload) 45 | } 46 | out.write(encodedBytes) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/requests/RgbRequest.scala: -------------------------------------------------------------------------------- 1 | package io.github.jisantuc.gtlambda.tile 2 | 3 | import geotrellis.raster.{IntArrayTile, MultibandTile, Tile} 4 | import geotrellis.raster.render.{ColorRamps, ColorMap} 5 | import geotrellis.spark.{LayerId, SpatialKey} 6 | import geotrellis.spark.io.ValueNotFoundError 7 | import geotrellis.spark.io.s3.{S3ValueReader, S3AttributeStore} 8 | import geotrellis.spark.io.avro._ 9 | 10 | import com.amazonaws.services.lambda.runtime.LambdaLogger 11 | import io.circe.generic.JsonCodec 12 | 13 | import java.net.URLDecoder 14 | import spray.json._ 15 | import DefaultJsonProtocol._ 16 | 17 | @JsonCodec 18 | case class DefaultRequest ( 19 | x: Int, 20 | y: Int, 21 | z: Int, 22 | bucket: String, 23 | prefix: String, 24 | layerName: String, 25 | vizType: String 26 | ) extends Request { 27 | implicit val spatialKeyFormat = jsonFormat2(SpatialKey.apply _) 28 | 29 | private def fetchValue[T: AvroRecordCodec](default: => T)(implicit logger: LambdaLogger) = { 30 | val p = URLDecoder.decode(prefix) 31 | val l = URLDecoder.decode(layerName) 32 | val store = S3AttributeStore(bucket, p) 33 | val layerId = new LayerId(l, z) 34 | val reader = new S3ValueReader(store).reader[SpatialKey, T](layerId) 35 | try { 36 | reader.read(SpatialKey(x, y)) 37 | } catch { 38 | case e: ValueNotFoundError => 39 | logger.log(s"Empty tile: ${bucket} ${p} ${l} ${e}") 40 | default 41 | } 42 | } 43 | 44 | def toTile(implicit logger: LambdaLogger): Tile = 45 | fetchValue[Tile] { IntArrayTile.ofDim(256, 256) } 46 | 47 | def toMultibandTile(implicit logger: LambdaLogger): MultibandTile = 48 | fetchValue[MultibandTile] { 49 | val t = toTile 50 | MultibandTile(t, t, t) 51 | } 52 | } 53 | 54 | @JsonCodec 55 | case class EmptyRequest ( 56 | x: Int, 57 | y: Int, 58 | z: Int 59 | ) extends Request { 60 | val fill = scala.util.Random.nextInt(255) 61 | def toTile(implicit logger: LambdaLogger) = IntArrayTile.ofDim(256, 256).map { x => fill } 62 | def toMultibandTile(implicit logger: LambdaLogger) = { 63 | val t = toTile 64 | MultibandTile(t, t, t) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/scala/requests/package.scala: -------------------------------------------------------------------------------- 1 | package io.github.jisantuc.gtlambda 2 | 3 | import geotrellis.raster.{Tile, MultibandTile} 4 | import geotrellis.raster.render.{ColorRamp, ColorMap} 5 | 6 | import com.amazonaws.services.lambda.runtime.LambdaLogger 7 | 8 | package object tile { 9 | abstract class Request { 10 | val x: Int 11 | val y: Int 12 | val z: Int 13 | def toTile(implicit logger: LambdaLogger): Tile 14 | def toMultibandTile(implicit logger: LambdaLogger): MultibandTile 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/passthrough.json: -------------------------------------------------------------------------------- 1 | { 2 | "getTile": { 3 | "x": "$input.params('x')", 4 | "y": "$input.params('y')", 5 | "z": "$input.params('z')", 6 | "bucket": "$input.params('b')", 7 | "prefix": "$input.params('p')", 8 | "layerName": "$input.params('l')", 9 | "vizType": "$input.params('v')" 10 | } 11 | } 12 | --------------------------------------------------------------------------------