Full Code of mapbox/landsat-tiler for AI

master 1d959f64d88e cached
12 files
39.0 KB
11.6k tokens
7 symbols
1 requests
Download .txt
Repository: mapbox/landsat-tiler
Branch: master
Commit: 1d959f64d88e
Files: 12
Total size: 39.0 KB

Directory structure:
gitextract_ul_6xwy0/

├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app/
│   ├── __init__.py
│   └── landsat.py
├── package.json
├── serverless.yml
└── viewer/
    ├── css/
    │   └── style.css
    ├── index.html
    └── js/
        └── app.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store
package.zip
.serverless/
node_modules/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

#Ipython Notebook
.ipynb_checkpoints

.*.swp

.python-version


================================================
FILE: Dockerfile
================================================
# Use the official amazonlinux AMI image
FROM amazonlinux:latest

# Install apt dependencies
RUN yum install -y \
  gcc gcc-c++ freetype-devel yum-utils findutils openssl-devel

RUN yum -y groupinstall development

RUN curl https://www.python.org/ftp/python/3.6.1/Python-3.6.1.tar.xz | tar -xJ \
    && cd Python-3.6.1 \
    && ./configure --prefix=/usr/local --enable-shared \
    && make \
    && make install \
    && cd .. \
    && rm -rf Python-3.6.1

ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

# Install Python dependencies
RUN pip3 install rio-tiler==1.0b2 lambda-proxy==0.0.4 aws-sat-api==1.0.0 --no-binary numpy -t /tmp/vendored -U

# Reduce Lambda package size to fit the 250Mb limit
# Mostly based on https://github.com/jamesandersen/aws-machine-learning-demo
RUN du -sh /tmp/vendored

# This is the list of available modules on AWS lambda Python 3
# ['boto3', 'botocore', 'docutils', 'jmespath', 'pip', 'python-dateutil', 's3transfer', 'setuptools', 'six']
RUN find /tmp/vendored -name "*-info" -type d -exec rm -rdf {} +
RUN rm -rdf /tmp/vendored/boto3/
RUN rm -rdf /tmp/vendored/botocore/
RUN rm -rdf /tmp/vendored/docutils/
RUN rm -rdf /tmp/vendored/dateutil/
RUN rm -rdf /tmp/vendored/jmespath/
RUN rm -rdf /tmp/vendored/s3transfer/
RUN rm -rdf /tmp/vendored/numpy/doc/

# Leave module precompiles for faster Lambda startup
RUN find /tmp/vendored -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-36//'); cp $f $n; done;
RUN find /tmp/vendored -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
RUN find /tmp/vendored -type f -a -name '*.py' -print0 | xargs -0 rm -f

RUN du -sh /tmp/vendored

COPY app /tmp/vendored/app

# Create archive
RUN cd /tmp/vendored && zip -r9q /tmp/package.zip *

# Cleanup
RUN rm -rf /tmp/vendored/


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2017, Mapbox
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: Makefile
================================================

SHELL = /bin/bash

all: build package

build:
	docker build --tag lambda:latest .

#Local Test
test:
	docker run \
		-w /var/task/ \
		--name lambda \
		--env AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
		--env AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
 		--env AWS_REGION=us-west-2 \
		--env PYTHONPATH=/var/task \
		--env GDAL_CACHEMAX=75% \
		--env GDAL_DISABLE_READDIR_ON_OPEN=TRUE \
		--env GDAL_TIFF_OVR_BLOCKSIZE=512 \
		--env VSI_CACHE=TRUE \
		--env VSI_CACHE_SIZE=536870912 \
		-itd \
		lambda:latest
	docker cp package.zip lambda:/tmp/package.zip
	docker exec -it lambda bash -c 'unzip -q /tmp/package.zip -d /var/task/'
	docker exec -it lambda bash -c 'pip3 install boto3 jmespath python-dateutil -t /var/task'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/bounds/LC80230312016320LGN00", "queryStringParameters": "null", "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/metadata/LC80230312016320LGN00", "queryStringParameters": {"pmin":"2", "pmax":"99.8"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/processing/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"ratio":"(b5-b4)/(b5+b4)"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/tiles/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"rgb":"11", "histo":"0,1000"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/tiles/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"rgb":"5,3,2", "histo":"722,5088;859,4861;1164,5204"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker exec -it lambda python3 -c 'from app.landsat import APP; print(APP({"path": "/landsat/tiles/LC80230312016320LGN00/8/65/94.png", "queryStringParameters": {"rgb":"4,3,2", "histo":"722,5088;859,4861;1164,5204", "pan":"true"}, "pathParameters": "null", "requestContext": "null", "httpMethod": "GET"}, None))'
	docker stop lambda
	docker rm lambda


package:
	docker run \
		-w /var/task/ \
		--name lambda \
		-itd \
		lambda:latest
	docker cp lambda:/tmp/package.zip package.zip
	docker stop lambda
	docker rm lambda

shell:
	docker run \
		--name lambda  \
		--volume $(shell pwd)/:/data \
		--env PYTHONPATH=/var/task/vendored \
		--env GDAL_CACHEMAX=75% \
		--env GDAL_DISABLE_READDIR_ON_OPEN=TRUE \
		--env GDAL_TIFF_OVR_BLOCKSIZE=512 \
		--env VSI_CACHE=TRUE \
		--env VSI_CACHE_SIZE=536870912 \
		--rm \
		-it \
		lambda:latest /bin/bash

