From ecf24f8bc4e90c899acdbbb956a46acb772c3f4e Mon Sep 17 00:00:00 2001 From: David Bitner Date: Wed, 1 Jul 2020 16:49:56 -0500 Subject: [PATCH 01/59] add search for every text anything --- db/deploy/searcheverything.sql | 55 ++++++++++++++++++++++++++++++++++ db/revert/searcheverything.sql | 7 +++++ db/sqitch.plan | 1 + db/verify/searcheverything.sql | 7 +++++ 4 files changed, 70 insertions(+) create mode 100644 db/deploy/searcheverything.sql create mode 100644 db/revert/searcheverything.sql create mode 100644 db/verify/searcheverything.sql diff --git a/db/deploy/searcheverything.sql b/db/deploy/searcheverything.sql new file mode 100644 index 00000000..c674442d --- /dev/null +++ b/db/deploy/searcheverything.sql @@ -0,0 +1,55 @@ +-- Deploy nasa-apt:searcheverything to pg +-- requires: textsearchAlias +BEGIN; +SET SEARCH_PATH TO apt, public; + +DROP VIEW IF EXISTS atbd_search CASCADE; +CREATE OR REPLACE VIEW atbd_search AS +SELECT +atbds.*, +json_agg(row_to_json(atbd_versions)) as atbd_versions, +json_agg(row_to_json(contacts)) as contacts, +json_agg(row_to_json(contact_groups)) as contact_groups, +json_agg(row_to_json(citations)) as citations, +json_agg(row_to_json(algorithm_input_variables)) as algorithm_input_variables, +json_agg(row_to_json(algorithm_output_variables)) as algorithm_output_variables, +json_agg(row_to_json(publication_references)) as publication_references, +json_agg(row_to_json(data_access_input_data)) as data_access_input_data, +json_agg(row_to_json(data_access_output_data)) as data_access_output_data, +json_agg(row_to_json(data_access_related_urls)) as data_access_related_urls, +to_tsvector(coalesce(atbds.title,'')) || +json_to_tsvector( + json_build_array( + json_agg(row_to_json(atbd_versions)), + json_agg(row_to_json(contacts)), + json_agg(row_to_json(contact_groups)), + json_agg(row_to_json(citations)), + json_agg(row_to_json(algorithm_input_variables)), + json_agg(row_to_json(algorithm_output_variables)), + json_agg(row_to_json(publication_references)), + json_agg(row_to_json(data_access_input_data)), + json_agg(row_to_json(data_access_output_data)), + json_agg(row_to_json(data_access_related_urls)) + ), + '["string"]' +) as search +FROM +atbds +LEFT JOIN atbd_versions USING (atbd_id) +LEFT JOIN atbd_contacts USING +(atbd_id) +LEFT JOIN contacts USING (contact_id) +LEFT JOIN atbd_contact_groups USING (atbd_id) +LEFT JOIN contact_groups USING (contact_group_id) +LEFT JOIN citations USING (atbd_id, atbd_version) +LEFT JOIN algorithm_input_variables USING (atbd_id, atbd_version) +LEFT JOIN algorithm_output_variables USING (atbd_id, atbd_version) +LEFT JOIN algorithm_implementations USING (atbd_id, atbd_version) +LEFT JOIN publication_references USING (atbd_id, atbd_version) +LEFT JOIN data_access_input_data USING (atbd_id, atbd_version) +LEFT JOIN data_access_output_data USING (atbd_id, atbd_version) +LEFT JOIN data_access_related_urls USING (atbd_id, atbd_version) +GROUP BY atbd_id, atbd_version, status; + + +COMMIT; diff --git a/db/revert/searcheverything.sql b/db/revert/searcheverything.sql new file mode 100644 index 00000000..9a1e787e --- /dev/null +++ b/db/revert/searcheverything.sql @@ -0,0 +1,7 @@ +-- Revert nasa-apt:searcheverything from pg + +BEGIN; + +-- XXX Add DDLs here. + +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index f0248714..0f15109e 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -13,3 +13,4 @@ copyATBD [tables] 2019-10-18T14:14:13Z Alyssa Harris # Add alias field to atbd copyATBDAlias [copyATBD] 2020-05-08T15:56:26Z Daniel da Silva # Creates a unique alias when copying atbd textsearchAlias [textsearch] 2020-05-08T15:57:18Z Daniel da Silva # Includes the atbs alias when searching +searcheverything [textsearchAlias] 2020-07-01T20:43:28Z David Bitner # add search over every json/text field diff --git a/db/verify/searcheverything.sql b/db/verify/searcheverything.sql new file mode 100644 index 00000000..39f87de8 --- /dev/null +++ b/db/verify/searcheverything.sql @@ -0,0 +1,7 @@ +-- Verify nasa-apt:searcheverything on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; From f50b94d4402a8e97b1e548f0cb3c9387bab5e7d6 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 27 Jul 2020 12:27:40 -0500 Subject: [PATCH 02/59] add elastic --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 00333e33..5d940f8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,14 @@ services: POSTGRES_DB: nasadb POSTGRES_USER: masteruser POSTGRES_PASSWORD: password + elastic: + # Elastic db for local development only. + # For staging/production, add elasticache instance + image: docker.elastic.co/elasticsearch/elasticsearch:7.8.1 + environment: + - discovery.type=single-node + ports: + - "9200:9200" # localstack for local development only. AWS S3 used for staging/production localstack: image: localstack/localstack:latest From ead5c970b2ad1d1b80d53645d26f2001d4e46d79 Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Tue, 28 Jul 2020 13:29:37 -0400 Subject: [PATCH 03/59] Add startserver redirects, set unicode supporting fonts in tex template --- pdf/app/latex/ATBD.tex | 6 +++++- startserver.sh | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 0eb11c1c..5f326119 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -22,6 +22,9 @@ \usepackage{url} \usepackage{booktabs} \usepackage{graphicx} +\usepackage{unicode-math} +\setmainfont{Latin Modern Math} +\setmathfont{Latin Modern Math} \hypersetup{ colorlinks=true, %set true if you want colored links linktoc=all, %set to all if you want both sections and subsections linked @@ -30,6 +33,7 @@ \begin{document} \maketitle +\section{I ADDED STUFF} \tableofcontents @@ -90,4 +94,4 @@ \section{Contacts} \bibliography{main} \end{document} - \ No newline at end of file + diff --git a/startserver.sh b/startserver.sh index 97681737..f36e3133 100755 --- a/startserver.sh +++ b/startserver.sh @@ -48,13 +48,13 @@ docker-compose up --detach # all the services are up, now create & populate the s3 buckets and the pg database # localstack: create s3 bucket for figures -aws --endpoint-url=${S3} s3 mb s3://"$FIGURES_S3_BUCKET" --no-sign-request -aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$FIGURES_S3_BUCKET" --acl public-read-write --no-sign-request +aws --endpoint-url=${S3} s3 mb s3://"$FIGURES_S3_BUCKET" --no-sign-request +aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$FIGURES_S3_BUCKET" --acl public-read-write --no-sign-request &>0 aws --endpoint-url=${S3} s3 cp ./figures/fullmoon.jpg s3://"$FIGURES_S3_BUCKET" --no-sign-request # localstack: create s3 bucket for pdfs aws --endpoint-url=${S3} s3 mb s3://"$PDFS_S3_BUCKET" --no-sign-request -aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$PDFS_S3_BUCKET" --acl public-read-write --no-sign-request +aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$PDFS_S3_BUCKET" --acl public-read-write --no-sign-request &>0 # create db with squitch and load mock data pushd db From a825ce9a3d479809c6e0abae6005795be1e32e80 Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Tue, 28 Jul 2020 16:51:46 -0400 Subject: [PATCH 04/59] Remove sanity check line --- pdf/app/latex/ATBD.tex | 1 - 1 file changed, 1 deletion(-) diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 5f326119..c5f0fd83 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -33,7 +33,6 @@ \begin{document} \maketitle -\section{I ADDED STUFF} \tableofcontents From 5fd1db3d471255b290edc28b3d5c80aceec8390c Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Wed, 29 Jul 2020 09:09:30 -0400 Subject: [PATCH 05/59] Update startserver.sh Co-authored-by: Daniel da Silva --- startserver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/startserver.sh b/startserver.sh index f36e3133..61e58b4c 100755 --- a/startserver.sh +++ b/startserver.sh @@ -48,7 +48,7 @@ docker-compose up --detach # all the services are up, now create & populate the s3 buckets and the pg database # localstack: create s3 bucket for figures -aws --endpoint-url=${S3} s3 mb s3://"$FIGURES_S3_BUCKET" --no-sign-request +aws --endpoint-url=${S3} s3 mb s3://"$FIGURES_S3_BUCKET" --no-sign-request aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$FIGURES_S3_BUCKET" --acl public-read-write --no-sign-request &>0 aws --endpoint-url=${S3} s3 cp ./figures/fullmoon.jpg s3://"$FIGURES_S3_BUCKET" --no-sign-request From b6034f28d83dd7038aa96593cdfc6ce85f1a1815 Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Wed, 29 Jul 2020 11:14:06 -0400 Subject: [PATCH 06/59] Add placement param to figures --- pdf/app/latex/serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 69ebf066..b2d0001d 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -117,7 +117,7 @@ def saveImage(imgUrl, img): def wrapImage(img, cap=''): if cap: cap = f'\\caption{{{cap}}}' - wrapper = f''' \\begin{{figure}} + wrapper = f''' \\begin{{figure}}[h] \\includegraphics[width=\\maxwidth{{\\linewidth}}]{{\\{img}}} {cap} \\end{{figure}} From 97c4af26b7bfff65630d1deaf02e24512c2370df Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 30 Jul 2020 16:10:43 -0500 Subject: [PATCH 07/59] elastic search --- .gitignore | 1 + db/deploy/notifytriggers.sql | 31 +++++ db/deploy/searcheverything.sql | 55 -------- db/revert/notifytriggers.sql | 7 + db/revert/searcheverything.sql | 7 - db/sqitch.plan | 2 +- ...earcheverything.sql => notifytriggers.sql} | 2 +- docker-compose.yml | 13 ++ pdf/app/main.py | 29 +++- pdf/app/requirements.txt | 3 + pdf/app/search/__init__.py | 0 pdf/app/search/searchindex.py | 128 ++++++++++++++++++ 12 files changed, 212 insertions(+), 66 deletions(-) create mode 100644 db/deploy/notifytriggers.sql delete mode 100644 db/deploy/searcheverything.sql create mode 100644 db/revert/notifytriggers.sql delete mode 100644 db/revert/searcheverything.sql rename db/verify/{searcheverything.sql => notifytriggers.sql} (54%) create mode 100644 pdf/app/search/__init__.py create mode 100644 pdf/app/search/searchindex.py diff --git a/.gitignore b/.gitignore index ddc361d2..8055fa9f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ecs/tex/Misc Test Files venv .env .resources +.vscode diff --git a/db/deploy/notifytriggers.sql b/db/deploy/notifytriggers.sql new file mode 100644 index 00000000..0b1ecfa7 --- /dev/null +++ b/db/deploy/notifytriggers.sql @@ -0,0 +1,31 @@ +-- Deploy nasa-apt:notifytriggers to pg + +BEGIN; +SET SEARCH_PATH to apt, public; + +CREATE OR REPLACE FUNCTION change_notification() RETURNS TRIGGER AS $$ +DECLARE +BEGIN +PERFORM pg_notify('atbd',NEW.atbd_id::text); +RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + +CREATE TRIGGER atbd_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbds FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER atbd_versions_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbd_versions FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER citations_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.citations FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER algorithm_input_variables_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.algorithm_input_variables FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER algorithm_output_variables_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.algorithm_output_variables FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER publication_references_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.publication_references FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER data_access_input_data_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.data_access_input_data FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER data_access_output_data_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.data_access_output_data FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER data_access_related_urls_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.data_access_related_urls FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); + + + + + + + + +COMMIT; diff --git a/db/deploy/searcheverything.sql b/db/deploy/searcheverything.sql deleted file mode 100644 index c674442d..00000000 --- a/db/deploy/searcheverything.sql +++ /dev/null @@ -1,55 +0,0 @@ --- Deploy nasa-apt:searcheverything to pg --- requires: textsearchAlias -BEGIN; -SET SEARCH_PATH TO apt, public; - -DROP VIEW IF EXISTS atbd_search CASCADE; -CREATE OR REPLACE VIEW atbd_search AS -SELECT -atbds.*, -json_agg(row_to_json(atbd_versions)) as atbd_versions, -json_agg(row_to_json(contacts)) as contacts, -json_agg(row_to_json(contact_groups)) as contact_groups, -json_agg(row_to_json(citations)) as citations, -json_agg(row_to_json(algorithm_input_variables)) as algorithm_input_variables, -json_agg(row_to_json(algorithm_output_variables)) as algorithm_output_variables, -json_agg(row_to_json(publication_references)) as publication_references, -json_agg(row_to_json(data_access_input_data)) as data_access_input_data, -json_agg(row_to_json(data_access_output_data)) as data_access_output_data, -json_agg(row_to_json(data_access_related_urls)) as data_access_related_urls, -to_tsvector(coalesce(atbds.title,'')) || -json_to_tsvector( - json_build_array( - json_agg(row_to_json(atbd_versions)), - json_agg(row_to_json(contacts)), - json_agg(row_to_json(contact_groups)), - json_agg(row_to_json(citations)), - json_agg(row_to_json(algorithm_input_variables)), - json_agg(row_to_json(algorithm_output_variables)), - json_agg(row_to_json(publication_references)), - json_agg(row_to_json(data_access_input_data)), - json_agg(row_to_json(data_access_output_data)), - json_agg(row_to_json(data_access_related_urls)) - ), - '["string"]' -) as search -FROM -atbds -LEFT JOIN atbd_versions USING (atbd_id) -LEFT JOIN atbd_contacts USING -(atbd_id) -LEFT JOIN contacts USING (contact_id) -LEFT JOIN atbd_contact_groups USING (atbd_id) -LEFT JOIN contact_groups USING (contact_group_id) -LEFT JOIN citations USING (atbd_id, atbd_version) -LEFT JOIN algorithm_input_variables USING (atbd_id, atbd_version) -LEFT JOIN algorithm_output_variables USING (atbd_id, atbd_version) -LEFT JOIN algorithm_implementations USING (atbd_id, atbd_version) -LEFT JOIN publication_references USING (atbd_id, atbd_version) -LEFT JOIN data_access_input_data USING (atbd_id, atbd_version) -LEFT JOIN data_access_output_data USING (atbd_id, atbd_version) -LEFT JOIN data_access_related_urls USING (atbd_id, atbd_version) -GROUP BY atbd_id, atbd_version, status; - - -COMMIT; diff --git a/db/revert/notifytriggers.sql b/db/revert/notifytriggers.sql new file mode 100644 index 00000000..50d6af61 --- /dev/null +++ b/db/revert/notifytriggers.sql @@ -0,0 +1,7 @@ +-- Revert nasa-apt:notifytriggers from pg + +BEGIN; + +-- XXX Add DDLs here. + +COMMIT; diff --git a/db/revert/searcheverything.sql b/db/revert/searcheverything.sql deleted file mode 100644 index 9a1e787e..00000000 --- a/db/revert/searcheverything.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert nasa-apt:searcheverything from pg - -BEGIN; - --- XXX Add DDLs here. - -COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index 0f15109e..d15dbf54 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -13,4 +13,4 @@ copyATBD [tables] 2019-10-18T14:14:13Z Alyssa Harris # Add alias field to atbd copyATBDAlias [copyATBD] 2020-05-08T15:56:26Z Daniel da Silva # Creates a unique alias when copying atbd textsearchAlias [textsearch] 2020-05-08T15:57:18Z Daniel da Silva # Includes the atbs alias when searching -searcheverything [textsearchAlias] 2020-07-01T20:43:28Z David Bitner # add search over every json/text field +notifytriggers 2020-07-30T16:31:36Z David Bitner # Add triggers to send notify to be used for updating elastic diff --git a/db/verify/searcheverything.sql b/db/verify/notifytriggers.sql similarity index 54% rename from db/verify/searcheverything.sql rename to db/verify/notifytriggers.sql index 39f87de8..c1c329a3 100644 --- a/db/verify/searcheverything.sql +++ b/db/verify/notifytriggers.sql @@ -1,4 +1,4 @@ --- Verify nasa-apt:searcheverything on pg +-- Verify nasa-apt:notifytriggers on pg BEGIN; diff --git a/docker-compose.yml b/docker-compose.yml index 5d940f8f..4999fa7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,14 +10,25 @@ services: POSTGRES_DB: nasadb POSTGRES_USER: masteruser POSTGRES_PASSWORD: password + command: postgres -c log_statement=all elastic: # Elastic db for local development only. # For staging/production, add elasticache instance image: docker.elastic.co/elasticsearch/elasticsearch:7.8.1 environment: - discovery.type=single-node + - http.port=9200 + - http.cors.allow-origin=* + - http.cors.enabled=true + - http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization + - http.cors.allow-credentials=true ports: - "9200:9200" + dejavu: + # Elastic browser to debug elastic issues for development + image: appbaseio/dejavu + ports: + - "1358:1358" # localstack for local development only. AWS S3 used for staging/production localstack: image: localstack/localstack:latest @@ -53,6 +64,8 @@ services: S3_ENDPOINT: $S3_ENDPOINT PDFS_S3_BUCKET: $PDFS_S3_BUCKET FIGURES_S3_BUCKET: $FIGURES_S3_BUCKET + DBURL: postgres://masteruser:password@db:5432/nasadb + ELASTICURL: http://elastic:9200 depends_on: - localstack - rest-api diff --git a/pdf/app/main.py b/pdf/app/main.py index 1a7575a4..2486e096 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -3,9 +3,9 @@ from tempfile import TemporaryDirectory from typing import Union, Dict, Type -from fastapi import FastAPI, HTTPException, BackgroundTasks +from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from fastapi.logger import logger -from fastapi.responses import FileResponse, RedirectResponse +from fastapi.responses import FileResponse, RedirectResponse, JSONResponse from .atbd.checksum_atbd import checksum_atbd from .atbd.get_atbd import get_atbd @@ -14,16 +14,37 @@ from .cache import Cache, CacheException from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException +from .search.searchindex import update_index, index_atbd, log_listener + +import asyncpg +import asyncio +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) root_path: str = environ.get('API_PREFIX', '/') rest_api_endpoint: str = environ.get('REST_API_ENDPOINT') or exit('REST_API_ENDPOINT env var required') s3_endpoint: str = environ.get('S3_ENDPOINT') or exit('S3_ENDPOINT env var required') pdfs_bucket_name: str = environ.get('PDFS_S3_BUCKET') or exit('PDFS_S3_BUCKET env var required') figures_bucket_name: str = environ.get('FIGURES_S3_BUCKET') or exit('FIGURES_S3_BUCKET env var required') +DBURL: str = environ.get('DBURL') or sys.exit('DBURL env var required') app: FastAPI = FastAPI() cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) +@app.on_event("startup") +async def startup() -> None: + app.state.connection = await asyncpg.connect(DBURL, server_settings={'search_path':'apt,public'}) + await app.state.connection.add_listener('atbd', index_atbd()) + +@app.on_event("shutdown") +async def shutdown() -> None: + # await app.state.connection.remove_listener('atbd', index_atbd) + await app.state.connection.close() + + + def get_cache_key(atbd_doc: Dict) -> str: """ @@ -119,3 +140,7 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): logger.info(f'cleaned up {tmp_dir.name}') +@app.get('/reindex',) +async def reindex(request: Request): + results = await update_index(connection=request.app.state.connection) + return JSONResponse(content=results) diff --git a/pdf/app/requirements.txt b/pdf/app/requirements.txt index a8a44949..9d9a3084 100644 --- a/pdf/app/requirements.txt +++ b/pdf/app/requirements.txt @@ -1,4 +1,5 @@ aiofiles==0.5.0 +asyncpg==0.20.1 boto3==1.13.18 botocore==1.16.18 botostubs==0.12.1.13.18 @@ -17,11 +18,13 @@ h11==0.9.0 httptools==0.1.1 idna==2.9 jmespath==0.10.0 +jq==1.0.2 latex==0.7.0 num2words==0.5.10 numpy==1.18.4 pandas==1.0.3 pipreqs==0.4.10 +psycopg2-binary==2.8.5 pydantic==1.5.1 python-dateutil==2.8.1 pytz==2020.1 diff --git a/pdf/app/search/__init__.py b/pdf/app/search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pdf/app/search/searchindex.py b/pdf/app/search/searchindex.py new file mode 100644 index 00000000..f39a5395 --- /dev/null +++ b/pdf/app/search/searchindex.py @@ -0,0 +1,128 @@ +import requests +import sys +from os import environ +from typing import Optional, Dict +from fastapi import APIRouter, HTTPException +from os import environ +import jq +import asyncpg +import asyncio +import logging +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) + +ELASTICURL: str = environ.get('ELASTICURL') or sys.exit('ELASTICURL env var required') + + +def prep_json(json: Dict) -> Dict: + """ + Cleans up json document returned from the database to be added + to Elasticsearch Index + """ + query=""" + def atbddoc: + del(._id) + | del(..|.type?) + | del(..|.object?) + | del(.[]|..|.atbd_id?) + | del(.[]|..|.atbd_version?) + | walk(if type=="object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) else . end) + | walk(if type=="object" and has("document") then {document:( .. | select(.text?) )} else . end); + + .[] | {"index": {"_index": "atbd", "_type": "atbd", "_id": ._id}}, atbddoc + """ + return jq.compile(query).input(text=json).text() + "\n" + + +def send_to_elastic(json: Dict): + """ + POST json to elastic endpoint + """ + json = prep_json(json).encode('utf-8') + url = f"{ELASTICURL}/atbd/_bulk" + response = requests.post(url, data=json, headers={'Content-Type':'application/json'}) + logger.debug(response.status_code, response.text) + print(response.status_code, response.text) + if not response.ok: + raise HTTPException(status_code=response.status_code, detail=response.text) + return response.json() + + +async def get_index(connection: asyncpg.connection, atbd_id: Optional[int] = None, atbd_version:Optional[int] = None) -> Dict: + """ + Get data for Index from PostgreSQL Database + """ + where="" + args=() + if atbd_id is not None: + where = " WHERE atbd_id=$1" + args=(atbd_id,) + if atbd_version is not None: + where = f"{where} AND atbd_version=$2" + args=(atbd_id,atbd_version,) + query=f""" + WITH t AS ( + SELECT + v.atbd_id *10000 + v.atbd_version as _id, + atbds.title, atbds.alias, + v.*, + (SELECT json_agg(c) FROM ( + SELECT * FROM atbd_contacts LEFT JOIN contacts USING (contact_id) WHERE atbd_id=atbds.atbd_id + ) as c ) as contacts, + (SELECT json_agg(c) FROM ( + SELECT * FROM atbd_contact_groups LEFT JOIN contact_groups USING (contact_group_id) WHERE atbd_id=atbds.atbd_id + ) as c ) as contact_groups, + (SELECT json_agg(c) FROM ( + SELECT * FROM atbd_contacts LEFT JOIN contacts USING (contact_id) WHERE atbd_id=atbds.atbd_id + ) as c ) as contacts, + (SELECT json_agg(c) FROM ( + SELECT * FROM citations WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as citations, + (SELECT json_agg(c) FROM ( + SELECT * FROM algorithm_input_variables WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as algorithm_input_variables, + (SELECT json_agg(c) FROM ( + SELECT * FROM algorithm_output_variables WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as algorithm_output_variables, + (SELECT json_agg(c) FROM ( + SELECT * FROM publication_references WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as publication_references, + (SELECT json_agg(c) FROM ( + SELECT * FROM data_access_input_data WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as data_access_input_data, + (SELECT json_agg(c) FROM ( + SELECT * FROM data_access_output_data WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as data_access_output_data, + (SELECT json_agg(c) FROM ( + SELECT * FROM data_access_related_urls WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + ) as c ) as data_access_related_urls + FROM + atbds + JOIN atbd_versions v USING (atbd_id) + {where} + ) + SELECT json_agg(json_strip_nulls(row_to_json(t))) FROM t; + """ + return await connection.fetchval(query, *args) + + +async def update_index(connection: asyncpg.connection, atbd_id: Optional[int] = None, atbd_version:Optional[int] = None) -> Dict: + """ + update data for Elastic from PostgreSQL Database + """ + logger.debug('Updating Index for', atbd_id, atbd_version) + content = await get_index(connection, atbd_id, atbd_version) + results = send_to_elastic(content) + return results + + +def index_atbd(connection: asyncpg.connection=None, pid: int=None, channel: str=None, payload: str=None): + logger.debug('In index_atbd', pid, channel, payload) + def callback(connection: asyncpg.connection, pid: int, channel: str, payload: str): + logger.debug('Listen',pid, channel, payload) + asyncio.ensure_future( update_index(connection=connection, atbd_id=int(payload)) ) + return callback + +async def log_listener(connection: asyncpg.connection, message: str): + logger.debug('Logs',message) From 1c26b079fc0e75dfcb94744d2aeee692ac5f9e75 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 30 Jul 2020 16:24:46 -0500 Subject: [PATCH 08/59] cleanup --- docker-compose.yml | 5 -- pdf/app/main.py | 82 ++++++++++++++++---------- pdf/app/requirements.txt | 1 - pdf/app/search/searchindex.py | 108 +++++++++++++++++++++++----------- 4 files changed, 126 insertions(+), 70 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4999fa7d..ecffba45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,11 +24,6 @@ services: - http.cors.allow-credentials=true ports: - "9200:9200" - dejavu: - # Elastic browser to debug elastic issues for development - image: appbaseio/dejavu - ports: - - "1358:1358" # localstack for local development only. AWS S3 used for staging/production localstack: image: localstack/localstack:latest diff --git a/pdf/app/main.py b/pdf/app/main.py index 2486e096..05417bb1 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -14,29 +14,39 @@ from .cache import Cache, CacheException from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException -from .search.searchindex import update_index, index_atbd, log_listener +from .search.searchindex import update_index, index_atbd import asyncpg -import asyncio import logging -logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -root_path: str = environ.get('API_PREFIX', '/') -rest_api_endpoint: str = environ.get('REST_API_ENDPOINT') or exit('REST_API_ENDPOINT env var required') -s3_endpoint: str = environ.get('S3_ENDPOINT') or exit('S3_ENDPOINT env var required') -pdfs_bucket_name: str = environ.get('PDFS_S3_BUCKET') or exit('PDFS_S3_BUCKET env var required') -figures_bucket_name: str = environ.get('FIGURES_S3_BUCKET') or exit('FIGURES_S3_BUCKET env var required') -DBURL: str = environ.get('DBURL') or sys.exit('DBURL env var required') +root_path: str = environ.get("API_PREFIX", "/") +rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or exit( + "REST_API_ENDPOINT env var required" +) +s3_endpoint: str = environ.get("S3_ENDPOINT") or exit( + "S3_ENDPOINT env var required" +) +pdfs_bucket_name: str = environ.get("PDFS_S3_BUCKET") or exit( + "PDFS_S3_BUCKET env var required" +) +figures_bucket_name: str = environ.get("FIGURES_S3_BUCKET") or exit( + "FIGURES_S3_BUCKET env var required" +) +DBURL: str = environ.get("DBURL") or exit("DBURL env var required") app: FastAPI = FastAPI() cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) + @app.on_event("startup") async def startup() -> None: - app.state.connection = await asyncpg.connect(DBURL, server_settings={'search_path':'apt,public'}) - await app.state.connection.add_listener('atbd', index_atbd()) + app.state.connection = await asyncpg.connect( + DBURL, server_settings={"search_path": "apt,public"} + ) + await app.state.connection.add_listener("atbd", index_atbd()) + @app.on_event("shutdown") async def shutdown() -> None: @@ -44,24 +54,28 @@ async def shutdown() -> None: await app.state.connection.close() - - def get_cache_key(atbd_doc: Dict) -> str: """ - Helper function to construct a cache keys from the checksum and the alias of the atbd doc. + Helper function to construct a cache keys from + the checksum and the alias of the atbd doc. :param atbd_doc: json atbd doc :type atbd_doc: dict :return: cache key in the format {hex_digest}/{alias}.pdf' :rtype: str """ hex_digest: str = checksum_atbd(atbd_doc) - alias: str = atbd_doc['atbd']['alias'] - return f'{hex_digest}/{alias}.pdf' if alias else f'{hex_digest}/nasa-atbd.pdf' + alias: str = atbd_doc["atbd"]["alias"] + return ( + f"{hex_digest}/{alias}.pdf" if alias else f"{hex_digest}/nasa-atbd.pdf" + ) -def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[Union[Type[RedirectResponse], Type[FileResponse]]]: +def atbd_pdf_handler( + atbd_doc: Dict, background_tasks: BackgroundTasks +) -> Type[Union[Type[RedirectResponse], Type[FileResponse]]]: """ - For Published atbd: Issues redirect response upon cache hit, or streams file response for cache failure. + For Published atbd: Issues redirect response upon cache hit, + or streams file response for cache failure. (Should always return a PDF unless the serialization pipeline has failed) For Draft atbd: stream file response and does not cache. @@ -84,7 +98,9 @@ def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[ except CacheException as e: # log and continue with pdf serialization workflow logger.error(e) - tmp_dir_resource: TemporaryDirectory[str] = TemporaryDirectory(prefix='nasa-apt-pdf-service-') + tmp_dir_resource: TemporaryDirectory[str] = TemporaryDirectory( + prefix="nasa-apt-pdf-service-" + ) tmp_dir: str = tmp_dir_resource.name background_tasks.add_task(cleanup_tmp_dir, tmp_dir_resource) try: @@ -92,27 +108,31 @@ def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[ except JsonToLatexException as e: raise HTTPException(status_code=500, detail=str(e)) from e try: - tmp_pdf_filename: str = latex_to_pdf(latex_filename=latex_filename, tmp_dir=tmp_dir) + tmp_pdf_filename: str = latex_to_pdf( + latex_filename=latex_filename, tmp_dir=tmp_dir + ) if status == Status.Published.name: try: - cache_url = cache.put_file(key=cache_key, filename=tmp_pdf_filename) + cache_url = cache.put_file( + key=cache_key, filename=tmp_pdf_filename + ) return RedirectResponse(url=cache_url) except CacheException as e: logger.error(str(e)) - alias: str = atbd_doc['atbd']['alias'] - filename = f'{alias}.pdf' if alias else 'nasa-atbd.pdf' + alias: str = atbd_doc["atbd"]["alias"] + filename = f"{alias}.pdf" if alias else "nasa-atbd.pdf" return FileResponse(path=tmp_pdf_filename, filename=filename) except LatexToPDFException as e: raise HTTPException(status_code=500, detail=str(e)) from e -@app.get(root_path + 'atbds/id/{atbd_id}.pdf') +@app.get(root_path + "atbds/id/{atbd_id}.pdf") def get_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks): atbd_doc = get_atbd(atbd_id=atbd_id) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) -@app.get(root_path + 'atbds/alias/{alias}.pdf') +@app.get(root_path + "atbds/alias/{alias}.pdf") def get_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks): atbd_doc = get_atbd(alias=alias) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) @@ -123,13 +143,15 @@ def health_check(): """ The root_path is only used by the ELB healthcheck. """ - return 'ok' + return "ok" def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): """ - Cleanup the temporary directory resource. This must wait until after the http response. Note: it might be cleaner to - implement with fastapi's "dependencies with yield" feature, but background_tasks seems to work fine. + Cleanup the temporary directory resource. This must wait until + after the http response. Note: it might be cleaner to + implement with fastapi's "dependencies with yield" feature, + but background_tasks seems to work fine. https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/ @@ -137,10 +159,10 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): :type tmp_dir: TemporaryDirectory[str] """ tmp_dir.cleanup() - logger.info(f'cleaned up {tmp_dir.name}') + logger.info(f"cleaned up {tmp_dir.name}") -@app.get('/reindex',) +@app.get("/reindex",) async def reindex(request: Request): results = await update_index(connection=request.app.state.connection) return JSONResponse(content=results) diff --git a/pdf/app/requirements.txt b/pdf/app/requirements.txt index 9d9a3084..7b2cdfa4 100644 --- a/pdf/app/requirements.txt +++ b/pdf/app/requirements.txt @@ -24,7 +24,6 @@ num2words==0.5.10 numpy==1.18.4 pandas==1.0.3 pipreqs==0.4.10 -psycopg2-binary==2.8.5 pydantic==1.5.1 python-dateutil==2.8.1 pytz==2020.1 diff --git a/pdf/app/search/searchindex.py b/pdf/app/search/searchindex.py index f39a5395..f5463207 100644 --- a/pdf/app/search/searchindex.py +++ b/pdf/app/search/searchindex.py @@ -2,17 +2,17 @@ import sys from os import environ from typing import Optional, Dict -from fastapi import APIRouter, HTTPException -from os import environ +from fastapi import HTTPException import jq import asyncpg import asyncio import logging -from fastapi.responses import JSONResponse logger = logging.getLogger(__name__) -ELASTICURL: str = environ.get('ELASTICURL') or sys.exit('ELASTICURL env var required') +ELASTICURL: str = environ.get("ELASTICURL") or sys.exit( + "ELASTICURL env var required" +) def prep_json(json: Dict) -> Dict: @@ -20,15 +20,18 @@ def prep_json(json: Dict) -> Dict: Cleans up json document returned from the database to be added to Elasticsearch Index """ - query=""" + query = """ def atbddoc: del(._id) | del(..|.type?) | del(..|.object?) | del(.[]|..|.atbd_id?) | del(.[]|..|.atbd_version?) - | walk(if type=="object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) else . end) - | walk(if type=="object" and has("document") then {document:( .. | select(.text?) )} else . end); + | walk(if type=="object" then + with_entries(select(.value != null and .value != "" + and .value != [] and .value != {})) else . end) + | walk(if type=="object" and has("document") + then {document:( .. | select(.text?) )} else . end); .[] | {"index": {"_index": "atbd", "_type": "atbd", "_id": ._id}}, atbddoc """ @@ -39,63 +42,84 @@ def send_to_elastic(json: Dict): """ POST json to elastic endpoint """ - json = prep_json(json).encode('utf-8') + json = prep_json(json).encode("utf-8") url = f"{ELASTICURL}/atbd/_bulk" - response = requests.post(url, data=json, headers={'Content-Type':'application/json'}) + response = requests.post( + url, data=json, headers={"Content-Type": "application/json"} + ) logger.debug(response.status_code, response.text) print(response.status_code, response.text) if not response.ok: - raise HTTPException(status_code=response.status_code, detail=response.text) + raise HTTPException( + status_code=response.status_code, detail=response.text + ) return response.json() -async def get_index(connection: asyncpg.connection, atbd_id: Optional[int] = None, atbd_version:Optional[int] = None) -> Dict: +async def get_index( + connection: asyncpg.connection, + atbd_id: Optional[int] = None, + atbd_version: Optional[int] = None, +) -> Dict: """ Get data for Index from PostgreSQL Database """ - where="" - args=() + where = "" + args = () if atbd_id is not None: where = " WHERE atbd_id=$1" - args=(atbd_id,) + args = (atbd_id,) if atbd_version is not None: where = f"{where} AND atbd_version=$2" - args=(atbd_id,atbd_version,) - query=f""" + args = ( + atbd_id, + atbd_version, + ) + query = f""" WITH t AS ( SELECT v.atbd_id *10000 + v.atbd_version as _id, atbds.title, atbds.alias, v.*, (SELECT json_agg(c) FROM ( - SELECT * FROM atbd_contacts LEFT JOIN contacts USING (contact_id) WHERE atbd_id=atbds.atbd_id + SELECT * FROM atbd_contacts LEFT JOIN contacts + USING (contact_id) WHERE atbd_id=atbds.atbd_id ) as c ) as contacts, (SELECT json_agg(c) FROM ( - SELECT * FROM atbd_contact_groups LEFT JOIN contact_groups USING (contact_group_id) WHERE atbd_id=atbds.atbd_id + SELECT * FROM atbd_contact_groups LEFT JOIN contact_groups + USING (contact_group_id) WHERE atbd_id=atbds.atbd_id ) as c ) as contact_groups, (SELECT json_agg(c) FROM ( - SELECT * FROM atbd_contacts LEFT JOIN contacts USING (contact_id) WHERE atbd_id=atbds.atbd_id + SELECT * FROM atbd_contacts LEFT JOIN contacts + USING (contact_id) WHERE atbd_id=atbds.atbd_id ) as c ) as contacts, (SELECT json_agg(c) FROM ( - SELECT * FROM citations WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM citations WHERE + atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as citations, (SELECT json_agg(c) FROM ( - SELECT * FROM algorithm_input_variables WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM algorithm_input_variables + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as algorithm_input_variables, (SELECT json_agg(c) FROM ( - SELECT * FROM algorithm_output_variables WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM algorithm_output_variables + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as algorithm_output_variables, (SELECT json_agg(c) FROM ( - SELECT * FROM publication_references WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM publication_references + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as publication_references, (SELECT json_agg(c) FROM ( - SELECT * FROM data_access_input_data WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM data_access_input_data + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as data_access_input_data, (SELECT json_agg(c) FROM ( - SELECT * FROM data_access_output_data WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM data_access_output_data + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as data_access_output_data, (SELECT json_agg(c) FROM ( - SELECT * FROM data_access_related_urls WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version + SELECT * FROM data_access_related_urls + WHERE atbd_id=atbds.atbd_id and atbd_version=v.atbd_version ) as c ) as data_access_related_urls FROM atbds @@ -107,22 +131,38 @@ async def get_index(connection: asyncpg.connection, atbd_id: Optional[int] = Non return await connection.fetchval(query, *args) -async def update_index(connection: asyncpg.connection, atbd_id: Optional[int] = None, atbd_version:Optional[int] = None) -> Dict: +async def update_index( + connection: asyncpg.connection, + atbd_id: Optional[int] = None, + atbd_version: Optional[int] = None, +) -> Dict: """ update data for Elastic from PostgreSQL Database """ - logger.debug('Updating Index for', atbd_id, atbd_version) + logger.debug("Updating Index for", atbd_id, atbd_version) content = await get_index(connection, atbd_id, atbd_version) results = send_to_elastic(content) return results -def index_atbd(connection: asyncpg.connection=None, pid: int=None, channel: str=None, payload: str=None): - logger.debug('In index_atbd', pid, channel, payload) - def callback(connection: asyncpg.connection, pid: int, channel: str, payload: str): - logger.debug('Listen',pid, channel, payload) - asyncio.ensure_future( update_index(connection=connection, atbd_id=int(payload)) ) +def index_atbd( + connection: asyncpg.connection = None, + pid: int = None, + channel: str = None, + payload: str = None, +): + logger.debug("In index_atbd", pid, channel, payload) + + def callback( + connection: asyncpg.connection, pid: int, channel: str, payload: str + ): + logger.debug("Listen", pid, channel, payload) + asyncio.ensure_future( + update_index(connection=connection, atbd_id=int(payload)) + ) + return callback + async def log_listener(connection: asyncpg.connection, message: str): - logger.debug('Logs',message) + logger.debug("Logs", message) From f75a547c7a4daa07f2bb890c8923a25370d004cb Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Fri, 31 Jul 2020 11:40:46 -0400 Subject: [PATCH 09/59] Use H placement param and float package --- pdf/app/latex/ATBD.tex | 3 ++- pdf/app/latex/serialize.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 0eb11c1c..69d2eb25 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -22,6 +22,7 @@ \usepackage{url} \usepackage{booktabs} \usepackage{graphicx} +\usepackage{float} \hypersetup{ colorlinks=true, %set true if you want colored links linktoc=all, %set to all if you want both sections and subsections linked @@ -90,4 +91,4 @@ \section{Contacts} \bibliography{main} \end{document} - \ No newline at end of file + diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index b2d0001d..acc48d88 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -117,7 +117,7 @@ def saveImage(imgUrl, img): def wrapImage(img, cap=''): if cap: cap = f'\\caption{{{cap}}}' - wrapper = f''' \\begin{{figure}}[h] + wrapper = f''' \\begin{{figure}}[H] \\includegraphics[width=\\maxwidth{{\\linewidth}}]{{\\{img}}} {cap} \\end{{figure}} From e2acb59c12c7fd87132e1d78bdf64fff150278d0 Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Mon, 3 Aug 2020 09:54:42 -0400 Subject: [PATCH 10/59] Change 0 to /dev/null --- startserver.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/startserver.sh b/startserver.sh index 61e58b4c..1df96e48 100755 --- a/startserver.sh +++ b/startserver.sh @@ -49,12 +49,12 @@ docker-compose up --detach # localstack: create s3 bucket for figures aws --endpoint-url=${S3} s3 mb s3://"$FIGURES_S3_BUCKET" --no-sign-request -aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$FIGURES_S3_BUCKET" --acl public-read-write --no-sign-request &>0 +aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$FIGURES_S3_BUCKET" --acl public-read-write --no-sign-request &>/dev/null aws --endpoint-url=${S3} s3 cp ./figures/fullmoon.jpg s3://"$FIGURES_S3_BUCKET" --no-sign-request # localstack: create s3 bucket for pdfs aws --endpoint-url=${S3} s3 mb s3://"$PDFS_S3_BUCKET" --no-sign-request -aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$PDFS_S3_BUCKET" --acl public-read-write --no-sign-request &>0 +aws --endpoint-url=${S3} s3api put-bucket-acl --bucket "$PDFS_S3_BUCKET" --acl public-read-write --no-sign-request &>/dev/null # create db with squitch and load mock data pushd db From e7a2a4e7e7662510f3929fd0fe928d4fcd7adb6a Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 3 Aug 2020 14:30:23 -0500 Subject: [PATCH 11/59] add contact triggers, add search proxy to fastapi --- db/deploy/notifytriggers.sql | 15 +++++++++++++-- pdf/app/main.py | 27 ++++++++++++++++++++++++++- pdf/app/search/searchindex.py | 9 +++------ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/db/deploy/notifytriggers.sql b/db/deploy/notifytriggers.sql index 0b1ecfa7..f856daa9 100644 --- a/db/deploy/notifytriggers.sql +++ b/db/deploy/notifytriggers.sql @@ -5,12 +5,23 @@ SET SEARCH_PATH to apt, public; CREATE OR REPLACE FUNCTION change_notification() RETURNS TRIGGER AS $$ DECLARE +atbd_id int; BEGIN -PERFORM pg_notify('atbd',NEW.atbd_id::text); +IF TG_TABLE_NAME = 'contacts' THEN + SELECT INTO atbd_id atbd_id from atbd_contacts WHERE contact_id=NEW.contact_id; +ELSIF TG_TABLE_NAME = 'contact_groups' THEN + SELECT INTO atbd_id atbd_id from atbd_contact_groupss WHERE contact_group_id=NEW.contact_group_id; +ELSE + atbd_id = NEW.atbd_id; +END IF; +PERFORM pg_notify('atbd',atbd_id::text); RETURN NEW; END; $$ LANGUAGE PLPGSQL; - +CREATE TRIGGER contacts_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.contacts FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER atbd_contacts_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbd_contacts FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER contact_groups_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.contact_groups FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); +CREATE TRIGGER atbd_contact_groups_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbd_contact_groups FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); CREATE TRIGGER atbd_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbds FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); CREATE TRIGGER atbd_versions_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.atbd_versions FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); CREATE TRIGGER citations_notify_change AFTER INSERT OR UPDATE OR DELETE ON apt.citations FOR EACH ROW EXECUTE PROCEDURE apt.change_notification(); diff --git a/pdf/app/main.py b/pdf/app/main.py index 05417bb1..e645e69c 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -14,9 +14,11 @@ from .cache import Cache, CacheException from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException -from .search.searchindex import update_index, index_atbd +from .search.searchindex import update_index, index_atbd, ELASTICURL import asyncpg +import asyncio +import requests import logging logger.setLevel(logging.DEBUG) @@ -42,6 +44,10 @@ @app.on_event("startup") async def startup() -> None: + """ + Create database connection when FastAPI App has started. + Add listener to atbd channel on database connection. + """ app.state.connection = await asyncpg.connect( DBURL, server_settings={"search_path": "apt,public"} ) @@ -164,5 +170,24 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): @app.get("/reindex",) async def reindex(request: Request): + """ + Reindex all ATBD's into ElasticSearch + """ results = await update_index(connection=request.app.state.connection) return JSONResponse(content=results) + + +@app.post("/search",) +async def search_elastic(request: Request): + """ + Proxies POST json to elastic search endpoint + """ + url = f"{ELASTICURL}/atbd/_search" + data = await request.body() + response = requests.post(url, data=data, headers=request.headers) + logger.debug(response.status_code, response.text) + if not response.ok: + raise HTTPException( + status_code=response.status_code, detail=response.text + ) + return response.json() diff --git a/pdf/app/search/searchindex.py b/pdf/app/search/searchindex.py index f5463207..3b215fe0 100644 --- a/pdf/app/search/searchindex.py +++ b/pdf/app/search/searchindex.py @@ -48,7 +48,6 @@ def send_to_elastic(json: Dict): url, data=json, headers={"Content-Type": "application/json"} ) logger.debug(response.status_code, response.text) - print(response.status_code, response.text) if not response.ok: raise HTTPException( status_code=response.status_code, detail=response.text @@ -151,7 +150,9 @@ def index_atbd( channel: str = None, payload: str = None, ): - logger.debug("In index_atbd", pid, channel, payload) + """ + Callback function to update index for an ATBD document + """ def callback( connection: asyncpg.connection, pid: int, channel: str, payload: str @@ -162,7 +163,3 @@ def callback( ) return callback - - -async def log_listener(connection: asyncpg.connection, message: str): - logger.debug("Logs", message) From eaab347be6d0b0dd80c8e7fbfba7a3e36727afce Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 3 Aug 2020 14:34:07 -0500 Subject: [PATCH 12/59] remove unused import --- pdf/app/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pdf/app/main.py b/pdf/app/main.py index e645e69c..badd0146 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -17,7 +17,6 @@ from .search.searchindex import update_index, index_atbd, ELASTICURL import asyncpg -import asyncio import requests import logging From 67fd1575067e5b829c35c868ec28bc27d94ed1ee Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Tue, 4 Aug 2020 13:09:20 -0400 Subject: [PATCH 13/59] Add documentation of font choice --- pdf/app/latex/ATBD.tex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index c5f0fd83..90c32d5a 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -22,9 +22,9 @@ \usepackage{url} \usepackage{booktabs} \usepackage{graphicx} -\usepackage{unicode-math} -\setmainfont{Latin Modern Math} -\setmathfont{Latin Modern Math} +\setmainfont{Latin Modern Math} %Using a font with good unicode math symbols +% coverage to support the use of unicode math symbols in LaTeX text mode +% See list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf \hypersetup{ colorlinks=true, %set true if you want colored links linktoc=all, %set to all if you want both sections and subsections linked From 4b7061efc92fe47a8468734d6142da7d352b6739 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 4 Aug 2020 14:10:15 -0500 Subject: [PATCH 14/59] add initial call to /reindex as part of startserver.sh --- startserver.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/startserver.sh b/startserver.sh index 97681737..b8ee44f7 100755 --- a/startserver.sh +++ b/startserver.sh @@ -62,6 +62,9 @@ pushd db ./loadTestData.sh popd +# Make sure to bootstrap the elastic index +curl -s -o /dev/null -v http://localhost:8000/reindex + # force postgrest restart to see new schema docker-compose restart rest-api From 57b52b1f54fa5235e04e1b42b61f8b03e414874b Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Wed, 5 Aug 2020 12:31:12 -0400 Subject: [PATCH 15/59] Add font documentation to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2ca794ab..e061d807 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,9 @@ docker build --target prod . -t nasa-apt/prod/pdf aws ecs update-service --force-new-deployment --cluster --service ``` +## Notes +The PDF serialization service supports unicode characters in text mode. The service uses the font `Latin Modern Math` which has a good coverage of unicode math symbols. See a list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf +A symbol which is not covered by the font will be rendered as a blank space. Unicode characters used in LaTeX math mode will not be rendered. ## Releases From 14b62fa703b95448979e6ff69884e088a3737b5c Mon Sep 17 00:00:00 2001 From: Jeevan Farias Date: Tue, 11 Aug 2020 16:26:05 -0400 Subject: [PATCH 16/59] Add CORS defns --- pdf/app/main.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pdf/app/main.py b/pdf/app/main.py index badd0146..a0b1c013 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -6,6 +6,7 @@ from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from fastapi.logger import logger from fastapi.responses import FileResponse, RedirectResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware from .atbd.checksum_atbd import checksum_atbd from .atbd.get_atbd import get_atbd @@ -20,6 +21,7 @@ import requests import logging + logger.setLevel(logging.DEBUG) root_path: str = environ.get("API_PREFIX", "/") @@ -41,6 +43,21 @@ cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) +origins = [ + "http://localhost:3000", + "http://localhost:3006", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + + @app.on_event("startup") async def startup() -> None: """ From 77a5f8b2be1356b5414d3eba93068f10662cfdd0 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 7 Sep 2020 10:34:49 -0500 Subject: [PATCH 17/59] add cloudformation (not working yet) mv pdf->fastapi --- README.md | 34 ++--- cloudformation/cloudformation.yaml | 154 +++++++++++++------- cloudformation/deploy.sh | 1 + docker-compose.yml | 10 +- {pdf => fastapi}/.gitignore | 0 {pdf => fastapi}/Dockerfile | 2 +- {pdf => fastapi}/README.md | 0 {pdf => fastapi}/__init__.py | 0 {pdf => fastapi}/app/__init__.py | 0 {pdf => fastapi}/app/atbd/Status.py | 0 {pdf => fastapi}/app/atbd/__init__.py | 0 {pdf => fastapi}/app/atbd/checksum_atbd.py | 0 {pdf => fastapi}/app/atbd/get_atbd.py | 0 {pdf => fastapi}/app/atbd/get_status.py | 0 {pdf => fastapi}/app/cache.py | 0 {pdf => fastapi}/app/latex/ATBD.tex | 0 {pdf => fastapi}/app/latex/__init__.py | 0 {pdf => fastapi}/app/latex/json_to_latex.py | 0 {pdf => fastapi}/app/latex/serialize.py | 0 {pdf => fastapi}/app/main.py | 22 ++- {pdf => fastapi}/app/pdf/__init__.py | 0 {pdf => fastapi}/app/pdf/latex_to_pdf.py | 0 {pdf => fastapi}/app/pdf/pdf.sh | 0 {pdf => fastapi}/app/requirements.txt | 1 + {pdf => fastapi}/app/search/__init__.py | 0 {pdf => fastapi}/app/search/searchindex.py | 31 +++- 26 files changed, 169 insertions(+), 86 deletions(-) rename {pdf => fastapi}/.gitignore (100%) rename {pdf => fastapi}/Dockerfile (90%) rename {pdf => fastapi}/README.md (100%) rename {pdf => fastapi}/__init__.py (100%) rename {pdf => fastapi}/app/__init__.py (100%) rename {pdf => fastapi}/app/atbd/Status.py (100%) rename {pdf => fastapi}/app/atbd/__init__.py (100%) rename {pdf => fastapi}/app/atbd/checksum_atbd.py (100%) rename {pdf => fastapi}/app/atbd/get_atbd.py (100%) rename {pdf => fastapi}/app/atbd/get_status.py (100%) rename {pdf => fastapi}/app/cache.py (100%) rename {pdf => fastapi}/app/latex/ATBD.tex (100%) rename {pdf => fastapi}/app/latex/__init__.py (100%) rename {pdf => fastapi}/app/latex/json_to_latex.py (100%) rename {pdf => fastapi}/app/latex/serialize.py (100%) rename {pdf => fastapi}/app/main.py (93%) rename {pdf => fastapi}/app/pdf/__init__.py (100%) rename {pdf => fastapi}/app/pdf/latex_to_pdf.py (100%) rename {pdf => fastapi}/app/pdf/pdf.sh (100%) rename {pdf => fastapi}/app/requirements.txt (96%) rename {pdf => fastapi}/app/search/__init__.py (100%) rename {pdf => fastapi}/app/search/searchindex.py (84%) diff --git a/README.md b/README.md index 2ca794ab..33afde58 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Code and issues relevant to the NASA APT project The project API is built using [Postgrest](https://github.com/PostgREST/postgrest). The startserver script uses `docker-compose` to build and run the development environment and -sample database: +sample database: ```shell script ./startserver.sh ``` This will create a complete development environment with an instance of the DB, the REST API, `localstack` for s3, and -the PDF serialization service. +the FastAPI Search / PDF serialization service. - The Swagger API documentation is accessible via [http://localhost:8080](http://localhost:8080). - The REST API is accessible via [http://localhost:3000](http://localhost:3000). @@ -61,11 +61,11 @@ You will be prompted for a stack name and a master db password. The current stacks are `nasa-aptv2-staging` and `nasa-aptv2-production`. After the stack has been successfully deployed you can create the database tables. -You will need an installation of the `psql` command line client. +You will need an installation of the `psql` command line client. -You will also need to update the RDS instance's security policy to allow inbound traffic from the IP address of the machine where you +You will also need to update the RDS instance's security policy to allow inbound traffic from the IP address of the machine where you are executing the deployment. (see Resources | DBInstance | Security and Network | Security Groups | -Edit inbound rules | Custom TCP, Port 5432, My IP). +Edit inbound rules | Custom TCP, Port 5432, My IP). To create the schema and tables in the AWS RDS from the project root run ```shell script @@ -73,8 +73,8 @@ cd db ./sqitch deploy --verify db:pg://{yourmasteruser}:{yourmasterpassword}@{yourRDSendpoint}:5432/nasadb ``` -Because of PostgREST's schema reloading [model](http://postgrest.org/en/v5.2/admin.html#schema-reloading) some -underlying database changes may require a forced redeployment of the PostgREST ECS service to reflect the changes. (See Note in +Because of PostgREST's schema reloading [model](http://postgrest.org/en/v5.2/admin.html#schema-reloading) some +underlying database changes may require a forced redeployment of the PostgREST ECS service to reflect the changes. (See Note in [Environments](#environments)) ## Environments @@ -94,20 +94,20 @@ Steps to deploy: ```shell script aws ecs update-service --force-new-deployment --cluster --service -# e.g. -aws ecs update-service --force-new-deployment --cluster stackname-ECSCluster-nWSsDVGj9NXS --service stackname-svc-pgr +# e.g. +aws ecs update-service --force-new-deployment --cluster stackname-ECSCluster-nWSsDVGj9NXS --service stackname-svc-pgr # then wait until the service's desired count == the running count (this will take about 10 minutes) ``` -## Updating the PDF service -The PDF generation service uses docker and it is stored on amazon ECR. During the first cloudformation deployment, the container is created and uploaded, but subsequent updates need to be performed manually. -We're currently using a single ECR repo (nasa-apt/prod/pdf) to store the container and it is shared between the production and staging environments. +## Updating the FastAPI (PDF/Search) service +The PDF generation service uses docker and it is stored on amazon ECR. During the first cloudformation deployment, the container is created and uploaded, but subsequent updates need to be performed manually. +We're currently using a single ECR repo (nasa-apt/prod/fastapi) to store the container and it is shared between the production and staging environments. 1) Build the container ``` -cd nasa-apt/pdf/ -# from the pdf/Readme -docker build --target prod . -t nasa-apt/prod/pdf +cd nasa-apt/fastapi/ +# from the fastapi/Readme +docker build --target prod . -t nasa-apt/prod/fastapi ``` 2) Go to the [ECR page](https://us-east-1.console.aws.amazon.com/ecr/repositories?region=us-east-1), select the correct repo and click "View Push Commands". 3) Follow steps 1, 3, and 4. @@ -121,8 +121,8 @@ aws ecs update-service --force-new-deployment --cluster --service ..`, ex: `v2.0.1`. +Releases are tied to a version number and created manually using GH's releases page. +The version in this README should be increased according to [semver](https://semver.org/) and the release tag should follow the format `v..`, ex: `v2.0.1`. The release description should have a [changelog](https://gist.github.com/vgeorge/e6fd828987b2f7d62a447df2bd132c4a) with "Features", "Improvements" and "Fixes". diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index a1fbcacf..fa06fa9b 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -47,25 +47,25 @@ Parameters: Description: The priority for the routing rule added to the load balancer. This only applies if your have multiple services which are assigned to different paths on the load balancer. - PDFContainerCpu: + FastapiContainerCpu: Type: Number Default: 512 Description: How much CPU to give the container. 1024 is 1 CPU - PDFContainerMemory: + FastapiContainerMemory: Type: Number Default: 1024 Description: How much memory in megabytes to give the container - PDFPATH: + FastapiPATH: Type: String - Default: "/pdf/*" + Default: "/fastapi/*" Description: A path on the public load balancer that this service should be connected to. Use * to send all load balancer traffic to this service. - PDFAPIPrefix: + FastapiAPIPrefix: Type: String - Default: "/pdf/" + Default: "/fastapi/" Description: API prefix as used by FastAPI in PDF service (must be consistent with PDFPATH) - PDFPriority: + FastapiPriority: Type: Number Default: 2 Description: The priority for the routing rule added to the load balancer. @@ -202,7 +202,29 @@ Resources: PubliclyAccessible: true VPCSecurityGroups: - !Ref FargateContainerSecurityGroup - + Elasticsearch: + Type: AWS::Elasticsearch::Domain + Properties: + DomainName: !Sub '${AWS::StackName}-elastic' + ElasticsearchVersion: "7.7" + ElasticsearchClusterConfig: + InstanceCount: 1 + InstanceType: "t2.small.elasticsearch" + EBSOptions: + EBSEnabled: true + Iops: 0 + VolumeSize: 10 + VolumeType: "gp2" + SnapshotOptions: + AutomatedSnapshotStartHour: 0 + AccessPolicies: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + Action: "es:*" + Resource: !Sub "arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${AWS::StackName}-elastic/*" # ECS Resources ECSCluster: Type: AWS::ECS::Cluster @@ -393,27 +415,27 @@ Resources: # The task definition. This is a simple metadata description of what # container to run, and what resource requirements it has. - PDFTaskDefinition: + FastapiTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: - Family: 'pdf' - Cpu: !Ref 'PDFContainerCpu' - Memory: !Ref 'PDFContainerMemory' + Family: 'fastapi' + Cpu: !Ref 'FastapiContainerCpu' + Memory: !Ref 'FastapiContainerMemory' NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: !Ref ECSTaskExecutionRole - TaskRoleArn: !GetAtt PDFTaskRole.Arn + TaskRoleArn: !GetAtt FastapiTaskRole.Arn ContainerDefinitions: - - Name: !Sub '${AWS::StackName}-ctr-pdf' - Cpu: !Ref 'PDFContainerCpu' - Memory: !Ref 'PDFContainerMemory' - Image: 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/prod/pdf:latest + - Name: !Sub '${AWS::StackName}-ctr-fastapi' + Cpu: !Ref 'FastapiContainerCpu' + Memory: !Ref 'FastapiContainerMemory' + Image: 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/prod/fastapi:latest PortMappings: - ContainerPort: 80 Environment: - Name: API_PREFIX - Value: !Ref 'PDFAPIPrefix' + Value: !Ref 'FastapiAPIPrefix' - Name: REST_API_ENDPOINT Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] - Name: S3_ENDPOINT @@ -422,21 +444,29 @@ Resources: Value: !Ref 'PDFsBucket' - Name: FIGURES_S3_BUCKET Value: !Ref 'FiguresBucket' + - Name: DBURL + Value: !Join ['', ['postgres://', 'masteruser', ':', + !Ref 'DBPassword', '@', + !GetAtt [DB, Endpoint.Address], + ':', !GetAtt [DB, Endpoint.Port], /, + !Ref 'DBName']] + - Name: ELASTICURL + Value: !Join ['', ['http://', !GetAtt [Elasticsearch, DomainEndpoint], ':9200']] LogConfiguration: LogDriver: 'awslogs' Options: 'awslogs-group': !Ref 'LogGroup' 'awslogs-region': !Ref 'AWS::Region' - 'awslogs-stream-prefix': 'pdf-task' + 'awslogs-stream-prefix': 'fastapi-task' # The service. The service is a resource which allows you to run multiple # copies of a type of task, and gather up their logs and metrics, as well # as monitor the number of running tasks and replace any that have crashed - PDFService: + FastapiService: Type: AWS::ECS::Service - DependsOn: PDFLoadBalancerRule + DependsOn: FastapiLoadBalancerRule Properties: - ServiceName: !Sub '${AWS::StackName}-svc-pdf' + ServiceName: !Sub '${AWS::StackName}-svc-fastapi' Cluster: !Ref ECSCluster LaunchType: FARGATE DeploymentConfiguration: @@ -451,11 +481,11 @@ Resources: Subnets: - !Ref PublicSubnetOne - !Ref PublicSubnetTwo - TaskDefinition: !Ref 'PDFTaskDefinition' + TaskDefinition: !Ref 'FastapiTaskDefinition' LoadBalancers: - - ContainerName: !Sub '${AWS::StackName}-ctr-pdf' + - ContainerName: !Sub '${AWS::StackName}-ctr-fastapi' ContainerPort: 80 - TargetGroupArn: !Ref 'PDFTargetGroup' + TargetGroupArn: !Ref 'FastapiTargetGroup' # A target group. This is used for keeping track of all the tasks, and # what IP addresses / port numbers they have. You can query it yourself, @@ -481,15 +511,15 @@ Resources: # to use the addresses yourself, but most often this target group is just # connected to an application load balancer, or network load balancer, so # it can automatically distribute traffic across all the targets. - PDFTargetGroup: + FastapiTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: - HealthCheckPath: !Ref 'PDFAPIPrefix' + HealthCheckPath: !Ref 'FastapiAPIPrefix' Matcher: HttpCode: '200' HealthCheckProtocol: HTTP TargetType: ip - Name: !Sub '${AWS::StackName}-tg-pdf' + Name: !Sub '${AWS::StackName}-tg-fastapi' Port: 80 Protocol: HTTP VpcId: !Ref VPC @@ -507,17 +537,17 @@ Resources: Priority: !Ref 'PostgRESTPriority' ListenerArn: !Ref PublicLoadBalancerListener - PDFLoadBalancerRule: + FastapiLoadBalancerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - - TargetGroupArn: !Ref 'PDFTargetGroup' + - TargetGroupArn: !Ref 'FastapiTargetGroup' Type: 'forward' Conditions: - Field: path-pattern PathPatternConfig: - Values: [!Ref 'PDFPATH'] - Priority: !Ref 'PDFPriority' + Values: [!Ref 'FastapiPATH'] + Priority: !Ref 'FastapiPriority' ListenerArn: !Ref PublicLoadBalancerListener # Figures bucket @@ -555,21 +585,6 @@ Resources: - '*' Resource: !Join ['', ['arn:aws:s3:::', !Ref 'AWS::StackName', '-', !Ref 'FiguresBucketName', '/*']] - PDFsBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Join ['', [!Ref 'AWS::StackName', '-', !Ref 'PDFsBucketName']] - PolicyDocument: - Statement: - Action: - - 's3:List*' - - 's3:Get*' - Effect: Allow - Principal: - AWS: - - '*' - Resource: !Join ['', ['arn:aws:s3:::', !Ref 'AWS::StackName', '-', !Ref 'PDFsBucketName', '/*']] - PDFsBucket: Type: AWS::S3::Bucket Properties: @@ -590,8 +605,24 @@ Resources: AllowedHeaders: - '*' + PDFsBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Join ['', [!Ref 'AWS::StackName', '-', !Ref 'PDFsBucketName']] + PolicyDocument: + Statement: + Action: + - 's3:List*' + - 's3:Get*' + Effect: Allow + Principal: + AWS: + - '*' + Resource: !Join ['', ['arn:aws:s3:::', !Ref 'AWS::StackName', '-', !Ref 'PDFsBucketName', '/*']] + + # IAM role for pdf task to have permission on the pdf cache bucket. - PDFTaskRole: + FastapiTaskRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -608,7 +639,12 @@ Resources: - Effect: Allow Action: ['s3:Put*', 's3:List*', 's3:Get*'] Resource: !Join ['', ['arn:aws:s3:::', !Ref 'AWS::StackName', '-', !Ref 'PDFsBucketName', '/*']] - + - PolicyName: elasticsearchPolicy + PolicyDocument: + Statement: + - Effect: Allow + Action: ['es:*'] + Resource: !Sub "arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${AWS::StackName}-elastic/*" LogGroup: Type: AWS::Logs::LogGroup Properties: @@ -733,7 +769,21 @@ Outputs: Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] Export: Name: !Sub '${AWS::StackName}-APIEndpoint' - PDFEndpoint: - Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName, !Ref 'PDFAPIPrefix']] + FastapiEndpoint: + Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName, !Ref 'FastapiAPIPrefix']] + Export: + Name: !Sub '${AWS::StackName}-FastapiEndpoint' + + + DBURL: + Value: !Join ['', ['postgres://', 'masteruser', ':', + !Ref 'DBPassword', '@', + !GetAtt [DB, Endpoint.Address], + ':', !GetAtt [DB, Endpoint.Port], /, + !Ref 'DBName']] + Export: + Name: !Sub '${AWS::StackName}-DBURL' + ELASTICURL: + Value: !Join ['', ['https://', !GetAtt [Elasticsearch, DomainEndpoint], ':9200']] Export: - Name: !Sub '${AWS::StackName}-PDFEndpoint' + Name: !Sub '${AWS::StackName}-ELASTICURL' diff --git a/cloudformation/deploy.sh b/cloudformation/deploy.sh index 849509bf..00d998ce 100755 --- a/cloudformation/deploy.sh +++ b/cloudformation/deploy.sh @@ -13,4 +13,5 @@ aws cloudformation deploy \ DBName=nasadb \ DBUser=masteruser \ DBPassword=$dbpassword \ + ElasticsearchDomainName=nasadb-$stackname \ --region us-east-1 --capabilities CAPABILITY_IAM diff --git a/docker-compose.yml b/docker-compose.yml index ecffba45..d20471c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,14 +42,14 @@ services: PGRST_DB_ANON_ROLE: app_user depends_on: - db - pdf-api: - build: ./pdf - image: nasa-apt/dev/pdf + fastapi: + build: ./fastapi + image: nasa-apt/dev/fastapi ports: - - "8000:8000" + - "8000:80" volumes: - type: bind - source: ./pdf/app + source: ./fastapi/app target: /app/app environment: # the boto3 library needs these AWS_* env vars, even though we are using localstack. diff --git a/pdf/.gitignore b/fastapi/.gitignore similarity index 100% rename from pdf/.gitignore rename to fastapi/.gitignore diff --git a/pdf/Dockerfile b/fastapi/Dockerfile similarity index 90% rename from pdf/Dockerfile rename to fastapi/Dockerfile index e69460af..916fd336 100644 --- a/pdf/Dockerfile +++ b/fastapi/Dockerfile @@ -14,4 +14,4 @@ RUN pip install -r requirements.txt COPY ./app /app/app FROM prod as dev -CMD uvicorn app.main:app --host 0.0.0.0 --reload +CMD uvicorn app.main:app --host 0.0.0.0 --port 80 --reload diff --git a/pdf/README.md b/fastapi/README.md similarity index 100% rename from pdf/README.md rename to fastapi/README.md diff --git a/pdf/__init__.py b/fastapi/__init__.py similarity index 100% rename from pdf/__init__.py rename to fastapi/__init__.py diff --git a/pdf/app/__init__.py b/fastapi/app/__init__.py similarity index 100% rename from pdf/app/__init__.py rename to fastapi/app/__init__.py diff --git a/pdf/app/atbd/Status.py b/fastapi/app/atbd/Status.py similarity index 100% rename from pdf/app/atbd/Status.py rename to fastapi/app/atbd/Status.py diff --git a/pdf/app/atbd/__init__.py b/fastapi/app/atbd/__init__.py similarity index 100% rename from pdf/app/atbd/__init__.py rename to fastapi/app/atbd/__init__.py diff --git a/pdf/app/atbd/checksum_atbd.py b/fastapi/app/atbd/checksum_atbd.py similarity index 100% rename from pdf/app/atbd/checksum_atbd.py rename to fastapi/app/atbd/checksum_atbd.py diff --git a/pdf/app/atbd/get_atbd.py b/fastapi/app/atbd/get_atbd.py similarity index 100% rename from pdf/app/atbd/get_atbd.py rename to fastapi/app/atbd/get_atbd.py diff --git a/pdf/app/atbd/get_status.py b/fastapi/app/atbd/get_status.py similarity index 100% rename from pdf/app/atbd/get_status.py rename to fastapi/app/atbd/get_status.py diff --git a/pdf/app/cache.py b/fastapi/app/cache.py similarity index 100% rename from pdf/app/cache.py rename to fastapi/app/cache.py diff --git a/pdf/app/latex/ATBD.tex b/fastapi/app/latex/ATBD.tex similarity index 100% rename from pdf/app/latex/ATBD.tex rename to fastapi/app/latex/ATBD.tex diff --git a/pdf/app/latex/__init__.py b/fastapi/app/latex/__init__.py similarity index 100% rename from pdf/app/latex/__init__.py rename to fastapi/app/latex/__init__.py diff --git a/pdf/app/latex/json_to_latex.py b/fastapi/app/latex/json_to_latex.py similarity index 100% rename from pdf/app/latex/json_to_latex.py rename to fastapi/app/latex/json_to_latex.py diff --git a/pdf/app/latex/serialize.py b/fastapi/app/latex/serialize.py similarity index 100% rename from pdf/app/latex/serialize.py rename to fastapi/app/latex/serialize.py diff --git a/pdf/app/main.py b/fastapi/app/main.py similarity index 93% rename from pdf/app/main.py rename to fastapi/app/main.py index badd0146..5db0d89a 100644 --- a/pdf/app/main.py +++ b/fastapi/app/main.py @@ -7,6 +7,7 @@ from fastapi.logger import logger from fastapi.responses import FileResponse, RedirectResponse, JSONResponse + from .atbd.checksum_atbd import checksum_atbd from .atbd.get_atbd import get_atbd from .atbd.get_status import get_status @@ -14,13 +15,13 @@ from .cache import Cache, CacheException from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException -from .search.searchindex import update_index, index_atbd, ELASTICURL +from .search.searchindex import update_index, index_atbd, ELASTICURL, aws_auth import asyncpg import requests import logging -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) root_path: str = environ.get("API_PREFIX", "/") rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or exit( @@ -55,7 +56,7 @@ async def startup() -> None: @app.on_event("shutdown") async def shutdown() -> None: - # await app.state.connection.remove_listener('atbd', index_atbd) + await app.state.connection.remove_listener('atbd', index_atbd) await app.state.connection.close() @@ -167,24 +168,31 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): logger.info(f"cleaned up {tmp_dir.name}") -@app.get("/reindex",) +@app.get(root_path + "reindex",) async def reindex(request: Request): """ Reindex all ATBD's into ElasticSearch """ + logger.info('Reindexing %s', ELASTICURL) results = await update_index(connection=request.app.state.connection) return JSONResponse(content=results) -@app.post("/search",) +@app.post(root_path + "search",) async def search_elastic(request: Request): """ Proxies POST json to elastic search endpoint """ url = f"{ELASTICURL}/atbd/_search" data = await request.body() - response = requests.post(url, data=data, headers=request.headers) - logger.debug(response.status_code, response.text) + logger.info("Searching %s %s", url, data) + response = requests.post( + url, + auth=aws_auth, + data=data, + headers=request.headers + ) + logger.info(response.status_code, response.text) if not response.ok: raise HTTPException( status_code=response.status_code, detail=response.text diff --git a/pdf/app/pdf/__init__.py b/fastapi/app/pdf/__init__.py similarity index 100% rename from pdf/app/pdf/__init__.py rename to fastapi/app/pdf/__init__.py diff --git a/pdf/app/pdf/latex_to_pdf.py b/fastapi/app/pdf/latex_to_pdf.py similarity index 100% rename from pdf/app/pdf/latex_to_pdf.py rename to fastapi/app/pdf/latex_to_pdf.py diff --git a/pdf/app/pdf/pdf.sh b/fastapi/app/pdf/pdf.sh similarity index 100% rename from pdf/app/pdf/pdf.sh rename to fastapi/app/pdf/pdf.sh diff --git a/pdf/app/requirements.txt b/fastapi/app/requirements.txt similarity index 96% rename from pdf/app/requirements.txt rename to fastapi/app/requirements.txt index 7b2cdfa4..c58fa6b2 100644 --- a/pdf/app/requirements.txt +++ b/fastapi/app/requirements.txt @@ -28,6 +28,7 @@ pydantic==1.5.1 python-dateutil==2.8.1 pytz==2020.1 requests==2.23.0 +requests-aws4auth==1.0 s3transfer==0.3.3 shutilwhich==1.1.0 six==1.15.0 diff --git a/pdf/app/search/__init__.py b/fastapi/app/search/__init__.py similarity index 100% rename from pdf/app/search/__init__.py rename to fastapi/app/search/__init__.py diff --git a/pdf/app/search/searchindex.py b/fastapi/app/search/searchindex.py similarity index 84% rename from pdf/app/search/searchindex.py rename to fastapi/app/search/searchindex.py index 3b215fe0..a82010d0 100644 --- a/pdf/app/search/searchindex.py +++ b/fastapi/app/search/searchindex.py @@ -1,4 +1,6 @@ import requests +from requests_aws4auth import AWS4Auth +import boto3 import sys from os import environ from typing import Optional, Dict @@ -9,10 +11,26 @@ import logging logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) ELASTICURL: str = environ.get("ELASTICURL") or sys.exit( "ELASTICURL env var required" ) +logging.debug('ELASTICURL %s', ELASTICURL) + + +def aws_auth(): + logger.info('Getting AWS Auth Credentials') + region = 'us-east-1' + credentials = boto3.Session().get_credentials() + awsauth = AWS4Auth( + credentials.access_key, + credentials.secret_key, + region, + 'es', + session_token=credentials.token + ) + return awsauth def prep_json(json: Dict) -> Dict: @@ -44,10 +62,14 @@ def send_to_elastic(json: Dict): """ json = prep_json(json).encode("utf-8") url = f"{ELASTICURL}/atbd/_bulk" + logger.info("sending %s %s", json, url) response = requests.post( - url, data=json, headers={"Content-Type": "application/json"} + url, + auth=aws_auth(), + data=json, + headers={"Content-Type": "application/json"} ) - logger.debug(response.status_code, response.text) + logger.info("%s %s %s", url, response.status_code, response.text) if not response.ok: raise HTTPException( status_code=response.status_code, detail=response.text @@ -138,8 +160,9 @@ async def update_index( """ update data for Elastic from PostgreSQL Database """ - logger.debug("Updating Index for", atbd_id, atbd_version) + logger.info("Updating Index for %s %s", atbd_id, atbd_version) content = await get_index(connection, atbd_id, atbd_version) + logger.info('dbcontent %s', content) results = send_to_elastic(content) return results @@ -157,7 +180,7 @@ def index_atbd( def callback( connection: asyncpg.connection, pid: int, channel: str, payload: str ): - logger.debug("Listen", pid, channel, payload) + logger.info("Listen %s %s %s", pid, channel, payload) asyncio.ensure_future( update_index(connection=connection, atbd_id=int(payload)) ) From df7408e4ce83949836a9af2521439ba398c5f4ac Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 21 Sep 2020 13:30:55 -0500 Subject: [PATCH 18/59] working elastic with cloudformation on aws --- cloudformation/cloudformation.yaml | 2 +- fastapi/app/search/searchindex.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index fa06fa9b..b34208df 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -451,7 +451,7 @@ Resources: ':', !GetAtt [DB, Endpoint.Port], /, !Ref 'DBName']] - Name: ELASTICURL - Value: !Join ['', ['http://', !GetAtt [Elasticsearch, DomainEndpoint], ':9200']] + Value: !Join ['', ['https://', !GetAtt [Elasticsearch, DomainEndpoint]]] LogConfiguration: LogDriver: 'awslogs' Options: diff --git a/fastapi/app/search/searchindex.py b/fastapi/app/search/searchindex.py index a82010d0..46bd827a 100644 --- a/fastapi/app/search/searchindex.py +++ b/fastapi/app/search/searchindex.py @@ -16,7 +16,7 @@ ELASTICURL: str = environ.get("ELASTICURL") or sys.exit( "ELASTICURL env var required" ) -logging.debug('ELASTICURL %s', ELASTICURL) +logger.info('ELASTICURL %s', ELASTICURL) def aws_auth(): @@ -30,6 +30,7 @@ def aws_auth(): 'es', session_token=credentials.token ) + logger.info('AWS Auth: %s', awsauth) return awsauth @@ -62,10 +63,11 @@ def send_to_elastic(json: Dict): """ json = prep_json(json).encode("utf-8") url = f"{ELASTICURL}/atbd/_bulk" - logger.info("sending %s %s", json, url) + auth = aws_auth() + logger.info("sending %s %s using auth: %s", json, url, auth) response = requests.post( url, - auth=aws_auth(), + auth=auth, data=json, headers={"Content-Type": "application/json"} ) From 1237ade66d0802aca78980a57038ee90c2f1d2fb Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Fri, 2 Oct 2020 14:09:50 -0500 Subject: [PATCH 19/59] initial saml work --- cloudformation/cloudformation.yaml | 12 +-- fastapi/app/saml.py | 141 +++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 fastapi/app/saml.py diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index b34208df..897d6e8f 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -298,12 +298,12 @@ Resources: Properties: HealthCheckIntervalSeconds: 6 HealthCheckPath: / - HealthCheckProtocol: HTTP + HealthCheckProtocol: HTTPS HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 - Name: !Join ['-', [!Ref 'AWS::StackName', 'drop-1']] - Port: 80 - Protocol: HTTP + Name: !Join ['-', [!Ref 'AWS::StackName', 'dummyTarget']] + Port: 443 + Protocol: HTTPS UnhealthyThresholdCount: 2 VpcId: !Ref 'VPC' @@ -314,8 +314,8 @@ Resources: - TargetGroupArn: !Ref 'DummyTargetGroupPublic' Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' - Port: 80 - Protocol: HTTP + Port: 443 + Protocol: HTTPS # This is a role which is used by the ECS tasks themselves. ECSTaskExecutionRole: diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py new file mode 100644 index 00000000..8e79ce3d --- /dev/null +++ b/fastapi/app/saml.py @@ -0,0 +1,141 @@ +import os + +from onelogin.saml2.auth import OneLogin_Saml2_Auth +from onelogin.saml2.utils import OneLogin_Saml2_Utils + +from flask import Flask, redirect + +from starlette.requests import HTTPConnection +from starlette.middleware.sessions import SessionMiddleware + +app.add_middleware(SessionMiddleware, secret_key="ASFDASDFASWEsdfsfsadfas") + +secret_key = 'lksjlksjlsdkajsdjlkf' +saml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml') + + +def init_saml_auth(req): + auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) + return auth + + +def prepare_request(request): + return { + 'https': 'on' if request.url.scheme == 'https' else 'off', + 'http_host': request.client.host, + 'server_port': request.url.port, + 'script_name': request.url.path, + 'get_data': request.query_params.copy(), + 'post_data': request.form.copy(), + } + + +def prepare_session(request): + session = request.session + rs = {} + name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None + if 'samlNameId' in session: + rs['name_id'] = session['samlNameId'] + if 'samlSessionIndex' in session: + rs['session_index'] = session['samlSessionIndex'] + if 'samlNameIdFormat' in session: + rs['name_id_format'] = session['samlNameIdFormat'] + if 'samlNameIdNameQualifier' in session: + rs['name_id_nq'] = session['samlNameIdNameQualifier'] + if 'samlNameIdSPNameQualifier' in session: + rs['name_id_spnq'] = session['samlNameIdSPNameQualifier'] + return rs + + +class SamlAuth: + def __init__(self, request): + self.auth = authenticate(request) + self.session = request.session + + def authenticate(self, request): + req = prepare_request(request) + auth = init_saml_auth(req) + + +@app.get('saml/sso') +def sso(): + return redirect(auth.login()) + +@app.get('saml/sso2') +def sso2(): + return_to = f'{req.host}/attrs' + return redirect(auth.login(return_to)) + +@app.get('saml/acs') +def acs(): + request_id = None + session = request.session + if 'AuthNRequestID' in session: + request_id = session['AuthNRequestID'] + + auth.process_response(request_id=request_id) + errors = auth.get_errors() + not_auth_warn = not auth.is_authenticated() + if len(errors) == 0: + if 'AuthNRequestID' in session: + del session['AuthNRequestID'] + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlNameIdFormat'] = auth.get_nameid_format() + session['samlNameIdNameQualifier'] = auth.get_nameid_nq() + session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + if 'RelayState' in request.form and self_url != request.form['RelayState']: + return redirect(auth.redirect_to(request.form['RelayState'])) + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + + +@app.get('saml/slo') +def slo(): + session = prepare_session(request) + return redirect(auth.logout(**session)) + + +@app.get('saml/sls') +def sls(): + request_id = None + session = request.session + if 'LogoutRequestID' in session: + request_id = session['LogoutRequestID'] + dscb = lambda: session.clear() + url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) + errors = auth.get_errors() + if len(errors) == 0: + if url is not None: + return redirect(url) + else: + success_slo = True + elif auth.get_settings().is_debug_active(): + error_reason = auth.get_last_error_reason() + + +@app.get('saml/attrs') +def attrs(): + session = request.session + if len(session['samlUserdata']) > 0: + attributes = session['samlUserdata'].items() + return render_template( + 'attrs.html', + paint_logout=paint_logout, + attributes=attributes + ) + + +@app.get('/metadata') +def metadata(): + settings = auth.get_settings() + metadata = auth.get_sp_metadata() + errors = settings.validate_metadata() + if len(errors) == 0: + resp = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(', '.join(errors), 500) + return resp From 26ef9843d52eb6159b510ee94b45bb717bf2fb08 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Fri, 2 Oct 2020 15:31:19 -0500 Subject: [PATCH 20/59] fix auth issue --- fastapi/app/main.py | 3 ++- fastapi/app/search/searchindex.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fastapi/app/main.py b/fastapi/app/main.py index 59cb7b4e..1cc8968a 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -202,9 +202,10 @@ async def search_elastic(request: Request): url = f"{ELASTICURL}/atbd/_search" data = await request.body() logger.info("Searching %s %s", url, data) + auth = aws_auth() response = requests.post( url, - auth=aws_auth, + auth=auth, data=data, headers=request.headers ) diff --git a/fastapi/app/search/searchindex.py b/fastapi/app/search/searchindex.py index 46bd827a..c8993860 100644 --- a/fastapi/app/search/searchindex.py +++ b/fastapi/app/search/searchindex.py @@ -11,7 +11,7 @@ import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) ELASTICURL: str = environ.get("ELASTICURL") or sys.exit( "ELASTICURL env var required" From c43754be4ac024c175a4a4538a2b902349486dc9 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Tue, 13 Oct 2020 09:17:56 -0500 Subject: [PATCH 21/59] fix for proxy issue with elastic search --- fastapi/app/main.py | 4 ++-- fastapi/testquery.json | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 fastapi/testquery.json diff --git a/fastapi/app/main.py b/fastapi/app/main.py index 1cc8968a..dbbd44be 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -207,9 +207,9 @@ async def search_elastic(request: Request): url, auth=auth, data=data, - headers=request.headers + headers={"Content-Type": "application/json"} ) - logger.info(response.status_code, response.text) + logger.info('status:%s response:%s', response.status_code, response.text) if not response.ok: raise HTTPException( status_code=response.status_code, detail=response.text diff --git a/fastapi/testquery.json b/fastapi/testquery.json new file mode 100644 index 00000000..5be64254 --- /dev/null +++ b/fastapi/testquery.json @@ -0,0 +1,25 @@ +{ + "query": { + "bool": { + "must": [ + { + "multi_match": { + "query": "lorem" + } + } + ], + "filter": [ + { + "match": { + "status": "draft OR published" + } + } + ] + } + }, + "highlight": { + "fields": { + "*": {} + } + } +} From 6dcd224f0ab585294faf7749f618f1171d0d3ac6 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Wed, 21 Oct 2020 15:12:28 -0500 Subject: [PATCH 22/59] add env var for frontend url to use for cors --- fastapi/app/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fastapi/app/main.py b/fastapi/app/main.py index dbbd44be..d0cf4942 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -39,6 +39,10 @@ ) DBURL: str = environ.get("DBURL") or exit("DBURL env var required") +frontend_url: str = environ.get("APT_FRONTEND_URL") or exit( + "APT_FRONTEND_URL env var required" +) + app: FastAPI = FastAPI() cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) @@ -46,6 +50,7 @@ origins = [ "http://localhost:3000", "http://localhost:3006", + frontend_url, ] app.add_middleware( From 8047787c087a073f93d4fd6cb84645946059c312 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Mon, 26 Oct 2020 09:37:15 -0500 Subject: [PATCH 23/59] add sync script for moving over database --- .env.sample | 3 +++ syncdb.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100755 syncdb.sh diff --git a/.env.sample b/.env.sample index aa77ed97..92ac157f 100644 --- a/.env.sample +++ b/.env.sample @@ -15,3 +15,6 @@ FIGURES_S3_BUCKET=nasa-apt-dev-figures # postgrest api REST_API_ENDPOINT=http://rest-api:3000 + +# Frontend Url used to set CORS origin for FastAPI +APT_FRONTEND_URL=http://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com diff --git a/syncdb.sh b/syncdb.sh new file mode 100755 index 00000000..fcccae81 --- /dev/null +++ b/syncdb.sh @@ -0,0 +1,46 @@ +#!/bin/bash +get_output(){ + echo $outputs | jq --raw-output ".[][] | select(.OutputKey==\"$1\").OutputValue" +} +outputs=`aws cloudformation describe-stacks --region us-east-1 --stack-name $1 --output json --query 'Stacks[*].Outputs[*]'` + +fromdb=`get_output PGConnection` +fromfigures=`get_output FiguresBucket` +froms3=`get_output S3Endpoint` +fromurl=${froms3}/${fromfigures} + +outputs=`aws cloudformation describe-stacks --region us-east-1 --stack-name $2 --output json --query 'Stacks[*].Outputs[*]'` + +todb=`get_output PGConnection` +tofigures=`get_output FiguresBucket` +tos3=`get_output S3Endpoint` +tourl=${tos3}/${tofigures} + +echo "syncing data from $fromfigures to $tofigures" +aws s3 sync --delete s3://${fromfigures} s3://${tofigures} + +echo "syncing data from $fromdb to $todb while replacing $fromurl with $tourl" +psql -e -1 -v ON_ERROR_STOP=1 $todb < Date: Wed, 28 Oct 2020 14:14:06 -0500 Subject: [PATCH 24/59] saml auth --- docker-compose.yml | 2 + fastapi/Dockerfile | 2 +- fastapi/app/main.py | 21 ++- fastapi/app/requirements.txt | 3 + fastapi/app/saml.py | 301 +++++++++++++++++++++++------------ 5 files changed, 217 insertions(+), 112 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d20471c7..9afea41f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,8 @@ services: FIGURES_S3_BUCKET: $FIGURES_S3_BUCKET DBURL: postgres://masteruser:password@db:5432/nasadb ELASTICURL: http://elastic:9200 + IDP_METADATA_URL: $IDP_METADATA_URL + FASTAPI_HOST: http://localhost:8000 depends_on: - localstack - rest-api diff --git a/fastapi/Dockerfile b/fastapi/Dockerfile index 916fd336..7de17339 100644 --- a/fastapi/Dockerfile +++ b/fastapi/Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update \ locales \ texlive-latex-recommended \ texlive-xetex \ - wget \ + wget libxml2-dev libxmlsec1-dev libxmlsec1-openssl pkg-config gcc \ && rm -rf /var/lib/apt/lists/* \ && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 ENV LANG en_US.utf8 diff --git a/fastapi/app/main.py b/fastapi/app/main.py index 59cb7b4e..9d791fb7 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -3,11 +3,11 @@ from tempfile import TemporaryDirectory from typing import Union, Dict, Type -from fastapi import FastAPI, HTTPException, BackgroundTasks, Request +from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, Depends from fastapi.logger import logger from fastapi.responses import FileResponse, RedirectResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware - +from starlette.middleware.sessions import SessionMiddleware from .atbd.checksum_atbd import checksum_atbd from .atbd.get_atbd import get_atbd @@ -17,6 +17,8 @@ from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException from .search.searchindex import update_index, index_atbd, ELASTICURL, aws_auth +from .saml import router as saml +from .saml import User, require_user import asyncpg import requests @@ -44,6 +46,7 @@ origins = [ + "*", "http://localhost:3000", "http://localhost:3006", ] @@ -56,6 +59,12 @@ allow_headers=["*"], ) +app.add_middleware( + SessionMiddleware, + secret_key = 'lk23j4l24jk23789098ulkhjljkjlk' +) + +app.include_router(saml) @app.on_event("startup") @@ -149,13 +158,13 @@ def atbd_pdf_handler( @app.get(root_path + "atbds/id/{atbd_id}.pdf") -def get_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks): +def get_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks, user: User=Depends(require_user)): atbd_doc = get_atbd(atbd_id=atbd_id) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) @app.get(root_path + "atbds/alias/{alias}.pdf") -def get_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks): +def get_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks, user: User=Depends(require_user)): atbd_doc = get_atbd(alias=alias) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) @@ -185,7 +194,7 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): @app.get(root_path + "reindex",) -async def reindex(request: Request): +async def reindex(request: Request, user: User=Depends(require_user)): """ Reindex all ATBD's into ElasticSearch """ @@ -195,7 +204,7 @@ async def reindex(request: Request): @app.post(root_path + "search",) -async def search_elastic(request: Request): +async def search_elastic(request: Request, user: User=Depends(require_user)): """ Proxies POST json to elastic search endpoint """ diff --git a/fastapi/app/requirements.txt b/fastapi/app/requirements.txt index c58fa6b2..4d2a06b6 100644 --- a/fastapi/app/requirements.txt +++ b/fastapi/app/requirements.txt @@ -39,3 +39,6 @@ uvicorn==0.11.3 uvloop==0.14.0 websockets==8.1 yarg==0.1.9 +itsdangerous==1.1.0 +python3-saml==1.9.0 +python-multipart diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index 8e79ce3d..723be4ea 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -1,141 +1,232 @@ import os - +from os import environ from onelogin.saml2.auth import OneLogin_Saml2_Auth -from onelogin.saml2.utils import OneLogin_Saml2_Utils - -from flask import Flask, redirect +from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + +from fastapi import APIRouter, Depends, Response, Request, HTTPException +from starlette.responses import RedirectResponse +from starlette.datastructures import UploadFile +from typing import Union + +from fastapi.logger import logger +import logging + +logger.setLevel(logging.DEBUG) + +host: str = environ.get("FASTAPI_HOST") or exit( + "FASTAPI_HOST env var required" +) + +idp_metadata_url: str = environ.get("IDP_METADATA_URL") or exit( + "IDP_METADATA_URL env var required" +) + + +router = APIRouter() + +base_path = os.path.dirname(os.path.abspath(__file__)) + +idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + idp_metadata_url +) +logger.debug('test debug') + +init_settings = { + "strict": True, + "debug": True, + "sp": { + "entityId": f"{host}/metadata", + "assertionConsumerService": { + "url": f"{host}/acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + }, + "singleLogoutService": { + "url": f"{host}/sls", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "x509cert": "", + "privateKey": "", + }, + "idp": idp_data["idp"], + "security": { + "nameIdEncrypted": False, + "authnRequestsSigned": False, + "logoutRequestSigned": False, + "logoutResponsesSigned": False, + "signMetadata": False, + "wantMessagesSigned": False, + "wantAssertionsSigned": False, + "wantNameId": False, + "wantNameIdEncrypted": False, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + }, +} + +settings = OneLogin_Saml2_Settings( + settings=init_settings, + custom_base_path=base_path, +) -from starlette.requests import HTTPConnection -from starlette.middleware.sessions import SessionMiddleware -app.add_middleware(SessionMiddleware, secret_key="ASFDASDFASWEsdfsfsadfas") - -secret_key = 'lksjlksjlsdkajsdjlkf' -saml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml') +class SamlAuth: + def __init__( + self, + request: Request, + ): + self.request = request + self.session = request.session + self.user = None + userdata = request.session.get("samlUserdata") + if userdata and len(userdata) > 0: + self.user = userdata.items() + self.auth = None + self.acs_url = request.url_for("acs") + self.sso_url = request.url_for("sso") + self.metadata_url = request.url_for("metadata") + self.attrs_url = request.url_for("attrs") + self.slo_url = request.url_for("slo") + url = request.url + self.base_url = f'{url.scheme}://{url.hostname}:{url.port}' + + async def prepare_saml_request(self): + url = self.request.url + get_data = self.request.query_params._dict + form = await self.request.form() + post_data = {} + for key, value in form.multi_items(): + if not isinstance(value, UploadFile): + post_data[key] = value + + return { + "https": "on" if url.scheme == "https" else "off", + "http_host": url.hostname, + "server_port": url.port, + "script_name": url.path, + "get_data": get_data, + "post_data": post_data, + } + async def get_auth(self): + self.saml_request = await self.prepare_saml_request() + self.auth = OneLogin_Saml2_Auth( + self.saml_request, + old_settings=settings, + ) + return self.auth -def init_saml_auth(req): - auth = OneLogin_Saml2_Auth(req, custom_base_path=app.config['SAML_PATH']) - return auth +async def saml_auth(request: Request): + saml = SamlAuth(request) + await saml.get_auth() + return saml -def prepare_request(request): - return { - 'https': 'on' if request.url.scheme == 'https' else 'off', - 'http_host': request.client.host, - 'server_port': request.url.port, - 'script_name': request.url.path, - 'get_data': request.query_params.copy(), - 'post_data': request.form.copy(), - } +User = Union[dict, None] -def prepare_session(request): - session = request.session - rs = {} - name_id = session_index = name_id_format = name_id_nq = name_id_spnq = None - if 'samlNameId' in session: - rs['name_id'] = session['samlNameId'] - if 'samlSessionIndex' in session: - rs['session_index'] = session['samlSessionIndex'] - if 'samlNameIdFormat' in session: - rs['name_id_format'] = session['samlNameIdFormat'] - if 'samlNameIdNameQualifier' in session: - rs['name_id_nq'] = session['samlNameIdNameQualifier'] - if 'samlNameIdSPNameQualifier' in session: - rs['name_id_spnq'] = session['samlNameIdSPNameQualifier'] - return rs +async def require_user(request: Request) -> User: + saml = SamlAuth(request) + if saml.user is not None: + return saml.user + raise HTTPException(status_code=401) -class SamlAuth: - def __init__(self, request): - self.auth = authenticate(request) - self.session = request.session - def authenticate(self, request): - req = prepare_request(request) - auth = init_saml_auth(req) +async def get_user(request: Request) -> User: + saml = SamlAuth(request) + return saml.user -@app.get('saml/sso') -def sso(): - return redirect(auth.login()) +@router.get("/sso") +async def sso( + saml: SamlAuth = Depends(saml_auth), return_to: str = host +): + return RedirectResponse(url=saml.auth.login(return_to)) -@app.get('saml/sso2') -def sso2(): - return_to = f'{req.host}/attrs' - return redirect(auth.login(return_to)) -@app.get('saml/acs') -def acs(): +@router.post("/acs") +async def acs( + saml: SamlAuth = Depends(saml_auth), +): + auth = saml.auth + session = saml.session + saml_request = saml.saml_request request_id = None - session = request.session - if 'AuthNRequestID' in session: - request_id = session['AuthNRequestID'] - + if "AuthNRequestID" in session: + request_id = session["AuthNRequestID"] auth.process_response(request_id=request_id) errors = auth.get_errors() - not_auth_warn = not auth.is_authenticated() if len(errors) == 0: - if 'AuthNRequestID' in session: - del session['AuthNRequestID'] - session['samlUserdata'] = auth.get_attributes() - session['samlNameId'] = auth.get_nameid() - session['samlNameIdFormat'] = auth.get_nameid_format() - session['samlNameIdNameQualifier'] = auth.get_nameid_nq() - session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() - session['samlSessionIndex'] = auth.get_session_index() - self_url = OneLogin_Saml2_Utils.get_self_url(req) - if 'RelayState' in request.form and self_url != request.form['RelayState']: - return redirect(auth.redirect_to(request.form['RelayState'])) + if "AuthNRequestID" in session: + del session["AuthNRequestID"] + session["samlUserdata"] = auth.get_attributes() + session["samlNameId"] = auth.get_nameid() + session["samlNameIdFormat"] = auth.get_nameid_format() + session["samlNameIdNameQualifier"] = auth.get_nameid_nq() + session["samlNameIdSPNameQualifier"] = auth.get_nameid_spnq() + session["samlSessionIndex"] = auth.get_session_index() + self_url = saml.acs_url + RelayState = saml_request.get("post_data").get("RelayState", None) + if RelayState and self_url != RelayState: + return RedirectResponse(url=RelayState, status_code=303) + else: + return RedirectResponse(url=saml.attrs_url, status_code=303) elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() + return {"authorization_error": error_reason} + + +@router.get("/slo") +@router.post("/slo") +async def slo( + saml: SamlAuth = Depends(saml_auth), return_to: str = host +): + auth = saml.auth + session = saml.session + url = auth.logout( + return_to=return_to, + name_id=session.get("samlNameId", None), + session_index=session.get("samlSessionIndex", None), + nq=session.get("samlNameIdNameQualifier", None), + name_id_format=session.get("samlNameIdFormat", None), + spnq=session.get("samlNameIdSPNameQualifier", None), + ) + return RedirectResponse(url=url) -@app.get('saml/slo') -def slo(): - session = prepare_session(request) - return redirect(auth.logout(**session)) - - -@app.get('saml/sls') -def sls(): +@router.get("/sls") +@router.post("/sls") +async def sls( + saml: SamlAuth = Depends(saml_auth), RelayState: str = host +): request_id = None - session = request.session - if 'LogoutRequestID' in session: - request_id = session['LogoutRequestID'] - dscb = lambda: session.clear() - url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) + session = saml.session + auth = saml.auth + if "LogoutRequestID" in session: + request_id = session["LogoutRequestID"] + auth.process_slo(request_id=request_id, delete_session_cb=session.clear) + session.clear() errors = auth.get_errors() if len(errors) == 0: - if url is not None: - return redirect(url) - else: - success_slo = True + return RedirectResponse(url=RelayState, status_code=303) elif auth.get_settings().is_debug_active(): error_reason = auth.get_last_error_reason() + return {"logout_error": error_reason} -@app.get('saml/attrs') -def attrs(): - session = request.session - if len(session['samlUserdata']) > 0: - attributes = session['samlUserdata'].items() - return render_template( - 'attrs.html', - paint_logout=paint_logout, - attributes=attributes - ) +@router.get("/attrs") +@router.post("/attrs") +async def attrs(user: User = Depends(require_user)): + return user -@app.get('/metadata') -def metadata(): - settings = auth.get_settings() - metadata = auth.get_sp_metadata() - errors = settings.validate_metadata() +@router.get("/metadata") +async def metadata(saml: SamlAuth = Depends(saml_auth)): + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) if len(errors) == 0: - resp = make_response(metadata, 200) - resp.headers['Content-Type'] = 'text/xml' + return Response(content=metadata, media_type="application/xml") else: - resp = make_response(', '.join(errors), 500) - return resp + return Response(content=", ".join(errors), status_code=500) From 919a1d202e76a0434cace9ce53a5137fb6a468c0 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Wed, 28 Oct 2020 17:10:00 -0500 Subject: [PATCH 25/59] initial somewhat working cloudformation for saml --- fastapi/app/saml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index 723be4ea..6ec67ceb 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -17,6 +17,7 @@ host: str = environ.get("FASTAPI_HOST") or exit( "FASTAPI_HOST env var required" ) +host = str.lower(host) idp_metadata_url: str = environ.get("IDP_METADATA_URL") or exit( "IDP_METADATA_URL env var required" From 7870e6f347834be7d3123dd39f451f2edda8fa9a Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 24 Nov 2020 20:09:18 -0500 Subject: [PATCH 26/59] added new endpoint atbd journal type document generation --- pdf/app/latex/ATBD_JOURNAL.tex | 102 ++++++++ pdf/app/latex/json_to_latex.py | 34 ++- pdf/app/latex/serialize.py | 460 +++++++++++++++++++++------------ pdf/app/main.py | 63 +++-- 4 files changed, 454 insertions(+), 205 deletions(-) create mode 100644 pdf/app/latex/ATBD_JOURNAL.tex diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex new file mode 100644 index 00000000..63732249 --- /dev/null +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -0,0 +1,102 @@ +\newcommand{\AlgDesc}{ +Some other text +\begin{equation} \int_0^\infty x^2 dx +\end{equation} Plus some more text +} + +\providecommand{\ScientificTheory}{} +\providecommand{\Contacts}{} +\providecommand{\AlgorithmImplementations}{} +\providecommand{\AlgorithmUsageConstraints}{} +\providecommand{\PerformanceAssessmentValidationMethods}{} +\providecommand{\PerformanceAssessmentValidationUncertainties}{} +\providecommand{\PerformanceAssessmentValidationErrors}{} + +\title{\ATBDTitle} +\date{\today} + +\documentclass[12pt]{article} +\usepackage{fontspec} +\usepackage{color} %May be necessary if you want to color links +\usepackage{hyperref} +\usepackage{url} +\usepackage{booktabs} +\usepackage{graphicx} +\usepackage{float} + +\setmainfont{Latin Modern Math} %Using a font with good unicode math symbols +% coverage to support the use of unicode math symbols in LaTeX text mode +% See list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf +\hypersetup{ + colorlinks=false, %set true if you want colored links + linktoc=all, %set to all if you want both sections and subsections linked + linkcolor=blue, %choose some color if you want links to stand out +} + +\begin{document} +\maketitle + +\section{Introduction} +\Introduction + +\section{Historical Perspective} +\HistoricalPerspective + +\section{Algorithm Description} + +\subsection{Scientific Theory} +\ScientificTheory + +\subsubsection{Assumptions} +\ScientificTheoryAssumptions + +\subsection{Mathematical Theory} +\MathematicalTheory + +\subsubsection{Assumptions} +\MathematicalTheoryAssumptions + +\subsection{Algorithm Input Variables} +\AlgorithmInputVariables + +\subsection{Algorithm Output Variables} +\AlgorithmOutputVariables + +\section{Algorithm Implementations} +\AlgorithmImplementations + +\section{Algorithm Usage Constraints} +\AlgorithmUsageConstraints + +\section{Performance Assessment Validation Methods} +\PerformanceAssessmentValidationMethods + +\section{Performance Assessment Validation Uncertainties} +\PerformanceAssessmentValidationUncertainties + +\section{Performance Assessment Validation Errors} +\PerformanceAssessmentValidationErrors + +\section{Data Access Input Data} +\DataAccessInputData + +\section{Data Access Output Data} +\DataAccessOutputData + +\section{Data Access Related URLs} +\DataAccessRelatedUrls + +\section{Discussion} +\Discussion + +\section{Acknowledgements} +\Acknowledgements + +\section{Contacts} +\Contacts + +\bibliographystyle{abbrv} +\bibliography{main} + +\end{document} + diff --git a/pdf/app/latex/json_to_latex.py b/pdf/app/latex/json_to_latex.py index b7370eee..d747a3e9 100644 --- a/pdf/app/latex/json_to_latex.py +++ b/pdf/app/latex/json_to_latex.py @@ -8,17 +8,18 @@ from typing import Dict, Tuple, Any, Union, IO, Final here: Final[str] = os.path.dirname(os.path.realpath(__file__)) -serialize_py: Final[str] = f'{here}/serialize.py' -atbd_tex: Final[str] = f'{here}/ATBD.tex' -main_bib: Final[str] = 'main.bib' -encoding: Final[str] = 'utf8' +serialize_py: Final[str] = f"{here}/serialize.py" +atbd_tex: Final[str] = f"{here}/ATBD.tex" +atbd_journal_tex: Final[str] = f"{here}/ATBD_JOURNAL.tex" +main_bib: Final[str] = "main.bib" +encoding: Final[str] = "utf8" class JsonToLatexException(Exception): pass -def json_to_latex(atbd_doc: Dict, tmp_dir: str) -> Tuple[str, str]: +def json_to_latex(atbd_doc: Dict, tmp_dir: str, journal: bool) -> Tuple[str, str]: """ Serialize atbd json to latex. This simply wraps the prototype shell script originally from https://github.com/developmentseed/nasa-apt/blob/eb0b6bc897efa29284dd11b4e46b085f388d9743/ecs/tex/serialize.py @@ -33,26 +34,31 @@ def json_to_latex(atbd_doc: Dict, tmp_dir: str) -> Tuple[str, str]: """ # serialize.py expects this template file, so copy it into tmp_dir. - copyfile(atbd_tex, f'{tmp_dir}/ATBD.tex') + if journal: + copyfile(atbd_journal_tex, f"{tmp_dir}/ATBD_JOURNAL.tex") + else: + copyfile(atbd_tex, f"{tmp_dir}/ATBD.tex") json_fp: IO[str] - with NamedTemporaryFile(dir=tmp_dir, mode='w') as json_fp: + with NamedTemporaryFile(dir=tmp_dir, mode="w") as json_fp: tmp_name: Final[str] = json_fp.name json_fp.write(json.dumps(atbd_doc)) json_fp.flush() completed: Union[CompletedProcess[bytes], CompletedProcess[Any]] = run( - args=[serialize_py, tmp_name], + args=[serialize_py, tmp_name, str(journal)], capture_output=True, cwd=tmp_dir, - encoding=encoding + encoding=encoding, ) if completed.returncode != 0: # for debugging purposes, return the stdout in addition to the stderr - raise JsonToLatexException({'stderr': completed.stderr, 'stdout': completed.stdout}) - tex_filename: Final[str] = f'{tmp_name}.tex' - bib_filename: Final[str] = f'{tmp_dir}/{main_bib}' + raise JsonToLatexException( + {"stderr": completed.stderr, "stdout": completed.stdout} + ) + tex_filename: Final[str] = f"{tmp_name}.tex" + bib_filename: Final[str] = f"{tmp_dir}/{main_bib}" if not Path(tex_filename).exists(): - raise JsonToLatexException(f'expect intermediate file: {tex_filename}') + raise JsonToLatexException(f"expect intermediate file: {tex_filename}") if not Path(bib_filename).exists(): - raise JsonToLatexException(f'expect intermediate file: {bib_filename}') + raise JsonToLatexException(f"expect intermediate file: {bib_filename}") return tex_filename, bib_filename diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index acc48d88..70e60b72 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -30,340 +30,456 @@ # from https://stackoverflow.com/questions/19053707/converting-snake-case-to-lower-camel-case-lowercamelcase def toCamelCase(snake_str): - components = snake_str.split('_') + components = snake_str.split("_") # We capitalize the first letter of each component except the first one # with the 'title' method and join them together. - return ''.join(x.title() for x in components) + return "".join(x.title() for x in components) + def toSpaceCase(snake_str): - components = snake_str.split('_') - return ' '.join(x.title() for x in components) + components = snake_str.split("_") + return " ".join(x.title() for x in components) + def processTable(nodeRows): tableList = [] for rows in nodeRows: tableList.append([]) - for row in rows['nodes']: - for cell in row['nodes']: + for row in rows["nodes"]: + for cell in row["nodes"]: # Since we kept each cell in the table as a flexible type, they must be processed as generic WYSIWYG elements tableList[-1].append(processWYSIWYGElement(cell)[0]) columnNames = tableList.pop(0) - pd.set_option('display.max_colwidth', 1000) + pd.set_option("display.max_colwidth", 1000) df = pd.DataFrame(tableList, columns=columnNames) latexTable = df.to_latex(index=False, escape=False, header=False) return latexTable + def addMarkup(text, marks): for mark in marks: - markupType = mark['type'] - if markupType == 'italic': - text= f'\\textit{{{text}}}' - elif markupType == 'bold': - text= f'\\textbf{{{text}}}' - elif markupType == 'underline': - text= f'\\underline{{{text}}}' - elif markupType == 'subscript': - text = f'\\textsubscript{{{text}}}' - elif markupType == 'superscript': - text = f'\\textsuperscript{{{text}}}' + markupType = mark["type"] + if markupType == "italic": + text = f"\\textit{{{text}}}" + elif markupType == "bold": + text = f"\\textbf{{{text}}}" + elif markupType == "underline": + text = f"\\underline{{{text}}}" + elif markupType == "subscript": + text = f"\\textsubscript{{{text}}}" + elif markupType == "superscript": + text = f"\\textsuperscript{{{text}}}" return text + def whiteSpaceStrip(text): - text = text.replace('\n', '\\\\') - while (text[:2].strip() == '\\\\'): + text = text.replace("\n", "\\\\") + while text[:2].strip() == "\\\\": text = text[2:] - while (text[-2:].strip() == '\\\\'): + while text[-2:].strip() == "\\\\": text = text[:-2] # The below line should perhaps be moved to the escapeSpecialChars function - text = text.replace('/', '\/') + text = text.replace("/", "\/") return text + def preserveStyle(text): text = whiteSpaceStrip(text) text = escapeSpecialChars(text) return text -#TODO: This is an incomplete list of special characters which cause errors when not escaped in LaTeX code - more will need to be added, or a different method is needed to escape them + +# TODO: This is an incomplete list of special characters which cause errors when not escaped in LaTeX code - more will need to be added, or a different method is needed to escape them def escapeSpecialChars(text): - return text.replace('%', '\%').replace('&', '\&').replace('_', '\_') + return text.replace("%", "\%").replace("&", "\&").replace("_", "\_") + def processText(nodes): - to_return = '' + to_return = "" for node in nodes: - if node['object'] == 'text': - for leaf in node['leaves']: - if 'marks' in leaf and leaf['marks']: - to_return += addMarkup(preserveStyle(leaf['text']), leaf['marks']) + if node["object"] == "text": + for leaf in node["leaves"]: + if "marks" in leaf and leaf["marks"]: + to_return += addMarkup(preserveStyle(leaf["text"]), leaf["marks"]) else: - to_return += preserveStyle(leaf['text']) - elif node['object'] == 'inline' and node['type'] == 'link': - url = node['data']['url'] - url_nodes = node['nodes'] - to_return += f'\\href{{{url}}}{{{processText(url_nodes)}}}' - elif node['object'] == 'inline' and node['type'] == 'reference': + to_return += preserveStyle(leaf["text"]) + elif node["object"] == "inline" and node["type"] == "link": + url = node["data"]["url"] + url_nodes = node["nodes"] + to_return += f"\\href{{{url}}}{{{processText(url_nodes)}}}" + elif node["object"] == "inline" and node["type"] == "reference": try: - refID = refIDs[node['data']['id']] - to_return += f'\\cite{{{refID}}}' + refID = refIDs[node["data"]["id"]] + to_return += f"\\cite{{{refID}}}" except KeyError: - to_return += '' + to_return += "" return to_return + def saveImage(imgUrl, img): imgLink = num2words(len(pdfImgs)) - pdfImgs.append(r'\immediate\write18{wget --quiet --timestamping "' + imgUrl + f'"}} \n \\newcommand{{\\{imgLink}}}{{{img}}}') - htmlImgs.append(f'\\newcommand{{\\{imgLink}}}{{{imgUrl}}}') + pdfImgs.append( + r'\immediate\write18{wget --quiet --timestamping "' + + imgUrl + + f'"}} \n \\newcommand{{\\{imgLink}}}{{{img}}}' + ) + htmlImgs.append(f"\\newcommand{{\\{imgLink}}}{{{imgUrl}}}") return imgLink -def wrapImage(img, cap=''): + +def wrapImage(img, cap=""): if cap: - cap = f'\\caption{{{cap}}}' - wrapper = f''' \\begin{{figure}}[H] + cap = f"\\caption{{{cap}}}" + wrapper = f""" \\begin{{figure}}[H] \\includegraphics[width=\\maxwidth{{\\linewidth}}]{{\\{img}}} {cap} \\end{{figure}} - ''' + """ return wrapper + def processList(nodeRows, listType): - itemList = '' + itemList = "" for item in nodeRows: - itemList += '\\item ' + processText(item['nodes']) + '\n' - if listType == 'unordered': - return f'\\begin{{itemize}} {itemList} \\end{{itemize}}' - elif listType == 'ordered': - return f'\\begin{{enumerate}} {itemList} \\end{{enumerate}}' + itemList += "\\item " + processText(item["nodes"]) + "\n" + if listType == "unordered": + return f"\\begin{{itemize}} {itemList} \\end{{itemize}}" + elif listType == "ordered": + return f"\\begin{{enumerate}} {itemList} \\end{{enumerate}}" + # Depending on the type of the node, call the corresponding function to correctly format and return def processWYSIWYGElement(node): - if node['type'] == 'table': - return '\n \n' + processTable(node['nodes']) + '\n \n', 'table' - elif node['type'] == 'table_cell': - return processWYSIWYGElement(node['nodes']), 'table_cell' - elif node['type'][-4:] == 'list': - return processList(node['nodes'], node['type'][:-5]), 'list' - elif node['type'] == 'image': - imgUrl = node['data']['src'] - filename = imgUrl.rsplit('/', 1)[1] + if node["type"] == "table": + return "\n \n" + processTable(node["nodes"]) + "\n \n", "table" + elif node["type"] == "table_cell": + return processWYSIWYGElement(node["nodes"]), "table_cell" + elif node["type"][-4:] == "list": + return processList(node["nodes"], node["type"][:-5]), "list" + elif node["type"] == "image": + imgUrl = node["data"]["src"] + filename = imgUrl.rsplit("/", 1)[1] imgCommand = saveImage(imgUrl, filename) try: - caption = node['data']['caption'] - cmd= '\n \n' + wrapImage(imgCommand, caption) + '\n \n' + caption = node["data"]["caption"] + cmd = "\n \n" + wrapImage(imgCommand, caption) + "\n \n" except: - cmd= '\n \n' + wrapImage(imgCommand) + '\n \n' - return cmd, 'image' - elif node['type'] == 'equation': - return ' \\begin{equation} ' + \ - node['nodes'][0]['leaves'][0]['text'] + ' \\end{equation} ', 'equation' - elif node['type'] == 'paragraph': - text = processText(node['nodes']) + cmd = "\n \n" + wrapImage(imgCommand) + "\n \n" + return cmd, "image" + elif node["type"] == "equation": + return ( + " \\begin{equation} " + + node["nodes"][0]["leaves"][0]["text"] + + " \\end{equation} ", + "equation", + ) + elif node["type"] == "paragraph": + text = processText(node["nodes"]) if text: - return text, 'text' + return text, "text" else: return None, None else: - print('oops! here with {}'.format(node)) + print("oops! here with {}".format(node)) return None, None + # Catch-all function which will handle each element (returnedElement) in the body of the ATBD in accordance with its type (elementType) def processWYSIWYG(element): if debug: - print('element in WYSIWYG is ' + str(element)) + print("element in WYSIWYG is " + str(element)) to_return = [] ctr = 0 - for node in element['document']['nodes']: - prepend = '' + for node in element["document"]["nodes"]: + prepend = "" returnedElement, elementType = processWYSIWYGElement(node) if returnedElement: # ignore newlines at the beginning # Only need to worry about adding newline characters around text elements - if elementType == 'text': + if elementType == "text": # Only prepend text with newlines if not the first item or preceded by image, table, or list - if ctr != 0 and to_return[ctr-1][1] != 'image' and to_return[ctr-1][1] != 'table' and to_return[ctr-1][1] != 'list': - prepend = '\\\\\\\\' + if ( + ctr != 0 + and to_return[ctr - 1][1] != "image" + and to_return[ctr - 1][1] != "table" + and to_return[ctr - 1][1] != "list" + ): + prepend = "\\\\\\\\" # Convert element to string, and add newlines following and before (if needed) - returnedElement = prepend + str(returnedElement) + '\\\\\\\\' + returnedElement = prepend + str(returnedElement) + "\\\\\\\\" to_return.append([returnedElement, elementType]) ctr += 1 - return reduce((lambda x, y: x + y), - list(map(lambda x: x[0], to_return)), '') + return reduce((lambda x, y: x + y), list(map(lambda x: x[0], to_return)), "") + def accessURL(url): - return f'\\textbf{{Access URL: }} {{{url}}} \\\\' + return f"\\textbf{{Access URL: }} {{{url}}} \\\\" + def simpleList(name, item): - return f'\\textbf{{{toSpaceCase(name)}: }} {item} \\\\' + return f"\\textbf{{{toSpaceCase(name)}: }} {item} \\\\" + def simpleListURLs(name, item): - return f'\\textbf{{{toSpaceCase(name)}: }} \\url{{{escapeSpecialChars(item)}}} \\\\' + return f"\\textbf{{{toSpaceCase(name)}: }} \\url{{{escapeSpecialChars(item)}}} \\\\" + def processImplementations(collection): - return reduce((lambda x, y: x + y), - list(map(lambda x: '\\subsection {}' + processWYSIWYG(x['execution_description']) + '\\\\' + simpleListURLs('access_url', x['access_url']), collection)), '') + return reduce( + (lambda x, y: x + y), + list( + map( + lambda x: "\\subsection {}" + + processWYSIWYG(x["execution_description"]) + + "\\\\" + + simpleListURLs("access_url", x["access_url"]), + collection, + ) + ), + "", + ) + def processDataAccess(collection): - return reduce((lambda x, y: x + y), - list(map(lambda x: '\\subsection {}' + processWYSIWYG(x['description']) + '\\\\' + simpleListURLs('access_url', x['access_url']), collection)), '') + return reduce( + (lambda x, y: x + y), + list( + map( + lambda x: "\\subsection {}" + + processWYSIWYG(x["description"]) + + "\\\\" + + simpleListURLs("access_url", x["access_url"]), + collection, + ) + ), + "", + ) + def processDataAccessURL(collection): - return reduce((lambda x, y: x + y), - list(map(lambda x: '\\subsection {}' + processWYSIWYG(x['description']) + '\\\\' + simpleListURLs('URL', x['url']), collection)), '') + return reduce( + (lambda x, y: x + y), + list( + map( + lambda x: "\\subsection {}" + + processWYSIWYG(x["description"]) + + "\\\\" + + simpleListURLs("URL", x["url"]), + collection, + ) + ), + "", + ) + def processContacts(collection): - allContacts = '' + allContacts = "" for contact in collection: - if contact['middle_name'] is not None: - contactString = contact['first_name'] + ' ' + \ - contact['middle_name'] + ' ' + contact['last_name'] + if contact["middle_name"] is not None: + contactString = ( + contact["first_name"] + + " " + + contact["middle_name"] + + " " + + contact["last_name"] + ) else: - contactString = contact['first_name'] + ' ' + contact['last_name'] - contactString += ' \\\\ ' - contactString += simpleList('uuid', contact['uuid']) if contact['uuid'] else '' - contactString += simpleListURLs('url', - contact['url']) if contact['url'] else '' - if 'mechanisms' in contact: - contactString += '\\subsubsection{Contact Mechanisms}' - for mechanism in contact['mechanisms']: - contactString += mechanism['mechanism_value'] + '\\\\' if mechanism is not None else '' - allContacts += '\\subsection{} ' + contactString + contactString = contact["first_name"] + " " + contact["last_name"] + contactString += " \\\\ " + contactString += simpleList("uuid", contact["uuid"]) if contact["uuid"] else "" + contactString += simpleListURLs("url", contact["url"]) if contact["url"] else "" + if "mechanisms" in contact: + contactString += "\\subsubsection{Contact Mechanisms}" + for mechanism in contact["mechanisms"]: + contactString += ( + mechanism["mechanism_value"] + "\\\\" + if mechanism is not None + else "" + ) + allContacts += "\\subsection{} " + contactString return allContacts + def processVarList(element): - varDF = pd.DataFrame.from_dict(element, orient='columns') + varDF = pd.DataFrame.from_dict(element, orient="columns") if not varDF.empty: - latexDF = varDF.to_latex(index=False, bold_rows=True, escape=False, column_format='p{9cm} p{3cm}', - columns=['long_name', 'unit'], header = ['\\textbf{{Name}}', '\\textbf{{Unit}}']) + latexDF = varDF.to_latex( + index=False, + bold_rows=True, + escape=False, + column_format="p{9cm} p{3cm}", + columns=["long_name", "unit"], + header=["\\textbf{{Name}}", "\\textbf{{Unit}}"], + ) return latexDF else: - return '' + return "" + def processATBD(element): - title = macroWrap('ATBDTitle', element['title']) + title = macroWrap("ATBDTitle", element["title"]) try: - contacts = macroWrap('Contacts', processContacts(element['contacts'])) + contacts = macroWrap("Contacts", processContacts(element["contacts"])) except KeyError: return [title] return [title, contacts] + # Map variables to be found in JSON to the functions which will correctly format them into their TeX counterparts mapVars = { - 'scientific_theory': processWYSIWYG, - 'scientific_theory_assumptions': processWYSIWYG, - 'mathematical_theory': processWYSIWYG, - 'mathematical_theory_assumptions': processWYSIWYG, - 'algorithm_input_variables': processVarList, - 'algorithm_output_variables': processVarList, - 'atbd': processATBD, - 'introduction': processWYSIWYG, - 'historical_perspective': processWYSIWYG, - 'algorithm_usage_constraints': processWYSIWYG, - 'performance_assessment_validation_methods': processWYSIWYG, - 'performance_assessment_validation_uncertainties': processWYSIWYG, - 'performance_assessment_validation_errors': processWYSIWYG, - 'algorithm_implementations': processImplementations, - 'data_access_input_data': processDataAccess, - 'data_access_output_data': processDataAccess, - 'data_access_related_urls': processDataAccessURL + "scientific_theory": processWYSIWYG, + "scientific_theory_assumptions": processWYSIWYG, + "mathematical_theory": processWYSIWYG, + "mathematical_theory_assumptions": processWYSIWYG, + "algorithm_input_variables": processVarList, + "algorithm_output_variables": processVarList, + "atbd": processATBD, + "introduction": processWYSIWYG, + "historical_perspective": processWYSIWYG, + "algorithm_usage_constraints": processWYSIWYG, + "performance_assessment_validation_methods": processWYSIWYG, + "performance_assessment_validation_uncertainties": processWYSIWYG, + "performance_assessment_validation_errors": processWYSIWYG, + "algorithm_implementations": processImplementations, + "data_access_input_data": processDataAccess, + "data_access_output_data": processDataAccess, + "data_access_related_urls": processDataAccessURL, + "discussion": processText, + "acknowledgements": processText, } + # Formats each reference in {refs} and appends to the references list which will comprise the BibTex file def processReferences(refs): # create BibTeX counter = 1 for ref in refs: - identifier = 'REF'+ num2words(counter) + identifier = "REF" + num2words(counter) if debug: - print('ref is {}'.format(ref)) - this_ref = '\n' + print("ref is {}".format(ref)) + this_ref = "\n" # currently just for Article - for element in ['title', 'pages', 'volume']: + for element in ["title", "pages", "volume"]: if ref[element] is not None: this_ref += element.upper() + '="{}", \n'.format(ref[element]) - if ref['authors'] is not None: - this_ref += 'AUTHOR' + '="{}", \n'.format(ref['authors']) + if ref["authors"] is not None: + this_ref += "AUTHOR" + '="{}", \n'.format(ref["authors"]) else: - this_ref += 'key' + '="{}", \n'.format(ref['title']) - if ref['publisher'] is not None : - this_ref += 'JOURNAL' + '="{}", \n'.format(ref['publisher']) - if ref['issue'] is not None: - this_ref += 'NUMBER' + '="{}", \n'.format(ref['issue']) - if ref['year'] is not None: - this_ref += 'YEAR' + '="{}", \n'.format(ref['year']) - bibtexRef = f'@ARTICLE{{{identifier},{this_ref}}}' + this_ref += "key" + '="{}", \n'.format(ref["title"]) + if ref["publisher"] is not None: + this_ref += "JOURNAL" + '="{}", \n'.format(ref["publisher"]) + if ref["issue"] is not None: + this_ref += "NUMBER" + '="{}", \n'.format(ref["issue"]) + if ref["year"] is not None: + this_ref += "YEAR" + '="{}", \n'.format(ref["year"]) + bibtexRef = f"@ARTICLE{{{identifier},{this_ref}}}" references.append(bibtexRef) - refIDs[ref['publication_reference_id']] = identifier - counter +=1 + refIDs[ref["publication_reference_id"]] = identifier + counter += 1 + # Creates a new TeX variable called {name} and defines it as {value} in the TeX file def macroWrap(name, value): - return '\\newcommand{{\\{fn}}}{{{val}}}'.format(fn=name, val=value) + return "\\newcommand{{\\{fn}}}{{{val}}}".format(fn=name, val=value) + -def texify (name, element): +def texify(name, element): if debug: - print('name: {} || element: {}'.format(name, element)) + print("name: {} || element: {}".format(name, element)) elif name in mapVars.keys() and element is not None: return macroWrap(toCamelCase(name), mapVars[name](element)) elif element is None: - return macroWrap(toCamelCase(name), '') + return macroWrap(toCamelCase(name), "") else: return name + # Include a section at the top of the ATBD which has filetype specific instructions so that images will render correctly in both HTML and PDF def filetypeSpecific(filetype): functionList = [] - if filetype == 'HTML': - functionList.append('\\def\\maxwidth#1{#1}') + if filetype == "HTML": + functionList.append("\\def\\maxwidth#1{#1}") functionList += htmlImgs - elif filetype == 'PDF': + elif filetype == "PDF": functionList.append( - ''' + """ \\makeatletter \\def\\maxwidth#1{\\ifdim\\Gin@nat@width>#1 #1\\else\\Gin@nat@width\\fi} \\makeatother - ''') + """ + ) functionList += pdfImgs return functionList + class ATBD: - def __init__(self, path): + def __init__(self, path: str, journal: str): + self.journal = journal self.filepath = path + # Parse the JSON file into the corresponding sections (variables) enumerated in the ATBD - def texVariables (self): + def texVariables(self): myJson = json.loads(open(self.filepath).read()) - processReferences(myJson.pop('publication_references')) - commands = processATBD(myJson.pop('atbd')) + # print("DATA DATA DATA: ", myJson) + processReferences(myJson.pop("publication_references")) + commands = processATBD(myJson.pop("atbd")) if debug: for item, value in myJson.items(): - print('item: {}, value: {}'.format(item, value)) - commands += [texify(x, y) for x,y in myJson.items() if x in mapVars.keys()] + print("item: {}, value: {}".format(item, value)) + + if not myJson.get("discussion"): + myJson["discussion"] = self.placeholder("Discussion omitted") + + if not myJson.get("acknowledgements"): + myJson["acknowledgements"] = self.placeholder("Acknowledgements omitted") + + commands += [texify(x, y) for x, y in myJson.items() if x in mapVars.keys()] + if debug: print(commands) self.texVars = commands def nameFile(self, ext): - atbd_name = self.filepath.rsplit('.json', 1)[0] + atbd_name = self.filepath.rsplit(".json", 1)[0] if debug: print(atbd_name) - return f'{atbd_name}.{ext}' + return f"{atbd_name}.{ext}" def filewrite(self): - with open('ATBD.tex', 'r') as original: + atbd_template_file = "ATBD.tex" + + if self.journal: + atbd_template_file = "ATBD_JOURNAL.tex" + + with open(atbd_template_file, "r") as original: data = original.read() - with open(self.nameFile('tex'), 'w') as modified: - modified.write('\\ifx \\convertType \\undefined \n') - modified.write('\n'.join(filetypeSpecific('HTML'))) - modified.write('\n \\else \n') - modified.write('\n'.join(filetypeSpecific('PDF'))) - modified.write('\n \\fi \n') - modified.write('\n'.join(self.texVars) + ' \n' + data) + + with open(self.nameFile("tex"), "w") as modified: + modified.write("\\ifx \\convertType \\undefined \n") + modified.write("\n".join(filetypeSpecific("HTML"))) + modified.write("\n \\else \n") + modified.write("\n".join(filetypeSpecific("PDF"))) + modified.write("\n \\fi \n") + modified.write("\n".join(self.texVars) + " \n" + data) fileName = modified.name - with open(os.path.join(os.path.dirname(fileName), 'main.bib'), 'w') as bibFile: - bibFile.write('\n'.join(references)) + with open(os.path.join(os.path.dirname(fileName), "main.bib"), "w") as bibFile: + bibFile.write("\n".join(references)) return fileName -def createLatex(args): - atbd_path = args - newTex = ATBD(atbd_path) + def placeholder(self, text): + return [ + { + "object": "text", + "leaves": [{"text": text}], + } + ] + + +def createLatex(atbd_path, journal): + + newTex = ATBD(atbd_path, journal == "True") newTex.texVariables() texFile = newTex.filewrite() print(texFile) -createLatex(sys.argv[1]) + +createLatex(sys.argv[1], sys.argv[2]) diff --git a/pdf/app/main.py b/pdf/app/main.py index 1a7575a4..ea0302a3 100644 --- a/pdf/app/main.py +++ b/pdf/app/main.py @@ -15,11 +15,17 @@ from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException -root_path: str = environ.get('API_PREFIX', '/') -rest_api_endpoint: str = environ.get('REST_API_ENDPOINT') or exit('REST_API_ENDPOINT env var required') -s3_endpoint: str = environ.get('S3_ENDPOINT') or exit('S3_ENDPOINT env var required') -pdfs_bucket_name: str = environ.get('PDFS_S3_BUCKET') or exit('PDFS_S3_BUCKET env var required') -figures_bucket_name: str = environ.get('FIGURES_S3_BUCKET') or exit('FIGURES_S3_BUCKET env var required') +root_path: str = environ.get("API_PREFIX", "/") +rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or exit( + "REST_API_ENDPOINT env var required" +) +s3_endpoint: str = environ.get("S3_ENDPOINT") or exit("S3_ENDPOINT env var required") +pdfs_bucket_name: str = environ.get("PDFS_S3_BUCKET") or exit( + "PDFS_S3_BUCKET env var required" +) +figures_bucket_name: str = environ.get("FIGURES_S3_BUCKET") or exit( + "FIGURES_S3_BUCKET env var required" +) app: FastAPI = FastAPI() cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) @@ -34,11 +40,13 @@ def get_cache_key(atbd_doc: Dict) -> str: :rtype: str """ hex_digest: str = checksum_atbd(atbd_doc) - alias: str = atbd_doc['atbd']['alias'] - return f'{hex_digest}/{alias}.pdf' if alias else f'{hex_digest}/nasa-atbd.pdf' + alias: str = atbd_doc["atbd"]["alias"] + return f"{hex_digest}/{alias}.pdf" if alias else f"{hex_digest}/nasa-atbd.pdf" -def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[Union[Type[RedirectResponse], Type[FileResponse]]]: +def atbd_pdf_handler( + atbd_doc: Dict, background_tasks: BackgroundTasks, journal=False +) -> Type[Union[Type[RedirectResponse], Type[FileResponse]]]: """ For Published atbd: Issues redirect response upon cache hit, or streams file response for cache failure. (Should always return a PDF unless the serialization pipeline has failed) @@ -53,6 +61,7 @@ def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[ :raises HTTPException: """ status: Status = get_status(atbd_doc) + cache_key: str = get_cache_key(atbd_doc) if status == Status.Published.name: # check cache: published atbds may be cached in s3 @@ -63,46 +72,64 @@ def atbd_pdf_handler(atbd_doc: Dict, background_tasks: BackgroundTasks) -> Type[ except CacheException as e: # log and continue with pdf serialization workflow logger.error(e) - tmp_dir_resource: TemporaryDirectory[str] = TemporaryDirectory(prefix='nasa-apt-pdf-service-') + tmp_dir_resource: TemporaryDirectory[str] = TemporaryDirectory( + prefix="nasa-apt-pdf-service-" + ) tmp_dir: str = tmp_dir_resource.name background_tasks.add_task(cleanup_tmp_dir, tmp_dir_resource) try: - (latex_filename, _) = json_to_latex(atbd_doc=atbd_doc, tmp_dir=tmp_dir) + (latex_filename, _) = json_to_latex( + atbd_doc=atbd_doc, tmp_dir=tmp_dir, journal=journal + ) except JsonToLatexException as e: raise HTTPException(status_code=500, detail=str(e)) from e try: - tmp_pdf_filename: str = latex_to_pdf(latex_filename=latex_filename, tmp_dir=tmp_dir) + tmp_pdf_filename: str = latex_to_pdf( + latex_filename=latex_filename, tmp_dir=tmp_dir + ) if status == Status.Published.name: try: cache_url = cache.put_file(key=cache_key, filename=tmp_pdf_filename) return RedirectResponse(url=cache_url) except CacheException as e: logger.error(str(e)) - alias: str = atbd_doc['atbd']['alias'] - filename = f'{alias}.pdf' if alias else 'nasa-atbd.pdf' + alias: str = atbd_doc["atbd"]["alias"] + filename = f"{alias}.pdf" if alias else "nasa-atbd.pdf" return FileResponse(path=tmp_pdf_filename, filename=filename) except LatexToPDFException as e: raise HTTPException(status_code=500, detail=str(e)) from e -@app.get(root_path + 'atbds/id/{atbd_id}.pdf') +@app.get(root_path + "atbds/id/{atbd_id}.pdf") def get_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks): atbd_doc = get_atbd(atbd_id=atbd_id) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) -@app.get(root_path + 'atbds/alias/{alias}.pdf') +@app.get(root_path + "atbds/alias/{alias}.pdf") def get_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks): atbd_doc = get_atbd(alias=alias) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) +@app.get(root_path + "atbds/journal/id/{atbd_id}.pdf") +def get_journal_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks): + atbd_doc = get_atbd(atbd_id=atbd_id) + return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks, journal=True) + + +@app.get(root_path + "atbds/journal/alias/{alias}.pdf") +def get_journal_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks): + atbd_doc = get_atbd(alias=alias) + return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks, journal=True) + + @app.get(root_path) def health_check(): """ The root_path is only used by the ELB healthcheck. """ - return 'ok' + return "ok" def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): @@ -116,6 +143,4 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): :type tmp_dir: TemporaryDirectory[str] """ tmp_dir.cleanup() - logger.info(f'cleaned up {tmp_dir.name}') - - + logger.info(f"cleaned up {tmp_dir.name}") From 1afcfa9262a18fc50cc8f7311ab7b7c4b0edc4de Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 24 Nov 2020 20:11:11 -0500 Subject: [PATCH 27/59] added separate name for journal atbds --- pdf/app/latex/serialize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 70e60b72..2dfa8381 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -419,7 +419,6 @@ def __init__(self, path: str, journal: str): # Parse the JSON file into the corresponding sections (variables) enumerated in the ATBD def texVariables(self): myJson = json.loads(open(self.filepath).read()) - # print("DATA DATA DATA: ", myJson) processReferences(myJson.pop("publication_references")) commands = processATBD(myJson.pop("atbd")) if debug: @@ -442,6 +441,8 @@ def nameFile(self, ext): atbd_name = self.filepath.rsplit(".json", 1)[0] if debug: print(atbd_name) + if self.journal: + return f"{atbd_name}_journal.{ext}" return f"{atbd_name}.{ext}" def filewrite(self): From 801a56eff7396022ec83055dacc5ffd27dcccfee Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 24 Nov 2020 20:13:20 -0500 Subject: [PATCH 28/59] Revert "added separate name for journal atbds" This reverts commit 1afcfa9262a18fc50cc8f7311ab7b7c4b0edc4de. --- pdf/app/latex/serialize.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 2dfa8381..70e60b72 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -419,6 +419,7 @@ def __init__(self, path: str, journal: str): # Parse the JSON file into the corresponding sections (variables) enumerated in the ATBD def texVariables(self): myJson = json.loads(open(self.filepath).read()) + # print("DATA DATA DATA: ", myJson) processReferences(myJson.pop("publication_references")) commands = processATBD(myJson.pop("atbd")) if debug: @@ -441,8 +442,6 @@ def nameFile(self, ext): atbd_name = self.filepath.rsplit(".json", 1)[0] if debug: print(atbd_name) - if self.journal: - return f"{atbd_name}_journal.{ext}" return f"{atbd_name}.{ext}" def filewrite(self): From 3868163a3f65ac4d9e26e97ab732fe55c6615a9f Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 3 Dec 2020 18:19:44 +0000 Subject: [PATCH 29/59] Add database fields for journal details section --- db/deploy/journalDetails.sql | 8 ++++++++ db/revert/journalDetails.sql | 7 +++++++ db/sqitch.plan | 1 + db/verify/journalDetails.sql | 7 +++++++ 4 files changed, 23 insertions(+) create mode 100644 db/deploy/journalDetails.sql create mode 100644 db/revert/journalDetails.sql create mode 100644 db/verify/journalDetails.sql diff --git a/db/deploy/journalDetails.sql b/db/deploy/journalDetails.sql new file mode 100644 index 00000000..2f2bdbee --- /dev/null +++ b/db/deploy/journalDetails.sql @@ -0,0 +1,8 @@ +-- Deploy nasa-apt:journalDetails to pg +-- requires: tables + +BEGIN; + ALTER TABLE apt.atbd_versions + ADD COLUMN journal_discussion json, + ADD COLUMN journal_acknowledgements json; +COMMIT; diff --git a/db/revert/journalDetails.sql b/db/revert/journalDetails.sql new file mode 100644 index 00000000..103347df --- /dev/null +++ b/db/revert/journalDetails.sql @@ -0,0 +1,7 @@ +-- Revert nasa-apt:journalDetails from pg + +BEGIN; + ALTER TABLE apt.atbds + DROP COLUMN journal_discussion, + DROP COLUMN journal_acknowledgements; +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index f0248714..5d0d1b9d 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -13,3 +13,4 @@ copyATBD [tables] 2019-10-18T14:14:13Z Alyssa Harris # Add alias field to atbd copyATBDAlias [copyATBD] 2020-05-08T15:56:26Z Daniel da Silva # Creates a unique alias when copying atbd textsearchAlias [textsearch] 2020-05-08T15:57:18Z Daniel da Silva # Includes the atbs alias when searching +journalDetails [tables] 2020-12-03T11:03:09Z Daniel da Silva # Add fields for journal details diff --git a/db/verify/journalDetails.sql b/db/verify/journalDetails.sql new file mode 100644 index 00000000..19bcba2d --- /dev/null +++ b/db/verify/journalDetails.sql @@ -0,0 +1,7 @@ +-- Verify nasa-apt:journalDetails on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; From b620bb7bc6faef2553bf5f18da8b67122baab66d Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Thu, 3 Dec 2020 15:52:37 -0500 Subject: [PATCH 30/59] updated algorithm_input_variables to use WYSIWYG element instead of text --- db/temp.json | 56 ++++ db/testData.sql | 28 +- pdf/app/latex/ATBD_JOURNAL.tex | 6 + pdf/app/latex/json_to_latex.py | 2 + pdf/app/latex/serialize.py | 56 +++- pdf/app/latex/temp.json | 541 +++++++++++++++++++++++++++++++++ pdf/app/pdf/latex_to_pdf.py | 17 +- 7 files changed, 681 insertions(+), 25 deletions(-) create mode 100644 db/temp.json create mode 100644 pdf/app/latex/temp.json diff --git a/db/temp.json b/db/temp.json new file mode 100644 index 00000000..a8a314b0 --- /dev/null +++ b/db/temp.json @@ -0,0 +1,56 @@ +{ + "object": "value", + "document": { + "object": "document", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "A historical perspective. We are now referencing ", + "marks": [] + } + ] + }, + { + "object": "inline", + "type": "reference", + "data": { + "id": 1, + "name": "Example Reference" + }, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "ref", + "marks": [] + } + ] + } + ] + }, + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "", + "marks": [] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/db/testData.sql b/db/testData.sql index 7ef82403..5f6c93f0 100644 --- a/db/testData.sql +++ b/db/testData.sql @@ -1,5 +1,5 @@ -INSERT INTO contacts(first_name, last_name, mechanisms, roles) -VALUES ('Leonardo', 'Davinci', '{ "(\"Email\",\"test@email.com\")" }', '{ "Science contact", "Metadata author" }'); +INSERT INTO contacts(first_name, last_name, mechanisms, roles) +VALUES ('Leonardo', 'Davinci', '{ "(\"Email\",\"test@email.com\")" }', '{ "Science contact", "Metadata author" }'); INSERT INTO contacts(first_name, last_name) VALUES ('Gregor', 'Mendel'); INSERT INTO atbds(title, alias) @@ -10,13 +10,25 @@ INSERT INTO atbd_versions(atbd_id, atbd_version, scientific_theory, introduction VALUES (1, 1, '{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A line of text in a paragraph."}]}]}, {"object":"block","type":"equation","nodes":[{"object":"text","leaves":[{"text":"\\int_0^\\infty x^2 dx"}]}]}, -{"object":"block","type":"image","data":{"src":"http://localstack:4572/nasa-apt-dev-figures/fullmoon.jpg"}}]}}', -'{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"An introduction."}]}]}]}}', -'{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A historical perspective."}]}]}]}}'); +{"object":"block","type":"image","data":{"src":"http://localstack:4572/nasa-apt-dev-figures/fullmoon.jpg", "caption": "Image of the full moon - 2019"}}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"An introduction.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","caption":"A Table containing important data", "data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 1","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 3","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A historical perspective. We are now referencing ","marks":[]}]},{"object":"inline","type":"reference","data":{"id":1,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}'); -- NOTE: the absolute url for fullmoon.jpg will work for the pdf serialization service running in docker-compose. it will not work for -- web browser rendering same image. This can be manually changed to http://localhost:4572/nasa-apt-dev-figures/fullmoon.jpg -- to render in browser. INSERT INTO algorithm_input_variables(atbd_id, atbd_version, name, long_name) -VALUES (1, 1, 'Input Var 1', 'Input variable 1'); -INSERT INTO algorithm_output_variables(atbd_id, atbd_version, name, long_name) -VALUES (1, 1, 'Output Var 1', 'Output variable 1'); +VALUES (1, 1, +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 1","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Variable 1","marks":[]}]}]}]}}'); +INSERT INTO algorithm_input_variables(atbd_id, atbd_version, name, long_name, unit) +VALUES (1, 1, +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 2","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input variable that is quite long and should be wrapped over at least two lines but possible also three","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); +INSERT INTO algorithm_output_variables(atbd_id, atbd_version, name, long_name, unit) +VALUES (1, 1, +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Var 1","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Variable 1","marks":[]}]}]}]}}', +'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); +INSERT INTO publication_references(publication_reference_id, atbd_version, atbd_id, authors, title, series, edition, volume, publication_place, publisher, pages, isbn, year) +VALUES (1,1,1, 'Charles Dickens, John Steinbeck', 'Example Reference', 'A', '3rd', '42ml', 'Boston','Penguin Books', '189-198', 123456789, 1995); \ No newline at end of file diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex index 63732249..1f8b423d 100644 --- a/pdf/app/latex/ATBD_JOURNAL.tex +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -24,6 +24,12 @@ \usepackage{graphicx} \usepackage{float} +\usepackage{setspace} +\doublespacing + +\usepackage{lineno} +\linenumbers + \setmainfont{Latin Modern Math} %Using a font with good unicode math symbols % coverage to support the use of unicode math symbols in LaTeX text mode % See list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf diff --git a/pdf/app/latex/json_to_latex.py b/pdf/app/latex/json_to_latex.py index d747a3e9..dec879f2 100644 --- a/pdf/app/latex/json_to_latex.py +++ b/pdf/app/latex/json_to_latex.py @@ -50,8 +50,10 @@ def json_to_latex(atbd_doc: Dict, tmp_dir: str, journal: bool) -> Tuple[str, str cwd=tmp_dir, encoding=encoding, ) + print(completed.stdout) if completed.returncode != 0: # for debugging purposes, return the stdout in addition to the stderr + raise JsonToLatexException( {"stderr": completed.stderr, "stdout": completed.stdout} ) diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 70e60b72..732454e9 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -41,7 +41,7 @@ def toSpaceCase(snake_str): return " ".join(x.title() for x in components) -def processTable(nodeRows): +def processTable(nodeRows, caption=None): tableList = [] for rows in nodeRows: tableList.append([]) @@ -52,7 +52,21 @@ def processTable(nodeRows): columnNames = tableList.pop(0) pd.set_option("display.max_colwidth", 1000) df = pd.DataFrame(tableList, columns=columnNames) - latexTable = df.to_latex(index=False, escape=False, header=False) + # latex default text width = 426 pts + column_format = "|".join( + f" p{{{int(426/len(columnNames))}pt}} " for _ in columnNames + ) + to_latex_params = dict( + index=False, + escape=False, + header=True, + column_format=column_format, + ) + if caption: + to_latex_params["caption"] = caption + + latexTable = df.to_latex(**to_latex_params) + return latexTable @@ -97,6 +111,7 @@ def escapeSpecialChars(text): def processText(nodes): to_return = "" for node in nodes: + if node["object"] == "text": for leaf in node["leaves"]: if "marks" in leaf and leaf["marks"]: @@ -151,7 +166,12 @@ def processList(nodeRows, listType): # Depending on the type of the node, call the corresponding function to correctly format and return def processWYSIWYGElement(node): if node["type"] == "table": - return "\n \n" + processTable(node["nodes"]) + "\n \n", "table" + return ( + "\n \n" + + processTable(node["nodes"], caption=node.get("caption")) + + "\n \n", + "table", + ) elif node["type"] == "table_cell": return processWYSIWYGElement(node["nodes"]), "table_cell" elif node["type"][-4:] == "list": @@ -190,9 +210,11 @@ def processWYSIWYG(element): print("element in WYSIWYG is " + str(element)) to_return = [] ctr = 0 + for node in element["document"]["nodes"]: prepend = "" returnedElement, elementType = processWYSIWYGElement(node) + if returnedElement: # ignore newlines at the beginning # Only need to worry about adding newline characters around text elements if elementType == "text": @@ -300,13 +322,27 @@ def processContacts(collection): def processVarList(element): + for var in element: + var["long_name"] = processWYSIWYG(json.loads(var["long_name"])).replace( + "\\", "" + ) + if var["unit"] is None: + continue + var["unit"] = processWYSIWYG(json.loads(var["unit"])).replace("\\", "") + + pd.set_option("display.max_colwidth", 1000) varDF = pd.DataFrame.from_dict(element, orient="columns") + + # varDF["long_name"] = varDF["long_name"].apply(axis=1, func=processWYSIWYG) + # varDF["unit"] = varDF["unit"].apply(axis=1, func=processWYSIWYG) + # page width is 426 pts - divide into 3/4 and 1/4 sections for algorithm name and units + column_format = f"p{{{int(426/4)*3}pt}} p{{{int(426/4)}pt}}" if not varDF.empty: latexDF = varDF.to_latex( index=False, bold_rows=True, escape=False, - column_format="p{9cm} p{3cm}", + column_format=column_format, columns=["long_name", "unit"], header=["\\textbf{{Name}}", "\\textbf{{Unit}}"], ) @@ -388,7 +424,7 @@ def texify(name, element): elif name in mapVars.keys() and element is not None: return macroWrap(toCamelCase(name), mapVars[name](element)) elif element is None: - return macroWrap(toCamelCase(name), "") + return macroWrap(toCamelCase(name), "Placeholder text") else: return name @@ -419,18 +455,18 @@ def __init__(self, path: str, journal: str): # Parse the JSON file into the corresponding sections (variables) enumerated in the ATBD def texVariables(self): myJson = json.loads(open(self.filepath).read()) - # print("DATA DATA DATA: ", myJson) + print("DATA DATA DATA: ", myJson) processReferences(myJson.pop("publication_references")) commands = processATBD(myJson.pop("atbd")) if debug: for item, value in myJson.items(): print("item: {}, value: {}".format(item, value)) - if not myJson.get("discussion"): - myJson["discussion"] = self.placeholder("Discussion omitted") + # if not myJson.get("discussion"): + # myJson["discussion"] = self.placeholder("Discussion omitted") - if not myJson.get("acknowledgements"): - myJson["acknowledgements"] = self.placeholder("Acknowledgements omitted") + # if not myJson.get("acknowledgements"): + # myJson["acknowledgements"] = self.placeholder("Acknowledgements omitted") commands += [texify(x, y) for x, y in myJson.items() if x in mapVars.keys()] diff --git a/pdf/app/latex/temp.json b/pdf/app/latex/temp.json new file mode 100644 index 00000000..7ed144b7 --- /dev/null +++ b/pdf/app/latex/temp.json @@ -0,0 +1,541 @@ +{ + "atbd_version": 1, + "atbd_id": 1, + "scientific_theory": { + "document": { + "nodes": [ + { + "object": "block", + "type": "paragraph", + "nodes": [ + { + "object": "text", + "leaves": [ + { + "text": "A line of text in a paragraph." + } + ] + } + ] + }, + { + "object": "block", + "type": "equation", + "nodes": [ + { + "object": "text", + "leaves": [ + { + "text": "\\int_0^\\infty x^2 dx" + } + ] + } + ] + }, + { + "object": "block", + "type": "image", + "data": { + "src": "http://localstack:4572/nasa-apt-dev-figures/fullmoon.jpg", + "caption": "Image of the full moon - 2019" + } + } + ] + } + }, + "scientific_theory_assumptions": None, + "mathematical_theory": None, + "mathematical_theory_assumptions": None, + "introduction": { + "object": "value", + "document": { + "object": "document", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "An introduction.", + "marks": [] + } + ] + } + ] + }, + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "", + "marks": [] + } + ] + } + ] + }, + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "", + "marks": [] + } + ] + } + ] + }, + { + "object": "block", + "type": "table", + "caption": "A Table containing important data", + "data": { + "headless": True + }, + "nodes": [ + { + "object": "block", + "type": "table_row", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Table Column 1", + "marks": [ + { + "object": "mark", + "type": "bold", + "data": {} + } + ] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Table Column 2", + "marks": [ + { + "object": "mark", + "type": "bold", + "data": {} + } + ] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Table Column 3", + "marks": [ + { + "object": "mark", + "type": "bold", + "data": {} + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_row", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (short)", + "marks": [] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ", + "marks": [] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (short)", + "marks": [] + } + ] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_row", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (short)", + "marks": [] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (short)", + "marks": [] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "table_cell", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "Cell value (short)", + "marks": [] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "", + "marks": [] + } + ] + } + ] + } + ] + } + }, + "historical_perspective": { + "object": "value", + "document": { + "object": "document", + "data": {}, + "nodes": [ + { + "object": "block", + "type": "paragraph", + "data": {}, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "A historical perspective. We are now referencing ", + "marks": [] + } + ] + }, + { + "object": "inline", + "type": "reference", + "data": { + "id": 1, + "name": "Example Reference" + }, + "nodes": [ + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "ref", + "marks": [] + } + ] + } + ] + }, + { + "object": "text", + "leaves": [ + { + "object": "leaf", + "text": "", + "marks": [] + } + ] + } + ] + } + ] + } + }, + "performance_assessment_validation_methods": None, + "performance_assessment_validation_uncertainties": None, + "performance_assessment_validation_errors": None, + "algorithm_usage_constraints": None, + "status": "Draft", + "atbd": { + "atbd_id": 1, + "title": "Test ATBD 1", + "alias": "test-atbd-1", + "contacts": [ + { + "contact_id": 1, + "first_name": "Leonardo", + "middle_name": None, + "last_name": "Davinci", + "uuid": None, + "url": None, + "mechanisms": [ + { + "mechanism_type": "Email", + "mechanism_value": "test@email.com" + } + ], + "roles": [ + "Science contact", + "Metadata author" + ] + } + ], + "contact_groups": [], + "atbd_versions": [ + { + "atbd_id": 1, + "atbd_version": 1, + "status": "Draft" + } + ] + }, + "algorithm_input_variables": [ + { + "algorithm_input_variable_id": 1, + "atbd_version": 1, + "atbd_id": 1, + "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 1","marks":[]}]}]}]}}", + "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Variable 1","marks":[]}]}]}]}}", + "unit": None + }, + { + "algorithm_input_variable_id": 2, + "atbd_version": 1, + "atbd_id": 1, + "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 2","marks":[]}]}]}]}}", + "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input variable that is quite long and should be wrapped over at least two lines but possible also three","marks":[]}]}]}]}}", + "unit": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}" + } + ], + "algorithm_output_variables": [ + { + "algorithm_output_variable_id": 1, + "atbd_version": 1, + "atbd_id": 1, + "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Var 1","marks":[]}]}]}]}}", + "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Variable 1","marks":[]}]}]}]}}", + "unit": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}" + } + ], + "algorithm_implementations": [], + "publication_references": [ + { + "publication_reference_id": 1, + "atbd_version": 1, + "atbd_id": 1, + "authors": "Charles Dickens, John Steinbeck", + "publication_date": None, + "title": "Example Reference", + "series": "A", + "edition": "3rd", + "volume": "42ml", + "issue": None, + "report_number": None, + "publication_place": "Boston", + "publisher": "Penguin Books", + "pages": "189-198", + "isbn": "123456789", + "doi": None, + "online_resource": None, + "other_reference_details": None, + "year": 1995 + } + ], + "data_access_input_data": [], + "data_access_output_data": [], + "data_access_related_urls": [], + "citations": [] +} \ No newline at end of file diff --git a/pdf/app/pdf/latex_to_pdf.py b/pdf/app/pdf/latex_to_pdf.py index 3eb8d850..d33bcb8d 100644 --- a/pdf/app/pdf/latex_to_pdf.py +++ b/pdf/app/pdf/latex_to_pdf.py @@ -5,8 +5,8 @@ from typing import Any, Union, Final here = os.path.dirname(os.path.realpath(__file__)) -pdf_sh: Final[str] = f'{here}/pdf.sh' -encoding: Final[str] = 'utf8' +pdf_sh: Final[str] = f"{here}/pdf.sh" +encoding: Final[str] = "utf8" class LatexToPDFException(Exception): @@ -30,14 +30,17 @@ def latex_to_pdf(latex_filename: str, tmp_dir: str) -> str: args=[pdf_sh, latex_filename], capture_output=True, cwd=tmp_dir, - encoding=encoding + encoding=encoding, ) + print("LATEX TO PDF OUTPUT: ") + print(completed) if completed.returncode != 0: # for debugging purposes, return the stdout in addition to the stderr - raise LatexToPDFException(({'stderr': completed.stderr, 'stdout': completed.stdout})) + raise LatexToPDFException( + ({"stderr": completed.stderr, "stdout": completed.stdout}) + ) working_name: Final[str] = Path(latex_filename).stem - pdf_filename: Final[str] = f'{tmp_dir}/{working_name}.pdf' + pdf_filename: Final[str] = f"{tmp_dir}/{working_name}.pdf" if not Path(pdf_filename).exists(): - raise LatexToPDFException(f'expect intermediate file: {pdf_filename}') + raise LatexToPDFException(f"expect intermediate file: {pdf_filename}") return pdf_filename - From 311a5750ec0159c807686c63d57852a89b8d637d Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Thu, 3 Dec 2020 18:03:58 -0500 Subject: [PATCH 31/59] finalized first pass of journal atbds --- db/temp.json | 56 ---- pdf/app/latex/ATBD.tex | 2 +- pdf/app/latex/serialize.py | 17 +- pdf/app/latex/temp.json | 541 ------------------------------------- 4 files changed, 11 insertions(+), 605 deletions(-) delete mode 100644 db/temp.json delete mode 100644 pdf/app/latex/temp.json diff --git a/db/temp.json b/db/temp.json deleted file mode 100644 index a8a314b0..00000000 --- a/db/temp.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "object": "value", - "document": { - "object": "document", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "A historical perspective. We are now referencing ", - "marks": [] - } - ] - }, - { - "object": "inline", - "type": "reference", - "data": { - "id": 1, - "name": "Example Reference" - }, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "ref", - "marks": [] - } - ] - } - ] - }, - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "", - "marks": [] - } - ] - } - ] - } - ] - } -} \ No newline at end of file diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index d6424a8c..5c50c998 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -28,7 +28,7 @@ % coverage to support the use of unicode math symbols in LaTeX text mode % See list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf \hypersetup{ - colorlinks=true, %set true if you want colored links + colorlinks=false, %set true if you want colored links linktoc=all, %set to all if you want both sections and subsections linked linkcolor=blue, %choose some color if you want links to stand out } diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 732454e9..d22dec4d 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -66,7 +66,8 @@ def processTable(nodeRows, caption=None): to_latex_params["caption"] = caption latexTable = df.to_latex(**to_latex_params) - + # insert [H] for block latex from "floating" the table to the top of the page + latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[H]") return latexTable @@ -379,8 +380,8 @@ def processATBD(element): "data_access_input_data": processDataAccess, "data_access_output_data": processDataAccess, "data_access_related_urls": processDataAccessURL, - "discussion": processText, - "acknowledgements": processText, + "journal_discussion": processText, + "jounral_acknowledgements": processText, } @@ -462,11 +463,13 @@ def texVariables(self): for item, value in myJson.items(): print("item: {}, value: {}".format(item, value)) - # if not myJson.get("discussion"): - # myJson["discussion"] = self.placeholder("Discussion omitted") + # TODO: remove this one `journal_discussion` and `journal_acknowledgements` + # get added as fields to the database + if self.journal and not myJson.get("journal_discussion"): + myJson["journal_discussion"] = None - # if not myJson.get("acknowledgements"): - # myJson["acknowledgements"] = self.placeholder("Acknowledgements omitted") + if self.journal and not myJson.get("journal_acknowledgements"): + myJson["journal_acknowledgements"] = None commands += [texify(x, y) for x, y in myJson.items() if x in mapVars.keys()] diff --git a/pdf/app/latex/temp.json b/pdf/app/latex/temp.json deleted file mode 100644 index 7ed144b7..00000000 --- a/pdf/app/latex/temp.json +++ /dev/null @@ -1,541 +0,0 @@ -{ - "atbd_version": 1, - "atbd_id": 1, - "scientific_theory": { - "document": { - "nodes": [ - { - "object": "block", - "type": "paragraph", - "nodes": [ - { - "object": "text", - "leaves": [ - { - "text": "A line of text in a paragraph." - } - ] - } - ] - }, - { - "object": "block", - "type": "equation", - "nodes": [ - { - "object": "text", - "leaves": [ - { - "text": "\\int_0^\\infty x^2 dx" - } - ] - } - ] - }, - { - "object": "block", - "type": "image", - "data": { - "src": "http://localstack:4572/nasa-apt-dev-figures/fullmoon.jpg", - "caption": "Image of the full moon - 2019" - } - } - ] - } - }, - "scientific_theory_assumptions": None, - "mathematical_theory": None, - "mathematical_theory_assumptions": None, - "introduction": { - "object": "value", - "document": { - "object": "document", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "An introduction.", - "marks": [] - } - ] - } - ] - }, - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "", - "marks": [] - } - ] - } - ] - }, - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "", - "marks": [] - } - ] - } - ] - }, - { - "object": "block", - "type": "table", - "caption": "A Table containing important data", - "data": { - "headless": True - }, - "nodes": [ - { - "object": "block", - "type": "table_row", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Table Column 1", - "marks": [ - { - "object": "mark", - "type": "bold", - "data": {} - } - ] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Table Column 2", - "marks": [ - { - "object": "mark", - "type": "bold", - "data": {} - } - ] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Table Column 3", - "marks": [ - { - "object": "mark", - "type": "bold", - "data": {} - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_row", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (short)", - "marks": [] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ", - "marks": [] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (short)", - "marks": [] - } - ] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_row", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (short)", - "marks": [] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (short)", - "marks": [] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "table_cell", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "Cell value (short)", - "marks": [] - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "", - "marks": [] - } - ] - } - ] - } - ] - } - }, - "historical_perspective": { - "object": "value", - "document": { - "object": "document", - "data": {}, - "nodes": [ - { - "object": "block", - "type": "paragraph", - "data": {}, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "A historical perspective. We are now referencing ", - "marks": [] - } - ] - }, - { - "object": "inline", - "type": "reference", - "data": { - "id": 1, - "name": "Example Reference" - }, - "nodes": [ - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "ref", - "marks": [] - } - ] - } - ] - }, - { - "object": "text", - "leaves": [ - { - "object": "leaf", - "text": "", - "marks": [] - } - ] - } - ] - } - ] - } - }, - "performance_assessment_validation_methods": None, - "performance_assessment_validation_uncertainties": None, - "performance_assessment_validation_errors": None, - "algorithm_usage_constraints": None, - "status": "Draft", - "atbd": { - "atbd_id": 1, - "title": "Test ATBD 1", - "alias": "test-atbd-1", - "contacts": [ - { - "contact_id": 1, - "first_name": "Leonardo", - "middle_name": None, - "last_name": "Davinci", - "uuid": None, - "url": None, - "mechanisms": [ - { - "mechanism_type": "Email", - "mechanism_value": "test@email.com" - } - ], - "roles": [ - "Science contact", - "Metadata author" - ] - } - ], - "contact_groups": [], - "atbd_versions": [ - { - "atbd_id": 1, - "atbd_version": 1, - "status": "Draft" - } - ] - }, - "algorithm_input_variables": [ - { - "algorithm_input_variable_id": 1, - "atbd_version": 1, - "atbd_id": 1, - "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 1","marks":[]}]}]}]}}", - "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Variable 1","marks":[]}]}]}]}}", - "unit": None - }, - { - "algorithm_input_variable_id": 2, - "atbd_version": 1, - "atbd_id": 1, - "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 2","marks":[]}]}]}]}}", - "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input variable that is quite long and should be wrapped over at least two lines but possible also three","marks":[]}]}]}]}}", - "unit": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}" - } - ], - "algorithm_output_variables": [ - { - "algorithm_output_variable_id": 1, - "atbd_version": 1, - "atbd_id": 1, - "name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Var 1","marks":[]}]}]}]}}", - "long_name": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Variable 1","marks":[]}]}]}]}}", - "unit": "{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}" - } - ], - "algorithm_implementations": [], - "publication_references": [ - { - "publication_reference_id": 1, - "atbd_version": 1, - "atbd_id": 1, - "authors": "Charles Dickens, John Steinbeck", - "publication_date": None, - "title": "Example Reference", - "series": "A", - "edition": "3rd", - "volume": "42ml", - "issue": None, - "report_number": None, - "publication_place": "Boston", - "publisher": "Penguin Books", - "pages": "189-198", - "isbn": "123456789", - "doi": None, - "online_resource": None, - "other_reference_details": None, - "year": 1995 - } - ], - "data_access_input_data": [], - "data_access_output_data": [], - "data_access_related_urls": [], - "citations": [] -} \ No newline at end of file From d34bbcbe69fb7e41d89b00ec44919c4ddd915cf1 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 4 Dec 2020 10:09:03 +0000 Subject: [PATCH 32/59] Include fixture data for atbd --- db/loadTestData.sh | 1 + db/testDataFullAtbd.sql | 29 ++++++++++++++++++ .../008ac610-6228-11ea-8129-896df9cb11c0.png | Bin 0 -> 23871 bytes .../2b9f0170-e939-11ea-b22d-c9533a470a15.png | Bin 0 -> 26818 bytes .../2f71a9c0-e938-11ea-b22d-c9533a470a15.png | Bin 0 -> 123991 bytes .../7f422370-e939-11ea-b22d-c9533a470a15.png | Bin 0 -> 26818 bytes .../d61e9c60-6490-11ea-820e-ed9a4324b758.png | Bin 0 -> 12277 bytes .../e496fcb0-6490-11ea-820e-ed9a4324b758.png | Bin 0 -> 18134 bytes startserver.sh | 6 +++- 9 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 db/testDataFullAtbd.sql create mode 100644 figures/008ac610-6228-11ea-8129-896df9cb11c0.png create mode 100644 figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png create mode 100644 figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png create mode 100644 figures/7f422370-e939-11ea-b22d-c9533a470a15.png create mode 100644 figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png create mode 100644 figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png diff --git a/db/loadTestData.sh b/db/loadTestData.sh index 9ec82b82..02523387 100755 --- a/db/loadTestData.sh +++ b/db/loadTestData.sh @@ -1,2 +1,3 @@ #!/bin/bash psql 'postgres://masteruser:password@localhost:5432/nasadb?options=--search_path%3dapt' -f ./testData.sql +psql 'postgres://masteruser:password@localhost:5432/nasadb?options=--search_path%3dapt' -f ./testDataFullAtbd.sql diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql new file mode 100644 index 00000000..57ce5b20 --- /dev/null +++ b/db/testDataFullAtbd.sql @@ -0,0 +1,29 @@ +INSERT INTO apt.atbds (atbd_id, title, alias) VALUES (2, 'Filled Atbd', 'filled-atbd'); +INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\LaTeX~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', NULL, NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES (1, 1, 2, 'https://www.google.com/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Entry point for the research","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_input_variables (algorithm_input_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Acceleration","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Meters per second squared","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"m/s","marks":[]},{"object":"leaf","text":"2","marks":[{"object":"mark","type":"superscript","data":{}}]}]}]}]}}'); +INSERT INTO apt.algorithm_output_variables (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (2, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temp","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temperature","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvin","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_output_variables (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kmh","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kilometers per hour","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"km","marks":[]},{"object":"leaf","text":"h","marks":[{"object":"mark","type":"subscript","data":{}}]}]}]}]}}'); +INSERT INTO apt.contact_groups (contact_group_id, group_name, uuid, url, mechanisms, roles) VALUES (1, 'NASA Impact Team', '', '', '{"(Email,impact@nasa.gov)"}', '{"Data center contact"}'); +INSERT INTO apt.atbd_contact_groups (atbd_id, contact_group_id) VALUES (2, 1); +INSERT INTO apt.contacts (contact_id, first_name, middle_name, last_name, uuid, url, mechanisms, roles) VALUES (3, 'Aaron', '', 'Kaulfus', '', '', '{"(Email,bmf0006@uah.edu)"}', '{"Technical contact",Investigator}'); +INSERT INTO apt.atbd_contacts (atbd_id, contact_id) VALUES (2, 3); +INSERT INTO apt.citations (citation_id, atbd_version, atbd_id, creators, editors, title, series_name, release_date, release_place, publisher, version, issue, additional_details, online_resource) VALUES (1, 1, 2, 'Aaron Kaulfus', 'Aaron Kaulfus', 'Harmonized Landsat - An ATBD demo', '003.01', '2020-12-04', 'The internet', 'Firefox web browser', '1.5', 'B', NULL, 'earthdata.nasa.gov/'); +INSERT INTO apt.data_access_input_data (data_access_input_data_id, atbd_version, atbd_id, access_url, description) VALUES (1, 1, 2, 'https://registry.opendata.aws/landsat-8/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Registry for data storage","marks":[]}]}]}]}}'); +INSERT INTO apt.data_access_output_data (data_access_output_data_id, atbd_version, atbd_id, access_url, description) VALUES (1, 1, 2, 'https://sentinel.esa.int/web/sentinel/sentinel-data-access', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Conditions for data access","marks":[]}]}]}]}}'); +INSERT INTO apt.publication_references (publication_reference_id, atbd_version, atbd_id, authors, publication_date, title, series, edition, volume, issue, report_number, publication_place, publisher, pages, isbn, doi, online_resource, other_reference_details, year) VALUES (1, 1, 1, 'Charles Dickens, John Steinbeck', NULL, 'Example Reference', 'A', '3rd', '42ml', NULL, NULL, 'Boston', 'Penguin Books', '189-198', '123456789', NULL, NULL, NULL, 1995); + + +SELECT pg_catalog.setval('apt.algorithm_implementations_algorithm_implementation_id_seq', 1, true); +SELECT pg_catalog.setval('apt.algorithm_input_variables_algorithm_input_variable_id_seq', 3, true); +SELECT pg_catalog.setval('apt.algorithm_output_variables_algorithm_output_variable_id_seq', 3, true); +SELECT pg_catalog.setval('apt.atbds_atbd_id_seq', 2, true); +SELECT pg_catalog.setval('apt.citations_citation_id_seq', 1, true); +SELECT pg_catalog.setval('apt.contact_groups_contact_group_id_seq', 1, true); +SELECT pg_catalog.setval('apt.contacts_contact_id_seq', 3, true); +SELECT pg_catalog.setval('apt.data_access_input_data_data_access_input_data_id_seq', 1, true); +SELECT pg_catalog.setval('apt.data_access_output_data_data_access_output_data_id_seq', 1, true); +SELECT pg_catalog.setval('apt.data_access_related_urls_data_access_related_url_id_seq', 1, false); +SELECT pg_catalog.setval('apt.publication_references_publication_reference_id_seq', 1, true); + + diff --git a/figures/008ac610-6228-11ea-8129-896df9cb11c0.png b/figures/008ac610-6228-11ea-8129-896df9cb11c0.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad4d661e11bf08db6f178a7e0f5feb4898374d5 GIT binary patch literal 23871 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 MP+drcvQ_Z^0g0NHeEP7`e-=-9={3@wkhLm03yV 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 C1K}|M literal 0 HcmV?d00001 diff --git a/figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png b/figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png new file mode 100644 index 0000000000000000000000000000000000000000..e566dbddb99108d758176b8a9761ca4690907bea GIT binary patch literal 123991 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

    u)Ay*jeqoEoYWnv&*nzArH(q=A%=Ao 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 z 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 PfXl|x-lERTC;tBcg81J_ literal 0 HcmV?d00001 diff --git a/figures/7f422370-e939-11ea-b22d-c9533a470a15.png b/figures/7f422370-e939-11ea-b22d-c9533a470a15.png new file mode 100644 index 0000000000000000000000000000000000000000..b0135eceb14357b1fe3b45f94b8821ebd72393e3 GIT binary patch literal 26818 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 C1K}|M literal 0 HcmV?d00001 diff --git a/figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png b/figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2080da960755a4fc77b79b87f91ac62592cd33 GIT binary patch literal 12277 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 literal 0 HcmV?d00001 diff --git a/figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png b/figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd8c56f3f9e84bac74a91030d9ba9c0a0acab1a GIT binary patch literal 18134 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 v9{{eQ{(K;5^/dev/null -aws --endpoint-url=${S3} s3 cp ./figures/fullmoon.jpg s3://"$FIGURES_S3_BUCKET" --no-sign-request +# Upload all images +for file in figures/* +do + aws --endpoint-url=${S3} s3 cp ${file} s3://"$FIGURES_S3_BUCKET" --no-sign-request +done # localstack: create s3 bucket for pdfs aws --endpoint-url=${S3} s3 mb s3://"$PDFS_S3_BUCKET" --no-sign-request From c7983dbcb8bc359d21e83c7fe816e7b2df229d02 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Mon, 7 Dec 2020 12:54:04 -0500 Subject: [PATCH 33/59] fixed NaN value in tables --- pdf/app/latex/serialize.py | 26 ++++++++++++++------------ pdf/app/latex/temp.json | 3 +++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 pdf/app/latex/temp.json diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index d22dec4d..dc794050 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -61,13 +61,14 @@ def processTable(nodeRows, caption=None): escape=False, header=True, column_format=column_format, + na_rep=" ", ) if caption: to_latex_params["caption"] = caption latexTable = df.to_latex(**to_latex_params) - # insert [H] for block latex from "floating" the table to the top of the page - latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[H]") + # insert [h] for block latex from "floating" the table to the top of the page + latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[h]") return latexTable @@ -324,12 +325,12 @@ def processContacts(collection): def processVarList(element): for var in element: - var["long_name"] = processWYSIWYG(json.loads(var["long_name"])).replace( - "\\", "" - ) + var["long_name"] = processWYSIWYG(json.loads(var["long_name"])).strip("\\") if var["unit"] is None: continue - var["unit"] = processWYSIWYG(json.loads(var["unit"])).replace("\\", "") + print("UNIT: ") + print(processWYSIWYG(json.loads(var["unit"]))) + var["unit"] = processWYSIWYG(json.loads(var["unit"])).strip("\\") pd.set_option("display.max_colwidth", 1000) varDF = pd.DataFrame.from_dict(element, orient="columns") @@ -344,6 +345,7 @@ def processVarList(element): bold_rows=True, escape=False, column_format=column_format, + na_rep=" ", columns=["long_name", "unit"], header=["\\textbf{{Name}}", "\\textbf{{Unit}}"], ) @@ -380,8 +382,8 @@ def processATBD(element): "data_access_input_data": processDataAccess, "data_access_output_data": processDataAccess, "data_access_related_urls": processDataAccessURL, - "journal_discussion": processText, - "jounral_acknowledgements": processText, + "discussion": processText, + "acknowledgements": processText, } @@ -465,11 +467,11 @@ def texVariables(self): # TODO: remove this one `journal_discussion` and `journal_acknowledgements` # get added as fields to the database - if self.journal and not myJson.get("journal_discussion"): - myJson["journal_discussion"] = None + if self.journal and not myJson.get("discussion"): + myJson["discussion"] = None - if self.journal and not myJson.get("journal_acknowledgements"): - myJson["journal_acknowledgements"] = None + if self.journal and not myJson.get("acknowledgements"): + myJson["acknowledgements"] = None commands += [texify(x, y) for x, y in myJson.items() if x in mapVars.keys()] diff --git a/pdf/app/latex/temp.json b/pdf/app/latex/temp.json new file mode 100644 index 00000000..0e90fa0b --- /dev/null +++ b/pdf/app/latex/temp.json @@ -0,0 +1,3 @@ +{ + "stderr": "", + "stdout": "This is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty)\\nNo file tmp0klbhwyg.aux.\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n\\nPackage hyperref Warning: Rerun to get /PageLabels entry.\\n\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\nThis is BibTeX, Version 0.99d (TeX Live 2019/dev/Debian)\\nThe top-level auxiliary file: tmp0klbhwyg.aux\\nI found no \\\\bibdata command---while reading file tmp0klbhwyg.aux\\nI found no \\\\bibstyle command---while reading file tmp0klbhwyg.aux\\n(There were 2 error messages)\\nThis is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty) (./tmp0klbhwyg.aux)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n(./tmp0klbhwyg.out) (./tmp0klbhwyg.out)\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\nThis is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty) (./tmp0klbhwyg.aux)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n(./tmp0klbhwyg.out) (./tmp0klbhwyg.out)\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\n\"}" \ No newline at end of file From dfbc956c503cf5965e0d08e6f9059ca1718d9ec5 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Mon, 7 Dec 2020 12:54:25 -0500 Subject: [PATCH 34/59] removed unused temp file --- pdf/app/latex/temp.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pdf/app/latex/temp.json diff --git a/pdf/app/latex/temp.json b/pdf/app/latex/temp.json deleted file mode 100644 index 0e90fa0b..00000000 --- a/pdf/app/latex/temp.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "stderr": "", - "stdout": "This is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty)\\nNo file tmp0klbhwyg.aux.\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n\\nPackage hyperref Warning: Rerun to get /PageLabels entry.\\n\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\nThis is BibTeX, Version 0.99d (TeX Live 2019/dev/Debian)\\nThe top-level auxiliary file: tmp0klbhwyg.aux\\nI found no \\\\bibdata command---while reading file tmp0klbhwyg.aux\\nI found no \\\\bibstyle command---while reading file tmp0klbhwyg.aux\\n(There were 2 error messages)\\nThis is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty) (./tmp0klbhwyg.aux)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n(./tmp0klbhwyg.out) (./tmp0klbhwyg.out)\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\nThis is XeTeX, Version 3.14159265-2.6-0.99999 (TeX Live 2019/dev/Debian) (preloaded format=xelatex)\\n \\\\write18 enabled.\\nentering extended mode\\nLaTeX2e <2018-12-01>\\n(/tmp/nasa-apt-pdf-service-7ycno4kt/tmp0klbhwyg.tex\\n(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls\\nDocument Class: article 2018/09/03 v1.4i Standard LaTeX document class\\n(/usr/share/texlive/texmf-dist/tex/latex/base/size12.clo))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3packages/xparse/xparse.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/expl3-code.tex)\\n(/usr/share/texlive/texmf-dist/tex/latex/l3kernel/l3xdvipdfmx.def)))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/fontenc.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/base/tuenc.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/fontspec/fontspec.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/color.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/color.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-def/xetex.def))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-hyperref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/hobsub-generic.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/keyval.sty)\\n(/usr/share/texlive/texmf-dist/tex/generic/ifxetex/ifxetex.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/auxhook.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/kvoptions.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/pd1enc.def)\\n(/usr/share/texlive/texmf-dist/tex/latex/latexconfig/hyperref.cfg)\\n(/usr/share/texlive/texmf-dist/tex/latex/url/url.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/hxetex.def\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/puenc.def)\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/stringenc.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/oberdiek/rerunfilecheck.sty))\\n(/usr/share/texlive/texmf-dist/tex/latex/booktabs/booktabs.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphicx.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/graphics.sty\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics/trig.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/graphics-cfg/graphics.cfg)))\\n(/usr/share/texlive/texmf-dist/tex/latex/float/float.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/setspace/setspace.sty)\\n(/usr/share/texlive/texmf-dist/tex/latex/lineno/lineno.sty) (./tmp0klbhwyg.aux)\\n(/usr/share/texlive/texmf-dist/tex/latex/hyperref/nameref.sty\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/gettitlestring.sty))\\n(./tmp0klbhwyg.out) (./tmp0klbhwyg.out)\\n\\nLaTeX Warning: No \\\\author given.\\n\\n\\nLaTeX Font Warning: Font shape `TU/LatinModernMath(0)/bx/n' undefined\\n(Font) using `TU/LatinModernMath(0)/m/n' instead on input line 115\\n.\\n\\n(/usr/share/texlive/texmf-dist/tex/generic/oberdiek/se-ascii-print.def)\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 116--116\\n\\n\\nOverfull \\\\hbox (72.0pt too wide) in paragraph at lines 116--116\\n [][] \\n\\nLaTeX Warning: Citation `REFone' on page 1 undefined on input line 119.\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 119--120\\n\\n[1]\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n\\nUnderfull \\\\hbox (badness 10000) in paragraph at lines 124--124\\n\\n[2] [3]\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 136--137\\n[] \\n\\nOverfull \\\\hbox (58.0pt too wide) in paragraph at lines 139--140\\n[] \\n[4]\\n! Undefined control sequence.\\nl.166 \\\\Discussion\\n \\n? \\n! Emergency stop.\\nl.166 \\\\Discussion\\n \\nOutput written on tmp0klbhwyg.pdf (4 pages).\\nTranscript written on tmp0klbhwyg.log.\\n\"}" \ No newline at end of file From 84fa0779cb9657b7780fa72dd3574bb72fc837f9 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Mon, 7 Dec 2020 22:11:02 -0500 Subject: [PATCH 35/59] WIP - working on docker-networking issue --- db/testData.sql | 80 +++++++++++++++++++++++++---------------- db/testDataFullAtbd.sql | 71 ++++++++++++++++++++++++++++-------- docker-compose.yml | 22 +++++++++--- pdf/app/cache.py | 21 +++++++---- startserver.sh | 12 +++---- 5 files changed, 145 insertions(+), 61 deletions(-) diff --git a/db/testData.sql b/db/testData.sql index 5f6c93f0..b54502c7 100644 --- a/db/testData.sql +++ b/db/testData.sql @@ -1,34 +1,52 @@ -INSERT INTO contacts(first_name, last_name, mechanisms, roles) -VALUES ('Leonardo', 'Davinci', '{ "(\"Email\",\"test@email.com\")" }', '{ "Science contact", "Metadata author" }'); -INSERT INTO contacts(first_name, last_name) -VALUES ('Gregor', 'Mendel'); -INSERT INTO atbds(title, alias) -VALUES ('Test ATBD 1', 'test-atbd-1'); -INSERT INTO atbd_contacts(atbd_id, contact_id) -VALUES (1, 1); -INSERT INTO atbd_versions(atbd_id, atbd_version, scientific_theory, introduction, historical_perspective) -VALUES (1, 1, -'{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A line of text in a paragraph."}]}]}, +INSERT INTO contacts + (first_name, last_name, mechanisms, roles) +VALUES + ('Leonardo', 'Davinci', '{ "(\"Email\",\"test@email.com\")" }', '{ "Science contact", "Metadata author" }'); +INSERT INTO contacts + (first_name, last_name) +VALUES + ('Gregor', 'Mendel'); +INSERT INTO atbds + (title, alias) +VALUES + ('Test ATBD 1', 'test-atbd-1'); +INSERT INTO atbd_contacts + (atbd_id, contact_id) +VALUES + (1, 1); +INSERT INTO atbd_versions + (atbd_id, atbd_version, scientific_theory, introduction, historical_perspective) +VALUES + (1, 1, + '{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A line of text in a paragraph."}]}]}, {"object":"block","type":"equation","nodes":[{"object":"text","leaves":[{"text":"\\int_0^\\infty x^2 dx"}]}]}, -{"object":"block","type":"image","data":{"src":"http://localstack:4572/nasa-apt-dev-figures/fullmoon.jpg", "caption": "Image of the full moon - 2019"}}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"An introduction.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","caption":"A Table containing important data", "data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 1","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 3","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A historical perspective. We are now referencing ","marks":[]}]},{"object":"inline","type":"reference","data":{"id":1,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}'); +{"object":"block","type":"image","data":{"src":"http://localhost:4566/nasa-apt-dev-figures/fullmoon_resized.jpg", "caption": "Image of the full moon - 2019"}}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"An introduction.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","caption":"A Table containing important data", "data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 1","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 3","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A historical perspective. We are now referencing ","marks":[]}]},{"object":"inline","type":"reference","data":{"id":1,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}'); -- NOTE: the absolute url for fullmoon.jpg will work for the pdf serialization service running in docker-compose. it will not work for --- web browser rendering same image. This can be manually changed to http://localhost:4572/nasa-apt-dev-figures/fullmoon.jpg +-- web browser rendering same image. This can be manually changed to http://localhost:4566/nasa-apt-dev-figures/fullmoon.jpg -- to render in browser. -INSERT INTO algorithm_input_variables(atbd_id, atbd_version, name, long_name) -VALUES (1, 1, -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 1","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Variable 1","marks":[]}]}]}]}}'); -INSERT INTO algorithm_input_variables(atbd_id, atbd_version, name, long_name, unit) -VALUES (1, 1, -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 2","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input variable that is quite long and should be wrapped over at least two lines but possible also three","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); -INSERT INTO algorithm_output_variables(atbd_id, atbd_version, name, long_name, unit) -VALUES (1, 1, -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Var 1","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Variable 1","marks":[]}]}]}]}}', -'{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); -INSERT INTO publication_references(publication_reference_id, atbd_version, atbd_id, authors, title, series, edition, volume, publication_place, publisher, pages, isbn, year) -VALUES (1,1,1, 'Charles Dickens, John Steinbeck', 'Example Reference', 'A', '3rd', '42ml', 'Boston','Penguin Books', '189-198', 123456789, 1995); \ No newline at end of file +INSERT INTO algorithm_input_variables + (atbd_id, atbd_version, name, long_name) +VALUES + (1, 1, + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 1","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Variable 1","marks":[]}]}]}]}}'); +INSERT INTO algorithm_input_variables + (atbd_id, atbd_version, name, long_name, unit) +VALUES + (1, 1, + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input Var 2","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input variable that is quite long and should be wrapped over at least two lines but possible also three","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); +INSERT INTO algorithm_output_variables + (atbd_id, atbd_version, name, long_name, unit) +VALUES + (1, 1, + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Var 1","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Output Variable 1","marks":[]}]}]}]}}', + '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvins","marks":[]}]}]}]}}'); +INSERT INTO publication_references + (publication_reference_id, atbd_version, atbd_id, authors, title, series, edition, volume, publication_place, publisher, pages, isbn, year) +VALUES + (1, 1, 1, 'Charles Dickens, John Steinbeck', 'Example Reference', 'A', '3rd', '42ml', 'Boston', 'Penguin Books', '189-198', 123456789, 1995); \ No newline at end of file diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql index 57ce5b20..099223e4 100644 --- a/db/testDataFullAtbd.sql +++ b/db/testDataFullAtbd.sql @@ -1,17 +1,60 @@ -INSERT INTO apt.atbds (atbd_id, title, alias) VALUES (2, 'Filled Atbd', 'filled-atbd'); -INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\LaTeX~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', NULL, NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); -INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES (1, 1, 2, 'https://www.google.com/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Entry point for the research","marks":[]}]}]}]}}'); -INSERT INTO apt.algorithm_input_variables (algorithm_input_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Acceleration","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Meters per second squared","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"m/s","marks":[]},{"object":"leaf","text":"2","marks":[{"object":"mark","type":"superscript","data":{}}]}]}]}]}}'); -INSERT INTO apt.algorithm_output_variables (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (2, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temp","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temperature","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvin","marks":[]}]}]}]}}'); -INSERT INTO apt.algorithm_output_variables (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) VALUES (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kmh","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kilometers per hour","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"km","marks":[]},{"object":"leaf","text":"h","marks":[{"object":"mark","type":"subscript","data":{}}]}]}]}]}}'); -INSERT INTO apt.contact_groups (contact_group_id, group_name, uuid, url, mechanisms, roles) VALUES (1, 'NASA Impact Team', '', '', '{"(Email,impact@nasa.gov)"}', '{"Data center contact"}'); -INSERT INTO apt.atbd_contact_groups (atbd_id, contact_group_id) VALUES (2, 1); -INSERT INTO apt.contacts (contact_id, first_name, middle_name, last_name, uuid, url, mechanisms, roles) VALUES (3, 'Aaron', '', 'Kaulfus', '', '', '{"(Email,bmf0006@uah.edu)"}', '{"Technical contact",Investigator}'); -INSERT INTO apt.atbd_contacts (atbd_id, contact_id) VALUES (2, 3); -INSERT INTO apt.citations (citation_id, atbd_version, atbd_id, creators, editors, title, series_name, release_date, release_place, publisher, version, issue, additional_details, online_resource) VALUES (1, 1, 2, 'Aaron Kaulfus', 'Aaron Kaulfus', 'Harmonized Landsat - An ATBD demo', '003.01', '2020-12-04', 'The internet', 'Firefox web browser', '1.5', 'B', NULL, 'earthdata.nasa.gov/'); -INSERT INTO apt.data_access_input_data (data_access_input_data_id, atbd_version, atbd_id, access_url, description) VALUES (1, 1, 2, 'https://registry.opendata.aws/landsat-8/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Registry for data storage","marks":[]}]}]}]}}'); -INSERT INTO apt.data_access_output_data (data_access_output_data_id, atbd_version, atbd_id, access_url, description) VALUES (1, 1, 2, 'https://sentinel.esa.int/web/sentinel/sentinel-data-access', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Conditions for data access","marks":[]}]}]}]}}'); -INSERT INTO apt.publication_references (publication_reference_id, atbd_version, atbd_id, authors, publication_date, title, series, edition, volume, issue, report_number, publication_place, publisher, pages, isbn, doi, online_resource, other_reference_details, year) VALUES (1, 1, 1, 'Charles Dickens, John Steinbeck', NULL, 'Example Reference', 'A', '3rd', '42ml', NULL, NULL, 'Boston', 'Penguin Books', '189-198', '123456789', NULL, NULL, NULL, 1995); +INSERT INTO apt.atbds + (atbd_id, title, alias) +VALUES + (2, 'Filled Atbd', 'filled-atbd'); +INSERT INTO apt.atbd_versions + (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) +VALUES + (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\LaTeX~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_implementations + (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) +VALUES + (1, 1, 2, 'https://www.google.com/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Entry point for the research","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_input_variables + (algorithm_input_variable_id, atbd_version, atbd_id, name, long_name, unit) +VALUES + (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Acceleration","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Meters per second squared","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"m/s","marks":[]},{"object":"leaf","text":"2","marks":[{"object":"mark","type":"superscript","data":{}}]}]}]}]}}'); +INSERT INTO apt.algorithm_output_variables + (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) +VALUES + (2, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temp","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Temperature","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kelvin","marks":[]}]}]}]}}'); +INSERT INTO apt.algorithm_output_variables + (algorithm_output_variable_id, atbd_version, atbd_id, name, long_name, unit) +VALUES + (3, 1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kmh","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Kilometers per hour","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"km","marks":[]},{"object":"leaf","text":"h","marks":[{"object":"mark","type":"subscript","data":{}}]}]}]}]}}'); +INSERT INTO apt.contact_groups + (contact_group_id, group_name, uuid, url, mechanisms, roles) +VALUES + (1, 'NASA Impact Team', '', '', '{"(Email,impact@nasa.gov)"}', '{"Data center contact"}'); +INSERT INTO apt.atbd_contact_groups + (atbd_id, contact_group_id) +VALUES + (2, 1); +INSERT INTO apt.contacts + (contact_id, first_name, middle_name, last_name, uuid, url, mechanisms, roles) +VALUES + (3, 'Aaron', '', 'Kaulfus', '', '', '{"(Email,bmf0006@uah.edu)"}', '{"Technical contact",Investigator}'); +INSERT INTO apt.atbd_contacts + (atbd_id, contact_id) +VALUES + (2, 3); +INSERT INTO apt.citations + (citation_id, atbd_version, atbd_id, creators, editors, title, series_name, release_date, release_place, publisher, version, issue, additional_details, online_resource) +VALUES + (1, 1, 2, 'Aaron Kaulfus', 'Aaron Kaulfus', 'Harmonized Landsat - An ATBD demo', '003.01', '2020-12-04', 'The internet', 'Firefox web browser', '1.5', 'B', NULL, 'earthdata.nasa.gov/'); +INSERT INTO apt.data_access_input_data + (data_access_input_data_id, atbd_version, atbd_id, access_url, description) +VALUES + (1, 1, 2, 'https://registry.opendata.aws/landsat-8/', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Registry for data storage","marks":[]}]}]}]}}'); +INSERT INTO apt.data_access_output_data + (data_access_output_data_id, atbd_version, atbd_id, access_url, description) +VALUES + (1, 1, 2, 'https://sentinel.esa.int/web/sentinel/sentinel-data-access', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Conditions for data access","marks":[]}]}]}]}}'); +INSERT INTO apt.publication_references + (publication_reference_id, atbd_version, atbd_id, authors, publication_date, title, series, edition, volume, issue, report_number, publication_place, publisher, pages, isbn, doi, online_resource, other_reference_details, year) +VALUES + (2, 1, 2, 'Charles Dickens, John Steinbeck', NULL, 'Example Reference', 'A', '3rd', '42ml', NULL, NULL, 'Boston', 'Penguin Books', '189-198', '123456789', NULL, NULL, NULL, 1995); + SELECT pg_catalog.setval('apt.algorithm_implementations_algorithm_implementation_id_seq', 1, true); diff --git a/docker-compose.yml b/docker-compose.yml index 00333e33..0a759fd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,12 +12,21 @@ services: POSTGRES_PASSWORD: password # localstack for local development only. AWS S3 used for staging/production localstack: - image: localstack/localstack:latest - ports: - - 4572:4572 #s3 + image: localstack/localstack + # # https://github.com/localstack/localstack/issues/2983 + # - 4566:4566 # edge service + # - 4572:4572 + # - 4571:4571 environment: - SERVICES=s3 - DATA_DIR=./figures + - HOSTNAME_EXTERNAL=localstack + - DEFAULT_REGION=us-east-1 + - DEBUG=1 + - LOCALSTACK_HOSTNAME=localstack + hostname: localstack + ports: + - "4563-4584:4563-4584" rest-api: image: postgrest/postgrest:v6.0.2 ports: @@ -48,6 +57,8 @@ services: depends_on: - localstack - rest-api + # links: + # - "localstack:localhost" swagger: image: swaggerapi/swagger-ui ports: @@ -67,4 +78,7 @@ services: image: dadarek/wait-for-dependencies depends_on: - localstack - command: localstack:4572 + command: localstack:4566 + + + \ No newline at end of file diff --git a/pdf/app/cache.py b/pdf/app/cache.py index 0493b712..a1c48c88 100644 --- a/pdf/app/cache.py +++ b/pdf/app/cache.py @@ -15,6 +15,7 @@ class Cache: A simple s3 based cache for atbd pdfs. It has no cache invalidation logic, because the app business rules enforce only Published atbds should have cached pdfs, and Published atbds cannot be edited. So they are static resources. """ + s3_endpoint: ParseResult bucket_name: str s3_client: botostubs.S3 @@ -29,7 +30,7 @@ def __init__(self, s3_endpoint: str, bucket_name: str): """ self.s3_endpoint = urlparse(s3_endpoint) self.bucket_name = bucket_name - self.s3_client = boto3.client('s3', endpoint_url=self.s3_endpoint.geturl()) + self.s3_client = boto3.client("s3", endpoint_url=self.s3_endpoint.geturl()) def get_file_url(self, key: str) -> Optional[str]: """ @@ -42,9 +43,10 @@ def get_file_url(self, key: str) -> Optional[str]: :rtype: Optional[str] :raises CacheException: """ + return None try: response = self.s3_client.head_object(Key=key, Bucket=self.bucket_name) - if response['ContentLength'] > 0: + if response["ContentLength"] > 0: return self.s3_url_for_object(key) except UnknownKeyError: pass # cache miss @@ -65,7 +67,9 @@ def put_file(self, key: str, filename: str) -> str: :raises CacheException: """ try: - self.s3_client.upload_file(filename, self.bucket_name, key, ExtraArgs={'ACL': 'public-read'}) + self.s3_client.upload_file( + filename, self.bucket_name, key, ExtraArgs={"ACL": "public-read"} + ) except ClientError as e: raise CacheException(str(e)) from e return self.s3_url_for_object(key) @@ -80,8 +84,13 @@ def s3_url_for_object(self, key: str) -> str: :return: s3 url :rtype: str """ + scheme = self.s3_endpoint.scheme # workaround for local port forwarding in dev environment - port = f':{self.s3_endpoint.port}' if self.s3_endpoint.port else '' - hostname = 'localhost' if self.s3_endpoint.hostname == 'localstack' else self.s3_endpoint.hostname - return f'{scheme}://{hostname}{port}/{self.bucket_name}/{key}' + port = f":{self.s3_endpoint.port}" if self.s3_endpoint.port else "" + hostname = ( + "localhost" + if self.s3_endpoint.hostname == "localstack" + else self.s3_endpoint.hostname + ) + return f"{scheme}://{hostname}{port}/{self.bucket_name}/{key}" diff --git a/startserver.sh b/startserver.sh index 552f5f46..030d39b8 100755 --- a/startserver.sh +++ b/startserver.sh @@ -5,8 +5,8 @@ set -e PG_PORT=5432 -S3_PORT=4572 -S3=http://localhost:$S3_PORT # localstack +S3_PORT=4566 +S3=http://localstack:$S3_PORT # localstack # .env loading in the shell dotenv () { @@ -36,14 +36,14 @@ then exit 1 fi -docker-compose build +docker-compose build # blocks until given endpoints are accessible over tcp -docker-compose run --rm localstack-ready -docker-compose run --rm db-ready +docker-compose run --rm localstack-ready +docker-compose run --rm db-ready # start remaining services -docker-compose up --detach +docker-compose up --detach # all the services are up, now create & populate the s3 buckets and the pg database From 22e112aa0051dd080db4dca58ac0afe6c70b2353 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Mon, 7 Dec 2020 22:23:18 -0500 Subject: [PATCH 36/59] WIP - working on container networking, fixed journal acknowledgements and discussion --- db/testData.sql | 2 +- db/testDataFullAtbd.sql | 2 +- pdf/app/latex/ATBD_JOURNAL.tex | 6 ++++-- pdf/app/latex/serialize.py | 16 +++++++--------- startserver.sh | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/db/testData.sql b/db/testData.sql index b54502c7..4978f3ce 100644 --- a/db/testData.sql +++ b/db/testData.sql @@ -20,7 +20,7 @@ VALUES (1, 1, '{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A line of text in a paragraph."}]}]}, {"object":"block","type":"equation","nodes":[{"object":"text","leaves":[{"text":"\\int_0^\\infty x^2 dx"}]}]}, -{"object":"block","type":"image","data":{"src":"http://localhost:4566/nasa-apt-dev-figures/fullmoon_resized.jpg", "caption": "Image of the full moon - 2019"}}]}}', +{"object":"block","type":"image","data":{"src":"http://localstack:4566/nasa-apt-dev-figures/fullmoon_resized.jpg", "caption": "Image of the full moon - 2019"}}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"An introduction.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","caption":"A Table containing important data", "data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 1","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 3","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A historical perspective. We are now referencing ","marks":[]}]},{"object":"inline","type":"reference","data":{"id":1,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}'); -- NOTE: the absolute url for fullmoon.jpg will work for the pdf serialization service running in docker-compose. it will not work for diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql index 099223e4..d19257ba 100644 --- a/db/testDataFullAtbd.sql +++ b/db/testDataFullAtbd.sql @@ -5,7 +5,7 @@ VALUES INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES - (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\LaTeX~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); + (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex index 1f8b423d..7ab4f727 100644 --- a/pdf/app/latex/ATBD_JOURNAL.tex +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -11,6 +11,8 @@ \providecommand{\PerformanceAssessmentValidationMethods}{} \providecommand{\PerformanceAssessmentValidationUncertainties}{} \providecommand{\PerformanceAssessmentValidationErrors}{} +\providecommand{\JournalDiscussion}{} +\providecommand{\JournalAcknowledgements}{} \title{\ATBDTitle} \date{\today} @@ -93,10 +95,10 @@ \section{Data Access Related URLs} \DataAccessRelatedUrls \section{Discussion} -\Discussion +\JournalDiscussion \section{Acknowledgements} -\Acknowledgements +\JournalAcknowledgements \section{Contacts} \Contacts diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index dc794050..e0416a14 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -28,6 +28,7 @@ references = [] refIDs = {} + # from https://stackoverflow.com/questions/19053707/converting-snake-case-to-lower-camel-case-lowercamelcase def toCamelCase(snake_str): components = snake_str.split("_") @@ -382,8 +383,8 @@ def processATBD(element): "data_access_input_data": processDataAccess, "data_access_output_data": processDataAccess, "data_access_related_urls": processDataAccessURL, - "discussion": processText, - "acknowledgements": processText, + "journal_discussion": processWYSIWYG, + "journal_acknowledgements": processWYSIWYG, } @@ -465,13 +466,10 @@ def texVariables(self): for item, value in myJson.items(): print("item: {}, value: {}".format(item, value)) - # TODO: remove this one `journal_discussion` and `journal_acknowledgements` - # get added as fields to the database - if self.journal and not myJson.get("discussion"): - myJson["discussion"] = None - - if self.journal and not myJson.get("acknowledgements"): - myJson["acknowledgements"] = None + if not self.journal and myJson.get("journal_discussion"): + del myJson["journal_discussion"] + if not self.journal and myJson.get("journal_acknowledgements"): + del myJson["journal_acknowledgements"] commands += [texify(x, y) for x, y in myJson.items() if x in mapVars.keys()] diff --git a/startserver.sh b/startserver.sh index 030d39b8..ab6e7310 100755 --- a/startserver.sh +++ b/startserver.sh @@ -6,7 +6,7 @@ set -e PG_PORT=5432 S3_PORT=4566 -S3=http://localstack:$S3_PORT # localstack +S3=http://localhost:$S3_PORT # localstack # .env loading in the shell dotenv () { From 164d145485e8028f293d7878edf06ce03ef2b5ec Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 8 Dec 2020 11:16:49 -0500 Subject: [PATCH 37/59] cleaned up docker --- db/testData.sql | 2 +- docker-compose.yml | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/db/testData.sql b/db/testData.sql index 4978f3ce..2adb1259 100644 --- a/db/testData.sql +++ b/db/testData.sql @@ -20,7 +20,7 @@ VALUES (1, 1, '{"document":{"nodes":[{"object":"block","type":"paragraph","nodes":[{"object":"text","leaves":[{"text":"A line of text in a paragraph."}]}]}, {"object":"block","type":"equation","nodes":[{"object":"text","leaves":[{"text":"\\int_0^\\infty x^2 dx"}]}]}, -{"object":"block","type":"image","data":{"src":"http://localstack:4566/nasa-apt-dev-figures/fullmoon_resized.jpg", "caption": "Image of the full moon - 2019"}}]}}', +{"object":"block","type":"image","data":{"src":"http://localstack:4566/nasa-apt-dev-figures/fullmoon.jpg", "caption": "Image of the full moon - 2019"}}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"An introduction.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","caption":"A Table containing important data", "data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 1","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Table Column 3","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (long) - This is a super long cell value. It should be wrapped several times, perhaps 2 but although at this point maybe even 3. ","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Cell value (short)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A historical perspective. We are now referencing ","marks":[]}]},{"object":"inline","type":"reference","data":{"id":1,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}'); -- NOTE: the absolute url for fullmoon.jpg will work for the pdf serialization service running in docker-compose. it will not work for diff --git a/docker-compose.yml b/docker-compose.yml index 0a759fd7..e4fe29be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,21 +12,13 @@ services: POSTGRES_PASSWORD: password # localstack for local development only. AWS S3 used for staging/production localstack: - image: localstack/localstack - # # https://github.com/localstack/localstack/issues/2983 - # - 4566:4566 # edge service - # - 4572:4572 - # - 4571:4571 + image: localstack/localstack:latest environment: - SERVICES=s3 - DATA_DIR=./figures - - HOSTNAME_EXTERNAL=localstack - - DEFAULT_REGION=us-east-1 - - DEBUG=1 - - LOCALSTACK_HOSTNAME=localstack - hostname: localstack ports: - - "4563-4584:4563-4584" + - "4566:4566" + - "4572:4572" rest-api: image: postgrest/postgrest:v6.0.2 ports: From 65d251380b3e59e052e87315092f62754f12be68 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 8 Dec 2020 11:27:21 -0500 Subject: [PATCH 38/59] removed cache bypass --- pdf/app/cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pdf/app/cache.py b/pdf/app/cache.py index a1c48c88..bba36acc 100644 --- a/pdf/app/cache.py +++ b/pdf/app/cache.py @@ -43,7 +43,6 @@ def get_file_url(self, key: str) -> Optional[str]: :rtype: Optional[str] :raises CacheException: """ - return None try: response = self.s3_client.head_object(Key=key, Bucket=self.bucket_name) if response["ContentLength"] > 0: From 0b43d978fe399facdffb1874d7f24dfdba827c44 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 07:26:41 -0600 Subject: [PATCH 39/59] saml auth updates --- cloudformation/cloudformation.yaml | 54 +++- db/deploy/anonymous.sql | 23 ++ db/revert/anonymous.sql | 7 + db/sqitch.plan | 1 + db/verify/anonymous.sql | 7 + fastapi/app/cache.py | 32 ++- fastapi/app/main.py | 109 ++++++-- fastapi/app/requirements.txt | 3 +- fastapi/app/saml.py | 399 ++++++++++++++++++++--------- nasa-apt | 62 +++++ testsearch.http | 21 ++ update_container.sh | 4 + 12 files changed, 552 insertions(+), 170 deletions(-) create mode 100644 db/deploy/anonymous.sql create mode 100644 db/revert/anonymous.sql create mode 100644 db/verify/anonymous.sql create mode 100755 nasa-apt create mode 100644 testsearch.http create mode 100755 update_container.sh diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 897d6e8f..02f74ce6 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -43,7 +43,7 @@ Parameters: traffic to this service. PostgRESTPriority: Type: Number - Default: 3 + Default: 2 Description: The priority for the routing rule added to the load balancer. This only applies if your have multiple services which are assigned to different paths on the load balancer. @@ -67,10 +67,14 @@ Parameters: Description: API prefix as used by FastAPI in PDF service (must be consistent with PDFPATH) FastapiPriority: Type: Number - Default: 2 + Default: 1 Description: The priority for the routing rule added to the load balancer. This only applies if your have multiple services which are assigned to different paths on the load balancer. + PostgRESTAPIPrefix: + Type: String + Default: "/" + Description: API prefix as used by PostGrest PDFsBucketName: Type: String Default: pdfs @@ -96,6 +100,10 @@ Parameters: Type: String Default: t2.medium ConstraintDescription: Please choose a valid instance type. + JWTSecret: + Type: String + Default: lkjlkjslkdjaflkjfsalkjfasdlkfdlkjlkjslkdjaflkjfsalkjfasdlkfdlkjlkjslkdjaflkjfsalkjfasdlkfd + Description: Secret to use for creating JWT Tokens Mappings: # Hard values for the subnet masks. These masks define # the range of internal IP addresses that can be assigned. @@ -291,6 +299,7 @@ Resources: - !Ref PublicSubnetTwo SecurityGroups: [!Ref 'PublicLoadBalancerSG'] + # A dummy target group is used to setup the ALB to just drop traffic # initially, before any real service target groups have been added. DummyTargetGroupPublic: @@ -375,6 +384,9 @@ Resources: - Name: PGRST_DB_SCHEMA Value: apt + - + Name: PGRST_JWT_SECRET + Value: !Ref 'JWTSecret' - Name: PGRST_DB_ANON_ROLE Value: app_user @@ -398,7 +410,7 @@ Resources: DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 - DesiredCount: 2 + DesiredCount: 1 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED @@ -430,7 +442,7 @@ Resources: - Name: !Sub '${AWS::StackName}-ctr-fastapi' Cpu: !Ref 'FastapiContainerCpu' Memory: !Ref 'FastapiContainerMemory' - Image: 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/prod/fastapi:latest + Image: 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/saml/fastapi:latest PortMappings: - ContainerPort: 80 Environment: @@ -452,6 +464,14 @@ Resources: !Ref 'DBName']] - Name: ELASTICURL Value: !Join ['', ['https://', !GetAtt [Elasticsearch, DomainEndpoint]]] + - Name: APT_FRONTEND_URL + Value: 'http://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com' + - Name: IDP_METADATA_URL + Value: 'https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3' + - Name: FASTAPI_HOST + Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] + - Name: JWT_SECRET + Value: !Ref 'JWTSecret' LogConfiguration: LogDriver: 'awslogs' Options: @@ -472,7 +492,7 @@ Resources: DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 - DesiredCount: 2 + DesiredCount: 1 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED @@ -495,14 +515,14 @@ Resources: PostgRESTTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: - HealthCheckPath: / + HealthCheckPath: !Ref 'PostgRESTAPIPrefix' Matcher: # if the database has not been migrated yet, then http 400 is expected HttpCode: '200,400' TargetType: ip Name: !Sub '${AWS::StackName}-tg-pgr' Port: 3000 - Protocol: HTTP + Protocol: HTTPS UnhealthyThresholdCount: 2 VpcId: !Ref VPC @@ -521,10 +541,28 @@ Resources: TargetType: ip Name: !Sub '${AWS::StackName}-tg-fastapi' Port: 80 - Protocol: HTTP + Protocol: HTTPS VpcId: !Ref VPC # Create a rule on the load balancer for routing traffic to the target group + AuthLoadBalancerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref 'FastapiTargetGroup' + Type: 'forward' + Conditions: + - Field: path-pattern + PathPatternConfig: + Values: + - /sso + - /slo + - /sls + - /attrs + - /acs + Priority: !Ref 'FastapiPriority' + ListenerArn: !Ref PublicLoadBalancerListener + PostgRESTLoadBalancerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: diff --git a/db/deploy/anonymous.sql b/db/deploy/anonymous.sql new file mode 100644 index 00000000..072cd8d2 --- /dev/null +++ b/db/deploy/anonymous.sql @@ -0,0 +1,23 @@ +-- Deploy nasa-apt:anonymous to pg + +BEGIN; +--create role if not anonymous noinherit; +grant anonymous to masteruser; +REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA apt FROM anonymous; +REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA apt FROM anonymous; +GRANT SELECT ON ALL TABLES IN SCHEMA apt TO anonymous; +GRANT USAGE ON SCHEMA apt TO anonymous; +GRANT EXECUTE ON FUNCTION apt.search_text TO anonymous; + +ALTER TABLE apt.atbd_versions ENABLE ROW LEVEL SECURITY; + +CREATE POLICY anon ON apt.atbd_versions TO anonymous + USING (status='Published'); + + +GRANT ALL ON SCHEMA apt to app_user; +GRANT ALL ON ALL TABLES IN SCHEMA apt TO app_user; +GRANT ALL ON ALL FUNCTIONS IN SCHEMA apt TO app_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA apt TO app_user; +CREATE POLICY appuser ON apt.atbd_versions TO app_user USING (true); +COMMIT; diff --git a/db/revert/anonymous.sql b/db/revert/anonymous.sql new file mode 100644 index 00000000..e8f0d43c --- /dev/null +++ b/db/revert/anonymous.sql @@ -0,0 +1,7 @@ +-- Revert nasa-apt:anonymous from pg + +BEGIN; + +-- XXX Add DDLs here. + +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index d15dbf54..cd3114eb 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -14,3 +14,4 @@ atbdAlias [tables] 2020-05-07T14:34:43Z Daniel da Silva # Creates a unique alias when copying atbd textsearchAlias [textsearch] 2020-05-08T15:57:18Z Daniel da Silva # Includes the atbs alias when searching notifytriggers 2020-07-30T16:31:36Z David Bitner # Add triggers to send notify to be used for updating elastic +anonymous 2020-12-10T13:24:06Z David Bitner # add row level security for anonymous users diff --git a/db/verify/anonymous.sql b/db/verify/anonymous.sql new file mode 100644 index 00000000..58ef0867 --- /dev/null +++ b/db/verify/anonymous.sql @@ -0,0 +1,7 @@ +-- Verify nasa-apt:anonymous on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/fastapi/app/cache.py b/fastapi/app/cache.py index 0493b712..2534d4a0 100644 --- a/fastapi/app/cache.py +++ b/fastapi/app/cache.py @@ -15,6 +15,7 @@ class Cache: A simple s3 based cache for atbd pdfs. It has no cache invalidation logic, because the app business rules enforce only Published atbds should have cached pdfs, and Published atbds cannot be edited. So they are static resources. """ + s3_endpoint: ParseResult bucket_name: str s3_client: botostubs.S3 @@ -29,7 +30,9 @@ def __init__(self, s3_endpoint: str, bucket_name: str): """ self.s3_endpoint = urlparse(s3_endpoint) self.bucket_name = bucket_name - self.s3_client = boto3.client('s3', endpoint_url=self.s3_endpoint.geturl()) + self.s3_client = boto3.client( + "s3", endpoint_url=self.s3_endpoint.geturl() + ) def get_file_url(self, key: str) -> Optional[str]: """ @@ -43,8 +46,10 @@ def get_file_url(self, key: str) -> Optional[str]: :raises CacheException: """ try: - response = self.s3_client.head_object(Key=key, Bucket=self.bucket_name) - if response['ContentLength'] > 0: + response = self.s3_client.head_object( + Key=key, Bucket=self.bucket_name + ) + if response["ContentLength"] > 0: return self.s3_url_for_object(key) except UnknownKeyError: pass # cache miss @@ -65,7 +70,12 @@ def put_file(self, key: str, filename: str) -> str: :raises CacheException: """ try: - self.s3_client.upload_file(filename, self.bucket_name, key, ExtraArgs={'ACL': 'public-read'}) + self.s3_client.upload_file( + filename, + self.bucket_name, + key, + ExtraArgs={"ACL": "public-read"}, + ) except ClientError as e: raise CacheException(str(e)) from e return self.s3_url_for_object(key) @@ -82,6 +92,14 @@ def s3_url_for_object(self, key: str) -> str: """ scheme = self.s3_endpoint.scheme # workaround for local port forwarding in dev environment - port = f':{self.s3_endpoint.port}' if self.s3_endpoint.port else '' - hostname = 'localhost' if self.s3_endpoint.hostname == 'localstack' else self.s3_endpoint.hostname - return f'{scheme}://{hostname}{port}/{self.bucket_name}/{key}' + port = ( + f":{self.s3_endpoint.port}" + if self.s3_endpoint.port + else "" + ) + hostname = ( + "localhost" + if self.s3_endpoint.hostname == "localstack" + else self.s3_endpoint.hostname + ) + return f"{scheme}://{hostname}{port}/{self.bucket_name}/{key}" diff --git a/fastapi/app/main.py b/fastapi/app/main.py index f12b400d..027d50e7 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -3,11 +3,20 @@ from tempfile import TemporaryDirectory from typing import Union, Dict, Type -from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, Depends +from fastapi import ( + FastAPI, + HTTPException, + BackgroundTasks, + Request, + Depends, +) from fastapi.logger import logger -from fastapi.responses import FileResponse, RedirectResponse, JSONResponse +from fastapi.responses import ( + FileResponse, + RedirectResponse, + JSONResponse, +) from fastapi.middleware.cors import CORSMiddleware -from starlette.middleware.sessions import SessionMiddleware from .atbd.checksum_atbd import checksum_atbd from .atbd.get_atbd import get_atbd @@ -16,9 +25,14 @@ from .cache import Cache, CacheException from .latex.json_to_latex import json_to_latex, JsonToLatexException from .pdf.latex_to_pdf import latex_to_pdf, LatexToPDFException -from .search.searchindex import update_index, index_atbd, ELASTICURL, aws_auth +from .search.searchindex import ( + update_index, + index_atbd, + ELASTICURL, + aws_auth, +) from .saml import router as saml -from .saml import User, require_user +from .saml import User, require_user, get_user import asyncpg import requests @@ -46,7 +60,9 @@ ) app: FastAPI = FastAPI() -cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) +cache: Cache = Cache( + s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name +) origins = [ @@ -64,12 +80,14 @@ allow_headers=["*"], ) -app.add_middleware( - SessionMiddleware, - secret_key = 'lk23j4l24jk23789098ulkhjljkjlk' -) +# app.add_middleware( +# SessionMiddleware, secret_key="lk23j4l24jk23789098ulkhjljkjlk" +# ) -app.include_router(saml) +app.include_router( + saml, + prefix="/saml", +) @app.on_event("startup") @@ -86,7 +104,7 @@ async def startup() -> None: @app.on_event("shutdown") async def shutdown() -> None: - await app.state.connection.remove_listener('atbd', index_atbd) + await app.state.connection.remove_listener("atbd", index_atbd) await app.state.connection.close() @@ -102,7 +120,9 @@ def get_cache_key(atbd_doc: Dict) -> str: hex_digest: str = checksum_atbd(atbd_doc) alias: str = atbd_doc["atbd"]["alias"] return ( - f"{hex_digest}/{alias}.pdf" if alias else f"{hex_digest}/nasa-atbd.pdf" + f"{hex_digest}/{alias}.pdf" + if alias + else f"{hex_digest}/nasa-atbd.pdf" ) @@ -140,7 +160,9 @@ def atbd_pdf_handler( tmp_dir: str = tmp_dir_resource.name background_tasks.add_task(cleanup_tmp_dir, tmp_dir_resource) try: - (latex_filename, _) = json_to_latex(atbd_doc=atbd_doc, tmp_dir=tmp_dir) + (latex_filename, _) = json_to_latex( + atbd_doc=atbd_doc, tmp_dir=tmp_dir + ) except JsonToLatexException as e: raise HTTPException(status_code=500, detail=str(e)) from e try: @@ -162,16 +184,33 @@ def atbd_pdf_handler( raise HTTPException(status_code=500, detail=str(e)) from e +@app.get("/") +def index(user: User = Depends(require_user)): + return {} + + @app.get(root_path + "atbds/id/{atbd_id}.pdf") -def get_atbd_by_id(atbd_id: int, background_tasks: BackgroundTasks, user: User=Depends(require_user)): +def get_atbd_by_id( + atbd_id: int, + background_tasks: BackgroundTasks, + user: User = Depends(get_user), +): atbd_doc = get_atbd(atbd_id=atbd_id) - return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) + return atbd_pdf_handler( + atbd_doc, background_tasks=background_tasks + ) @app.get(root_path + "atbds/alias/{alias}.pdf") -def get_atbd_pdf_by_alias(alias: str, background_tasks: BackgroundTasks, user: User=Depends(require_user)): +def get_atbd_pdf_by_alias( + alias: str, + background_tasks: BackgroundTasks, + user: User = Depends(get_user), +): atbd_doc = get_atbd(alias=alias) - return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) + return atbd_pdf_handler( + atbd_doc, background_tasks=background_tasks + ) @app.get(root_path) @@ -199,31 +238,49 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): @app.get(root_path + "reindex",) -async def reindex(request: Request, user: User=Depends(require_user)): +async def reindex( + request: Request, user: User = Depends(require_user) +): """ Reindex all ATBD's into ElasticSearch """ - logger.info('Reindexing %s', ELASTICURL) - results = await update_index(connection=request.app.state.connection) + logger.info("Reindexing %s", ELASTICURL) + results = await update_index( + connection=request.app.state.connection + ) return JSONResponse(content=results) @app.post(root_path + "search",) -async def search_elastic(request: Request, user: User=Depends(require_user)): +async def search_elastic( + request: Request, user: User = Depends(get_user) +): """ Proxies POST json to elastic search endpoint """ url = f"{ELASTICURL}/atbd/_search" - data = await request.body() + data = await request.json() + logger.info("User %s", user) + logger.info("data: %s", data) + if user is None: + data['query']['bool']['filter'] = [ + { + "match": { + "status": "published" + } + } + ] logger.info("Searching %s %s", url, data) auth = aws_auth() response = requests.post( url, auth=auth, - data=data, - headers={"Content-Type": "application/json"} + json=data, + headers={"Content-Type": "application/json"}, + ) + logger.info( + "status:%s response:%s", response.status_code, response.text ) - logger.info('status:%s response:%s', response.status_code, response.text) if not response.ok: raise HTTPException( status_code=response.status_code, detail=response.text diff --git a/fastapi/app/requirements.txt b/fastapi/app/requirements.txt index 4d2a06b6..73ca8c3c 100644 --- a/fastapi/app/requirements.txt +++ b/fastapi/app/requirements.txt @@ -41,4 +41,5 @@ websockets==8.1 yarg==0.1.9 itsdangerous==1.1.0 python3-saml==1.9.0 -python-multipart +python-multipart==0.0.5 +python-jose==3.2.0 diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index 6ec67ceb..5c670f55 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -1,28 +1,51 @@ import os from os import environ +from datetime import datetime, timedelta +from urllib.parse import urlparse, urlencode, urlunparse from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.settings import OneLogin_Saml2_Settings -from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser +from onelogin.saml2.idp_metadata_parser import ( + OneLogin_Saml2_IdPMetadataParser, +) -from fastapi import APIRouter, Depends, Response, Request, HTTPException +from fastapi import ( + APIRouter, + Depends, + Response, + Request, + HTTPException, + Cookie, +) from starlette.responses import RedirectResponse from starlette.datastructures import UploadFile -from typing import Union +from typing import Union, Optional + +from jose import jwt +from jose.exceptions import JWTError from fastapi.logger import logger import logging logger.setLevel(logging.DEBUG) +secret:str = environ.get('JWT_SECRET') or exit ( "JWT_SECRET ENV var required") +token_life = 3600 + host: str = environ.get("FASTAPI_HOST") or exit( "FASTAPI_HOST env var required" ) host = str.lower(host) +host_parsed = urlparse(host) idp_metadata_url: str = environ.get("IDP_METADATA_URL") or exit( "IDP_METADATA_URL env var required" ) +# idp_metadata_url = 'https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3' +idp_metadata_url = 'https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml' +default_return: str = environ.get("APT_FRONTEND_URL") or exit( + "APT_FRONTEND_URL env var required" +) router = APIRouter() @@ -31,76 +54,79 @@ idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( idp_metadata_url ) -logger.debug('test debug') - -init_settings = { - "strict": True, - "debug": True, - "sp": { - "entityId": f"{host}/metadata", - "assertionConsumerService": { - "url": f"{host}/acs", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - }, - "singleLogoutService": { - "url": f"{host}/sls", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", - }, - "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", - "x509cert": "", - "privateKey": "", - }, - "idp": idp_data["idp"], - "security": { - "nameIdEncrypted": False, - "authnRequestsSigned": False, - "logoutRequestSigned": False, - "logoutResponsesSigned": False, - "signMetadata": False, - "wantMessagesSigned": False, - "wantAssertionsSigned": False, - "wantNameId": False, - "wantNameIdEncrypted": False, - "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", - }, -} - -settings = OneLogin_Saml2_Settings( - settings=init_settings, - custom_base_path=base_path, -) +logger.debug("test debug") + + +def url_for_path(path): + url = urlparse(host) + return urlunparse(url._replace(path=path)) class SamlAuth: def __init__( self, request: Request, + response: Response, + RelayState: Optional[str] = default_return, + return_to: Optional[str] = default_return, + token: Optional[str] = Cookie(None), + jwt: Optional[str] = None, ): self.request = request - self.session = request.session - self.user = None - userdata = request.session.get("samlUserdata") - if userdata and len(userdata) > 0: - self.user = userdata.items() + self.response = response + # self.session = request.session + + self.base_url = host + self.url = url_for_path(request.url.path) + self.relay_state = ( + RelayState if RelayState != self.url else default_return + ) + self.return_to = ( + return_to if return_to != self.url else default_return + ) self.auth = None - self.acs_url = request.url_for("acs") - self.sso_url = request.url_for("sso") - self.metadata_url = request.url_for("metadata") - self.attrs_url = request.url_for("attrs") - self.slo_url = request.url_for("slo") - url = request.url - self.base_url = f'{url.scheme}://{url.hostname}:{url.port}' + self.settings = None + self.name_id = None + self.name_id_format = None + self.nq = None + self.spnq = None + self.session_index = None + self.userdata = None + self.COOKIE_token = token + self.GET_token = jwt + self.token_data = self.get_token_data() + self.user = self.userdata + logger.warning( + "url: %s %s, relay_state: %s %s return_to: %s %s", + self.request.url, + self.url, + RelayState, + self.relay_state, + return_to, + self.return_to, + ) + + def url_for(self, path): + path = urlparse(self.request.url_for(path)).path + return url_for_path(path) async def prepare_saml_request(self): - url = self.request.url + url = urlparse(self.url) get_data = self.request.query_params._dict form = await self.request.form() post_data = {} for key, value in form.multi_items(): if not isinstance(value, UploadFile): post_data[key] = value - + if key == 'RelayState': + if value != self.url: + self.relay_state = value + logger.warning( + "prepare_saml_request url: %s, relay_state: %s return_to: %s", + self.url, + self.relay_state, + self.return_to, + ) return { "https": "on" if url.scheme == "https" else "off", "http_host": url.hostname, @@ -112,15 +138,130 @@ async def prepare_saml_request(self): async def get_auth(self): self.saml_request = await self.prepare_saml_request() + init_settings = { + "strict": True, + "debug": True, + "sp": { + "entityId": f"{self.base_url}/", + "assertionConsumerService": { + "url": f"{self.url_for('acs')}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + }, + "singleLogoutService": { + "url": f"{self.url_for('slo')}", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + }, + "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "x509cert": "", + "privateKey": "", + }, + "idp": idp_data["idp"], + "security": { + "nameIdEncrypted": False, + "authnRequestsSigned": False, + "logoutRequestSigned": False, + "logoutResponsesSigned": False, + "signMetadata": False, + "wantMessagesSigned": False, + "wantAssertionsSigned": False, + "wantNameId": False, + "wantNameIdEncrypted": False, + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + }, + } + + self.settings = OneLogin_Saml2_Settings( + settings=init_settings, custom_base_path=base_path, + ) self.auth = OneLogin_Saml2_Auth( - self.saml_request, - old_settings=settings, + self.saml_request, old_settings=self.settings, ) return self.auth - -async def saml_auth(request: Request): - saml = SamlAuth(request) + def save_session(self): + if self.user is not None: + self.response.set_cookie( + key="jwt", value=self.create_token() + ) + + def create_token_data(self): + exp = datetime.utcnow() + timedelta(seconds=token_life) + data = { + "userdata": self.userdata, + "name_id": self.name_id, + "nq": self.nq, + "spnq": self.spnq, + "name_id_format": self.name_id_format, + "session_index": self.session_index, + "exp": exp, + "role": "app_user", + } + return data + + def create_token(self): + return jwt.encode(self.create_token_data(), secret) + + def parse_token(self, token): + if token is None: + return None + try: + contents = jwt.decode(token, secret) + for key, value in contents.items(): + if hasattr(self, key): + setattr(self, key, value) + except JWTError: + return None + return contents + + def get_auth_from_header(self): + auth_header = self.request.headers.get("Authorization", None) + if auth_header is None: + return None + scheme, _, token = auth_header.partition(" ") + if scheme.lower() != "bearer": + return None + self.parse_token(token) + + def get_token_data(self): + token_data = self.get_auth_from_header() + if token_data is None or self.userdata is None: + token_data = self.parse_token(self.COOKIE_token) + if token_data is None or self.userdata is None: + token_data = self.parse_token(self.GET_token) + self.token_data = token_data + return token_data + + def raise_autherror(self): + if self.auth is not None: + errors = self.auth.get_errors() + if len(errors) > 0: + msg = ",".join(errors) + raise HTTPException( + status_code=401, detail=f"Errors: {msg}" + ) + return None + + def redirect(self, url): + # only redirect when there are no errors + logger.warning('redirecting %s', url) + self.raise_autherror() + if self.userdata is None: + response = RedirectResponse(url=url, status_code=303) + response.delete_cookie("token") + else: + token = self.create_token() + logger.warning('redirect %s %s', url, token) + if token is not None: + params = f"token={token}" + url += ('&' if urlparse(url).query else '?') + params + logger.warning('return url %s', url) + response = RedirectResponse(url=url, status_code=303) + response.set_cookie("token", token, max_age=3600) + return response + + +async def saml_auth(saml: SamlAuth = Depends(SamlAuth)): await saml.get_auth() return saml @@ -128,106 +269,108 @@ async def saml_auth(request: Request): User = Union[dict, None] -async def require_user(request: Request) -> User: - saml = SamlAuth(request) - if saml.user is not None: - return saml.user - raise HTTPException(status_code=401) +async def require_user(saml: SamlAuth = Depends(SamlAuth)) -> User: + if saml.userdata is not None: + return saml.userdata + raise HTTPException( + status_code=401, + detail=f"Not logged in. Please log in at {saml.url_for('sso')}", + ) -async def get_user(request: Request) -> User: - saml = SamlAuth(request) - return saml.user +async def get_user(saml: SamlAuth = Depends(SamlAuth)) -> User: + return saml.userdata @router.get("/sso") async def sso( - saml: SamlAuth = Depends(saml_auth), return_to: str = host + saml: SamlAuth = Depends(saml_auth) ): - return RedirectResponse(url=saml.auth.login(return_to)) + return RedirectResponse(url=saml.auth.login(saml.return_to)) @router.post("/acs") -async def acs( - saml: SamlAuth = Depends(saml_auth), -): +async def acs(saml: SamlAuth = Depends(saml_auth),): auth = saml.auth - session = saml.session - saml_request = saml.saml_request - request_id = None - if "AuthNRequestID" in session: - request_id = session["AuthNRequestID"] - auth.process_response(request_id=request_id) - errors = auth.get_errors() - if len(errors) == 0: - if "AuthNRequestID" in session: - del session["AuthNRequestID"] - session["samlUserdata"] = auth.get_attributes() - session["samlNameId"] = auth.get_nameid() - session["samlNameIdFormat"] = auth.get_nameid_format() - session["samlNameIdNameQualifier"] = auth.get_nameid_nq() - session["samlNameIdSPNameQualifier"] = auth.get_nameid_spnq() - session["samlSessionIndex"] = auth.get_session_index() - self_url = saml.acs_url - RelayState = saml_request.get("post_data").get("RelayState", None) - if RelayState and self_url != RelayState: - return RedirectResponse(url=RelayState, status_code=303) - else: - return RedirectResponse(url=saml.attrs_url, status_code=303) - elif auth.get_settings().is_debug_active(): - error_reason = auth.get_last_error_reason() - return {"authorization_error": error_reason} + auth.process_response() + saml.raise_autherror() + saml.name_id = auth.get_nameid() + saml.name_id_format = auth.get_nameid_format() + saml.nq = auth.get_nameid_nq() + saml.spnq = auth.get_nameid_spnq() + saml.session_index = auth.get_session_index() + saml.userdata = auth.get_attributes() + logger.warning('acs relay_state: %s', saml.relay_state) + if saml.relay_state != saml.url_for('sso'): + relay_state = saml.relay_state + else: + logger.warning('Relay State set to SSO, changing to %s', default_return) + relay_state = default_return + return saml.redirect(relay_state) @router.get("/slo") @router.post("/slo") -async def slo( - saml: SamlAuth = Depends(saml_auth), return_to: str = host -): +async def slo(saml: SamlAuth = Depends(saml_auth)): auth = saml.auth - session = saml.session + logger.warning('slo return_to: %s, relay_state: %s', saml.return_to, saml.relay_state) url = auth.logout( - return_to=return_to, - name_id=session.get("samlNameId", None), - session_index=session.get("samlSessionIndex", None), - nq=session.get("samlNameIdNameQualifier", None), - name_id_format=session.get("samlNameIdFormat", None), - spnq=session.get("samlNameIdSPNameQualifier", None), + return_to=saml.return_to, + name_id=saml.name_id, + session_index=saml.session_index, + nq=saml.nq, + name_id_format=saml.name_id_format, + spnq=saml.spnq, ) - return RedirectResponse(url=url) + saml.userdata = None + return saml.redirect(url) @router.get("/sls") @router.post("/sls") -async def sls( - saml: SamlAuth = Depends(saml_auth), RelayState: str = host -): - request_id = None - session = saml.session +async def sls(saml: SamlAuth = Depends(saml_auth)): auth = saml.auth - if "LogoutRequestID" in session: - request_id = session["LogoutRequestID"] - auth.process_slo(request_id=request_id, delete_session_cb=session.clear) - session.clear() - errors = auth.get_errors() - if len(errors) == 0: - return RedirectResponse(url=RelayState, status_code=303) - elif auth.get_settings().is_debug_active(): - error_reason = auth.get_last_error_reason() - return {"logout_error": error_reason} + auth.process_slo() + logger.warning('acs relay_state: %s', saml.relay_state) + if saml.relay_state != saml.request.url_for('slo'): + relay_state = saml.relay_state + else: + logger.warning('Relay State set to SLO, changing to %s', default_return) + relay_state = default_return + return saml.redirect(relay_state) @router.get("/attrs") @router.post("/attrs") -async def attrs(user: User = Depends(require_user)): - return user +async def attrs(saml: SamlAuth = Depends(saml_auth)): + if saml.userdata is not None: + ret = saml.userdata.copy() + ret['token'] = saml.create_token() + return ret + raise HTTPException( + status_code=401, + detail=f"Not logged in. Please log in at {saml.request.url_for('sso')}", + ) + + +@router.get("/token") +async def token(saml: SamlAuth = Depends(saml_auth)): + if saml.userdata is not None: + return {"token": saml.create_token()} + raise HTTPException( + status_code=401, + detail=f"Not logged in. Please log in at {saml.url_for('sso')}", + ) @router.get("/metadata") async def metadata(saml: SamlAuth = Depends(saml_auth)): + settings = saml.settings metadata = settings.get_sp_metadata() errors = settings.validate_metadata(metadata) if len(errors) == 0: - return Response(content=metadata, media_type="application/xml") + return Response( + content=metadata, media_type="application/xml" + ) else: return Response(content=", ".join(errors), status_code=500) diff --git a/nasa-apt b/nasa-apt new file mode 100755 index 00000000..c60417c7 --- /dev/null +++ b/nasa-apt @@ -0,0 +1,62 @@ +#!/bin/bash + +command=$1 +shift + +set -a +source .env +set +a +REGION=us-east-1 +ECR=552819999234.dkr.ecr.us-east-1.amazonaws.com +CONTAINER=${ECR}/nasa-apt/saml/fastapi + +if [ -z ${STACKNAME+x} ]; + then read -p 'Stack name: ' STACKNAME +fi + +if [ -z ${DBPASS+x} ]; + then read -p 'Stack name: ' DBPASS +fi + + +get_output(){ + echo $outputs | jq --raw-output ".[][] | select(.OutputKey==\"$1\").OutputValue" +} +outputs=`aws cloudformation describe-stacks --region us-east-1 --stack-name $STACKNAME --output json --query 'Stacks[*].Outputs[*]'` + +# CLUSTER=$(get_output CLUsTER) +# FASTAPISERVICE=$(get_output SERVICE) + + + +deploy(){ + cd cloudformation + aws cloudformation deploy \ + --template-file cloudformation.yaml \ + --stack-name $STACKNAME \ + --tags Project=nasa-apt \ + --parameter-overrides \ + DBName=nasadb \ + DBUser=masteruser \ + DBPassword=$DBPASS \ + ElasticsearchDomainName=nasadb-$STACKNAME \ + --region us-east-1 --capabilities CAPABILITY_IAM +} + +update_fastapi(){ + aws ecr get-login-password --region $REGION | \ + docker login --username AWS --password-stdin $ECR + + docker build -t $CONTAINER fastapi/. && \ + docker push $CONTAINER && \ + aws ecs update-service --force-new-deployment \ + --cluster $CLUSTER \ + --service $FASTAPISERVICE \ + --region $REGION +} + +dburl(){ + get_output PGConnection +} + +$command "$@" diff --git a/testsearch.http b/testsearch.http new file mode 100644 index 00000000..64c358d2 --- /dev/null +++ b/testsearch.http @@ -0,0 +1,21 @@ +POST https://apt.ds.io/fastapi/search HTTP/1.1 +content-type: application/json + +{ + "query": { + "bool": { + "must": [ + { + "multi_match": { + "query": "is" + } + } + ] + } + }, + "highlight": { + "fields": { + "*": {} + } + } +} diff --git a/update_container.sh b/update_container.sh new file mode 100755 index 00000000..b61f3221 --- /dev/null +++ b/update_container.sh @@ -0,0 +1,4 @@ +aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 552819999234.dkr.ecr.us-east-1.amazonaws.com +docker build -t 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/saml/fastapi fastapi/. +docker push 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/saml/fastapi +aws ecs update-service --force-new-deployment --cluster nasa-apt-samltest2-ECSCluster-4EbHcIESsbQD --service nasa-apt-samltest2-svc-fastapi --region us-east-1 From a63da1bb51a592d1580a5bd40417d768d1a21f20 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 07:30:54 -0600 Subject: [PATCH 40/59] update .env.sample with saml conf --- .env.sample | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 92ac157f..0193b828 100644 --- a/.env.sample +++ b/.env.sample @@ -13,8 +13,18 @@ PDFS_S3_BUCKET=nasa-apt-dev-pdfs # s3 bucket name for figures, pngs for example. used by cloudformation/deploy.sh and by docker-compose.yml FIGURES_S3_BUCKET=nasa-apt-dev-figures -# postgrest api -REST_API_ENDPOINT=http://rest-api:3000 +# url for database +DBURL=postgres://masteruser:password@db:5432/nasadb + +# url for elasticsearch +ELASTICURL=http://localhost:9200 + +# url for FastApi +FASTAPI_HOST=http://localhost:8000 # Frontend Url used to set CORS origin for FastAPI APT_FRONTEND_URL=http://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com + +# url for SAML IDP metadata +# IDP_METADATA_URL=https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3 +IDP_METADATA_URL=https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml \ No newline at end of file From ce347cb89b8965563dbc7814eed659acb85096ad Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 08:50:38 -0600 Subject: [PATCH 41/59] add mock option for authentication --- .env.sample | 4 ++- db/deploy/anonymous.sql | 2 +- db/deploy/notifytriggers.sql | 10 +++---- db/deploy/triggerfix.sql | 23 ++++++++++++++++ db/revert/triggerfix.sql | 7 +++++ db/sqitch.plan | 1 + db/verify/triggerfix.sql | 7 +++++ docker-compose.yml | 2 ++ fastapi/app/saml.py | 53 ++++++++++++++++++++++++++---------- 9 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 db/deploy/triggerfix.sql create mode 100644 db/revert/triggerfix.sql create mode 100644 db/verify/triggerfix.sql diff --git a/.env.sample b/.env.sample index 0193b828..c59e15d3 100644 --- a/.env.sample +++ b/.env.sample @@ -27,4 +27,6 @@ APT_FRONTEND_URL=http://nasa-apt-eltest-application.s3-website-us-east-1.amazona # url for SAML IDP metadata # IDP_METADATA_URL=https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3 -IDP_METADATA_URL=https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml \ No newline at end of file +# IDP_METADATA_URL=https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml + +JWT_SECRET=lksdjlkasjasdlkjasdlkjfdlasdfkkllkjasdfhlksdjlkasdjasdlkjasdlkjsdflkjwelkwejrlkrjwlkwejlwekjrwelkrjewlkrjwelkrj \ No newline at end of file diff --git a/db/deploy/anonymous.sql b/db/deploy/anonymous.sql index 072cd8d2..8039a24a 100644 --- a/db/deploy/anonymous.sql +++ b/db/deploy/anonymous.sql @@ -1,7 +1,7 @@ -- Deploy nasa-apt:anonymous to pg BEGIN; ---create role if not anonymous noinherit; +create role anonymous noinherit; grant anonymous to masteruser; REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA apt FROM anonymous; REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA apt FROM anonymous; diff --git a/db/deploy/notifytriggers.sql b/db/deploy/notifytriggers.sql index f856daa9..be47cb20 100644 --- a/db/deploy/notifytriggers.sql +++ b/db/deploy/notifytriggers.sql @@ -5,16 +5,16 @@ SET SEARCH_PATH to apt, public; CREATE OR REPLACE FUNCTION change_notification() RETURNS TRIGGER AS $$ DECLARE -atbd_id int; +_atbd_id int; BEGIN IF TG_TABLE_NAME = 'contacts' THEN - SELECT INTO atbd_id atbd_id from atbd_contacts WHERE contact_id=NEW.contact_id; + SELECT INTO _atbd_id atbd_id from atbd_contacts WHERE contact_id=NEW.contact_id; ELSIF TG_TABLE_NAME = 'contact_groups' THEN - SELECT INTO atbd_id atbd_id from atbd_contact_groupss WHERE contact_group_id=NEW.contact_group_id; + SELECT INTO _atbd_id atbd_id from atbd_contact_groups WHERE contact_group_id=NEW.contact_group_id; ELSE - atbd_id = NEW.atbd_id; + _atbd_id = NEW.atbd_id; END IF; -PERFORM pg_notify('atbd',atbd_id::text); +PERFORM pg_notify('atbd',_atbd_id::text); RETURN NEW; END; $$ LANGUAGE PLPGSQL; diff --git a/db/deploy/triggerfix.sql b/db/deploy/triggerfix.sql new file mode 100644 index 00000000..08c7d397 --- /dev/null +++ b/db/deploy/triggerfix.sql @@ -0,0 +1,23 @@ +-- Deploy nasa-apt:triggerfix to pg + +BEGIN; + +SET SEARCH_PATH to apt, public; + +CREATE OR REPLACE FUNCTION change_notification() RETURNS TRIGGER AS $$ +DECLARE +_atbd_id int; +BEGIN +IF TG_TABLE_NAME = 'contacts' THEN + SELECT INTO _atbd_id atbd_id from atbd_contacts WHERE contact_id=NEW.contact_id; +ELSIF TG_TABLE_NAME = 'contact_groups' THEN + SELECT INTO _atbd_id atbd_id from atbd_contact_groups WHERE contact_group_id=NEW.contact_group_id; +ELSE + _atbd_id = NEW.atbd_id; +END IF; +PERFORM pg_notify('atbd',_atbd_id::text); +RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + +COMMIT; diff --git a/db/revert/triggerfix.sql b/db/revert/triggerfix.sql new file mode 100644 index 00000000..9fa47d9b --- /dev/null +++ b/db/revert/triggerfix.sql @@ -0,0 +1,7 @@ +-- Revert nasa-apt:triggerfix from pg + +BEGIN; + +-- XXX Add DDLs here. + +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index cd3114eb..56a1c979 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -15,3 +15,4 @@ copyATBDAlias [copyATBD] 2020-05-08T15:56:26Z Daniel da Silva # Includes the atbs alias when searching notifytriggers 2020-07-30T16:31:36Z David Bitner # Add triggers to send notify to be used for updating elastic anonymous 2020-12-10T13:24:06Z David Bitner # add row level security for anonymous users +triggerfix 2020-12-10T14:01:42Z David Bitner # fix trigger for notifications diff --git a/db/verify/triggerfix.sql b/db/verify/triggerfix.sql new file mode 100644 index 00000000..a5394901 --- /dev/null +++ b/db/verify/triggerfix.sql @@ -0,0 +1,7 @@ +-- Verify nasa-apt:triggerfix on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/docker-compose.yml b/docker-compose.yml index 9afea41f..4dfc2461 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,6 +63,8 @@ services: ELASTICURL: http://elastic:9200 IDP_METADATA_URL: $IDP_METADATA_URL FASTAPI_HOST: http://localhost:8000 + JWT_SECRET: $JWT_SECRET + APT_FRONTEND_URL: $APT_FRONTEND_URL depends_on: - localstack - rest-api diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index 5c670f55..88029ed1 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -37,11 +37,15 @@ host = str.lower(host) host_parsed = urlparse(host) +mockauth=False idp_metadata_url: str = environ.get("IDP_METADATA_URL") or exit( "IDP_METADATA_URL env var required" ) -# idp_metadata_url = 'https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3' -idp_metadata_url = 'https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml' +if idp_metadata_url == 'mock': + # Use Fake Key + logger.warning('using mock authentication') + mockauth = True + default_return: str = environ.get("APT_FRONTEND_URL") or exit( "APT_FRONTEND_URL env var required" @@ -51,16 +55,19 @@ base_path = os.path.dirname(os.path.abspath(__file__)) -idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( - idp_metadata_url -) -logger.debug("test debug") +if not mockauth: + idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + idp_metadata_url + ) def url_for_path(path): url = urlparse(host) return urlunparse(url._replace(path=path)) +class MockAuth: + def get_errors(self): + return [] class SamlAuth: def __init__( @@ -137,6 +144,10 @@ async def prepare_saml_request(self): } async def get_auth(self): + if mockauth: + self.auth = MockAuth() + return self.auth + self.saml_request = await self.prepare_saml_request() init_settings = { "strict": True, @@ -286,20 +297,31 @@ async def get_user(saml: SamlAuth = Depends(SamlAuth)) -> User: async def sso( saml: SamlAuth = Depends(saml_auth) ): + if mockauth: + return RedirectResponse(url=f"{saml.url_for('acs')}?RelayState={saml.relay_state}") return RedirectResponse(url=saml.auth.login(saml.return_to)) @router.post("/acs") +@router.get("/acs") async def acs(saml: SamlAuth = Depends(saml_auth),): auth = saml.auth - auth.process_response() - saml.raise_autherror() - saml.name_id = auth.get_nameid() - saml.name_id_format = auth.get_nameid_format() - saml.nq = auth.get_nameid_nq() - saml.spnq = auth.get_nameid_spnq() - saml.session_index = auth.get_session_index() - saml.userdata = auth.get_attributes() + if mockauth: + saml.name_id = 'nameid' + saml.name_id_format = 'format' + saml.nq = 'nq' + saml.spnq = 'spnq' + saml.session_index = 'index' + saml.userdata = {"myattribute":1} + else: + auth.process_response() + saml.raise_autherror() + saml.name_id = auth.get_nameid() + saml.name_id_format = auth.get_nameid_format() + saml.nq = auth.get_nameid_nq() + saml.spnq = auth.get_nameid_spnq() + saml.session_index = auth.get_session_index() + saml.userdata = auth.get_attributes() logger.warning('acs relay_state: %s', saml.relay_state) if saml.relay_state != saml.url_for('sso'): relay_state = saml.relay_state @@ -314,6 +336,9 @@ async def acs(saml: SamlAuth = Depends(saml_auth),): async def slo(saml: SamlAuth = Depends(saml_auth)): auth = saml.auth logger.warning('slo return_to: %s, relay_state: %s', saml.return_to, saml.relay_state) + if mockauth: + saml.userdata = None + saml.redirect(saml.return_to) url = auth.logout( return_to=saml.return_to, name_id=saml.name_id, From c8cac1d6b92c720cdd4f802c32508ad5cb2b3bfe Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 09:09:58 -0600 Subject: [PATCH 42/59] adjustments to cloud formation load balancer targets --- cloudformation/cloudformation.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 02f74ce6..5fb7b8df 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -43,7 +43,7 @@ Parameters: traffic to this service. PostgRESTPriority: Type: Number - Default: 2 + Default: 3 Description: The priority for the routing rule added to the load balancer. This only applies if your have multiple services which are assigned to different paths on the load balancer. @@ -67,7 +67,7 @@ Parameters: Description: API prefix as used by FastAPI in PDF service (must be consistent with PDFPATH) FastapiPriority: Type: Number - Default: 1 + Default: 2 Description: The priority for the routing rule added to the load balancer. This only applies if your have multiple services which are assigned to different paths on the load balancer. @@ -555,12 +555,8 @@ Resources: - Field: path-pattern PathPatternConfig: Values: - - /sso - - /slo - - /sls - - /attrs - - /acs - Priority: !Ref 'FastapiPriority' + - /saml/* + Priority: 1 ListenerArn: !Ref PublicLoadBalancerListener PostgRESTLoadBalancerRule: From be36eb756bfa26785355c3b4092275cd76604935 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 09:24:53 -0600 Subject: [PATCH 43/59] add documentation to syncdb.sh --- syncdb.sh | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/syncdb.sh b/syncdb.sh index fcccae81..4fc41118 100755 --- a/syncdb.sh +++ b/syncdb.sh @@ -1,4 +1,19 @@ #!/bin/bash +if [ $# -ne 2]; then +echo < +# This script will update an instance using the data from another instance +# It goes through the following steps +# 1) Gets connection information from cloudformation for each stack +# 2) syncs figures in s3 buckets +# 3) uses pg_dump -a (only dump data) to dump the apt schema from the source data set +# 4) modifies the generated sql to change the url paths in the output +# 5) loads the data into the new database +# Note: the load is done in a transaction, so if there are any errors, +# the entire transaction is rolled back +EOD +fi + get_output(){ echo $outputs | jq --raw-output ".[][] | select(.OutputKey==\"$1\").OutputValue" } @@ -20,25 +35,6 @@ echo "syncing data from $fromfigures to $tofigures" aws s3 sync --delete s3://${fromfigures} s3://${tofigures} echo "syncing data from $fromdb to $todb while replacing $fromurl with $tourl" -psql -e -1 -v ON_ERROR_STOP=1 $todb < Date: Thu, 10 Dec 2020 09:31:19 -0600 Subject: [PATCH 44/59] add documentation .env.sample --- .env.sample | 2 ++ nasa-apt | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index c59e15d3..e775605d 100644 --- a/.env.sample +++ b/.env.sample @@ -28,5 +28,7 @@ APT_FRONTEND_URL=http://nasa-apt-eltest-application.s3-website-us-east-1.amazona # url for SAML IDP metadata # IDP_METADATA_URL=https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3 # IDP_METADATA_URL=https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml +# IDP_METADATA_URL=mock # Setting IDP_METADATA_URL=mock will bypass use of an idp server and will allow anyone to log in by going to /saml/sso +# Note: If the JWT_SECRET isn't sufficiently long, postgrest may complain JWT_SECRET=lksdjlkasjasdlkjasdlkjfdlasdfkkllkjasdfhlksdjlkasdjasdlkjasdlkjsdflkjwelkwejrlkrjwlkwejlwekjrwelkrjewlkrjwelkrj \ No newline at end of file diff --git a/nasa-apt b/nasa-apt index c60417c7..8935b367 100755 --- a/nasa-apt +++ b/nasa-apt @@ -1,5 +1,7 @@ #!/bin/bash - +# Start of a script for consolidating control of actions +# for managing stacks +# WIP command=$1 shift From f4cfb0ce0b70f138da92b9ea2162098fe5f163eb Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 13:17:04 -0600 Subject: [PATCH 45/59] add readme for deploying to https --- cloudformation/README.txt | 18 ++++++++++++++++++ cloudformation/cloudformation.yaml | 16 ++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 cloudformation/README.txt diff --git a/cloudformation/README.txt b/cloudformation/README.txt new file mode 100644 index 00000000..ed4339aa --- /dev/null +++ b/cloudformation/README.txt @@ -0,0 +1,18 @@ +Instructions for changing a Load Balancer to use HTTPS + +1) Deploy the stack using Cloudformation +2) Find the Load Balancer for the stack at https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LoadBalancers:sort=loadBalancerName +3) Select the checkbox for the Load Balancer and check on the Listeners tab in the box at the bottom +4) Click "Add listener" +5) Select HTTPS 443 +6) Select "+ Add action" -> Forward to -> -dummyTarget +7) Select "From ACM" and "apt.ds.io - ..." under Default SSL Certificate +8) Hit Save in the upper right and then the Back arrow next to Listeners in the upper left +9) Back at the bottom, select view/edit rules under the entry you just created +10) Click the + button then "+ Insert Rule" +11) Add the following rules making sure they end up in this order: + - IF Path is /saml/* THEN Forward to -tg-fastapi + - IF Path is /fastapi/* THEN Forward to -tg-fastapi + - IF Path is /* THEN Forward to -tg-pgr + 12) Go back to the list of Listeners and then to the HTTP 80 view/edit rules Remove all but the dummy rule + 13) Have Olaf or someone with Route53 Permissions move apt.ds.io (or setup a new domain) to point to that load balancer \ No newline at end of file diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 5fb7b8df..306e3d59 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -307,12 +307,12 @@ Resources: Properties: HealthCheckIntervalSeconds: 6 HealthCheckPath: / - HealthCheckProtocol: HTTPS + HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 Name: !Join ['-', [!Ref 'AWS::StackName', 'dummyTarget']] - Port: 443 - Protocol: HTTPS + Port: 80 + Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: !Ref 'VPC' @@ -323,8 +323,8 @@ Resources: - TargetGroupArn: !Ref 'DummyTargetGroupPublic' Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' - Port: 443 - Protocol: HTTPS + Port: 80 + Protocol: HTTP # This is a role which is used by the ECS tasks themselves. ECSTaskExecutionRole: @@ -467,7 +467,7 @@ Resources: - Name: APT_FRONTEND_URL Value: 'http://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com' - Name: IDP_METADATA_URL - Value: 'https://dbspatial.us.auth0.com/samlp/metadata/vEJRZ0X6OtSMzkhBqIUMM62KDJhSNyG3' + Value: 'https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml' - Name: FASTAPI_HOST Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] - Name: JWT_SECRET @@ -522,7 +522,7 @@ Resources: TargetType: ip Name: !Sub '${AWS::StackName}-tg-pgr' Port: 3000 - Protocol: HTTPS + Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: !Ref VPC @@ -541,7 +541,7 @@ Resources: TargetType: ip Name: !Sub '${AWS::StackName}-tg-fastapi' Port: 80 - Protocol: HTTPS + Protocol: HTTP VpcId: !Ref VPC # Create a rule on the load balancer for routing traffic to the target group From 27f059c0ad8c5eda6e715308eb4c0160023a7e85 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Thu, 10 Dec 2020 13:39:13 -0600 Subject: [PATCH 46/59] updates to syncdb.sh to run migrations first --- syncdb.sh | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/syncdb.sh b/syncdb.sh index 4fc41118..6e7eaf57 100755 --- a/syncdb.sh +++ b/syncdb.sh @@ -1,17 +1,20 @@ #!/bin/bash -if [ $# -ne 2]; then -echo < -# This script will update an instance using the data from another instance -# It goes through the following steps -# 1) Gets connection information from cloudformation for each stack -# 2) syncs figures in s3 buckets -# 3) uses pg_dump -a (only dump data) to dump the apt schema from the source data set -# 4) modifies the generated sql to change the url paths in the output -# 5) loads the data into the new database -# Note: the load is done in a transaction, so if there are any errors, -# the entire transaction is rolled back -EOD +set -e +if [ $# -ne 2 ]; then +echo " + Usage: + syncdb.sh + This script will update an instance using the data from another instance + It goes through the following steps + 1) Gets connection information from cloudformation for each stack + 2) syncs figures in s3 buckets + 3) uses pg_dump -a (only dump data) to dump the apt schema from the source data set + 4) modifies the generated sql to change the url paths in the output + 5) loads the data into the new database + Note: the load is done in a transaction, so if there are any errors, + the entire transaction is rolled back +" +exit fi get_output(){ @@ -34,6 +37,12 @@ tourl=${tos3}/${tofigures} echo "syncing data from $fromfigures to $tofigures" aws s3 sync --delete s3://${fromfigures} s3://${tofigures} +echo "Running sqitch to run any migrations on the to database" + +cd db +./sqitch deploy --verify db:$todb +cd .. + echo "syncing data from $fromdb to $todb while replacing $fromurl with $tourl" pg_dump -a --schema=apt $fromdb | \ From e0544b7565fcd9f71596fce32b67fe58242631ee Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Fri, 11 Dec 2020 12:25:27 -0500 Subject: [PATCH 47/59] added `align` wrapping around sample equation in full atbd and enabled `split` environment in equations --- db/testDataFullAtbd.sql | 2 +- pdf/app/latex/ATBD.tex | 2 ++ pdf/app/latex/ATBD_JOURNAL.tex | 2 ++ pdf/app/latex/serialize.py | 3 +++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql index d19257ba..87fb1716 100644 --- a/db/testDataFullAtbd.sql +++ b/db/testDataFullAtbd.sql @@ -5,7 +5,7 @@ VALUES INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES - (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); + (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\begin{aligned} \\\\ m_{12} (\\{ \\mathit{Red},\\mathit{Blue} \\})= K^\\prime* [ &m_{1}(\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) \\\\ & + m_{1} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\}) \\\\ & + m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{1} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\})] \\end{aligned}","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 5c50c998..6499e0b2 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -23,6 +23,8 @@ \usepackage{booktabs} \usepackage{graphicx} \usepackage{float} +\usepackage{amsmath} +\usepackage{breqn} \setmainfont{Latin Modern Math} %Using a font with good unicode math symbols % coverage to support the use of unicode math symbols in LaTeX text mode diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex index 7ab4f727..196bb61d 100644 --- a/pdf/app/latex/ATBD_JOURNAL.tex +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -25,6 +25,8 @@ \usepackage{booktabs} \usepackage{graphicx} \usepackage{float} +\usepackage{amsmath} +\usepackage{breqn} \usepackage{setspace} \doublespacing diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index e0416a14..36601d7e 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -190,9 +190,12 @@ def processWYSIWYGElement(node): cmd = "\n \n" + wrapImage(imgCommand) + "\n \n" return cmd, "image" elif node["type"] == "equation": + print(node["nodes"][0]["leaves"][0]["text"]) return ( " \\begin{equation} " + + " \\begin{split} " + node["nodes"][0]["leaves"][0]["text"] + + " \\end{split} " + " \\end{equation} ", "equation", ) From 4335883af6a3cfece918a84932e5f2b84e02a362 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 15 Dec 2020 16:59:11 -0500 Subject: [PATCH 48/59] WIP --- db/testDataFullAtbd.sql | 2 +- pdf/app/latex/ATBD.tex | 1 - pdf/app/latex/ATBD_JOURNAL.tex | 1 - pdf/app/latex/serialize.py | 10 ++++------ 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql index 87fb1716..263d224e 100644 --- a/db/testDataFullAtbd.sql +++ b/db/testDataFullAtbd.sql @@ -5,7 +5,7 @@ VALUES INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES - (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\begin{aligned} \\\\ m_{12} (\\{ \\mathit{Red},\\mathit{Blue} \\})= K^\\prime* [ &m_{1}(\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) \\\\ & + m_{1} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\}) \\\\ & + m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{1} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\})] \\end{aligned}","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/view angle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); + (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\begin{aligned} \\\\ m_{12} (\\{ \\mathit{Red},\\mathit{Blue} \\})= K^\\prime* [ &m_{1}(\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) \\\\ & + m_{1} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\}) \\\\ & + m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{1} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\})] \\end{aligned}","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true,"caption":"Sample table caption"},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/viewangle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sample long tablar input","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 6499e0b2..59b5eb35 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -24,7 +24,6 @@ \usepackage{graphicx} \usepackage{float} \usepackage{amsmath} -\usepackage{breqn} \setmainfont{Latin Modern Math} %Using a font with good unicode math symbols % coverage to support the use of unicode math symbols in LaTeX text mode diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex index 196bb61d..144d685e 100644 --- a/pdf/app/latex/ATBD_JOURNAL.tex +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -26,7 +26,6 @@ \usepackage{graphicx} \usepackage{float} \usepackage{amsmath} -\usepackage{breqn} \usepackage{setspace} \doublespacing diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 36601d7e..5e165f6e 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -50,7 +50,8 @@ def processTable(nodeRows, caption=None): for cell in row["nodes"]: # Since we kept each cell in the table as a flexible type, they must be processed as generic WYSIWYG elements tableList[-1].append(processWYSIWYGElement(cell)[0]) - columnNames = tableList.pop(0) + + columnNames = [" " if not x else x for x in tableList.pop(0)] pd.set_option("display.max_colwidth", 1000) df = pd.DataFrame(tableList, columns=columnNames) # latex default text width = 426 pts @@ -70,6 +71,7 @@ def processTable(nodeRows, caption=None): latexTable = df.to_latex(**to_latex_params) # insert [h] for block latex from "floating" the table to the top of the page latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[h]") + print("Latex Table: ", latexTable) return latexTable @@ -171,7 +173,7 @@ def processWYSIWYGElement(node): if node["type"] == "table": return ( "\n \n" - + processTable(node["nodes"], caption=node.get("caption")) + + processTable(node["nodes"], caption=node.get("data", {}).get("caption")) + "\n \n", "table", ) @@ -190,7 +192,6 @@ def processWYSIWYGElement(node): cmd = "\n \n" + wrapImage(imgCommand) + "\n \n" return cmd, "image" elif node["type"] == "equation": - print(node["nodes"][0]["leaves"][0]["text"]) return ( " \\begin{equation} " + " \\begin{split} " @@ -332,8 +333,6 @@ def processVarList(element): var["long_name"] = processWYSIWYG(json.loads(var["long_name"])).strip("\\") if var["unit"] is None: continue - print("UNIT: ") - print(processWYSIWYG(json.loads(var["unit"]))) var["unit"] = processWYSIWYG(json.loads(var["unit"])).strip("\\") pd.set_option("display.max_colwidth", 1000) @@ -462,7 +461,6 @@ def __init__(self, path: str, journal: str): # Parse the JSON file into the corresponding sections (variables) enumerated in the ATBD def texVariables(self): myJson = json.loads(open(self.filepath).read()) - print("DATA DATA DATA: ", myJson) processReferences(myJson.pop("publication_references")) commands = processATBD(myJson.pop("atbd")) if debug: From fe0319c4539fbcbd7b1ca5b132ad88578ce42a21 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Tue, 15 Dec 2020 20:24:29 -0500 Subject: [PATCH 49/59] fixed `NaN` in table header, fixed table width and missing table captions --- db/testDataFullAtbd.sql | 2 +- pdf/app/latex/ATBD.tex | 2 ++ pdf/app/latex/ATBD_JOURNAL.tex | 2 ++ pdf/app/latex/serialize.py | 17 ++++++++++------- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/db/testDataFullAtbd.sql b/db/testDataFullAtbd.sql index 263d224e..555c5760 100644 --- a/db/testDataFullAtbd.sql +++ b/db/testDataFullAtbd.sql @@ -5,7 +5,7 @@ VALUES INSERT INTO apt.atbd_versions (atbd_version, atbd_id, scientific_theory, scientific_theory_assumptions, mathematical_theory, mathematical_theory_assumptions, introduction, historical_perspective, performance_assessment_validation_methods, performance_assessment_validation_uncertainties, performance_assessment_validation_errors, algorithm_usage_constraints, status, journal_discussion, journal_acknowledgements) VALUES - (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\begin{aligned} \\\\ m_{12} (\\{ \\mathit{Red},\\mathit{Blue} \\})= K^\\prime* [ &m_{1}(\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) \\\\ & + m_{1} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\}) \\\\ & + m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{1} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\})] \\end{aligned}","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true,"caption":"Sample table caption"},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/viewangle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sample long tablar input","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); + (1, 2, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"~equation \\sum n^2 \\times n_1 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"equation","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\\begin{aligned} \\\\ m_{12} (\\{ \\mathit{Red},\\mathit{Blue} \\})= K^\\prime* [ &m_{1}(\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) \\\\ & + m_{1} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{2} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\}) \\\\ & + m_{2} (\\{ \\mathit{Red}, \\mathit{Blue} \\}) * m_{1} (\\{ \\mathit{Red}, \\mathit{Blue}, \\mathit{Green} \\})] \\end{aligned}","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses a processing chain involving several separate radiometric and geometric adjustments, with a goal of eliminating differences in retrieved surface reflectance arising solely from differences in instrumentation. The overall chain is shown in Fig. 1. Input data products from Landsat 8 (Collection 2 Level 1T top-of-atmosphere reflectance or top-of-atmosphere apparent temperature) and Sentinel-2 (L1C top-of-atmosphere reflectance) are ingested for HLS processing.A series of radiometric and geometric corrections are applied as described below to convert data to surface reflectance, adjust for BRDF differences, and adjust for spectral bandpass differences. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" Three types of products are then generated: “S10” products – atmospherically corrected Sentinel-2 images in their native resolution and geometry; and the harmonized products “S30” and “L30”. These products have been radiometrically harmonized to the maximum extent, and then gridded to a common 30-meter UTM basis using the Sentinel-2 tile system. Note that S10 products are not normally archived. The S30 and L30 products are resampled as needed to a common 30-meter resolution UTM projection, and tiled using the Sentinel-2 Military Grid Reference System (MGRS) UTM grid.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Below we describe the algorithms use for (1) atmospheric correction; (2) BRDF adjustment; (3) bandpass normalization; and (4) geometric processing. More detailed descriptions can be found in Claverie et al. (2018); we note below cases where algorithms have been changed or updated from that reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"1. Atmospheric Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS relies on the operational LaSRC (Landsat Surface Reflectance Correction) algorithm for atmospherically correcting top-of-atmosphere reflectance to surface reflectance. As described in Vermote et al., (2016), the LaSRC approach relies on the inversion of the relatively simple equation in the Lambertian case, with no adjacency effects that account for a simplified coupling of the absorption by atmospheric gases and scattering by molecules and aerosols as it is implemented in the 6SV radiative transfer code (Vermote et al. 1997b, Kotchenova et al. 2006):","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/008ac610-6228-11ea-8129-896df9cb11c0.png","caption":"Eq. 1"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where rho","marks":[]},{"object":"leaf","text":"TOA","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the reflectance at the top of the atmosphere, rho","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere intrinsic reflectance, Tr","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the total atmosphere transmission (downward and upward), S","marks":[]},{"object":"leaf","text":"atm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the atmosphere spherical albedo, and rho","marks":[]},{"object":"leaf","text":"s","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" is the surface reflectance to be retrieved by the atmospheric correction procedure: the geometric conditions are described by the solar zenith angle qs, the view zenith angle qv, and the relative azimuth f (or the difference between the solar and view azimuth angles); P is the pressure that influences the number of molecules and the concentration of absorbing gases in the atmosphere, Tg designates the gaseous transmission by water vapor (TgH2O), ozone (TgO3), or other gases (TgOG), UH2O is the integrated water vapor content, UO3 is the integrated ozone content, and m is the so-called “air-mass” computed as 1/cos(qs)+1/cos(qv); tA, w0 and PA describe the aerosol properties and are spectrally dependent: ta is the aerosol optical thickness, w0 is the aerosol single scattering albedo, and PA is the aerosol phase function.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The main limitation of 6SV is the plane parallel assumption for the atmosphere, which limits the quoted accuracy (0.4%) to Sun and view zenith angles lower than 75 degrees. In addition to this limitation, Equation [1] supposes that scattering and absorption could be decoupled, which is not true where both strong absorption and scattering regimes occur for example near strong water vapor absorption lines in the near-infrared (e.g. in MODIS band 18 and 19). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The atmospheric pressure P is obtained from a combination of data available from a coarse-resolution (one deg., six hours time step) weather prediction model available from NCEP GDAS (2015), sea level pressure Psl and the altitude z [km] given by a Digital Elevation Model at 0.05 degree resolution (ETOPO5, 1988) and computed as:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":" P = P","marks":[]},{"object":"leaf","text":"sl","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" e","marks":[]},{"object":"leaf","text":"-z/8 ","marks":[{"object":"mark","type":"superscript","data":{}}]},{"object":"leaf","text":" Eq 2 ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The pressure should be representative of the average atmospheric pressure along the path from the Sun to the target and back to the satellite for primary scattering, and along an even more complicated path for multiple scattering. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Gaseous absorption terms include ozone and water vapor. The ozone amount UO3 is obtained via NCEP GDAS (2015) (at 1deg., 6 hours time step) via the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA). The surface reflectance Climate Modeling Grid (CMG) adopted a simple Geographic latitude/longitude projection at 0.05 degree (~ 5.5 km). The water vapor is also extracted from the ancillary information included in the MODIS surface reflectance Climate Modeling Grid (MOD09CMA) for Terra, which is itself computed from the MODIS near-infrared band 18 (931-941 nm) and 19 (915-965 nm) at 1 km spatial resolution (Gao and Kaufman, 2003). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Estimating aerosol optical thickness and phase function remains the most challenging aspect of atmospheric correction. In LaSRC a simplifying assumption is that a single aerosol model (\"urban clean\", Dubovik, 2002) can adequately represent global aerosol distributions. Then the aerosol optical thickness is inverted using the two blue bands available on Landsat8/OLI (band 1 and 2) and red band for each non-water pixel that has not been flagged as cirrus. The approach relies on empirical correlation between ratios of the blue and red bands and aerosol optical thickness observed from MODIS, which has been used also as the basis for MODIS Collection 6 implementation. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The method for inverting the AOT from OLI or MSI is relatively simple if the ratio between the red and blue bands can be known for every 10-30m pixel. First, this ratio is computed at coarse resolution (0.05 degree) from 10 years of MODIS (Terra) and MISR data. The MISR AOT product is used as input to the atmospheric correction of the MODIS TOA data for each valid observation, providing the MODIS surface reflectances that can be used to derive a ratio at 0.05 degree. The data are carefully filtered for clouds and high AOT values. This data processing enables one to account for the accuracy of that ratio globally and across several years and seasons. The ratio is computed for each valid observation and subsequently fitted as a linear function of NDVI","marks":[]},{"object":"leaf","text":"MIR","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":", a vegetation index analogous to NDVI that uses the Mid-IR (2.1µm) channel instead of Red. The per-pixel ratio at the OLI/MSI resolution is then calculated from the 30m NDVI","marks":[]},{"object":"leaf","text":"MIR ","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":"values.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The version of the LaSRC atmospheric correction code used for HLS v1.5 is derived from a C-language implementation of LaSRC version 3.5.5. used operationally for Landsat processing at USGS EROS. The only significant change from the version documented in Vermote et al. (2016) is that the aerosol optical thickness is calculated on a coarser (1km) spacing in order to speed processing time.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"2. Bi-directional Reflectance Distribution Function (BRDF) Correction","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The relative view angle between a Landsat 8 OLI and Sentinel-2 MSI observation of a single ground target can be as great as 7.5+10.0 = 17.5 degrees. This view angle difference is sufficient to generate several percent absolute reflectance difference for normal vegetation materials (Gao et al., 2009). The HLS BRDF correction attempts to normalize the surface reflectance to an optimal nadir-view value. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS has opted to use the c-factor technique and global coefficients provided by Roy et al. (2016) because the technique is very stable, reversible, easy to implement for operational processing and has been evaluated for Sentinel-2 data (Roy et al., 2017). The c-factor technique uses fixed BRDF coefficients for each spectral band, i.e., a constant BRDF shape, derived from a large number of pixels in the MODIS 500 m BRDF product (MCD43) that are globally and temporally distributed (>15 billion pixels). The technique has been evaluated using ETM+ data off-nadir (i.e. on the overlap areas of adjacent swaths, Roy et al., 2016) and MSI data (Roy et al., 2017). The technique is applied in HLS on OLI and MSI bands equivalent to MODIS ones; MSI red-edge spectral bands are therefore not normalized. Normalized reflectance is calculated for original reflectance and a c-factor (Eq. 3). The latter is deduced (Eq. 3) from BRDF coefficients for the three kernels (isotropic, volumetric and geometric). The kernel definitions are described in the ATBD of the MOD43 product (Strahler et al., 1999), and the specific c-factor coefficients are provided in Roy et al. (2016) and Claverie et al. (2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/d61e9c60-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/e496fcb0-6490-11ea-820e-ed9a4324b758.png","caption":""},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Eq 3","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"where θ","marks":[]},{"object":"leaf","text":"Sensor","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration (i.e., ϴv, ϴs, Δϕ) of the input data and θ","marks":[]},{"object":"leaf","text":"Norm","marks":[{"object":"mark","type":"subscript","data":{}}]},{"object":"leaf","text":" refers to the sun-illumination geometry configuration of the normalized data (θv = 0, θs = θsout, ∆φ = 0).","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that HLS v1.4 applied eq 3 to correct both view and solar elevation angles, the latter an attempt to normalize for BRDF changes associated with solar elevation changes during the growing season. However, further investigation and discussions suggested that the solar elevation correction was not appropriate for the simplified C-factor formulation. As a result, HLS v1.5 only corrects for view angle differences.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"3. Bandpass Adjustments","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The harmonization also requires adjustment of the small differences between the equivalent spectral bands of MSI and OLI. The OLI spectral bandpasses are used as reference, to which the MSI spectral bands are adjusted. No bandpass adjustment is defined for the (i) MSI red-edge bands (B05, B06 and B07), (ii) broad NIR band (B08), and (iii) atmospheric bands (B09 and B10). MSI bandpasses are based on the revised Sentinel-2a relative spectral responses (RSR''s) for bands 1 and 2 provided by ESA in 2017. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"As described in Claverie et al. (2018), the bandpass adjustment algorithm was derived from a selection of EO-1 Hyperion hyperspectral imager spectra. 160 million per-pixel spectra were extracted from a set of 158 hyperion scenes, distributed globally by latitude, Using the RSR for OLI and MSI, the raw (MSI) and target (OLI) spectral reflectance values were calculate for each pixel from the hyperspectral spectra. A global linear regression was then developed to transform MSI spectral reflectance to \"psudo-OLI\" spectral reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"4. Geometric Processing","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95% for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geoloction referecne images to which images of earlier procesing baselines are to be registered. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The coregistration is aided by the Automated Registration and Orthorectification Package (AROP) (Gao, Wolfe and Masek, 2009), which automatically identifies tie points and fits a coordinate transformation function between a target image and the reference image for the tile. The spectral measurement in the target image is resampled with the cubic convolution technique during coregistration and saved in native 10/20/60m spatial resolutions for S10. In the subsequent production of S30, the 10/20/60m pixels of S10 are resampled to 30 meters with a simple area-weighted average. The HLS internal set of geolocation reference images will continue to be used until ESA reprocesses all images before processing baseline 2.04 to a better quality.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Collection-1 Landsat-8 data often do not align with Sentinel-2 data and in general show lesser geolocation accruacy (Storey et al 2016). HLS v1.4 has applied AROP to register Landsat-8 data to the HLS internal Sentinel-2 based geolocation refereence images and uses cubic convolution to resample the spectral data. With the use of ESA-provided Sentinel-2 GRI to improve the density and accuracy of Landsat ground control points, USGS will release Collection-2 Landsat data with a better geolocation accuracy in mid-2020. AROP will not be needed on Collection-2 Landsat-8 data by that time, but resampling is still necessary because the UTM coordinate origin in the Landsat-8 system cooresponds to a pixel center but in the Sentinel-2 system corresponds to a pixel corner. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"5. Implementation","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS processing flow has been implemented on the University of Alabama/Marshall Space Flight Center (UAH/MSFC) IMPACT cloud computing environment, utilizing Amazon Web Services (AWS) services. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Input data sources include:","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8 Collection 2 \"Real Time\" TOA reflectance products from USGS EROS. To reduce HLS latency, IMPACT uses the \"real time\" Landsat 8 products rather than the final Tier 1 products. Users should note that this can introduce positional uncertainty in the Landsat 8 TIR observations compared to the final Tier 1 products. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2 L1C TOA products. Input data for forward processing (2020 onwards) from the ESA International Hub; Input data for archival products (2015-2020) from the USGS EROS Sentinel-2 L1C mirror site. ","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction inputs include ozone concentration, water vapor, and atmospheric temperature from MODIS CMA products, and surface topography (for pressure calculation) based on the Global Climate Model DEM. ","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Data production is kicked off daily based on new input granules from the ESA or USGS archives. HLS latency is scaled by the availability of both input TOA imagery, as well as the availability of the atmospheric correction inputs. HLS products are typically available within 2-3 days of image aquisition. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The following assumptions apply to the HLS algorithms:","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"LaSRC atmospheric correction assumes a plane-parallel atmosphere and Lambertian surface","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"BRDF correction is valid for small ranges of view angle (<20 degrees) near nadir","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS does not attempt BRDF or bandpass corrections for bands that have no MODIS counterpart (e.g. Sentinel-2 red edge bands). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS uses \"Real Time\" Landsat 8 Collection 2 products to reduce latency; positional accuracy of the Landsat 8 TIR bands may be lower than Tier 1 data available ~2 weeks later. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"This section is intended to demonstrate the use of a reference","marks":[]}]},{"object":"inline","type":"reference","data":{"id":2,"name":"Example Reference"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"ref","marks":[]}]}]},{"object":"text","leaves":[{"object":"leaf","text":" . Using the (ref) superscript will ensure the appropriate reference gets included in the \"References\" section of the paper. ","marks":[]}]}]}]}}', NULL, '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The S30 and L30 products are coregistered in the Sentinel-2 Military Grid Reference System (MGRS), partially resulting from the use of an HLS internal set of geolocation reference images when necessary. Since the processing baseline 2.04 initiated in June 2016 and up to the current 2.09 the Sentinel-2 L1C geolocation is quite stable, other than a few incidental anomalies, with long-term absolute accuracy close to 11 meters at 95$%$ for both satellites (7 Apr 2020 L1C data quality report). This accuracy is sufficient for the HLS 30-meter pixel size. When ESA applies its precisely geolocated Global Reference Image (GRI) in late 2020, the Sentinel-2 geolocation accuracy will be further improved. Therefore, HLS does not adjust the geolocation of Sentinel-2 data of this period. However, the Sentinel-2 L1C data before processing baseline 2.04 can show large geolocation error, especially a yaw angle bias apparent at the swath edges of adjacent orbits. To mitigate this problem, the HLS processing system has selected for each tile a 10-meter near-infrared summer image of minimal cloud contamination from processing baseline 2.04 to build its own internal geolocation reference images to which images of earlier processing baselines are to be registered. ","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The Landsat series of satellites have provided land imagery of the globe since 1972. Each satellite provides 16-day repeat coverage of global land areas. Landsat 8, launched in February 2013, introduced significant improvements in the number of spectral bands and the radiometric quality of the data. Landsat 8 includes two instruments: the Operational Land Imager (OLI) covering the visible/near/shortwave reflective bands, and the Thermal Infrared Sensor (TIRS) covering the longwave (thermal) infrared bands. The European Sentinel-2 satellites (part of the Copernicus environmental monitoring service) provide a similar type of imagery to Landsat, but with additional spectral bands and higher resolution via its Multispectral Imager (MSI) instrument. Sentinel-2a was launched in 2015, and Sentinel-2b in 2017. Each Sentinel-2 satellite provides global land coverage every 10 days, or every 5 days for the two-satellite constellation. Combining the data from Landsat 8, Sentinel-2a, and Sentinel-2b provides some 100 observations per year for equatorial regions, and over 200 acquisitions per year in mid-latitudes and regions of orbit overlap (Li and Roy, 2017). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"While similar, Landsat 8 and Sentinel-2 are not identical. The table below lists some of the differences between the systems. HLS attempts to adjust each product to create a harmonized “Landsat like” reflectance image. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"table","data":{"headless":true,"caption":"Sample table caption"},"nodes":[{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Landsat 8","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sentinel-2","marks":[{"object":"mark","type":"bold","data":{}}]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Orbital altitude","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"705 km","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"786 km","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Equatorial crossing time (MLT)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:00 am","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10:30 am","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Image swath/viewangle field of view","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"180km/15 deg FOV","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"290 km/20.1 deg FOV","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spatial resolution (VIS/SWIR/TIR)","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"30/30/100m","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10/20m (no TIR)","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Spectral bands","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"9 VSWIR, 2 TIR","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"12 VSWIR","marks":[]}]}]}]}]},{"object":"block","type":"table_row","data":{},"nodes":[{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Ground track repeat","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"16 days","marks":[]}]}]}]},{"object":"block","type":"table_cell","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"10 days (per platform)","marks":[]}]}]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The HLS Project distinguishes between “Quality Assurance” and “Validation”. Quality Assurance (or QA) provides per-granule or per-pixel information on the relative quality of the observation, as a flag for users to either use or discard that observation. Validation presents a quantitative assessment of product accuracy and uncertainty against and absolute reference.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Validation","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"HLS Surface Reflectance products have been validated in several ways. The LaSRC atmospheric correction has been validated by comparing Landsat 8 surface reflectance products with imagery corrected by the 6S radiative transfer model using aerosol optical thickness (AOT) derived from AERONET in-situ observations (Vermote et al., 2016). Since aerosols remain the primary source of uncertainty for retrieving surface reflectance from Landsat data, this comparison primarily tests the ability of the LaSRC algorithm to accurately retrieve AOT. Results indicate an overall uncertainty of 0.001-0.011 absolute reflectance, depending on band, with minimal dependence on target brightness (Figs. XX, YYY), and similar performance for Sentinel-2. Complete results are presented in Vermote et al. (2016) and have also been included in the CEOS Atmospheric Correction Intercomparison Experiment (ACIX) (Doxani et al., 2018).","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2f71a9c0-e938-11ea-b22d-c9533a470a15.png","caption":"Accuracy (bias), precision, and uncertainty of the LaSRC atmospheric correction applied to Landsat 8 and Sentinel-2 data for the red band (Band 4)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/2b9f0170-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"image","data":{"src":"http://s3.amazonaws.com/nasa-aptv2-production-figures/7f422370-e939-11ea-b22d-c9533a470a15.png","caption":"Fig. YYY. LaSRC accuracy, precision, uncertainty compared to Aerojet-Derived surface reflectance for Landsat 8 OLI, data from Vermote et al. (2016). Units are absolute reflectance x10000 (e.g. 10=1 percent reflectance or 0.01)"},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"A second validation exercise focused on using ground-based albedometer networks (SURFRAD and OZFLUX) to compare HLS nadir-adjusted reflectance to observed albedo. There are several factors that must be considered when performing this comparison:\r","marks":[]}]}]},{"object":"block","type":"unordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Comparing directional reflectance with albedo requires a hemispheric integration using a known bi-directional reflectance distribution function (BRDF), as well as a spectral integration to compare the relatively narrow HLS band passes with the broader channels of the albedometer instruments.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"The view footprint of the SURFAD and OZFLUX instruments is much smaller than an HLS 30-meter pixel, so local heterogeneity beyond the albedometer field of view will affect the comparison. \r","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Results are presented in Franch et al. (2019) (Fig. ZZ). Using sites in both the US and Australia, albedo estimates using S30 and L30 products as input provide comparable RMSE (0.015 – 0.03 albedo). ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Finally, Claverie et al. (2018) presented a theoretical error budget for the HLS product derived from published assessments of component errors from each of the algorithms (atmospheric correction, BRDF adjustment, spectral band pass correction). Rolling up the published component errors and assuming they are independent (e.g. total error is the root sum square of component errors) indicates per-band uncertainty of 0.01 to 0.02 absolute reflectance. ","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"It should be noted that the above analyses compare HLS products to absolute reflectance or albedo estimates. Users may care more about the temporal stability (comparability) of reflectance from the S30 and L30 products, rather than the absolute error. Time series analysis over stable (e.g. invariant desert) sites are ongoing, but short-term variability from these sites is generally less than 0.5% absolute reflectance over a period of days.\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Quality Assurance (QA)","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":"\r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Per-pixel QA information is included with each v1.5 HLS S30 or L30 image via the QA layer. Clouds and cloud shadow are identified using the Fmask 4.0 algorithm published by Zhu et al (2015). \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"\rIn previous HLS versions, per-granule QA statistics were derived by comparing aggregated HLS reflectance values to near-simultaneous, cloud-free MODIS CMG reflectances (Claverie et al., 2018, Fig. XX). In general, this approach showed that HLS reflectance values were consistent with MODIS reflectance values, except for those cases where HLS cloud masking failed. Version 1.5 has discontinued the per-granule MODIS comparisons as the approach has not proved critical for flagging “poor” granules. \r","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', NULL, NULL, NULL, 'Draft', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque nisl magna, vitae fermentum nulla facilisis et. Nunc non nunc viverra, efficitur eros ac, efficitur mauris. Nulla neque dui, iaculis et pulvinar non, iaculis eleifend ex. Fusce lobortis leo vitae felis hendrerit ullamcorper. Sed tincidunt venenatis diam. Curabitur lorem tellus, porta eu lacus quis, aliquam consectetur velit. Ut sagittis tellus vitae risus euismod euismod finibus a nunc. Duis rhoncus odio euismod, aliquet urna vitae, tempor tellus. Donec ullamcorper ligula sed sem bibendum dignissim. Aenean congue non massa ut bibendum. Duis in bibendum arcu.","marks":[]}]}]},{"object":"block","type":"ordered-list","data":{},"nodes":[{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Integer suscipit tincidunt laoreet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur porttitor ex id nisl euismod venenatis. Curabitur bibendum est id lectus maximus vehicula.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Sed eget malesuada elit. Etiam vitae metus a lectus eleifend rhoncus eu eu eros. Nulla vulputate nunc ipsum, in cursus nisl dignissim quis. Duis porta libero ut porta vulputate. Sed eget congue ante. Donec non sapien scelerisque nibh varius imperdiet.","marks":[]}]}]},{"object":"block","type":"list-item","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Donec sed erat vitae augue suscipit dictum.","marks":[]}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"","marks":[]}]}]}]}}', '{"object":"value","document":{"object":"document","data":{},"nodes":[{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Duis et velit convallis, volutpat dolor a, convallis eros.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur nec mollis diam.","marks":[{"object":"mark","type":"bold","data":{}}]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Phasellus pellentesque, metus at pretium sagittis, nisl tortor elementum nisi, quis mollis libero augue vel urna. Sed volutpat elit ac pulvinar bibendum.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Curabitur sagittis ligula nec turpis maximus vestibulum. Pellentesque scelerisque egestas ipsum, et ornare est cursus quis. Donec ex sem, tincidunt ac suscipit ut, faucibus a nunc. Mauris porta neque in fringilla porttitor. Proin cursus, lectus ornare rhoncus ornare, massa sem vulputate sapien, non pharetra lacus nisl eu lorem. Aliquam porta a nibh nec blandit.","marks":[]}]}]},{"object":"block","type":"paragraph","data":{},"nodes":[{"object":"text","leaves":[{"object":"leaf","text":"Proin mollis erat et vestibulum eleifend. ","marks":[]},{"object":"leaf","text":"Maecenas","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":" sed orci quis nunc molestie ","marks":[]},{"object":"leaf","text":"dignissim","marks":[{"object":"mark","type":"bold","data":{}}]},{"object":"leaf","text":". Cras tempus lacus fringilla consectetur facilisis. Vestibulum venenatis varius dui a maximus. Vestibulum sed nisi quis urna aliquet condimentum in eu lacus. Quisque non tincidunt tellus. Sed in dui pulvinar, vestibulum odio sit amet, efficitur orci. Phasellus laoreet cursus accumsan. Maecenas ut nulla sed ante pellentesque efficitur eget ac neque. Curabitur a mattis ligula, in posuere elit. ","marks":[]}]}]}]}}'); INSERT INTO apt.algorithm_implementations (algorithm_implementation_id, atbd_version, atbd_id, access_url, execution_description) VALUES diff --git a/pdf/app/latex/ATBD.tex b/pdf/app/latex/ATBD.tex index 59b5eb35..41892cb9 100644 --- a/pdf/app/latex/ATBD.tex +++ b/pdf/app/latex/ATBD.tex @@ -24,6 +24,8 @@ \usepackage{graphicx} \usepackage{float} \usepackage{amsmath} +\usepackage{array} + \setmainfont{Latin Modern Math} %Using a font with good unicode math symbols % coverage to support the use of unicode math symbols in LaTeX text mode diff --git a/pdf/app/latex/ATBD_JOURNAL.tex b/pdf/app/latex/ATBD_JOURNAL.tex index 144d685e..9189641a 100644 --- a/pdf/app/latex/ATBD_JOURNAL.tex +++ b/pdf/app/latex/ATBD_JOURNAL.tex @@ -26,6 +26,8 @@ \usepackage{graphicx} \usepackage{float} \usepackage{amsmath} +\usepackage{array} + \usepackage{setspace} \doublespacing diff --git a/pdf/app/latex/serialize.py b/pdf/app/latex/serialize.py index 5e165f6e..09460df4 100755 --- a/pdf/app/latex/serialize.py +++ b/pdf/app/latex/serialize.py @@ -47,6 +47,9 @@ def processTable(nodeRows, caption=None): for rows in nodeRows: tableList.append([]) for row in rows["nodes"]: + if "nodes" not in row: + tableList.pop(-1) + continue for cell in row["nodes"]: # Since we kept each cell in the table as a flexible type, they must be processed as generic WYSIWYG elements tableList[-1].append(processWYSIWYGElement(cell)[0]) @@ -56,22 +59,21 @@ def processTable(nodeRows, caption=None): df = pd.DataFrame(tableList, columns=columnNames) # latex default text width = 426 pts column_format = "|".join( - f" p{{{int(426/len(columnNames))}pt}} " for _ in columnNames + f" m{{{int(375/len(columnNames))}pt}} " for _ in columnNames ) + to_latex_params = dict( - index=False, - escape=False, - header=True, - column_format=column_format, - na_rep=" ", + index=False, escape=False, header=True, column_format=f"{column_format}" ) + if caption: to_latex_params["caption"] = caption latexTable = df.to_latex(**to_latex_params) # insert [h] for block latex from "floating" the table to the top of the page latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[h]") - print("Latex Table: ", latexTable) + + print("Latex table: ", latexTable) return latexTable @@ -501,6 +503,7 @@ def filewrite(self): modified.write("\n \\fi \n") modified.write("\n".join(self.texVars) + " \n" + data) fileName = modified.name + with open(os.path.join(os.path.dirname(fileName), "main.bib"), "w") as bibFile: bibFile.write("\n".join(references)) return fileName From c166327ec5626dc98d983fddc650f7ac9f149e19 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Wed, 16 Dec 2020 11:56:21 -0600 Subject: [PATCH 50/59] fix return to when using mock auth --- cloudformation/cloudformation.yaml | 7 ++++--- fastapi/app/saml.py | 3 ++- nasa-apt | 8 ++++++++ update_container.sh | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 306e3d59..1e97beed 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -389,7 +389,7 @@ Resources: Value: !Ref 'JWTSecret' - Name: PGRST_DB_ANON_ROLE - Value: app_user + Value: anonymous LogConfiguration: LogDriver: 'awslogs' Options: @@ -465,11 +465,12 @@ Resources: - Name: ELASTICURL Value: !Join ['', ['https://', !GetAtt [Elasticsearch, DomainEndpoint]]] - Name: APT_FRONTEND_URL - Value: 'http://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com' + Value: 'https://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com' - Name: IDP_METADATA_URL Value: 'https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml' - Name: FASTAPI_HOST - Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] + # Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] + Value: 'https://apt.ds.io' - Name: JWT_SECRET Value: !Ref 'JWTSecret' LogConfiguration: diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index 88029ed1..e4f6315b 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -179,6 +179,7 @@ async def get_auth(self): "wantNameIdEncrypted": False, "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", + "requestedAuthnContext": ["urn:oasis:names:tc:SAML:2.0:ac:classes:Password"], }, } @@ -298,7 +299,7 @@ async def sso( saml: SamlAuth = Depends(saml_auth) ): if mockauth: - return RedirectResponse(url=f"{saml.url_for('acs')}?RelayState={saml.relay_state}") + return RedirectResponse(url=f"{saml.url_for('acs')}?RelayState={saml.return_to}") return RedirectResponse(url=saml.auth.login(saml.return_to)) diff --git a/nasa-apt b/nasa-apt index 8935b367..1daceecf 100755 --- a/nasa-apt +++ b/nasa-apt @@ -57,8 +57,16 @@ update_fastapi(){ --region $REGION } +apiurl(){ + get_output FastapiEndpoint +} + dburl(){ get_output PGConnection } +outputs(){ + echo $outputs | jq +} + $command "$@" diff --git a/update_container.sh b/update_container.sh index b61f3221..912f6d47 100755 --- a/update_container.sh +++ b/update_container.sh @@ -1,4 +1,4 @@ aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 552819999234.dkr.ecr.us-east-1.amazonaws.com docker build -t 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/saml/fastapi fastapi/. docker push 552819999234.dkr.ecr.us-east-1.amazonaws.com/nasa-apt/saml/fastapi -aws ecs update-service --force-new-deployment --cluster nasa-apt-samltest2-ECSCluster-4EbHcIESsbQD --service nasa-apt-samltest2-svc-fastapi --region us-east-1 +aws ecs update-service --force-new-deployment --cluster nasa-apt-v5-prod-ECSCluster-tNTELpBkxUy7 --service nasa-apt-v5-prod-svc-fastapi --region us-east-1 From 218bd8fb9dd3bb7beb1fd463f9ffbdb621902926 Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Wed, 16 Dec 2020 12:39:22 -0600 Subject: [PATCH 51/59] add env variable for jwt to docker compose --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 4dfc2461..421aef9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: PGRST_DB_URI: postgres://masteruser:password@db:5432/nasadb PGRST_DB_SCHEMA: apt PGRST_DB_ANON_ROLE: app_user + PGRST_JWT_SECRET: $JWT_SECRET depends_on: - db fastapi: From 37ed8dc9c1ea2048240f9fe2679fe5182d83705c Mon Sep 17 00:00:00 2001 From: David W Bitner Date: Wed, 16 Dec 2020 13:29:04 -0600 Subject: [PATCH 52/59] update env var for anonymous user --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 421aef9a..7b4c51a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: environment: PGRST_DB_URI: postgres://masteruser:password@db:5432/nasadb PGRST_DB_SCHEMA: apt - PGRST_DB_ANON_ROLE: app_user + PGRST_DB_ANON_ROLE: anonymous PGRST_JWT_SECRET: $JWT_SECRET depends_on: - db From 4daf9b22717cd2560eb071b0ccb5379c9f326385 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Wed, 16 Dec 2020 19:19:10 +0000 Subject: [PATCH 53/59] Fix logout option --- fastapi/app/saml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/app/saml.py b/fastapi/app/saml.py index e4f6315b..6d7b9994 100644 --- a/fastapi/app/saml.py +++ b/fastapi/app/saml.py @@ -339,7 +339,7 @@ async def slo(saml: SamlAuth = Depends(saml_auth)): logger.warning('slo return_to: %s, relay_state: %s', saml.return_to, saml.relay_state) if mockauth: saml.userdata = None - saml.redirect(saml.return_to) + return saml.redirect(saml.return_to) url = auth.logout( return_to=saml.return_to, name_id=saml.name_id, From c40110079b6fa4f180bc49cc47a9b8da9259cce9 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Wed, 16 Dec 2020 19:19:37 +0000 Subject: [PATCH 54/59] Fix cloudformation variables --- .env.sample | 3 +++ cloudformation/cloudformation.yaml | 19 +++++++++++++++---- cloudformation/deploy.sh | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index e775605d..bb6e4ac7 100644 --- a/.env.sample +++ b/.env.sample @@ -13,6 +13,9 @@ PDFS_S3_BUCKET=nasa-apt-dev-pdfs # s3 bucket name for figures, pngs for example. used by cloudformation/deploy.sh and by docker-compose.yml FIGURES_S3_BUCKET=nasa-apt-dev-figures +# postgrest api +REST_API_ENDPOINT=http://rest-api:3000 + # url for database DBURL=postgres://masteruser:password@db:5432/nasadb diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 1e97beed..4c19258e 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -102,8 +102,20 @@ Parameters: ConstraintDescription: Please choose a valid instance type. JWTSecret: Type: String + AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' Default: lkjlkjslkdjaflkjfsalkjfasdlkfdlkjlkjslkdjaflkjfsalkjfasdlkfdlkjlkjslkdjaflkjfsalkjfasdlkfd Description: Secret to use for creating JWT Tokens + APTFrontendUrl: + Type: String + Description: Frontend Url used to set CORS origin for FastAPI + IDPMetadataUrl: + Type: String + Description: Url for SAML IDP metadata + FastapiHost: + Type: String + Description: Url for FastApi +Conditions: + HasFastapiHost: !Not [!Or [!Equals [!Ref 'FastapiHost', ''], !Equals [!Ref 'FastapiHost', 'http://localhost:8000']]] Mappings: # Hard values for the subnet masks. These masks define # the range of internal IP addresses that can be assigned. @@ -465,12 +477,11 @@ Resources: - Name: ELASTICURL Value: !Join ['', ['https://', !GetAtt [Elasticsearch, DomainEndpoint]]] - Name: APT_FRONTEND_URL - Value: 'https://nasa-apt-eltest-application.s3-website-us-east-1.amazonaws.com' + Value: !Ref 'APTFrontendUrl' - Name: IDP_METADATA_URL - Value: 'https://auth.launchpad-sbx.nasa.gov/unauth/metadata/launchpad-sbx.idp.xml' + Value: !Ref 'IDPMetadataUrl' - Name: FASTAPI_HOST - # Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] - Value: 'https://apt.ds.io' + Value: !If [HasFastapiHost, !Ref 'FastapiHost', !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]]] - Name: JWT_SECRET Value: !Ref 'JWTSecret' LogConfiguration: diff --git a/cloudformation/deploy.sh b/cloudformation/deploy.sh index 00d998ce..95e4eee4 100755 --- a/cloudformation/deploy.sh +++ b/cloudformation/deploy.sh @@ -5,6 +5,25 @@ set -e read -p 'Stack name: ' stackname read -sp 'Database password: ' dbpassword +source ../.env + +echo "Using env variables" +echo "APT_FRONTEND_URL $APT_FRONTEND_URL" +echo "IDP_METADATA_URL $IDP_METADATA_URL" +echo "JWT_SECRET $JWT_SECRET" +echo "FASTAPI_HOST $FASTAPI_HOST" + +echo + +read -p "Continue (y/n)? " -n 1 -r +echo # (optional) move to a new line +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 # handle exits from shell or function but don't exit interactive shell +fi + +echo "Deploying" + aws cloudformation deploy \ --template-file cloudformation.yaml \ --stack-name $stackname \ @@ -13,5 +32,9 @@ aws cloudformation deploy \ DBName=nasadb \ DBUser=masteruser \ DBPassword=$dbpassword \ + JWTSecret=$JWT_SECRET \ + APTFrontendUrl=$APT_FRONTEND_URL \ + IDPMetadataUrl=$IDP_METADATA_URL \ + FastapiHost=$FASTAPI_HOST \ ElasticsearchDomainName=nasadb-$stackname \ --region us-east-1 --capabilities CAPABILITY_IAM From 1a2abd6df885eca65959564103fba0f276e19e07 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Wed, 16 Dec 2020 21:27:15 -0500 Subject: [PATCH 55/59] WIP --- fastapi/app/atbd/get_atbd.py | 39 +++++----- fastapi/app/latex/ATBD_JOURNAL.tex | 112 +++++++++++++++++++++++++++++ fastapi/app/latex/serialize.py | 1 - fastapi/app/main.py | 81 +++++++++------------ fastapi/app/pdf/latex_to_pdf.py | 3 +- 5 files changed, 170 insertions(+), 66 deletions(-) create mode 100644 fastapi/app/latex/ATBD_JOURNAL.tex diff --git a/fastapi/app/atbd/get_atbd.py b/fastapi/app/atbd/get_atbd.py index 1180145f..d89127e2 100644 --- a/fastapi/app/atbd/get_atbd.py +++ b/fastapi/app/atbd/get_atbd.py @@ -4,7 +4,9 @@ from typing import Optional, Dict from fastapi import HTTPException -rest_api_endpoint: str = environ.get('REST_API_ENDPOINT') or sys.exit('REST_API_ENDPOINT env var required') +rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or sys.exit( + "REST_API_ENDPOINT env var required" +) def get_atbd(atbd_id: Optional[int] = None, alias: Optional[str] = None) -> Dict: @@ -19,39 +21,42 @@ def get_atbd(atbd_id: Optional[int] = None, alias: Optional[str] = None) -> Dict :rtype: dict (from json) :raises HTTPException: if the api request fails """ + if alias: - from_column = 'alias' + from_column = "alias" q = alias else: - from_column = 'atbd_id' + from_column = "atbd_id" q = atbd_id - url: str = f'{rest_api_endpoint}/atbds?{from_column}=eq.{q}&select=*,contacts(*),contact_groups(*),atbd_versions(atbd_id,atbd_version,status)&limit=1' if alias else f'{rest_api_endpoint}/atbds?atbd_id=eq.{atbd_id}&select=*,contacts(*),contact_groups(*),atbd_versions(atbd_id,atbd_version,status)&limit=1' + + url: str = ( + f"{rest_api_endpoint}/atbds?{from_column}=eq.{q}&select=*,contacts(*),contact_groups(*),atbd_versions(atbd_id,atbd_version,status)&limit=1" + if alias + else f"{rest_api_endpoint}/atbds?atbd_id=eq.{atbd_id}&select=*,contacts(*),contact_groups(*),atbd_versions(atbd_id,atbd_version,status)&limit=1" + ) response = requests.get(url) + if not response.ok: raise HTTPException(status_code=response.status_code, detail=response.text) records = response.json() + if not len(records): - raise HTTPException(status_code=404, detail='not found') + raise HTTPException(status_code=404, detail="not found") + [atbd_metadata] = records - atbd_id = atbd_metadata['atbd_id'] - [version] = atbd_metadata['atbd_versions'] - version_id: int = version['atbd_version'] + atbd_id = atbd_metadata["atbd_id"] + [version] = atbd_metadata["atbd_versions"] + version_id: int = version["atbd_version"] # fetch the atbd doc using the atbd_id and version_id - url: str = f'{rest_api_endpoint}/atbd_versions?atbd_id=eq.{atbd_id}&atbd_version=eq.{version_id}&select=*,atbd(*),algorithm_input_variables(*),algorithm_output_variables(*),algorithm_implementations(*),publication_references(*),data_access_input_data(*),data_access_output_data(*),data_access_related_urls(*),citations(*)' + url: str = f"{rest_api_endpoint}/atbd_versions?atbd_id=eq.{atbd_id}&atbd_version=eq.{version_id}&select=*,atbd(*),algorithm_input_variables(*),algorithm_output_variables(*),algorithm_implementations(*),publication_references(*),data_access_input_data(*),data_access_output_data(*),data_access_related_urls(*),citations(*)" response = requests.get(url) if not response.ok: raise HTTPException(status_code=response.status_code, detail=response.text) records = response.json() if not len(records): - raise HTTPException(status_code=404, detail='not found') + raise HTTPException(status_code=404, detail="not found") [atbd_content] = records # combine the two results to make json style atbd doc (here just emulating what the nasa-apt-frontend does) - return { - **atbd_content, - 'atbd': atbd_metadata - } - - - + return {**atbd_content, "atbd": atbd_metadata} diff --git a/fastapi/app/latex/ATBD_JOURNAL.tex b/fastapi/app/latex/ATBD_JOURNAL.tex new file mode 100644 index 00000000..d0db11d7 --- /dev/null +++ b/fastapi/app/latex/ATBD_JOURNAL.tex @@ -0,0 +1,112 @@ +\newcommand{\AlgDesc}{ +Some other text +\begin{equation} \int_0^\infty x^2 dx +\end{equation} Plus some more text +} + +\providecommand{\ScientificTheory}{} +\providecommand{\Contacts}{} +\providecommand{\AlgorithmImplementations}{} +\providecommand{\AlgorithmUsageConstraints}{} +\providecommand{\PerformanceAssessmentValidationMethods}{} +\providecommand{\PerformanceAssessmentValidationUncertainties}{} +\providecommand{\PerformanceAssessmentValidationErrors}{} +\providecommand{\JournalDiscussion}{} +\providecommand{\JournalAcknowledgements}{} + +\title{\ATBDTitle} +\date{\today} + +\documentclass[12pt]{article} +\usepackage{fontspec} +\usepackage{color} %May be necessary if you want to color links +\usepackage{hyperref} +\usepackage{url} +\usepackage{booktabs} +\usepackage{graphicx} +\usepackage{float} +\usepackage{amsmath} +\usepackage{array} + + +\usepackage{setspace} +\doublespacing + +\usepackage{lineno} +\linenumbers + +\setmainfont{Latin Modern Math} %Using a font with good unicode math symbols +% coverage to support the use of unicode math symbols in LaTeX text mode +% See list of symobls here: https://ctan.math.illinois.edu/macros/latex/contrib/unicode-math/unimath-symbols.pdf +\hypersetup{ + colorlinks=false, %set true if you want colored links + linktoc=all, %set to all if you want both sections and subsections linked + linkcolor=blue, %choose some color if you want links to stand out +} + +\begin{document} +\maketitle + +\section{Introduction} +\Introduction + +\section{Historical Perspective} +\HistoricalPerspective + +\section{Algorithm Description} + +\subsection{Scientific Theory} +\ScientificTheory + +\subsubsection{Assumptions} +\ScientificTheoryAssumptions + +\subsection{Mathematical Theory} +\MathematicalTheory + +\subsubsection{Assumptions} +\MathematicalTheoryAssumptions + +\subsection{Algorithm Input Variables} +\AlgorithmInputVariables + +\subsection{Algorithm Output Variables} +\AlgorithmOutputVariables + +\section{Algorithm Implementations} +\AlgorithmImplementations + +\section{Algorithm Usage Constraints} +\AlgorithmUsageConstraints + +\section{Performance Assessment Validation Methods} +\PerformanceAssessmentValidationMethods + +\section{Performance Assessment Validation Uncertainties} +\PerformanceAssessmentValidationUncertainties + +\section{Performance Assessment Validation Errors} +\PerformanceAssessmentValidationErrors + +\section{Data Access Input Data} +\DataAccessInputData + +\section{Data Access Output Data} +\DataAccessOutputData + +\section{Data Access Related URLs} +\DataAccessRelatedUrls + +\section{Discussion} +\JournalDiscussion + +\section{Acknowledgements} +\JournalAcknowledgements + +\section{Contacts} +\Contacts + +\bibliographystyle{abbrv} +\bibliography{main} + +\end{document} \ No newline at end of file diff --git a/fastapi/app/latex/serialize.py b/fastapi/app/latex/serialize.py index 09460df4..c83cc068 100755 --- a/fastapi/app/latex/serialize.py +++ b/fastapi/app/latex/serialize.py @@ -73,7 +73,6 @@ def processTable(nodeRows, caption=None): # insert [h] for block latex from "floating" the table to the top of the page latexTable = latexTable.replace("\\begin{table}", "\\begin{table}[h]") - print("Latex table: ", latexTable) return latexTable diff --git a/fastapi/app/main.py b/fastapi/app/main.py index bb774517..523d2a99 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -44,9 +44,7 @@ rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or exit( "REST_API_ENDPOINT env var required" ) -s3_endpoint: str = environ.get("S3_ENDPOINT") or exit( - "S3_ENDPOINT env var required" -) +s3_endpoint: str = environ.get("S3_ENDPOINT") or exit("S3_ENDPOINT env var required") pdfs_bucket_name: str = environ.get("PDFS_S3_BUCKET") or exit( "PDFS_S3_BUCKET env var required" ) @@ -60,9 +58,7 @@ ) app: FastAPI = FastAPI() -cache: Cache = Cache( - s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name -) +cache: Cache = Cache(s3_endpoint=s3_endpoint, bucket_name=pdfs_bucket_name) origins = [ @@ -108,7 +104,7 @@ async def shutdown() -> None: await app.state.connection.close() -def get_cache_key(atbd_doc: Dict) -> str: +def get_cache_key(atbd_doc: Dict, journal: bool) -> str: """ Helper function to construct a cache keys from the checksum and the alias of the atbd doc. @@ -119,11 +115,18 @@ def get_cache_key(atbd_doc: Dict) -> str: """ hex_digest: str = checksum_atbd(atbd_doc) alias: str = atbd_doc["atbd"]["alias"] - return ( - f"{hex_digest}/{alias}.pdf" - if alias - else f"{hex_digest}/nasa-atbd.pdf" - ) + + key_items = [hex_digest] + if journal: + key_items.append("journal") + + if alias: + key_items.append(f"{alias}.pdf") + else: + key_items.append("nasa-atbd.pdf") + + return "/".join(key_items) + # return f"{hex_digest}/{alias}.pdf" if alias else f"{hex_digest}/nasa-atbd.pdf" def atbd_pdf_handler( @@ -144,7 +147,7 @@ def atbd_pdf_handler( :raises HTTPException: """ status: Status = get_status(atbd_doc) - cache_key: str = get_cache_key(atbd_doc) + cache_key: str = get_cache_key(atbd_doc, journal=journal) if status == Status.Published.name: # check cache: published atbds may be cached in s3 try: @@ -163,6 +166,7 @@ def atbd_pdf_handler( (latex_filename, _) = json_to_latex( atbd_doc=atbd_doc, tmp_dir=tmp_dir, journal=journal ) + except JsonToLatexException as e: raise HTTPException(status_code=500, detail=str(e)) from e try: @@ -171,9 +175,7 @@ def atbd_pdf_handler( ) if status == Status.Published.name: try: - cache_url = cache.put_file( - key=cache_key, filename=tmp_pdf_filename - ) + cache_url = cache.put_file(key=cache_key, filename=tmp_pdf_filename) return RedirectResponse(url=cache_url) except CacheException as e: logger.error(str(e)) @@ -196,9 +198,7 @@ def get_atbd_by_id( user: User = Depends(get_user), ): atbd_doc = get_atbd(atbd_id=atbd_id) - return atbd_pdf_handler( - atbd_doc, background_tasks=background_tasks - ) + return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) @app.get(root_path + "atbds/alias/{alias}.pdf") @@ -208,9 +208,8 @@ def get_atbd_pdf_by_alias( user: User = Depends(get_user), ): atbd_doc = get_atbd(alias=alias) - return atbd_pdf_handler( - atbd_doc, background_tasks=background_tasks - ) + return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) + @app.get(root_path + "atbds/journal/id/{atbd_id}.pdf") def get_journal_atbd_by_id( @@ -255,24 +254,23 @@ def cleanup_tmp_dir(tmp_dir: Type[TemporaryDirectory]): tmp_dir.cleanup() logger.info(f"cleaned up {tmp_dir.name}") -@app.get(root_path + "reindex",) -async def reindex( - request: Request, user: User = Depends(require_user) -): + +@app.get( + root_path + "reindex", +) +async def reindex(request: Request, user: User = Depends(require_user)): """ Reindex all ATBD's into ElasticSearch """ logger.info("Reindexing %s", ELASTICURL) - results = await update_index( - connection=request.app.state.connection - ) + results = await update_index(connection=request.app.state.connection) return JSONResponse(content=results) -@app.post(root_path + "search",) -async def search_elastic( - request: Request, user: User = Depends(get_user) -): +@app.post( + root_path + "search", +) +async def search_elastic(request: Request, user: User = Depends(get_user)): """ Proxies POST json to elastic search endpoint """ @@ -280,14 +278,9 @@ async def search_elastic( data = await request.json() logger.info("User %s", user) logger.info("data: %s", data) + if user is None: - data['query']['bool']['filter'] = [ - { - "match": { - "status": "published" - } - } - ] + data["query"]["bool"]["filter"] = [{"match": {"status": "published"}}] logger.info("Searching %s %s", url, data) auth = aws_auth() response = requests.post( @@ -296,11 +289,7 @@ async def search_elastic( json=data, headers={"Content-Type": "application/json"}, ) - logger.info( - "status:%s response:%s", response.status_code, response.text - ) + logger.info("status:%s response:%s", response.status_code, response.text) if not response.ok: - raise HTTPException( - status_code=response.status_code, detail=response.text - ) + raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() diff --git a/fastapi/app/pdf/latex_to_pdf.py b/fastapi/app/pdf/latex_to_pdf.py index d33bcb8d..05a541fd 100644 --- a/fastapi/app/pdf/latex_to_pdf.py +++ b/fastapi/app/pdf/latex_to_pdf.py @@ -32,8 +32,7 @@ def latex_to_pdf(latex_filename: str, tmp_dir: str) -> str: cwd=tmp_dir, encoding=encoding, ) - print("LATEX TO PDF OUTPUT: ") - print(completed) + if completed.returncode != 0: # for debugging purposes, return the stdout in addition to the stderr raise LatexToPDFException( From 223ae52d0a5de1401742a57e1f378aba57aff308 Mon Sep 17 00:00:00 2001 From: Leo Thomas Date: Thu, 17 Dec 2020 10:58:20 -0500 Subject: [PATCH 56/59] Added JWT token to get request for logged in user when generating atbd pdf --- fastapi/app/atbd/get_atbd.py | 40 +++++++++++++++++++++++++++++++++--- fastapi/app/main.py | 4 ++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/fastapi/app/atbd/get_atbd.py b/fastapi/app/atbd/get_atbd.py index d89127e2..17e9f5bb 100644 --- a/fastapi/app/atbd/get_atbd.py +++ b/fastapi/app/atbd/get_atbd.py @@ -3,13 +3,37 @@ from os import environ from typing import Optional, Dict from fastapi import HTTPException +from ..saml import SamlAuth, User +from jose import jwt +from datetime import datetime, timedelta + +secret: str = environ.get("JWT_SECRET") or exit("JWT_SECRET ENV var required") +token_life: int = 3600 rest_api_endpoint: str = environ.get("REST_API_ENDPOINT") or sys.exit( "REST_API_ENDPOINT env var required" ) -def get_atbd(atbd_id: Optional[int] = None, alias: Optional[str] = None) -> Dict: +def create_token_data(user: User): + exp = datetime.utcnow() + timedelta(seconds=token_life) + data = { + "userdata": user, + "exp": exp, + "role": "app_user", + } + return data + + +def create_token(user: User): + return jwt.encode(create_token_data(user=user), secret) + + +def get_atbd( + atbd_id: Optional[int] = None, + alias: Optional[str] = None, + user: Optional[User] = None, +) -> Dict: """ Query postgrest api for an atbd document by an id or an alias. @@ -34,7 +58,12 @@ def get_atbd(atbd_id: Optional[int] = None, alias: Optional[str] = None) -> Dict if alias else f"{rest_api_endpoint}/atbds?atbd_id=eq.{atbd_id}&select=*,contacts(*),contact_groups(*),atbd_versions(atbd_id,atbd_version,status)&limit=1" ) - response = requests.get(url) + request_params = {"url": url} + if user: + request_params["headers"] = {"Authorization": f"Bearer {create_token(user)}"} + + print(f"Get params: {request_params}") + response = requests.get(**request_params) if not response.ok: raise HTTPException(status_code=response.status_code, detail=response.text) @@ -50,7 +79,12 @@ def get_atbd(atbd_id: Optional[int] = None, alias: Optional[str] = None) -> Dict # fetch the atbd doc using the atbd_id and version_id url: str = f"{rest_api_endpoint}/atbd_versions?atbd_id=eq.{atbd_id}&atbd_version=eq.{version_id}&select=*,atbd(*),algorithm_input_variables(*),algorithm_output_variables(*),algorithm_implementations(*),publication_references(*),data_access_input_data(*),data_access_output_data(*),data_access_related_urls(*),citations(*)" - response = requests.get(url) + request_params = {"url": url} + if user: + request_params["headers"] = {"Authorization": f"Bearer {create_token(user)}"} + + response = requests.get(**request_params) + if not response.ok: raise HTTPException(status_code=response.status_code, detail=response.text) records = response.json() diff --git a/fastapi/app/main.py b/fastapi/app/main.py index 523d2a99..2b12b738 100644 --- a/fastapi/app/main.py +++ b/fastapi/app/main.py @@ -207,7 +207,7 @@ def get_atbd_pdf_by_alias( background_tasks: BackgroundTasks, user: User = Depends(get_user), ): - atbd_doc = get_atbd(alias=alias) + atbd_doc = get_atbd(alias=alias, user=user) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks) @@ -227,7 +227,7 @@ def get_journal_atbd_pdf_by_alias( background_tasks: BackgroundTasks, user: User = Depends(get_user), ): - atbd_doc = get_atbd(alias=alias) + atbd_doc = get_atbd(alias=alias, user=user) return atbd_pdf_handler(atbd_doc, background_tasks=background_tasks, journal=True) From 12c4da1f3224e759453a782198e68432d9e57d2c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 18 Dec 2020 11:32:55 +0000 Subject: [PATCH 57/59] Fix cloudformation deploy --- cloudformation/cloudformation.yaml | 6 +++++- cloudformation/deploy.sh | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cloudformation/cloudformation.yaml b/cloudformation/cloudformation.yaml index 4c19258e..a831dfb2 100644 --- a/cloudformation/cloudformation.yaml +++ b/cloudformation/cloudformation.yaml @@ -114,8 +114,12 @@ Parameters: FastapiHost: Type: String Description: Url for FastApi + RestApiEndpoint: + Type: String + Description: Url for FastApi Conditions: HasFastapiHost: !Not [!Or [!Equals [!Ref 'FastapiHost', ''], !Equals [!Ref 'FastapiHost', 'http://localhost:8000']]] + HasRestApiEndpoint: !Not [!Or [!Equals [!Ref 'RestApiEndpoint', ''], !Equals [!Ref 'RestApiEndpoint', 'http://rest-api:3000']]] Mappings: # Hard values for the subnet masks. These masks define # the range of internal IP addresses that can be assigned. @@ -461,7 +465,7 @@ Resources: - Name: API_PREFIX Value: !Ref 'FastapiAPIPrefix' - Name: REST_API_ENDPOINT - Value: !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]] + Value: !If [HasRestApiEndpoint, !Ref 'RestApiEndpoint', !Join ['', ['http://', !GetAtt PublicLoadBalancer.DNSName]]] - Name: S3_ENDPOINT Value: 'http://s3.amazonaws.com' - Name: PDFS_S3_BUCKET diff --git a/cloudformation/deploy.sh b/cloudformation/deploy.sh index 95e4eee4..a97825c7 100755 --- a/cloudformation/deploy.sh +++ b/cloudformation/deploy.sh @@ -12,6 +12,7 @@ echo "APT_FRONTEND_URL $APT_FRONTEND_URL" echo "IDP_METADATA_URL $IDP_METADATA_URL" echo "JWT_SECRET $JWT_SECRET" echo "FASTAPI_HOST $FASTAPI_HOST" +echo "REST_API_ENDPOINT $REST_API_ENDPOINT" echo @@ -36,5 +37,6 @@ aws cloudformation deploy \ APTFrontendUrl=$APT_FRONTEND_URL \ IDPMetadataUrl=$IDP_METADATA_URL \ FastapiHost=$FASTAPI_HOST \ + RestApiEndpoint=$REST_API_ENDPOINT \ ElasticsearchDomainName=nasadb-$stackname \ --region us-east-1 --capabilities CAPABILITY_IAM From 83f29ba177de1e3bed73d0e36a8f65cc7899f1d4 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 21 Dec 2020 12:02:54 +0000 Subject: [PATCH 58/59] Prep v1 release --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2876bd81..7e24468d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # nasa-apt -**Version:** 0.3.0 +**Version:** 1.0.0 ## Local development Code and issues relevant to the NASA APT project @@ -58,7 +58,7 @@ cd cloudformation ``` You will be prompted for a stack name and a master db password. The current -stacks are `nasa-aptv2-staging` and `nasa-aptv2-production`. +stack is `nasa-apt-v5-prod` After the stack has been successfully deployed you can create the database tables. You will need an installation of the `psql` command line client. @@ -79,8 +79,7 @@ underlying database changes may require a forced redeployment of the PostgREST E ## Environments There are currently 2 environments defined for NASA-APT, which follow specific branches -- Staging (`develop`): http://nasa-Publi-1UDVJHRLIQD2G-1353740340.us-east-1.elb.amazonaws.com -- Production (`master`): http://nasa-Publi-1LGW8ZYHL7SF7-1834206210.us-east-1.elb.amazonaws.com +- Production (`master`): https://apt.ds.io **Given that deployment is a manual process it is important that the environments are kept up to date after a merge to `master` or `develop`.** From 02a6858c0595dc570c1ab5bfcfb3e1b0ad554c25 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 21 Dec 2020 12:04:18 +0000 Subject: [PATCH 59/59] Remove circle ci badge Circle is not being used --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 7e24468d..22b0c4b2 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ ## Local development Code and issues relevant to the NASA APT project -[![CircleCI](https://circleci.com/gh/developmentseed/nasa-apt/tree/develop.svg?style=svg&circle-token=ffc901ab7ce00ffa5cef07cce59ff64a2c635d2b)](https://circleci.com/gh/developmentseed/nasa-apt/tree/develop) - The project API is built using [Postgrest](https://github.com/PostgREST/postgrest). The startserver script uses `docker-compose` to build and run the development environment and