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 ``` sls deploy :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:* `/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:* - `/landsat/tile/LC08_L1TP_016037_20170813_20170814_01_RT/8/71/102.png` - `/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/', 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/', 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////.', 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////.', 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 ================================================ Landsat viewer
RGB Combination
Histogramm cut
- %
Loading
Error
================================================ 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( '
' + '
' + '' + '
' + '
' + '' + results[i].sceneID + '' + ' ' + results[i].date + '' + '
' + '
'); } } 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 = ' © USGS/NASA Landsat'; $('.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'); });