deploy:
	sls deploy

clean:
	docker stop lambda
	docker rm lambda


================================================
FILE: README.md
================================================
# landsat-tiler

#### AWS Lambda + Landsat AWS PDS = landsat-tiler

### Description

Create a highly customizable `serverless` tile server for Amazon's Landsat Public Dataset.
This project is based on [rio-tiler](https://github.com/mapbox/rio-tiler) python library.

![landsat-tiler-small](https://cloud.githubusercontent.com/assets/10407788/22255896/ec49f448-e226-11e6-8798-82794174eafe.gif)


#### Landsat data on AWS

Since 2015 Landsat 8 data is hosted on AWS and can be freely accessed. This dataset is growing over 700 scenes a day and have archive up to 2013.

> AWS has made Landsat 8 data freely available on Amazon S3 so that anyone can use our on-demand computing resources to perform analysis and create new products without needing to worry about the cost of storing Landsat data or the time required to download it.

more info: https://aws.amazon.com/public-datasets/landsat/

Something important about AWS Landsat-pds is that each Landsat scene has its individual bands stored as [cloud optimized GeoTIFF](https://trac.osgeo.org/gdal/wiki/CloudOptimizedGeoTIFF). While this is a critical point to work with the data, it also means that to create an RGB image and visualize it, you have to go through a lot of manual steps.

#### Lambda function

AWS Lambda is a service that lets you run functions in Node, Python, or Java in response to different triggers like API calls, file creation, database edits, etc.
In addition to only have to provide code, an other crucial point of AWS Lambda it that you only pay for the execution of the function, you don't have to pay for a 24/24h running server. It's called **serverless** cause you only need to care about the code you provide.

---

# Installation

##### Requirement
  - AWS Account
  - Docker
  - node + npm


#### Create the package

Creating a python lambda package with some C (or Cython) libraries like Rasterio/GDAL has never been an easy task because you have to compile and build it on the same infrastructure where it's going to be used (Amazon linux AMI). Until recently, to create your package you had to launch an EC2 instance using the official Amazon Linux AMI and create your package on it (see [perrygeo blog](http://www.perrygeo.com/running-python-with-compiled-code-on-aws-lambda.html) or [Remotepixel blog](https://remotepixel.ca/blog/landsat8-ndvi-20160212.html)).

But this was before, Late 2016, the AWS team released the Amazon Linux image on docker, so it's now possible to use it `locally` to compile C libraries and create complex lambda package ([see Dockerfile](https://github.com/mapbox/landsat-tiler/blob/master/Dockerfile)).

Note: to stay under AWS lambda package sizes limits (100Mb zipped file / 250Mb unzipped archive) we need to use some [`tricks`](https://github.com/mapbox/landsat-tiler/blob/e4eebb512f51c55d95607daa483a14d2091fa0a1/Dockerfile#L30).
- use Rasterio wheels which is a complete rasterio distribution that support GeoTIFF, OpenJPEG formats.
- remove every packages that are already available natively in AWS Lambda (boto3, botocore ...)
- keep only precompiled python code (`.pyc`) so it lighter and it loads faster

```bash
# Build Amazon linux AMI docker container + Install Python modules + create package
git clone https://github.com/mapbox/landsat-tiler.git
cd landsat-tiler/
make all
```

#### Deploy to AWS
One of the easiest way to **Build** and **Deploy** a Lambda function is to use [Serverless](https://serverless.com) toolkit. We took care of the `building` part with docker so we will just ask **Serverless** to *only* upload our package file to AWS S3, to setup AWS Lambda and AWS API Gateway.

```bash
#configure serverless (https://serverless.com/framework/docs/providers/aws/guide/credentials/)
npm install
sls deploy
```

<img width="500" alt="sls deploy" src="https://cloud.githubusercontent.com/assets/10407788/22188728/d9ffec44-e0e5-11e6-9a77-569a791ccaf2.png">

:tada: You should be all set there.

---
# Use it: Landsat-viewer

#### lambda-tiler + Mapbox GL + Satellite API

The `viewer/` directory contains a UI example to use with your new Lambda Landsat tiler endpoint. It combine the power of mapbox-gl and the nice developmentseed [sat-api](https://github.com/sat-utils/sat-api) to create a simple and fast **Landsat-viewer**.

To be able to run it, edit those [two lines](https://github.com/mapbox/landsat-tiler/blob/master/viewer/js/app.js#L3-L4) in `viewer/js/app.js`
```js
// viewer/js/app.js
3  mapboxgl.accessToken = '{YOUR-MAPBOX-TOKEN}';
4  const landsat_tiler_url = "{YOUR-API-GATEWAY-URL}";
```

## Workflow

1. One AWS λ call to get min/max percent cut value for all the bands and bounds

  *Path:* **/landsat/metdata/{landsat scene id}**

  *Inputs:*

  - sceneid: Landsat product id (or scene id for scene < 1st May 2017)

  *Options:*

  - pmin: Histogram cut minimum value in percent (default: 2)  
  - pmax: Histogram cut maximum value in percent (default: 98)  

  *Output:* (dict)

  - bounds: (minX, minY, maxX, maxY) (list)
  - sceneid: scene id (string)
  - rgbMinMax: Min/Max DN values for the linear rescaling (dict)

  *Example:* `<api-gateway-url>/landsat/metadata/LC08_L1TP_016037_20170813_20170814_01_RT?pmin=5&pmax=95`

2. Parallel AWS λ calls (one per mercator tile) to retrieve corresponding Landsat data

  *Path:* **/landsat/tiles/{landsat scene id}/{z}/{x}/{y}.{ext}**

  *Inputs:*

  - sceneid: Landsat product id (or scene id for scene < 1st May 2017)
  - x: Mercator tile X index
  - y: Mercator tile Y index
  - z: Mercator tile ZOOM level
  - ext: Image format to return ("jpg" or "png")

  *Options:*

  - rgb: Bands index for the RGB combination (default: (4, 3, 2))
  - histo: DN min and max values (default: (0, 16000))
  - tile: Output image size (default: 256)
  - pan: If True, apply pan-sharpening(default: False)

  *Output:*

  - base64 encoded image PNG or JPEG (string)

  *Example:*
  - `<api-gateway-url>/landsat/tile/LC08_L1TP_016037_20170813_20170814_01_RT/8/71/102.png`
  - `<api-gateway-url>/landsat/tile/LC08_L1TP_016037_20170813_20170814_01_RT/8/71/102.png?rgb=5,4,3&histo=100,3000-130,270-500,4500&tile=1024&pan=true`


---
#### Live Demo: https://viewer.remotepixel.ca

#### Infos & links
- [rio-tiler](https://github.com/mapbox/rio-tiler) rasterio plugin that process Landsat data hosted on AWS S3.
- [Introducing the AWS Lambda Tiler](https://hi.stamen.com/stamen-aws-lambda-tiler-blog-post-76fc1138a145)
- Humanitarian OpenStreetMap Team [oam-dynamic-tiler](https://github.com/hotosm/oam-dynamic-tiler)
- [Linux Amazon AMI container](http://docs.aws.amazon.com/AmazonECR/latest/userguide/amazon_linux_container_image.html)


================================================
FILE: app/__init__.py
================================================
# app

__version__ = '2.0.0'


================================================
FILE: app/landsat.py
================================================
"""app.landsat: handle request for Landsat-tiler"""

import re
import json

import numpy as np

from rio_tiler import landsat8
from rio_tiler.utils import array_to_img, linear_rescale, get_colormap, expression, b64_encode_img

from aws_sat_api.search import landsat as landsat_search

from lambda_proxy.proxy import API

APP = API(app_name="landsat-tiler")


class LandsatTilerError(Exception):
    """Base exception class"""


@APP.route('/landsat/search', methods=['GET'], cors=True)
def search():
    """Handle search requests
    """
    query_args = APP.current_request.query_params
    query_args = query_args if isinstance(query_args, dict) else {}

    path = query_args['path']
    row = query_args['row']
    full = query_args.get('full', True)

    data = list(landsat_search(path, row, full))
    info = {
        'request': {'path': path, 'row': row, 'full': full},
        'meta': {'found': len(data)},
        'results': data}

    return ('OK', 'application/json', json.dumps(info))


@APP.route('/landsat/bounds/<scene>', methods=['GET'], cors=True)
def bounds(scene):
    """Handle bounds requests
    """
    info = landsat8.bounds(scene)
    return ('OK', 'application/json', json.dumps(info))


@APP.route('/landsat/metadata/<scene>', methods=['GET'], cors=True)
def metadata(scene):
    """Handle metadata requests
    """
    query_args = APP.current_request.query_params
    query_args = query_args if isinstance(query_args, dict) else {}

    pmin = query_args.get('pmin', 2)
    pmin = float(pmin) if isinstance(pmin, str) else pmin

    pmax = query_args.get('pmax', 98)
    pmax = float(pmax) if isinstance(pmax, str) else pmax

    info = landsat8.metadata(scene, pmin, pmax)
    return ('OK', 'application/json', json.dumps(info))


@APP.route('/landsat/tiles/<scene>/<int:z>/<int:x>/<int:y>.<ext>', methods=['GET'], cors=True)
def tile(scene, tile_z, tile_x, tile_y, tileformat):
    """Handle tile requests
    """
    if tileformat == 'jpg':
        tileformat = 'jpeg'

    query_args = APP.current_request.query_params
    query_args = query_args if isinstance(query_args, dict) else {}

    bands = query_args.get('rgb', '4,3,2')
    bands = tuple(re.findall(r'\d+', bands))

    histoCut = query_args.get('histo', ';'.join(['0,16000'] * len(bands)))
    histoCut = re.findall(r'\d+,\d+', histoCut)
    histoCut = list(map(lambda x: list(map(int, x.split(','))), histoCut))

    if len(bands) != len(histoCut):
        raise LandsatTilerError('The number of bands doesn\'t match the number of histogramm values')

    tilesize = query_args.get('tile', 256)
    tilesize = int(tilesize) if isinstance(tilesize, str) else tilesize

    pan = True if query_args.get('pan') else False
    tile, mask = landsat8.tile(scene, tile_x, tile_y, tile_z, bands, pan=pan, tilesize=tilesize)

    rtile = np.zeros((len(bands), tilesize, tilesize), dtype=np.uint8)
    for bdx in range(len(bands)):
        rtile[bdx] = np.where(mask, linear_rescale(tile[bdx], in_range=histoCut[bdx], out_range=[0, 255]), 0)
    img = array_to_img(rtile, mask=mask)
    str_img = b64_encode_img(img, tileformat)
    return ('OK', f'image/{tileformat}', str_img)


@APP.route('/landsat/processing/<scene>/<int:z>/<int:x>/<int:y>.<ext>', methods=['GET'], cors=True)
def ratio(scene, tile_z, tile_x, tile_y, tileformat):
    """Handle processing requests
    """
    if tileformat == 'jpg':
        tileformat = 'jpeg'

    query_args = APP.current_request.query_params
    query_args = query_args if isinstance(query_args, dict) else {}

    ratio_value = query_args['ratio']
    APP.log.debug(f'{ratio_value}')

    range_value = query_args.get('range', [-1, 1])

    tilesize = query_args.get('tile', 256)
    tilesize = int(tilesize) if isinstance(tilesize, str) else tilesize

    tile, mask = expression(scene, tile_x, tile_y, tile_z, ratio_value, tilesize=tilesize)
    if len(tile.shape) == 2:
        tile = np.expand_dims(tile, axis=0)

    rtile = np.where(mask, linear_rescale(tile, in_range=range_value, out_range=[0, 255]), 0).astype(np.uint8)
    img = array_to_img(rtile, color_map=get_colormap(name='cfastie'), mask=mask)
    str_img = b64_encode_img(img, tileformat)
    return ('OK', f'image/{tileformat}', str_img)


@APP.route('/favicon.ico', methods=['GET'], cors=True)
def favicon():
    """favicon
    """
    return('NOK', 'text/plain', '')


================================================
FILE: package.json
================================================
{
    "description": "Create a highly customizable `serverless` tile server for Amazon's Landsat Public Dataset",
    "devDependencies": {
        "serverless-apigw-binary": "^0.4.1",
        "serverless": "^1.23.0"
    },
    "repository": {
        "type": "git",
        "url": "git://github.com/mapbox/landsat-tiler.git"
    },
    "author": "Vincent Sarago"
}


================================================
FILE: serverless.yml
================================================
service: landsat-tiler

provider:
  name: aws
  runtime: python3.6
  stage: production

  region: us-west-2

  iamRoleStatements:
  -  Effect: "Allow"
     Action:
       - "s3:GetObject"
     Resource:
       - "arn:aws:s3:::landsat-pds/*"

  environment:
    GDAL_CACHEMAX: 75%
    GDAL_TIFF_OVR_BLOCKSIZE: 512
    VSI_CACHE: TRUE
    VSI_CACHE_SIZE: 536870912
    GDAL_DISABLE_READDIR_ON_OPEN: true
    CPL_VSIL_CURL_ALLOWED_EXTENSIONS: ".TIF,.ovr"

  #Optional Bucket where you store your lambda package
  # deploymentBucket: {YOUR-BUCKET}

custom:
  apigwBinary:
    types:
      - '*/*'

plugins:
  - serverless-apigw-binary

package:
  artifact: package.zip

functions:
  landsat-tiler:
    handler: app.landsat.APP
    memorySize: 1536
    timeout: 20
    events:
      - http:
          path: landsat/{proxy+}
          method: get
          cors: true


================================================
FILE: viewer/css/style.css
================================================
body {
  position: fixed;
	width: 100%;
	height: 100%;
	color: #000;
	background-color: #FFF;
  letter-spacing: 0;
}

.content {
    height:100%;
    width: 100%;
    font-size: 0;
}

.row, .col {
    padding: 0;
    margin: 0;
}

.center {
    padding: 20px;
    margin: auto;
}

.main-container {
    height:100%;
    width: 100%;
    font-size: 0;
    padding: 0;
    margin: 0;
    display: inline-block;
}

.map {
    display: inline-block;
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
    -webkit-touch-callout: none;
      -webkit-user-select: none;
         -moz-user-select: none;
          -ms-user-select: none;
              user-select: none;
}

.right-panel {
    position: relative;
    display: none;
    vertical-align: top;
    width: 350px;
    z-index: 1;
    color: #FFF;
    background-color: rgba(111, 111, 112, 0.53);
    height: 100%;
    margin: 0;
    padding: 0;
}

.right-panel.in {
  float: right;
  display: inherit;
}

.right-panel.in ~ .map {
    float: left;
    width: calc(100% - 350px);
    margin: 0;
    padding: 0;
    vertical-align: top;
}

.right-panel .close-button {
  display: none;
  position: absolute;
  top: 0;
  left: -36px;
  width: 36px;
  height: 36px;
  line-height: 36px;
  text-align: center;
  vertical-align: middle;
  z-index: 1000;
  color: #000;
  background-color: rgba(255, 255, 255, 0.5);
}

.right-panel .close-button:hover {
  color: #FFF;
  background-color: rgba(0, 0, 0, 0.5);
  cursor: pointer;
}

.right-panel.in .close-button {
  display: block;
}

.right-panel-content {
  position: relative;
  display: flex;
  flex-flow: column;
  height: 100%;
}

.right-panel-content .list-img {
    flex: 0 1 auto;
    -webkit-flex: 0 1 auto;
    overflow-y: auto;
    height: 400px;
    z-index: 2;
    overflow-y: scroll;
}

.right-panel-content .img-display-options {
    flex: 0 1 auto;
    padding: 15px 10px;
    overflow-y: scroll;
}

.switch-container {
  display: block;
  font-size: 18px;
  line-height: 18px;
  padding: 4px;
}

.switch-container * {
  display: inline-block;
}

.img-display-options .toggle-group {
  display: block;
}

.img-display-options .toggle-group * {
  display: inline-block;
}

.img-display-options .toggle {
  font-size: 10px;
  color: #000;
}

.img-display-options span {
  font-size: 15px;
  color: #000;
  display: block;
}

.img-display-options .inputHisto * {
  display: inline-block;
  font-size: 12px;
  color: #000;
}

.list-img .list-element {
  position: relative;
  color: #FFF;
  background-color: #404040;
  cursor: pointer;
}

.list-img .list-element:hover {
  -o-box-shadow: inset 0 0 10px #000;
  -webkit-box-shadow: inset 0 0 10px #000;
  -moz-box-shadow: inset 0 0 10px #000;
  -ms-box-shadow: inset 0 0 10px #000;
}

.list-img .list-element .block-info {
    font-weight: 100;
    display: inline-block;
    padding: 5px;
    vertical-align: middle;
}

.list-img .list-element .block-info .scene-info {
    display: inline-block;
    padding: 3px;
    font-size: 12px;
}


.list-img .list-element .block-info img {
  width: 40px
}

.list-img .list-element .block-info img:before {
	content: '';
	display: block;
	padding-top: 40px;
}

.lazyload {
    opacity: 0;
}

.lazyloading {
    opacity: 1;
    transition: opacity 300ms;
    background: #000 url(/img/spinner3.gif) no-repeat center;
}

.lazyloaded {
    background: none;
    opacity: 1;
    transition: opacity 300ms;
}

/*    */

.landsat-info {
  position: absolute;
  left: 0;
  bottom: 0;
  padding: 3px;
  font-size: 14px;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.77);
  z-index: 10;
}
.landsat-info span {
  display: block;
}

.l8id:before {
    content: 'Scene-id: '
}

.l8date:before {
    content: 'Date: '
}

.l8rgb:before {
    content: 'RGB: '
}

.loading-map {
    position: absolute;
    width: 100%;
    height: 100%;
    color: #FFF;
    background-color: #000;
    font-size: 18px;
    text-align: center;
    z-index: 100;
    opacity: 1;
}

.loading-map.off{
    opacity: 0;
    -o-transition: all 1.5s ease;
    -webkit-transition: all 1.5s ease;
    -moz-transition: all 1.5s ease;
    -ms-transition: all 1.5s ease;
    transition: all ease 1.5s;
    visibility:hidden;
}

.loading-map .middle-center{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.loading-map .middle-center * {
    display: block;
    padding: 5px;
}
.loading-map .middle-center i{
    font-size: 24px;
}

.spin {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    font-size: 14px;
    text-align: center;
}

.metaloader,
.errorMessage {
  position: absolute;
  bottom: 20px;
  left: 20px;
  text-align: center;
  z-index: 100;
  color: #fff;
  z-index: 10;
}

.errorMessage {
  color: #ff0000;
}

@media(max-width: 767px){

    .mapboxgl-ctrl-attrib {
        font-size: 10px;
    }

    .map {
        display: block;
        width: 100%;
        float: none;
    }

    .right-panel.in ~ .map {
        height: calc(100% - 200px);
        width: 100%;
    }

    .right-panel.in {
        display: block;
        position: absolute;
        width: 100%;
        height: 200px;
        margin: 0;
        padding: 0;
        overflow-y: auto;
        float: none;
        bottom: 0;
        left: 0;
    }
}


================================================
FILE: viewer/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">

	<title>Landsat viewer</title>
	<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
	<script src='https://api.mapbox.com/mapbox-gl-js/v0.42.2/mapbox-gl.js'></script>
	<link href='https://api.mapbox.com/mapbox-gl-js/v0.42.2/mapbox-gl.css' rel='stylesheet' />

	<link href="https://api.mapbox.com/mapbox-assembly/v0.19.0/assembly.min.css" rel="stylesheet">
	<script async defer src="https://api.mapbox.com/mapbox-assembly/v0.19.0/assembly.js"></script>

  <link href="css/style.css" rel="stylesheet">
</head>

<body>
	<div class="content">
		<div class="main-container">

			<div class="right-panel">

				<div class="right-panel-content">

					<div class="list-img"></div>

					<div class="img-display-options">
						<span>RGB Combination </span>

						<div class='toggle-group mr18'>
							<label class='toggle-container'>
								<input data="6,5,4" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>vegetation analysis</div>
							</label>
							<label class='toggle-container'>
								<input data="7,6,4" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>urban</div>
							</label>
							<label class='toggle-container'>
								<input data="4,3,2" checked name='toggle' type='radio'/>
								<div class='btn btn--stroke btn--darken50 toggle'>natural color</div>
							</label>
							<label class='toggle-container'>
								<input data="5,4,3" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>false color</div>
							</label>
							<label class='toggle-container'>
								<input data="5,6,4" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>land/water</div>
							</label>
							<label class='toggle-container'>
								<input data="7,6,5" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>atmospheric penetration</div>
							</label>
							<label class='toggle-container'>
								<input data="6,5,2" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>agriculture</div>
							</label>
							<label class='toggle-container'>
								<input data="5,6,2" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>healthy veg</div>
							</label>
							<label class='toggle-container'>
								<input data="7,5,2" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>forest burn</div>
							</label>
							<label class='toggle-container'>
								<input data="7,5,4" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>shortwave ir</div>
							</label>
							<label class='toggle-container'>
								<input data="5,7,1" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>false2</div>
							</label>
							<label class='toggle-container'>
								<input data="7,5,3" name='toggle' type='radio' />
								<div class='btn btn--stroke btn--darken50 toggle'>natural w/ atmo</div>
							</label>
						</div>

						<span>Histogramm cut </span>

						<div class="inputHisto">
							<input id="minCount" class='input col--2' value='5' />
							<span class="col--2 center"> - </span>
							<input id="maxCount" class='input col--2' value='95' />
							<span class="col--2 pl6"> %</span>

							<button class='btn' onclick="updateMetadata()">Apply</button>
						</div>
						<button id='btn-clear' class='btn'><span>Clear</span></button>

					</div>

					<span class="spin none">
						<div class="round animation-spin animation--infinite animation--speed-1">
							<svg class='icon icon--l inline-block'><use xlink:href='#icon-satellite'/></svg>
						</div>
					</span>

				</div>

			</div>

			<div id="map" class="map">

				<div class="landsat-info none">
					<span class="l8id"></span>
					<span class="l8date"></span>
					<span class="l8rgb"></span>
				</div>

				<div class="loading-map">
					<div class="middle-center">
						<span>Loading</span>
						<div class="round animation-spin animation--infinite animation--speed-1">
							<svg class='icon icon--l inline-block'><use xlink:href='#icon-satellite'/></svg>
						</div>
					</div>
				</div>

				<span class="metaloader none">
					<div class="round animation-spin animation--infinite animation--speed-1">
						<svg class='icon icon--l inline-block'><use xlink:href='#icon-satellite'/></svg>
					</div>
				</span>

				<span class="errorMessage none">
						<svg class='icon icon--l'><use xlink:href='#icon-alert'/></svg>
						<span >Error</span>
				</span>

			</div>

		</div>
	</div>

  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
	<script src='https://npmcdn.com/@turf/turf@3.5.1/turf.min.js'></script>
	<script src="js/app.js" charset="utf-8"></script>

</body>
</html>


================================================
FILE: viewer/js/app.js
================================================
'use strict';

mapboxgl.accessToken = '{YOUR-MAPBOX-TOKEN}';
const landsat_tiler_url = '{YOUR-API-GATEWAY-ENDPOINT}'; //e.g https://xxxxxxxxxx.execute-api.xxxxxxx.amazonaws.com/production
const sat_api = 'https://api.developmentseed.org/satellites/?search=';

let scope = {};

const sortScenes = (a, b) => {
    return Date.parse(b.date) - Date.parse(a.date);
};


const parseSceneid_c1 = (sceneid) => {

    const sceneid_info = sceneid.split('_');

    return {
        satellite: sceneid_info[0].slice(0,1) + sceneid_info[0].slice(3),
        sensor:  sceneid_info[0].slice(1,2),
        correction_level: sceneid_info[1],
        path: sceneid_info[2].slice(0,3),
        row: sceneid_info[2].slice(3),
        acquisition_date: sceneid_info[3],
        ingestion_date: sceneid_info[4],
        collection: sceneid_info[5],
        category: sceneid_info[6]
    };
};


const parseSceneid_pre = (sceneid) => {

    return {
        sensor:  sceneid.slice(1,2),
        satellite: sceneid.slice(2,3),
        path: sceneid.slice(3,6),
        row: sceneid.slice(6,9),
        acquisitionYear: sceneid.slice(9,13),
        acquisitionJulianDay: sceneid.slice(13,16),
        groundStationIdentifier: sceneid.slice(16,19),
        archiveVersion: sceneid.slice(19,21)
    };
};


const buildQueryAndRequestL8 = (features) => {
    $('.list-img').scrollTop(0);
    $('.list-img').empty();
    $('.errorMessage').addClass('none');
    $('.landsat-info').addClass('none');

    if (map.getSource('landsat-tiles')) map.removeSource('landsat-tiles');
    if (map.getLayer('landsat-tiles')) map.removeLayer('landsat-tiles');

    const prStr = [].concat.apply([], features.map(function(e){
        return '(path:' + e.properties.PATH.toString() + '+AND+row:' + e.properties.ROW.toString() + ')';
    })).join('+OR+');

    const query = `${sat_api}satellite_name:landsat-8+AND+(${prStr})&limit=2000`;
    const results = [];

    $.getJSON(query, (data) => {
        if (data.meta.found !== 0) {

            for (let i = 0; i < data.results.length; i += 1) {
                let scene = {};
                scene.path = data.results[i].path.toString();
                scene.row = data.results[i].row.toString();
                scene.grid = data.results[i].path + '/' + data.results[i].row;
                scene.date = data.results[i].date;
                scene.cloud = data.results[i].cloud_coverage;
                scene.browseURL = data.results[i].browseURL.replace('http://', 'https://');
                scene.thumbURL = scene.browseURL.replace('browse/', 'browse/thumbnails/')
                scene.sceneID = data.results[i].scene_id;
                scene.awsID = (Date.parse(scene.date) < Date.parse('2017-05-01')) ?  data.results[i].scene_id.replace(/LGN0[0-9]/, 'LGN00') : data.results[i].LANDSAT_PRODUCT_ID;
                results.push(scene);
            }

            results.sort(sortScenes);

          for (let i = 0; i < results.length; i += 1) {

              $('.list-img').append(
                  '<div class="list-element" onclick="initScene(\'' + results[i].awsID + '\',\'' + results[i].date + '\')">' +
                        '<div class="block-info">' +
                            '<img "class="img-item lazy lazyload" src="' + results[i].thumbURL + '">' +
                        '</div>' +
                        '<div class="block-info">' +
                            '<span class="scene-info">' + results[i].sceneID + '</span>' +
                            '<span class="scene-info"><svg class="icon inline-block"><use xlink:href="#icon-clock"/></svg> ' + results[i].date + '</span>' +
                        '</div>' +
                    '</div>');
          }

      } else {
          $('.errorMessage').removeClass('none');
      }
    })
    .always(() => {
        $('.spin').addClass('none');
    })
    .fail(() => {
        $('.errorMessage').removeClass('none');
    });
}

const initScene = (sceneID, sceneDate) => {
    $('.metaloader').removeClass('none');
    $('.errorMessage').addClass('none');

    let min = $("#minCount").val();
    let max = $("#maxCount").val();
    const query = `${landsat_tiler_url}/landsat/metadata/${sceneID}?'pmim=${min}&pmax=${max}`;

    $.getJSON(query, (data) => {
        scope.imgMetadata = data;
        updateRasterTile();
        $('.landsat-info').removeClass('none');
        $('.landsat-info .l8id').text(sceneID);
        $('.landsat-info .l8date').text(sceneDate);
    })
        .fail(() => {
            if (map.getSource('landsat-tiles')) map.removeSource('landsat-tiles');
            if (map.getLayer('landsat-tiles')) map.removeLayer('landsat-tiles');
            $('.landsat-info span').text('');
            $('.landsat-info').addClass('none');
            $('.errorMessage').removeClass('none');
        })
        .always(() => {
            $('.metaloader').addClass('none');
        });
};


const updateRasterTile = () => {
    if (map.getSource('landsat-tiles')) map.removeSource('landsat-tiles');
    if (map.getLayer('landsat-tiles')) map.removeLayer('landsat-tiles');

    let meta = scope.imgMetadata;

    let rgb = $(".img-display-options .toggle-group input:checked").attr("data");
    const bands = rgb.split(',');

    // NOTE: Calling 512x512px tiles is a bit longer but gives a
    // better quality image and reduce the number of tiles requested

    // HACK: Trade-off between quality and speed. Setting source.tileSize to 512 and telling landsat-tiler
    // to get 256x256px reduces the number of lambda calls (but they are faster)
    // and reduce the quality because MapboxGl will oversample the tile.

    const tileURL = `${landsat_tiler_url}/landsat/tiles/${meta.sceneid}/{z}/{x}/{y}.png?` +
        `rgb=${rgb}` +
        '&tile=256' +
        `&histo=${meta.rgbMinMax[bands[0]]}-${meta.rgbMinMax[bands[1]]}-${meta.rgbMinMax[bands[2]]}`;

    const attrib = '<a href="https://landsat.usgs.gov/landsat-8"> &copy; USGS/NASA Landsat</a>';

    $('.landsat-info .l8rgb').text(rgb);

    map.addSource('landsat-tiles', {
        type: 'raster',
        tiles: [tileURL],
        attribution : attrib,
        bounds: scope.imgMetadata.bounds,
        minzoom: 7,
        maxzoom: 14,
        tileSize: 256
    });

    map.addLayer({
        'id': 'landsat-tiles',
        'type': 'raster',
        'source': 'landsat-tiles'
    });
};


const updateMetadata = () => {
    if (!map.getSource('landsat-tiles')) return;
    initScene(scope.imgMetadata.sceneid, scope.imgMetadata.date);
}


$(".img-display-options .toggle-group").change(() => {
    if (map.getSource('landsat-tiles')) updateRasterTile();
});

document.getElementById("btn-clear").onclick = () => {
  if (map.getLayer('landsat-tiles')) map.removeLayer('landsat-tiles');
  if (map.getSource('landsat-tiles')) map.removeSource('landsat-tiles');
  map.setFilter("L8_Highlighted", ["in", "PATH", ""]);
  map.setFilter("L8_Selected", ["in", "PATH", ""]);

  $('.list-img').scrollLeft(0);
  $('.list-img').empty();

  $(".metaloader").addClass('off');
  $('.errorMessage').addClass('none');
  $(".landsat-info span").text('');
  $(".landsat-info").addClass('none');

  scope = {};

  $("#minCount").val(5);
  $("#maxCount").val(95);

  $(".img-display-options .toggle-group input").prop('checked', false);
  $(".img-display-options .toggle-group input[data='4,3,2']").prop('checked', true);

  $('.map').removeClass('in');
  $('.right-panel').removeClass('in');
  map.resize();
};

////////////////////////////////////////////////////////////////////////////////

var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/vincentsarago/ciy1m6t8y005a2rr09jhfplg3',
    center: [-70.50, 40],
    zoom: 3,
    attributionControl: true,
    minZoom: 3,
    maxZoom: 14
});

map.addControl(new mapboxgl.NavigationControl(), 'top-right');

map.on('mousemove', (e) => {
    const features = map.queryRenderedFeatures(e.point, {layers: ['landsat8-pathrow']});

    let pr = ['in', 'PATH', ''];

    if (features.length !== 0) {
        pr =  [].concat.apply([], ['any', features.map(e => {
            return ['all', ['==', 'PATH', e.properties.PATH], ['==', 'ROW', e.properties.ROW]];
        })]);
    }
    map.setFilter('L8_Highlighted', pr);
});

map.on('click', (e) => {
    $('.right-panel').addClass('in');
    $('.spin').removeClass('none');
    const features = map.queryRenderedFeatures(e.point, {layers: ['landsat8-pathrow']});

    if (features.length !== 0) {
        $('.map').addClass('in');
        $('.list-img').removeClass('none');
        map.resize();

        const pr =  [].concat.apply([], ['any', features.map(e => {
            return ['all', ['==', 'PATH', e.properties.PATH], ['==', 'ROW', e.properties.ROW]];
        })]);

        map.setFilter('L8_Selected', pr);

        buildQueryAndRequestL8(features);

        const geojson = {
          'type': 'FeatureCollection',
          'features': features
        };

        const extent = turf.bbox(geojson);
        const llb = mapboxgl.LngLatBounds.convert([[extent[0], extent[1]], [extent[2], extent[3]]]);
        map.fitBounds(llb, {padding: 50});

    } else {
        $('.spin').addClass('none');
        map.setFilter('L8_Selected', ['in', 'PATH', '']);
    }
});

map.on('load', () => {
    map.addSource('landsat', {
        'type': 'vector',
        'url': 'mapbox://vincentsarago.8ib6ynrs'
    });

    map.addLayer({
        'id': 'L8_Highlighted',
        'type': 'fill',
        'source': 'landsat',
        'source-layer': 'Landsat8_Desc_filtr2',
        'paint': {
            'fill-outline-color': '#1386af',
            'fill-color': '#0f6d8e',
            'fill-opacity': 0.3
        },
        'filter': ['in', 'PATH', '']
    });

    map.addLayer({
        'id': 'L8_Selected',
        'type': 'line',
        'source': 'landsat',
        'source-layer': 'Landsat8_Desc_filtr2',
        'paint': {
            'line-color': '#000',
            'line-width': 1
        },
        'filter': ['in', 'PATH', '']
    });

    $('.loading-map').addClass('off');
});
Download .txt
gitextract_ul_6xwy0/

├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app/
│   ├── __init__.py
│   └── landsat.py
├── package.json
├── serverless.yml
└── viewer/
    ├── css/
    │   └── style.css
    ├── index.html
    └── js/
        └── app.js
Download .txt
SYMBOL INDEX (7 symbols across 1 files)

FILE: app/landsat.py
  class LandsatTilerError (line 18) | class LandsatTilerError(Exception):
  function search (line 23) | def search():
  function bounds (line 43) | def bounds(scene):
  function metadata (line 51) | def metadata(scene):
  function tile (line 68) | def tile(scene, tile_z, tile_x, tile_y, tileformat):
  function ratio (line 102) | def ratio(scene, tile_z, tile_x, tile_y, tileformat):
  function favicon (line 130) | def favicon():
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (43K chars).
[
  {
    "path": ".gitignore",
    "chars": 839,
    "preview": ".DS_Store\npackage.zip\n.serverless/\nnode_modules/\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.cl"
  },
  {
    "path": "Dockerfile",
    "chars": 1807,
    "preview": "# Use the official amazonlinux AMI image\nFROM amazonlinux:latest\n\n# Install apt dependencies\nRUN yum install -y \\\n  gcc "
  },
  {
    "path": "LICENSE",
    "chars": 1506,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2017, Mapbox\nAll rights reserved.\n\nRedistribution and use in source and binary forms"
  },
  {
    "path": "Makefile",
    "chars": 2998,
    "preview": "\nSHELL = /bin/bash\n\nall: build package\n\nbuild:\n\tdocker build --tag lambda:latest .\n\n#Local Test\ntest:\n\tdocker run \\\n\t\t-w"
  },
  {
    "path": "README.md",
    "chars": 6631,
    "preview": "# landsat-tiler\n\n#### AWS Lambda + Landsat AWS PDS = landsat-tiler\n\n### Description\n\nCreate a highly customizable `serve"
  },
  {
    "path": "app/__init__.py",
    "chars": 29,
    "preview": "# app\n\n__version__ = '2.0.0'\n"
  },
  {
    "path": "app/landsat.py",
    "chars": 4368,
    "preview": "\"\"\"app.landsat: handle request for Landsat-tiler\"\"\"\n\nimport re\nimport json\n\nimport numpy as np\n\nfrom rio_tiler import la"
  },
  {
    "path": "package.json",
    "chars": 365,
    "preview": "{\n    \"description\": \"Create a highly customizable `serverless` tile server for Amazon's Landsat Public Dataset\",\n    \"d"
  },
  {
    "path": "serverless.yml",
    "chars": 862,
    "preview": "service: landsat-tiler\n\nprovider:\n  name: aws\n  runtime: python3.6\n  stage: production\n\n  region: us-west-2\n\n  iamRoleSt"
  },
  {
    "path": "viewer/css/style.css",
    "chars": 5317,
    "preview": "body {\n  position: fixed;\n\twidth: 100%;\n\theight: 100%;\n\tcolor: #000;\n\tbackground-color: #FFF;\n  letter-spacing: 0;\n}\n\n.c"
  },
  {
    "path": "viewer/index.html",
    "chars": 5179,
    "preview": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n\t<meta charset=\"utf-8\">\r\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge"
  },
  {
    "path": "viewer/js/app.js",
    "chars": 10065,
    "preview": "'use strict';\n\nmapboxgl.accessToken = '{YOUR-MAPBOX-TOKEN}';\nconst landsat_tiler_url = '{YOUR-API-GATEWAY-ENDPOINT}'; //"
  }
]

About this extraction

This page contains the full source code of the mapbox/landsat-tiler GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (39.0 KB), approximately 11.6k tokens, and a symbol index with 7 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!