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 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
```
: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