├── .idea
├── .name
├── watcherTasks.xml
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── inspectionProfiles
│ ├── profiles_settings.xml
│ └── Project_Default.xml
├── modules.xml
├── misc.xml
├── codeStyleSettings.xml
├── encodings.xml
└── aqua-monitor.iml
├── .gitattributes
├── app
├── static
│ ├── styles
│ │ ├── searchbox.css
│ │ ├── time-selector.css
│ │ ├── chart.css
│ │ ├── colormaps.scss
│ │ ├── query-chart.css
│ │ ├── compare.css
│ │ └── main.scss
│ ├── robots.txt
│ ├── images
│ │ ├── GEE.png
│ │ ├── jrc.png
│ │ ├── favicon.ico
│ │ ├── TUDelftLogo.png
│ │ ├── DeltaresLogoSmall.png
│ │ └── future-shorelines-legend.png
│ ├── libs
│ │ ├── sprite-skin-modern.png
│ │ ├── ion.rangeSlider.skinModern.css
│ │ ├── ion.rangeSlider.css
│ │ └── normalize.css
│ └── scripts
│ │ ├── console-hello.js
│ │ ├── searchbox.js
│ │ ├── compare-ee.js
│ │ ├── chart.js
│ │ ├── compare.js
│ │ ├── time-selector.js
│ │ ├── query-chart.js
│ │ ├── main.js
│ │ ├── shore-chart.js
│ │ └── script.js
├── requirements.txt
├── config.py
├── config_web.py
├── app.yaml
├── error_handler.py
├── templates
│ ├── compare.html
│ └── index.html
└── server.py
├── .bowerrc
├── .babelrc
├── BUG
├── deploy.cmd
├── bower.json
├── test
├── spec
│ └── test.js
└── index.html
├── .editorconfig
├── .gitignore
├── package.json
├── NOTES
├── request_refresh_token.py
├── README.md
├── gulpfile.babel.js
└── LICENSE
/.idea/.name:
--------------------------------------------------------------------------------
1 | aqua-monitor
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/app/static/styles/searchbox.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/BUG:
--------------------------------------------------------------------------------
1 | copy manyally:
2 |
3 | dist/templates/static/* to dist/static/
--------------------------------------------------------------------------------
/app/static/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
4 | Disallow:
5 |
--------------------------------------------------------------------------------
/app/static/images/GEE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/GEE.png
--------------------------------------------------------------------------------
/app/static/images/jrc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/jrc.png
--------------------------------------------------------------------------------
/app/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/favicon.ico
--------------------------------------------------------------------------------
/app/static/images/TUDelftLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/TUDelftLogo.png
--------------------------------------------------------------------------------
/app/static/images/DeltaresLogoSmall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/DeltaresLogoSmall.png
--------------------------------------------------------------------------------
/app/static/libs/sprite-skin-modern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/libs/sprite-skin-modern.png
--------------------------------------------------------------------------------
/app/static/images/future-shorelines-legend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/images/future-shorelines-legend.png
--------------------------------------------------------------------------------
/app/static/libs/ion.rangeSlider.skinModern.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/aqua-monitor/master/app/static/libs/ion.rangeSlider.skinModern.css
--------------------------------------------------------------------------------
/deploy.cmd:
--------------------------------------------------------------------------------
1 | cd dist
2 | rem gcloud app deploy --project aqua-monitor
3 | gcloud app deploy --project aqua-monitor -v upgrade-ee --bucket gs://aqua-monitor-src
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.1.2
2 | Flask-Cors==3.0.9
3 | gunicorn==20.0.4
4 | #earthengine-api==0.1.237
5 | earthengine-api==1.0.0
6 | oauth2client==4.1.3
7 | jinja2<3.1.0
8 | itsdangerous==2.0.1
9 | werkzeug==2.0.3
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Required credentials configuration."""
3 | EE_ACCOUNT = '578920177147-ul189ho0h6f559k074lrodsd7i7b84rc@developer.gserviceaccount.com'
4 | EE_PRIVATE_KEY_FILE = 'privatekey.json'
5 |
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/config_web.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | keys = json.loads(open('privatekey-web.json').read())
4 |
5 | EE_CLIENT_ID = keys["EE_CLIENT_ID"]
6 | EE_CLIENT_SECRET = keys["EE_CLIENT_SECRET"]
7 | EE_REFRESH_TOKEN = keys["EE_REFRESH_TOKEN"]
8 | EE_TOKEN_TYPE = 'Bearer'
9 | EE_TOKEN_EXPIRE_IN_SEC = 36000
10 | EE_ACCESS_TOKEN = ''
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/static/styles/time-selector.css:
--------------------------------------------------------------------------------
1 | #time-selector-container {
2 | position: absolute;
3 | left: 50%;
4 | bottom: 50px;
5 | border-radius: 3px;
6 | background-color: rgba(255, 255, 255, 1.0);
7 | padding-left: 20px;
8 | padding-right: 20px;
9 | padding-top: 4px;
10 | margin-left: -20%;
11 | width: 40%;
12 | height: 68px;
13 | display: none;
14 | }
15 |
--------------------------------------------------------------------------------
/app/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: python39
2 | entrypoint: gunicorn -t 300 -b :$PORT server:app
3 |
4 | runtime_config:
5 | python_version: 3
6 |
7 | automatic_scaling:
8 | max_concurrent_requests: 40
9 | min_idle_instances: 0
10 | max_idle_instances: 1
11 | min_pending_latency: 10s
12 | max_pending_latency: 15s
13 |
14 | handlers:
15 | - url: /.*
16 | script: auto
17 | secure: always
18 | redirect_http_response_code: 301
19 |
20 |
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webapp-default",
3 | "private": true,
4 | "dependencies": {
5 | "bootstrap-sass": "~3.3.5",
6 | "modernizr": "~2.8.1",
7 | "lodash": "^4.13.1"
8 | },
9 | "overrides": {
10 | "bootstrap-sass": {
11 | "main": [
12 | "assets/fonts/bootstrap/*",
13 | "assets/javascripts/bootstrap.js"
14 | ]
15 | }
16 | },
17 | "devDependencies": {
18 | "html5shiv": "^3.7.3",
19 | "mocha": "^2.5.3",
20 | "chai": "^3.5.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/test/spec/test.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | describe('If we are working with a map', function () {
5 | describe('and we are zoomed in to level 3', function () {
6 | var map = {
7 | getCurrentZoom: function() {return 3;}
8 | };
9 | it('should return the url for map layer 3', function () {
10 | // uses global map.getCurrentZoom
11 | var url = urlForCurrentZoom();
12 | assert.equal(url, '/zoom3');
13 | done();
14 |
15 | });
16 | });
17 | });
18 | })();
19 |
--------------------------------------------------------------------------------
/app/error_handler.py:
--------------------------------------------------------------------------------
1 | '''Application error handlers.'''
2 | from flask import Blueprint, jsonify
3 |
4 | import traceback
5 |
6 | error_handler = Blueprint('errors', __name__)
7 |
8 |
9 | @error_handler.app_errorhandler(Exception)
10 | def handle_unexpected_error(error):
11 | print(traceback.print_exc())
12 |
13 | status_code = 500
14 | success = False
15 | response = {
16 | 'success': success,
17 | 'error': {
18 | 'type': 'UnexpectedException',
19 | 'message': str(error)
20 | }
21 | }
22 |
23 | return jsonify(response), status_code
24 |
--------------------------------------------------------------------------------
/app/static/scripts/console-hello.js:
--------------------------------------------------------------------------------
1 | var styles = [
2 | 'border: 1px solid #3E0E02'
3 | , 'display: block'
4 | , 'text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3)'
5 | , 'box-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) inset, 0 5px 3px -5px rgba(0, 0, 0, 0.5), 0 -13px 5px -10px rgba(255, 255, 255, 0.4) inset'
6 | , 'line-height: 40px'
7 | , 'text-align: center'
8 | , 'font-weight: bold'
9 | ].join(';');
10 |
11 | console.log('%c Hi, welcome to the aqua-monitor. Please see the LICENSE file in our github repository (deltares/aqua-monitor) for details on the license of data (limited use) and our open source software. Let us know if you want to cooperate!', styles);
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/static/styles/chart.css:
--------------------------------------------------------------------------------
1 | #chart-dashboard {
2 | position: absolute;
3 | visibility: hidden;
4 | padding: 10px;
5 | width: 1120px;
6 | font-size: 12px;
7 | background-color: rgba(255, 255, 255, 0.9);
8 | bottom: 30px;
9 | left: 10px;
10 | text-align: center;
11 | }
12 |
13 | .chart-modal-close-button {
14 | position: absolute;
15 | padding-left: 5px;
16 | padding-right: 5px;
17 | padding-top: 5px;
18 | padding-bottom: 5px;
19 | height: 20px;
20 | width: 20px;
21 | right: 0px;
22 | margin: 0px;
23 | }
24 |
25 | #chart-table {
26 | margin-top: 1px;
27 | }
28 |
29 | #note-text {
30 | text-align: justify;
31 | font-weight: normal;
32 | }
33 |
34 | #note-text-row {
35 | display: none;
36 | }
--------------------------------------------------------------------------------
/app/static/styles/colormaps.scss:
--------------------------------------------------------------------------------
1 | .legend-box {
2 | width: 1em;
3 | min-width: 10px;
4 | }
5 | .legend {
6 | display: flex;
7 | margin-top: 16px;
8 | margin-bottom: 16px;
9 | .label-left {
10 | padding-right: 10px;
11 | }
12 | .label-right {
13 | padding-left: 10px;
14 | }
15 | }
16 | .RdYlGn {
17 | .q0-5{
18 | fill: rgb(215,25,28);
19 | background-color: rgb(215,25,28);
20 | }
21 | .q1-5{
22 | fill: rgb(253,174,97);
23 | background-color: rgb(253,174,97);
24 | }
25 | .q2-5{
26 | fill: rgb(255,255,191);
27 | background-color: rgb(255,255,191);
28 | }
29 | .q3-5{
30 | fill: rgb(166,217,106);
31 | background-color: rgb(166,217,106);
32 | }
33 | .q4-5 {
34 | fill: rgb(26,150,65);
35 | background-color: rgb(26,150,65);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # change these settings to your own preference
11 | indent_style = space
12 | indent_size = 4
13 | trim_trailing_whitespace = true
14 |
15 | # we recommend you to keep these unchanged
16 | end_of_line = lf
17 | charset = utf-8
18 | trim_trailing_whitespace = true
19 | insert_final_newline = true
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
24 | [{package,bower}.json]
25 | indent_style = space
26 | indent_size = 2
27 |
28 |
29 | [**.js]
30 | indent_size = 2
31 | indent_brace_style = 1TBS
32 | quote_type = single
33 |
34 |
35 | [**.scss]
36 | indent_size = 2
37 |
38 | [**.css]
39 | indent_size = 2
40 |
--------------------------------------------------------------------------------
/app/static/styles/query-chart.css:
--------------------------------------------------------------------------------
1 | svg.query-chart path.line {
2 | stroke: steelblue;
3 | stroke-width: 2;
4 | fill: none;
5 | }
6 |
7 | .query-chart text {
8 | font-size: 10px;
9 | }
10 |
11 | svg.query-chart .axis path,
12 | svg.query-chart .axis line {
13 | fill: none;
14 | stroke: grey;
15 | stroke-width: 1;
16 | shape-rendering: crispEdges;
17 | }
18 |
19 | .d3-tip {
20 | line-height: 1;
21 | font-weight: bold;
22 | font-size: 10px;
23 | padding: -9px;
24 | }
25 |
26 |
27 | .abline path {
28 | stroke-width: 1px;
29 | stroke: black
30 | }
31 |
32 | .ci { fill-opacity: 0.2; }
33 | .query-chart .sl45 { fill: black;}
34 | .query-chart .sl85 { fill: blue; }
35 | .query-chart .sl45 .line { stroke: black; }
36 | .query-chart .sl85 .line { stroke: blue; }
37 | .abline.future {
38 | stroke-dasharray: 15 15;
39 | }
40 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mocha Spec Runner
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/aqua-monitor.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config_web.py
2 | node_modules
3 | dist
4 | .tmp
5 | .sass-cache
6 | bower_components
7 | test/bower_components
8 | *.pem
9 | temp
10 | privatekey.json
11 | privatekey*.json
12 |
13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
15 |
16 | # User-specific stuff:
17 | .idea/workspace.xml
18 | .idea/tasks.xml
19 | .idea/dictionaries
20 | .idea/vcs.xml
21 | .idea/jsLibraryMappings.xml
22 |
23 | # Sensitive or high-churn files:
24 | .idea/dataSources.ids
25 | .idea/dataSources.xml
26 | .idea/dataSources.local.xml
27 | .idea/sqlDataSources.xml
28 | .idea/dynamic.xml
29 | .idea/uiDesigner.xml
30 |
31 | # Gradle:
32 | .idea/gradle.xml
33 | .idea/libraries
34 |
35 | # Mongo Explorer plugin:
36 | .idea/mongoSettings.xml
37 |
38 | ## File-based project format:
39 | *.iws
40 |
41 | ## Plugin-specific files:
42 |
43 | # IntelliJ
44 | /out/
45 |
46 | # mpeltonen/sbt-idea plugin
47 | .idea_modules/
48 |
49 | # JIRA plugin
50 | atlassian-ide-plugin.xml
51 |
52 | # Crashlytics plugin (for Android Studio and IntelliJ)
53 | com_crashlytics_export_strings.xml
54 | crashlytics.properties
55 | crashlytics-build.properties
56 | fabric.properties
57 | dist
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "engines": {
4 | "node": ">=0.12.0"
5 | },
6 | "devDependencies": {
7 | "babel-core": "^6.4.0",
8 | "babel-preset-es2015": "^6.3.13",
9 | "babel-register": "^6.26.0",
10 | "browser-sync": "^2.2.1",
11 | "del": "^1.1.1",
12 | "gulp": "^3.9.1",
13 | "gulp-autoprefixer": "^3.0.1",
14 | "gulp-babel": "^6.1.3",
15 | "gulp-cache": "^0.2.8",
16 | "gulp-cssnano": "^2.0.0",
17 | "gulp-debug": "^3.2.0",
18 | "gulp-eslint": "^0.13.2",
19 | "gulp-filter": "^5.1.0",
20 | "gulp-gae": "0.0.4",
21 | "gulp-htmlmin": "^1.3.0",
22 | "gulp-if": "^1.2.5",
23 | "gulp-imagemin": "^2.2.1",
24 | "gulp-load-plugins": "^0.10.0",
25 | "gulp-modernizr": "^1.0.0-alpha",
26 | "gulp-plumber": "^1.0.1",
27 | "gulp-sass": "^4.0.2",
28 | "gulp-size": "^1.2.1",
29 | "gulp-sourcemaps": "^1.5.0",
30 | "gulp-uglify": "^1.1.0",
31 | "gulp-useref": "^3.0.0",
32 | "main-bower-files": "^2.5.0",
33 | "natives": "^1.1.6",
34 | "wiredep": "^2.2.2"
35 | },
36 | "eslintConfig": {
37 | "env": {
38 | "es6": true,
39 | "node": true,
40 | "browser": true
41 | },
42 | "rules": {
43 | "quotes": [
44 | 2,
45 | "single"
46 | ]
47 | }
48 | },
49 | "dependencies": {
50 | "bower": "^1.8.14"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/NOTES:
--------------------------------------------------------------------------------
1 | http://localhost:16080/?min_year=1985&max_year=2010&averaging_months1=200&averaging_months2=60&site=1&percentile=10&filter_count=20&ndvi_filter=0.1
2 |
3 | http://localhost:16080/?min_year=1990&max_year=2013&averaging_months1=120&averaging_months2=48&percentile=30&filter_count=30&water_slope_opacity=0.7
4 |
5 |
6 |
7 | var mapStyles = [{"featureType":"all","elementType":"geometry","stylers":[{"hue":"#ff4400"},{"saturation":-68},{"lightness":-4},{"gamma":0.72}]},{"featureType":"all","elementType":"labels.text.fill","stylers":[{"color":"#999999"},{"lightness":"-60"}]},{"featureType":"landscape","elementType":"geometry","stylers":[{"color":"#cccccc"}]},{"featureType":"landscape.man_made","elementType":"geometry","stylers":[{"hue":"#0077ff"},{"gamma":3.1}]},{"featureType":"poi","elementType":"geometry","stylers":[{"lightness":"60"}]},{"featureType":"poi.park","elementType":"all","stylers":[{"saturation":-23}]},{"featureType":"road.highway","elementType":"geometry.fill","stylers":[{"color":"#cccccc"},{"lightness":"0"}]},{"featureType":"road.highway","elementType":"geometry.stroke","stylers":[{"color":"#cccccc"},{"lightness":"-25"}]},{"featureType":"transit","elementType":"labels.text.stroke","stylers":[{"saturation":-64},{"lightness":16},{"gamma":"2.15"},{"weight":2.7},{"color":"#ffffff"}]},{"featureType":"transit.line","elementType":"geometry","stylers":[{"lightness":"-60"},{"gamma":"1.20"},{"hue":"#00ffff"}]},{"featureType":"water","elementType":"all","stylers":[{"hue":"#00ccff"},{"gamma":0.44},{"saturation":-33}]},{"featureType":"water","elementType":"labels.text.fill","stylers":[{"hue":"#007fff"},{"gamma":0.77},{"saturation":65},{"lightness":99}]},{"featureType":"water","elementType":"labels.text.stroke","stylers":[{"weight":5.6},{"hue":"#0091ff"},{"saturation":"100"},{"gamma":"120"},{"lightness":"-70"}]}]
8 | var styledMap = new google.maps.StyledMapType(mapStyles, {name: "Styled Map"});
9 | map.mapTypes.set('map_style', styledMap);
10 | map.mapTypes.set('map_style', styledMap);
11 |
--------------------------------------------------------------------------------
/app/static/scripts/searchbox.js:
--------------------------------------------------------------------------------
1 | // globals: map
2 |
3 | // Source:
4 | // https://developers.google.com/maps/documentation/javascript/examples/places-searchbox
5 | function initAutocomplete(map) {
6 | // Create the search box and link it to the UI element.
7 | var input = document.getElementById('pac-input');
8 | var searchBox = new google.maps.places.SearchBox(input);
9 | // expect map to be global
10 | map.controls[google.maps.ControlPosition.TOP_LEFT].push(input);
11 |
12 | // Bias the SearchBox results towards current map's viewport.
13 | map.addListener('bounds_changed', function() {
14 | searchBox.setBounds(map.getBounds());
15 | });
16 |
17 | /*
18 | var markers = [];
19 | */
20 |
21 | // Listen for the event fired when the user selects a prediction and retrieve
22 | // more details for that place.
23 | searchBox.addListener('places_changed', function() {
24 | var places = searchBox.getPlaces();
25 |
26 | if (places.length == 0) {
27 | return;
28 | }
29 |
30 | /*
31 | // Clear out the old markers.
32 | markers.forEach(function(marker) {
33 | marker.setMap(null);
34 | });
35 | markers = [];
36 | */
37 |
38 | // For each place, get the icon, name and location.
39 | var bounds = new google.maps.LatLngBounds();
40 | places.forEach(function(place) {
41 | /*
42 | var icon = {
43 | url: place.icon,
44 | size: new google.maps.Size(71, 71),
45 | origin: new google.maps.Point(0, 0),
46 | anchor: new google.maps.Point(17, 34),
47 | scaledSize: new google.maps.Size(25, 25)
48 | };
49 |
50 | // Create a marker for each place.
51 | markers.push(new google.maps.Marker({
52 | map: map,
53 | icon: icon,
54 | title: place.name,
55 | position: place.geometry.location
56 | }));
57 | */
58 |
59 | if (place.geometry.viewport) {
60 | // Only geocodes have viewport.
61 | bounds.union(place.geometry.viewport);
62 | } else {
63 | bounds.extend(place.geometry.location);
64 | }
65 | });
66 | map.fitBounds(bounds);
67 | map.setZoom(11);
68 | });
69 | }
70 |
--------------------------------------------------------------------------------
/app/static/scripts/compare-ee.js:
--------------------------------------------------------------------------------
1 | // globals: token, comparison
2 | (function () {
3 | 'use strict';
4 | function addLayer(map, layer) {
5 | layer.getMap({}, function(mapId) {
6 | var id = mapId.mapid;
7 | var token = mapId.token;
8 |
9 | // The Google Maps API calls getTileUrl() when it tries to display a map
10 | // tile. This is a good place to swap in the MapID and token we got from
11 | // the Python script. The other values describe other properties of the
12 | // custom map type.
13 | var eeMapOptions = {
14 | getTileUrl: function (tile, zoom) {
15 | var baseUrl = 'https://earthengine.googleapis.com/map';
16 | var url = [baseUrl, id, zoom, tile.x, tile.y].join('/');
17 | url += '?token=' + token;
18 | return url;
19 | },
20 | tileSize: new google.maps.Size(256, 256)
21 | };
22 |
23 | // Create the map type.
24 | var mapType = new google.maps.ImageMapType(eeMapOptions);
25 | map.overlayMapTypes.push(mapType);
26 |
27 | });
28 |
29 | }
30 | function renderLandsatMosaic(percentile, start, end) {
31 | var bands = ['swir1', 'nir', 'green'];
32 | var l8 = new ee.ImageCollection('LANDSAT/LC8_L1T_TOA').filterDate(start, end).select(['B6', 'B5', 'B3'], bands);
33 | var l7 = new ee.ImageCollection('LANDSAT/LE7_L1T_TOA').filterDate(start, end).select(['B5', 'B4', 'B2'], bands);
34 | var l5 = new ee.ImageCollection('LANDSAT/LT5_L1T_TOA').filterDate(start, end).select(['B5', 'B4', 'B2'], bands);
35 |
36 | var images = ee.ImageCollection(l8.merge(l7).merge(l5))
37 | .reduce(ee.Reducer.percentile([percentile]))
38 | .rename(bands);
39 |
40 | return images.visualize({min: 0.05, max: [0.5, 0.5, 0.6], gamma: 1.4});
41 | }
42 |
43 | $(document).ready(function(){
44 | // client variables should be set by global template
45 | ee.data.setAuthToken(client_id, token_type, access_token, token_expires_in_sec, true);
46 |
47 | ee.initialize(null, null, function () {
48 | addLayer(comparison.bottomMap, renderLandsatMosaic(20, '2000-01-01', '2001-01-01'));
49 | addLayer(comparison.topMap, renderLandsatMosaic(20, '2016-01-01', '2017-01-01'));
50 |
51 | });
52 |
53 | });
54 |
55 | }());
56 |
--------------------------------------------------------------------------------
/app/templates/compare.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Map compare
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/request_refresh_token.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, redirect, request, url_for, session
2 | import requests
3 | import json
4 |
5 | app = Flask(__name__)
6 | app.secret_key = ''
7 |
8 | # OAuth 2.0 credentials
9 | CLIENT_ID = ''
10 | CLIENT_SECRET = ''
11 | REDIRECT_URI = 'http://localhost:5000/oauth2callback'
12 | SCOPE = 'https://www.googleapis.com/auth/earthengine'
13 |
14 | # Google's OAuth 2.0 endpoints
15 | AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'
16 | TOKEN_URL = 'https://oauth2.googleapis.com/token'
17 |
18 | @app.route('/')
19 | def index():
20 | return 'Welcome to the OAuth 2.0 sample app! Log in with Google '
21 |
22 | @app.route('/login')
23 | def login():
24 | # Build the authorization URL
25 | auth_url = (
26 | f'{AUTH_URL}?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}'
27 | '&response_type=code&scope={scope}&access_type=offline'
28 | ).format(scope=SCOPE)
29 | return redirect(auth_url)
30 |
31 | @app.route('/oauth2callback')
32 | def oauth2callback():
33 | # Get the authorization code from the query parameters
34 | code = request.args.get('code')
35 |
36 | # Exchange the authorization code for tokens
37 | token_data = {
38 | 'code': code,
39 | 'client_id': CLIENT_ID,
40 | 'client_secret': CLIENT_SECRET,
41 | 'redirect_uri': REDIRECT_URI,
42 | 'grant_type': 'authorization_code'
43 | }
44 |
45 | response = requests.post(TOKEN_URL, data=token_data)
46 | tokens = response.json()
47 |
48 | # Store the tokens in the session
49 | session['access_token'] = tokens['access_token']
50 | session['refresh_token'] = tokens['refresh_token']
51 |
52 | return 'Logged in successfully! Refresh Token '
53 |
54 | @app.route('/refresh')
55 | def refresh():
56 | # Use the refresh token to get a new access token
57 | refresh_token = session.get('refresh_token')
58 | token_data = {
59 | 'client_id': CLIENT_ID,
60 | 'client_secret': CLIENT_SECRET,
61 | 'refresh_token': refresh_token,
62 | 'grant_type': 'refresh_token'
63 | }
64 |
65 | print(refresh_token)
66 |
67 | response = requests.post(TOKEN_URL, data=token_data)
68 | new_tokens = response.json()
69 |
70 | # Update the access token in the session
71 | session['access_token'] = new_tokens['access_token']
72 |
73 | return 'Access token refreshed successfully!'
74 |
75 | if __name__ == '__main__':
76 | app.run(debug=True)
--------------------------------------------------------------------------------
/app/static/scripts/chart.js:
--------------------------------------------------------------------------------
1 | function toChartValues(values) {
2 | var results = [];
3 | for (var i = 0; i < values.length; i++) {
4 | var v = values[i];
5 | var date = new Date(v.time);
6 | results.push([date, 100 - parseInt(v.cloud_cover),
7 | 'Date: ' + date.getFullYear() + '-' + (date.getMonth() + 1) + "-" + date.getDate()
8 | + '
CloudCover: ' + v.cloud_cover + '
Id: ' + v.id + '
']);
9 | }
10 | return results;
11 | }
12 |
13 | function setChartData(data) {
14 | // load the image information here in dictionary with keys based on satellite
15 | // add first data series -> Landsat4
16 | var table = new google.visualization.DataTable();
17 | table.addColumn('date', 'Date');
18 | table.addColumn('number', 'Quality');
19 | table.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
20 | /*
21 | table.addRows(toChartValues(data[0].values));
22 | table.addRows(toChartValues(data[1].values));
23 | table.addRows(toChartValues(data[2].values));
24 | */
25 | table.addRows(toChartValues(data));
26 |
27 |
28 |
29 | var dash = new google.visualization.Dashboard(document.getElementById('chart-dashboard'));
30 |
31 | var control = new google.visualization.ControlWrapper({
32 | controlType: 'ChartRangeFilter',
33 | containerId: 'chart-range',
34 | options: {
35 | filterColumnIndex: 0,
36 | ui: {
37 | chartType: 'ColumnChart',
38 | chartOptions: {
39 | height: 50,
40 | width: 1100,
41 | chartArea: {
42 | width: '95%',
43 | //height: '20%'
44 | }
45 | },
46 | chartView: {
47 | columns: [0, 1]
48 | }
49 | }
50 | }
51 | });
52 |
53 | var gchart = new google.visualization.ChartWrapper({
54 | chartType: 'ColumnChart',
55 | containerId: 'chart',
56 | options: {
57 | tooltip: {isHtml: true},
58 | legend: {position: 'none'},
59 | //vAxis: {title: 'Cloud Cover [%]'},
60 | bar: {groupWidth: 3},
61 | height: 150,
62 | width: 1100,
63 | interpolateNulls: true,
64 | //backgroundColor: {fill: 'transparent'},
65 | hAxis: {format: 'yyyy-MM-dd'},
66 | chartArea: {width: '95%'},
67 | animation: {duration: 0}
68 | }
69 | });
70 |
71 |
72 |
73 | dash.bind([control], [gchart]);
74 | dash.draw(table);
75 | }
76 |
77 | // TODO: replace google chart by something that is a bit more performant
78 | // google.charts.load('current', {packages: ['corechart']});
79 | // google.load('visualization', '1', {packages: ['controls', 'charteditor']});
80 |
--------------------------------------------------------------------------------
/app/static/scripts/compare.js:
--------------------------------------------------------------------------------
1 | // globals: google, view
2 |
3 | // exported variables
4 | // global comparison object
5 | var comparison = {};
6 |
7 | var exports = (function () {
8 | 'use strict';
9 |
10 | function initializeComparison() {
11 |
12 | // empty styles
13 | var styles = [{
14 | stylers:[{ color: '#444444' }]
15 | }];
16 | var blankMap = new google.maps.StyledMapType(styles);
17 |
18 | // from the global view
19 | var zoom = _.get(view, 'zoom', 12);
20 | var lat = _.get(view, 'lat', 52.05);
21 | var lng = _.get(view, 'lng', 4.19);
22 |
23 | // We want to be able to run multiple comparisons for now we only support the first
24 | var container = document.getElementsByClassName('compare-container')[0];
25 | comparison.container = container;
26 | // initialize with different options
27 | var topOptions = {
28 | zoom: zoom,
29 | center: new google.maps.LatLng(
30 | lat,
31 | lng
32 | ),
33 | mapTypeId: google.maps.MapTypeId.SATELLITE
34 | };
35 | var bottomOptions = {
36 | zoom: zoom,
37 | center: new google.maps.LatLng(
38 | lat,
39 | lng
40 | ),
41 | mapTypeId: google.maps.MapTypeId.SATELLITE
42 | };
43 | var bottomMap = new google.maps.Map(container.getElementsByClassName('bottom-map')[0], bottomOptions);
44 | bottomMap.mapTypes.set('blank', blankMap);
45 | var topMap = new google.maps.Map(container.getElementsByClassName('top-map')[0], topOptions);
46 | topMap.mapTypes.set('blank', blankMap);
47 |
48 | // We might run into some ping-pong here.
49 | // for now it seems to work..
50 | google.maps.event.addListener(bottomMap, 'bounds_changed', (function () {
51 | topMap.setCenter(bottomMap.getCenter());
52 | topMap.setZoom(bottomMap.getZoom());
53 | }));
54 |
55 | google.maps.event.addListener(topMap, 'bounds_changed', (function () {
56 | bottomMap.setCenter(topMap.getCenter());
57 | bottomMap.setZoom(topMap.getZoom());
58 | }));
59 |
60 | // store in exported object
61 | comparison.topMap = topMap;
62 | comparison.bottomMap = bottomMap;
63 |
64 |
65 | };
66 |
67 | // is this general load or google maps load?
68 | google.maps.event.addDomListener(window, 'load', initializeComparison);
69 |
70 |
71 | // set slider logic
72 | $('.comparison-slider').on('input', function(evt){
73 |
74 | // compute new width
75 | var width = evt.target.value + 'vw';
76 |
77 | $(evt.target).siblings('.top-map-mask').css('width', width);
78 | });
79 |
80 | // initialize first slider
81 | // TODO: make sure this works on multipage document
82 | var slider = $('.comparison-slider');
83 | var width = slider[0].value + 'vw';
84 | slider.siblings('.top-map-mask').css('width', width);
85 |
86 |
87 |
88 | return comparison;
89 | }());
90 |
91 | // If we're in node for testing
92 | if (typeof module !== 'undefined' && module.exports) {
93 | module.exports = exports;
94 | }
95 |
--------------------------------------------------------------------------------
/app/static/styles/compare.css:
--------------------------------------------------------------------------------
1 | /* variables: padding: 10px */
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | }
6 | .compare-container {
7 | width: 100vw ;
8 | height: 100vh;
9 | padding: 0;
10 | margin: 0;
11 | }
12 |
13 | /* apply padding */
14 | .bottom-map {
15 | /* we can't use box-size: border-box because the mask */
16 | position: absolute;
17 |
18 | }
19 | .top-map-mask {
20 | position: absolute;
21 | }
22 | .top-map {
23 | top: 0;
24 | left: 0;
25 | position: absolute;
26 |
27 | }
28 |
29 | .top-map, .bottom-map {
30 | width: 100vw;
31 | height: 100vh;
32 | }
33 |
34 | .top-map-mask {
35 | /* compute size relative to the padding */
36 | width: 50vw;
37 | height: 100vh;
38 | overflow-x: hidden;
39 | }
40 |
41 |
42 |
43 | .bottom-map {
44 |
45 | }
46 | .top-map-mask {
47 | border-right-color: rgba(255,255,255,0.2);
48 | border-right-style: solid;
49 | border-right-width: 1px;
50 | }
51 |
52 |
53 |
54 |
55 |
56 | /* style based on http://thenewcode.com/819/A-Before-And-After-Image-Comparison-Slide-Control-in-HTML5 */
57 |
58 | .comparison-slider {
59 | /* hide default appearance */
60 | -webkit-appearance:none;
61 | -moz-appearance:none;
62 | /* create a light effect */
63 | background-color: rgba(255,255,255,0.2);
64 | position: absolute;
65 | padding: 0;
66 | margin: 0;
67 | /* at the bottom */
68 | top: calc(90vh - 10px);
69 | /* from the left */
70 | left: 0;
71 | /* full width */
72 | /* The slider is bound within the track */
73 | /* https://bugs.webkit.org/show_bug.cgi?id=94158 */
74 | width: 100vw;
75 | /* 1 letter height */
76 | height: 1rem;
77 | }
78 |
79 |
80 | .comparison-slider:focus {
81 | outline: none;
82 | }
83 | .comparison-slider:active {
84 | outline: none;
85 | }
86 |
87 |
88 | /* for chrome */
89 | .comparison-slider::-webkit-slider-runnable-track {
90 | -webkit-appearance:none;
91 | /* bit wider so the thumb stays in the middle */
92 | width: 100vw;
93 | outline: none;
94 | }
95 |
96 |
97 | .comparison-slider::-moz-range-track {
98 | -moz-appearance:none;
99 | width: 100vw;
100 | outline: none;
101 | }
102 |
103 |
104 | .comparison-slider::active {
105 | border: none;
106 | outline: none;
107 | }
108 | .comparison-slider::-webkit-slider-thumb {
109 | -webkit-appearance:none;
110 | width: 1.4rem;
111 | height: 1.4rem;
112 | background: rgba(255, 255, 255, 0.8);
113 | box-shadow: 0px 0px 3px #333;
114 | /* bug? */
115 | border-radius: 3px;
116 | }
117 | .comparison-slider::-moz-range-thumb {
118 | -moz-appearance: none;
119 | width: 1.4rem;
120 | height: 1.4rem;
121 | background: rgba(200, 200, 200, 0.8);
122 | box-shadow: 0px 0px 3px #333;
123 | border-radius: 3px;
124 | }
125 | .comparison-slider:focus::-webkit-slider-thumb {
126 | background: rgba(255,255,255,0.9);
127 | }
128 | .comparison-slider:focus::-moz-range-thumb {
129 | background: rgba(255,255,255,0.9);
130 | }
131 |
--------------------------------------------------------------------------------
/app/static/scripts/time-selector.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 |
3 | var prevFrom = null;
4 | var prevTo = null;
5 |
6 | $('#time-selector-range').ionRangeSlider({
7 | type: 'double',
8 | keyboard: true,
9 | min: minYear,
10 | max: maxYear,
11 | from: minYearSelection,
12 | to: maxYearSelection,
13 | min_interval: 1,
14 | grid: true,
15 | grid_snap: true,
16 | prettify: function (num) {
17 | var m = moment(num, 'YYYY').locale('en');
18 | return m.format('YYYY');
19 | },
20 | onFinish: function (data) {
21 | function fmt(date) { return date.format('YYYY-MM-DD');}
22 |
23 | var years = _.map(
24 | [
25 | moment([data.from]),
26 | moment([data.from]).add(averagingMonths1, 'month'),
27 | moment([data.to]).add(-averagingMonths2, 'month'),
28 | moment([data.to])
29 | ],
30 | fmt
31 | );
32 | // reused variable;
33 | var layer;
34 |
35 | if(data.to != prevTo || data.from != prevFrom) {
36 | var yearsAndPeriods = [[years[0], averagingMonths1], [years[3], averagingMonths2]];
37 | var waterChangeTrendRatio = getWaterTrendChangeRatio(data.from, data.to);
38 | layer = layerByName('dynamic-change');
39 | layer.urls = renderWaterTrend(percentile, yearsAndPeriods, waterSlopeThreshold, waterSlopeThresholdSensitive, waterChangeTrendRatio);
40 | setLayer(map, layer);
41 | layer = layerByName('dynamic-change-refined');
42 | layer.urls = renderWaterTrend(percentile, yearsAndPeriods, waterSlopeThreshold, waterSlopeThresholdSensitive, waterChangeTrendRatio, true);
43 | setLayer(map, layer);
44 | }
45 |
46 | if(data.from != prevFrom) {
47 | layer = layerByName('before-percentile');
48 | layer.urls = renderLandsatMosaic(percentile, years[0], fmt(moment([data.from]).add(averagingMonths1, 'month')));
49 | setLayer(map, layer);
50 | layer = layerByName('before-percentile-sharpened');
51 | layer.urls = renderLandsatMosaic(percentile, years[0], fmt(moment([data.from]).add(averagingMonths1, 'month')));
52 | setLayer(map, layer);
53 | prevFrom = data.from;
54 | }
55 |
56 | if(data.to != prevTo) {
57 | layer = layerByName('after-percentile');
58 | layer.urls = renderLandsatMosaic(percentile, years[3], fmt(moment([data.to]).add(averagingMonths2, 'month')));
59 | setLayer(map, layer);
60 | layer = layerByName('after-percentile-sharpened');
61 | layer.urls = renderLandsatMosaic(percentile, years[3], fmt(moment([data.to]).add(averagingMonths2, 'month')), true);
62 | setLayer(map, layer);
63 | prevTo = data.to;
64 | }
65 |
66 | minYear = data.from;
67 | maxYear = data.to;
68 | },
69 | onChange: function (data) {
70 | $('#slider-label-before').text(data.from);
71 | $('#slider-label-after').text(data.to);
72 | $('#label-year-before').text(data.from);
73 | $('#label-year-after').text(data.to);
74 | minYearSelection = data.from
75 | maxYearSelection = data.to
76 | }
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/app/static/libs/ion.rangeSlider.css:
--------------------------------------------------------------------------------
1 | /* Ion.RangeSlider
2 | // css version 2.0.3
3 | // © 2013-2014 Denis Ineshin | IonDen.com
4 | // ===================================================================================================================*/
5 |
6 | /* =====================================================================================================================
7 | // RangeSlider */
8 |
9 | .irs {
10 | position: relative; display: block;
11 | -webkit-touch-callout: none;
12 | -webkit-user-select: none;
13 | -khtml-user-select: none;
14 | -moz-user-select: none;
15 | -ms-user-select: none;
16 | user-select: none;
17 | }
18 | .irs-line {
19 | position: relative; display: block;
20 | overflow: hidden;
21 | outline: none !important;
22 | }
23 | .irs-line-left, .irs-line-mid, .irs-line-right {
24 | position: absolute; display: block;
25 | top: 0;
26 | }
27 | .irs-line-left {
28 | left: 0; width: 11%;
29 | }
30 | .irs-line-mid {
31 | left: 9%; width: 82%;
32 | }
33 | .irs-line-right {
34 | right: 0; width: 11%;
35 | }
36 |
37 | .irs-bar {
38 | position: absolute; display: block;
39 | left: 0; width: 0;
40 | }
41 | .irs-bar-edge {
42 | position: absolute; display: block;
43 | top: 0; left: 0;
44 | }
45 |
46 | .irs-shadow {
47 | position: absolute; display: none;
48 | left: 0; width: 0;
49 | }
50 |
51 | .irs-slider {
52 | position: absolute; display: block;
53 | cursor: default;
54 | z-index: 1;
55 | }
56 | .irs-slider.single {
57 |
58 | }
59 | .irs-slider.from {
60 |
61 | }
62 | .irs-slider.to {
63 |
64 | }
65 | .irs-slider.type_last {
66 | z-index: 2;
67 | }
68 |
69 | .irs-min {
70 | position: absolute; display: block;
71 | left: 0;
72 | cursor: default;
73 | }
74 | .irs-max {
75 | position: absolute; display: block;
76 | right: 0;
77 | cursor: default;
78 | }
79 |
80 | .irs-from, .irs-to, .irs-single {
81 | position: absolute; display: block;
82 | top: 0; left: 0;
83 | cursor: default;
84 | white-space: nowrap;
85 | }
86 |
87 | .irs-grid {
88 | position: absolute; display: none;
89 | bottom: 0; left: 0;
90 | width: 100%; height: 20px;
91 | }
92 | .irs-with-grid .irs-grid {
93 | display: block;
94 | }
95 | .irs-grid-pol {
96 | position: absolute;
97 | top: 0; left: 0;
98 | width: 1px; height: 8px;
99 | background: #000;
100 | }
101 | .irs-grid-pol.small {
102 | height: 4px;
103 | }
104 | .irs-grid-text {
105 | position: absolute;
106 | bottom: 0; left: 0;
107 | white-space: nowrap;
108 | text-align: center;
109 | font-size: 9px; line-height: 9px;
110 | padding: 0 3px;
111 | color: #000;
112 | }
113 |
114 | .irs-disable-mask {
115 | position: absolute; display: block;
116 | top: 0; left: -1%;
117 | width: 102%; height: 100%;
118 | cursor: default;
119 | background: rgba(0,0,0,0.0);
120 | z-index: 2;
121 | }
122 | .irs-disabled {
123 | opacity: 0.4;
124 | }
125 | .lt-ie9 .irs-disabled {
126 | filter: alpha(opacity=40);
127 | }
128 |
129 |
130 | .irs-hidden-input {
131 | position: absolute !important;
132 | display: block !important;
133 | top: 0 !important;
134 | left: 0 !important;
135 | width: 0 !important;
136 | height: 0 !important;
137 | font-size: 0 !important;
138 | line-height: 0 !important;
139 | padding: 0 !important;
140 | margin: 0 !important;
141 | outline: none !important;
142 | z-index: -9999 !important;
143 | background: none !important;
144 | border-style: solid !important;
145 | border-color: transparent !important;
146 | }
147 |
--------------------------------------------------------------------------------
/app/static/scripts/query-chart.js:
--------------------------------------------------------------------------------
1 | function createQueryChart(elementId, data) {
2 | var margin = {top: 20, right: 20, bottom: 20, left: 20},
3 | width = 300 - margin.left - margin.right,
4 | height = 170 - margin.top - margin.bottom;
5 |
6 | // Parse the date / time
7 | var formatDate = d3.time.format("%Y-%m-%d"),
8 | bisectDate = d3.bisector(function (d) {
9 | return d.date;
10 | }).left;
11 |
12 | // Set the ranges
13 | var x = d3.time.scale().range([0, width]);
14 | var y = d3.scale.linear().range([height, 0]);
15 |
16 | // Define the axes
17 | var xAxis = d3.svg.axis().scale(x)
18 | .orient("bottom").ticks(10);
19 |
20 | var yAxis = d3.svg.axis().scale(y)
21 | .orient("left").ticks(5);
22 |
23 | // Define the line
24 | var valueline = d3.svg.line()
25 | .x(function (d) {
26 | return x(formatDate.parse(d.date));
27 | })
28 | .y(function (d) {
29 | return y(+d.value);
30 | });
31 |
32 | // Adds the svg canvas
33 | var svg = d3.select(elementId)
34 | .append("svg")
35 | .classed({'query-chart': true})
36 | .attr("width", width + margin.left + margin.right)
37 | .attr("height", height + margin.top + margin.bottom)
38 | .append("g")
39 | .attr("transform",
40 | "translate(" + margin.left + "," + margin.top + ")");
41 |
42 | var tip = d3.tip()
43 | .attr('class', 'd3-tip')
44 | .offset([-10, 0])
45 | .html(function (d) {
46 | return 'Date: ' + formatDate(d.date) + ', Value: ' + d.value;
47 | });
48 |
49 | svg.call(tip);
50 |
51 | var lineSvg = svg.append("g");
52 |
53 | var focus = svg.append("g")
54 | .style("display", "none");
55 |
56 | // Scale the range of the data
57 | x.domain(d3.extent(data, function (d) {
58 | return d.date;
59 | }));
60 | y.domain([0, d3.max(data, function (d) {
61 | return d.value;
62 | })]);
63 |
64 | // Add the valueline path.
65 | lineSvg.append("path")
66 | .attr("class", "line")
67 | .attr("d", valueline(data))
68 | ;
69 |
70 | // Add the X Axis
71 | svg.append("g")
72 | .attr("class", "x axis")
73 | .attr("transform", "translate(0," + height + ")")
74 | .call(xAxis);
75 |
76 | // Add the Y Axis
77 | svg.append("g")
78 | .attr("class", "y axis")
79 | .call(yAxis);
80 |
81 | // append the x line
82 | focus.append("line")
83 | .attr("class", "x")
84 | .style("stroke", "blue")
85 | .style("stroke-dasharray", "3,3")
86 | .style("opacity", 0.5)
87 | .attr("y1", 0)
88 | .attr("y2", height);
89 |
90 | // append the y line
91 | focus.append("line")
92 | .attr("class", "y")
93 | .style("stroke", "blue")
94 | .style("stroke-dasharray", "3,3")
95 | .style("opacity", 0.5)
96 | .attr("x1", width)
97 | .attr("x2", width);
98 |
99 | // append the circle at the intersection
100 | focus.append("circle")
101 | .attr("class", "y")
102 | .style("fill", "none")
103 | .style("stroke", "blue")
104 | .attr("r", 3);
105 |
106 | // place the value at the intersection
107 | focus.append("text")
108 | .attr("class", "y1")
109 | .style("stroke", "white")
110 | .style("stroke-width", "3.5px")
111 | .style("opacity", 0.8)
112 | .attr("dx", 8)
113 | .attr("dy", "-.3em");
114 |
115 | focus.append("text")
116 | .attr("class", "y2")
117 | .attr("dx", 8)
118 | .attr("dy", "-.3em");
119 |
120 | // place the date at the intersection
121 | focus.append("text")
122 | .attr("class", "y3")
123 | .style("stroke", "white")
124 | .style("stroke-width", "3.5px")
125 | .style("opacity", 0.8)
126 | .attr("dx", 8)
127 | .attr("dy", "1em");
128 |
129 | focus.append("text")
130 | .attr("class", "y4")
131 | .attr("dx", 8)
132 | .attr("dy", "1em");
133 |
134 |
135 | // append the rectangle to capture mouse
136 | svg.append("rect")
137 | .attr("width", width)
138 | .attr("height", height)
139 | .style("fill", "none")
140 | .style("pointer-events", "all")
141 | .on("mouseover", function () {
142 | focus.style("display", null);
143 | })
144 | .on("mouseout", function () {
145 | focus.style("display", "none");
146 | tip.hide();
147 | })
148 | .on("mousemove", mousemove);
149 |
150 | function mousemove() {
151 | var x0 = x.invert(d3.mouse(this)[0]),
152 | i = bisectDate(data, x0, 1),
153 | d0 = data[i - 1],
154 | d1 = data[i],
155 | d = x0 - d0.date > d1.date - x0 ? d1 : d0;
156 |
157 | tip.show(d);
158 |
159 | focus.select("circle.y")
160 | .attr("transform",
161 | "translate(" + x(d.date) + "," +
162 | y(d.value) + ")");
163 |
164 | /*
165 | focus.select(".x")
166 | .attr("transform",
167 | "translate(" + x(d.date) + "," +
168 | y(d.value) + ")")
169 | .attr("y2", height - y(d.value));
170 |
171 | focus.select(".y")
172 | .attr("transform",
173 | "translate(" + width * -1 + "," +
174 | y(d.value) + ")")
175 | .attr("x2", width + width)
176 | */
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Aqua Monitor shows how the Earth's surface water has changed during the last 30 years.
2 |
3 | http://aqua-monitor.appspot.com
4 |
5 | See also the following Nature Climate Change paper: http://nature.com/nclimate/journal/v6/n9/full/nclimate3111.html
6 |
7 | The website shows static map by default, visualizing surface water changes which occurred during 1985-2016 as explained in the above paper (see supplementary materials).
8 |
9 | A dynamic mode can be turned on for a more detailed analysis. In this mode, the surface water changes are computed on-the-fly using parameters provided by the user. Additionally, percentile composite images are generated for two selected years.
10 |
11 | The following Google Earth Engine script can be used to generate surface water changes on-the-fly: https://code.earthengine.google.com/ca025b5b16b195c18793936cb56b6a69. And the following was used to generate static maps: https://code.earthengine.google.com/ca025b5b16b195c18793936cb56b6a69.
12 |
13 | The results presented in the paper include a few additional clean-up steps (noise clean-up in the mountains using HAND mask, deforestation-like changes). But they were excluded during static map generation.
14 |
15 | # How to build?
16 |
17 | Install Node.js, which is used by build scripts to compile sources and prepare everything for deployment.
18 |
19 | Then run the following commands:
20 |
21 | * npm install - downloads and installs all packages required to build the app (see packages.json)
22 | * bower install - downloads and installs all client-side packages (see bower.json)
23 |
24 | After that, run:
25 |
26 | * gulp build - compiles everything (minifies code, generates styles, etc.) and places results in dist/
27 |
28 | The resulting dist/ directory can be used to deploy everything as a Google App Engine app.
29 |
30 | Use "gulp watch" to monitor sources continuously during development - they will be automatically compiled into dist/ on every change.
31 | See gulpfile.babel.js for the rules used to do this.
32 |
33 | # How to run and deploy?
34 |
35 | To deploy the Aqua Monitor under Google App Engine. The following files need to be added / modified:
36 |
37 | * app/privatekey.json - add your service account key, this is used by Python backend.
38 | * app/privatekey-web.json - add your client id, secret, and a refresh token, used at runtime to generate access to GEE for the JavaScript code.
39 |
40 | Deploy using gcloud:
41 |
42 | * gcloud app deploy --project=
43 |
44 | # Test locally
45 |
46 | On Linux (after you upgrade gcloud with the relevant components) `gcloud components install app-engine-python-extras`
47 | * `gulp serve:gae`
48 |
49 | Open the aqua monitor at port 8081
50 |
51 | # Advanced parameter, not exposed yet to the user interface
52 |
53 | This will work only at high zoom levels, the following (experimental) arguments can be used to tune the algorithm:
54 |
55 | Format: http://aqua-monitor.appspot.com?min_year=1990&max_year=1995
56 |
57 | * averaging_months1 = 36 - first filtering period, in months, all images from that period are used to compute percentile
58 | * averaging_months2 = 18 - second filtering period, in months, all images from that period are used to compute percentile
59 | * all_years = false - when true - linear regression in time will be computed using *ALL* years (moving average), >> can be extremely slow and even crash <<, but also produce much more accurate results.
60 | * all_years_step - step in years
61 | * min_year = 2000 - default value is 2000, but currently up to 1985 is supported. 1972 (60m resolution) will be added later
62 | * max_year = 2015
63 | * min_year_selection - first year to select in the timeline
64 | * max_year_selection - last year to select in the timeline
65 | * min_doy = 0
66 | * max_doy = 365
67 | * water_slope_opacity = 0.4 - can be used to show all slope values, not only the largest ones, ~0.7 is nice
68 | * percentile=20 - default percentile is 20, use smaller values to select darker ones (also higher chance of shadows)
69 | * filter_count = 0 - minimum number of images required, otherwise the result will be empty
70 | * slope_threshold = 0.03 - slope threshold used to filter large changes
71 | * slope_threshold_sensitive = 0.02 - threshold for sensitive wetness slope, used after zoom in to refine change around larger change
72 | * ndvi_filter = -99 - can be used to filter-out changes which are not due to surface water (vegetation cover, like deforestation). This can be x2 slower. Using 0.1 to exclude deforestation woks in most cases.
73 | * smoothen=true - smoothen changes image or leave it blocky
74 | * debug = false - currently only shows surface water change refinement regions at higher zoom levels
75 |
76 | # Request refresh token
77 |
78 | In the app/privatekey-web.json you configure a client id, client secret and refresh token. This is used to generate an access token during runtime which enables the application to communicate with Google APIs.
79 |
80 | Access tokens have limited lifetimes. If your application needs access to a Google API beyond the lifetime of a single access token, it can obtain a refresh token. A refresh token allows your application to obtain new access tokens.
81 |
82 | See more information here: https://developers.google.com/identity/protocols/oauth2
83 |
84 | Although refresh tokens should be unlimited, this is not actually documented anywhere by Google. Observed was that the refresh token was no longer valid after 2+ years and was not allowed to request an access token. A simple Flask app was added to this project to be able to request a refresh token.
85 |
86 | Run the Flask app in request_refresh_token.py to obtain a refresh token. Configure the app to include the correct client id and client secret.
--------------------------------------------------------------------------------
/app/static/scripts/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Here we get the mapid and token for the map tiles that were generated
3 | * by Earth Engine using the Python script server.py and injected using
4 | * the Jinja2 templating engine.
5 | */
6 | var error;
7 |
8 | var queryMap = false;
9 |
10 | var maxChartIndex = 1;
11 |
12 | var beginDate = minYear.toString() + '-01-01', endDate = (maxYear+1).toString() + '2021-01-01';
13 |
14 | var mapLoaded = false;
15 |
16 | // Adds a marker to the map.
17 | function queryWaterTimeSeries(location, map) {
18 | var template = $('#query-chart-template')[0].innerHTML;
19 | var chartElement = $($.parseXML(template)).contents();
20 | var chartElementId = 'query-chart-' + maxChartIndex;
21 | chartElement.attr('id', chartElementId);
22 |
23 | maxChartIndex++;
24 |
25 | var infoWindow = new google.maps.InfoWindow({content: chartElement[0]});
26 |
27 | // Add the marker at the clicked location, and add the next-available label
28 | // from the array of alphabetical characters.
29 | var marker = new google.maps.Marker({
30 | position: location,
31 | label: '',
32 | draggable: true,
33 | map: map
34 | });
35 |
36 | marker.addListener('dblclick', function () {
37 | marker.setMap(null);
38 | });
39 |
40 | marker.addListener('click', function () {
41 | infoWindow.open(map, marker);
42 | });
43 |
44 | infoWindow.open(map, marker);
45 |
46 | google.maps.event.addListener(infoWindow, 'domready', function () {
47 | $.ajax({
48 | url: '/get_ndwi_time_series',
49 | data: {location: JSON.stringify(marker.position), begin: beginDate, end: endDate},
50 | dataType: 'json',
51 | success: function (data) {
52 | // convert dates
53 | var data = data.map(function (d) {
54 | return {
55 | date: new Date(d.date).format('%Y-%m-%d'),
56 | value: d.value
57 | };
58 | });
59 |
60 |
61 | createQueryChart('#' + chartElementId, data);
62 | },
63 | error: function (data) {
64 | console.log(data.responseText);
65 | }
66 | });
67 | });
68 |
69 | queryMap = false;
70 | map.setOptions({draggableCursor: null});
71 | }
72 |
73 | var map = initializeMap();
74 |
75 |
76 | if($('body').width() < 590) {
77 | $('#message-initializing-ee').css('left', '50%');
78 | $('#message-initializing-ee').css('width', '100vw');
79 | $('#message-initializing-ee').css('margin-left', '-50vw');
80 | } else {
81 | $('#message-initializing-ee').css('left', '50%');
82 | $('#message-initializing-ee').css('width', '350px');
83 | $('#message-initializing-ee').css('margin-left', '-175px');
84 | }
85 |
86 | $('#message-initializing-ee').show();
87 |
88 | $('.ui.dropdown')
89 | .dropdown();
90 |
91 | function inIframe () {
92 | try {
93 | return window.self !== window.top;
94 | } catch (e) {
95 | return true;
96 | }
97 | }
98 |
99 | var embedded = inIframe();
100 |
101 | if(embedded) {
102 | console.log('embedded')
103 | }
104 |
105 | // client variables should be set by global template
106 | ee.data.setAuthToken(client_id, token_type, access_token, token_expires_in_sec, true);
107 |
108 | ee.initialize(null, null, function () {
109 | addLayers();
110 |
111 | $('#button-info').on('click', function (evt) {
112 | $('#help').fadeToggle();
113 |
114 | $.ajax({
115 | url: '/get_aoi_image_info_time_series',
116 | data: {aoi: shapesJson},
117 | dataType: 'json',
118 | success: function (data) {
119 | setChartData(data);
120 | $('#chart-dashboard').css('visibility', 'visible');
121 | },
122 | error: function (data) {
123 | console.log(data.responseText);
124 | }
125 | });
126 | });
127 |
128 | $('#download-button').on('click', function (evt) {
129 | var bounds = map.getBounds().toJSON();
130 | var query = $.param(bounds);
131 | window.open('/bbox/info?' + query);
132 | });
133 |
134 | $('#button-download').click(function () {
135 | $('#message-download').toggle();
136 | });
137 |
138 | $('#button-query').click(function () {
139 | map.setOptions({draggableCursor: 'crosshair'});
140 | queryMap = true;
141 | });
142 |
143 | $('.info-close-button').click(function () {
144 | $('#info-box').transition('slide right');
145 | //$('#twitter-timeline-box').transition('slide right');
146 | $('#info-button').toggleClass('active');
147 | });
148 |
149 | $('#info-button').click(function () {
150 | $('#info-box').transition('slide right');
151 | // $('#twitter-timeline-box').transition('slide right');
152 | $('#info-button').toggleClass('active');
153 | });
154 |
155 | $('#label-year-before').text(minYearSelection);
156 | $('#label-year-after').text(maxYearSelection);
157 | $('#slider-label-before').text(minYearSelection);
158 | $('#slider-label-after').text(maxYearSelection);
159 | $('#twitter-button').show();
160 |
161 | $('.chart-modal-close-button').on('click', function() {
162 | $('#chart-modal').hide();
163 | });
164 | $('#chart-modal').hide();
165 |
166 |
167 | if($('body').width() > 1024) {
168 | $('#info-box').show();
169 | }
170 |
171 | // This event listener calls queryWaterTimeSeries() when the map is clicked.
172 | google.maps.event.addListener(map, 'click', function (event) {
173 | if (!queryMap) {
174 | return;
175 | }
176 |
177 | queryWaterTimeSeries(event.latLng, map);
178 | });
179 |
180 |
181 | if(mode === 'dynamic') {
182 | //toggleMode()
183 | }
184 | // TODO: enable mode selector after initialization
185 | });
186 |
187 | $('#button-menu-sidebar').click(function () {
188 | $('.ui.labeled.icon.sidebar')
189 | .sidebar('toggle')
190 | ;
191 | });
192 |
193 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
194 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
195 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
196 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
197 |
198 | ga('create', 'UA-74927830-1', 'auto');
199 | ga('send', 'pageview');
200 |
201 |
202 | /* buggy
203 | twttr.events.bind(
204 | 'loaded',
205 | function (event) {
206 | event.widgets.forEach(function (widget) {
207 | if($('body').width() > 1024 && $('#info-box').is(':visible')) {
208 | $('#twitter-timeline-box').show();
209 | }
210 | });
211 | }
212 | );
213 | */
214 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | // generated on 2016-03-22 using generator-gulp-webapp 1.1.1
2 | import gulp from 'gulp';
3 | import gulpLoadPlugins from 'gulp-load-plugins';
4 | import browserSync from 'browser-sync';
5 | import del from 'del';
6 | import {stream as wiredep} from 'wiredep';
7 | import gae from 'gulp-gae';
8 |
9 | const debug = require('gulp-debug');
10 |
11 | const $ = gulpLoadPlugins();
12 | const reload = browserSync.reload;
13 |
14 | gulp.task('styles-scss', () => {
15 | return gulp.src('app/static/styles/*.scss')
16 | .pipe($.plumber())
17 | .pipe($.sourcemaps.init())
18 | .pipe($.sass.sync({
19 | outputStyle: 'expanded',
20 | precision: 10,
21 | includePaths: ['.']
22 | }).on('error', $.sass.logError))
23 | .pipe($.autoprefixer({browsers: ['> 1%', 'last 2 versions', 'Firefox ESR']}))
24 | .pipe($.sourcemaps.write('../maps'))
25 | .pipe(gulp.dest('dist/static/styles'))
26 | .pipe(reload({stream: true}));
27 | });
28 |
29 | gulp.task('styles', () => {
30 | return gulp.src('app/static/styles/*.css')
31 | .pipe(gulp.dest('dist/static/styles'))
32 | .pipe(reload({stream: true}));
33 | });
34 |
35 | gulp.task('scripts', () => {
36 | return gulp.src('app/static/scripts/*.js')
37 | //.pipe($.plumber())
38 | .pipe($.sourcemaps.init())
39 | .pipe($.babel())
40 | //.pipe($.uglify({mangle: true}))
41 | .pipe($.sourcemaps.write('../maps'))
42 | .pipe(gulp.dest('dist/static/scripts'))
43 | .pipe(reload({stream: true}));
44 | });
45 |
46 | function lint(files, options) {
47 | return () => {
48 | return gulp.src(files)
49 | .pipe(reload({stream: true, once: true}))
50 | .pipe($.eslint(options))
51 | .pipe($.eslint.format())
52 | .pipe($.if(!browserSync.active, $.eslint.failAfterError()));
53 | }
54 | }
55 |
56 | const testLintOptions = {
57 | env: {
58 | mocha: true
59 | }
60 | };
61 |
62 | gulp.task('lint', lint('app/static/scripts/**/*.js'));
63 |
64 | gulp.task('lint:test', lint('test/spec/**/*.js', testLintOptions));
65 |
66 | gulp.task('html', ['styles-scss', 'styles', 'scripts', 'libs'], () => {
67 | return gulp.src('app/static/*.html')
68 | //.pipe($.htmlmin({collapseWhitespace: true}))
69 | .pipe(gulp.dest('dist/static'));
70 | });
71 |
72 | gulp.task('templates', ['styles-scss', 'styles'], () => {
73 |
74 | const htmlFilter = $.filter('**/*.html', {restore: true});
75 | const otherFilter = $.filter('**/*.html', {restore: true});
76 |
77 | return gulp.src(['app/templates/*.html'])
78 | .pipe(
79 | $.useref({searchPath: ['dist', '.']})
80 | )
81 | .pipe(
82 | // html to templates
83 | gulp.dest('dist/templates'),
84 | )
85 | .pipe(
86 | gulp.dest('dist')
87 | )
88 | });
89 |
90 | gulp.task('images', () => {
91 | return gulp.src('app/static/images/**/*')
92 | .pipe($.if($.if.isFile, $.cache($.imagemin({
93 | progressive: true,
94 | interlaced: true,
95 | // don't remove IDs from SVGs, they are often used
96 | // as hooks for embedding and styling
97 | svgoPlugins: [{cleanupIDs: false}]
98 | }))
99 | .on('error', function (err) {
100 | console.log(err);
101 | this.end();
102 | })))
103 | .pipe(gulp.dest('dist/static/images'));
104 | });
105 |
106 | gulp.task('fonts', () => {
107 | return gulp.src(require('main-bower-files')('**/*.{eot,svg,ttf,woff,woff2}', function (err) {
108 | })
109 | .concat('app/static/fonts/**/*'))
110 | .pipe(gulp.dest('.tmp/static/fonts'))
111 | .pipe(gulp.dest('dist/static/fonts'));
112 | });
113 |
114 | gulp.task('extras', () => {
115 | return gulp.src(['app/**/*.yaml', 'app/**/*.pem', 'app/**/*.py', 'app/**/*.txt', 'app/*.json'])
116 | .pipe(gulp.dest('dist'));
117 | });
118 |
119 | gulp.task('libs', () => {
120 | return gulp.src(['app/static/libs/**/*'])
121 | .pipe(gulp.dest('dist/static/libs'));
122 | });
123 |
124 | gulp.task('watch', () => {
125 | gulp.watch('app/templates/*', ['templates']);
126 | gulp.watch('app/static/*.html', ['html']);
127 | gulp.watch('app/static/styles/**/*.scss', ['styles-scss', 'templates']);
128 | gulp.watch('app/static/styles/**/*.css', ['styles', 'templates']);
129 | gulp.watch('app/static/scripts/**/*.js', ['scripts', 'libs']);
130 | gulp.watch('app/static/fonts/**/*', ['fonts']);
131 | gulp.watch('bower.json', ['wiredep', 'fonts']);
132 | gulp.watch('app/*.{py,yaml}', ['extras']);
133 | })
134 | ;
135 |
136 | gulp.task('clean', del.bind(null, ['.tmp', 'dist']));
137 |
138 | gulp.task('serve:test', ['scripts'], () => {
139 | browserSync({
140 | notify: false,
141 | port: 9000,
142 | ui: false,
143 | server: {
144 | baseDir: 'test',
145 | routes: {
146 | '/static/scripts': '.tmp/static/scripts',
147 | '/bower_components': 'bower_components'
148 | }
149 | }
150 | });
151 |
152 | gulp.watch('app/static/scripts/**/*.js', ['scripts']);
153 | gulp.watch('test/spec/**/*.js').on('change', reload);
154 | gulp.watch('test/spec/**/*.js', ['lint:test']);
155 | })
156 | ;
157 |
158 | // inject bower components
159 | gulp.task('wiredep', () => {
160 | gulp.src('app/static/styles/*.scss')
161 | .pipe(wiredep({
162 | ignorePath: /^(\.\.\/)+/
163 | }))
164 | .pipe(gulp.dest('app/static/styles'));
165 |
166 | gulp.src('app/templates/*.html')
167 | .pipe(wiredep({
168 | exclude: ['bootstrap-sass'],
169 | ignorePath: /^(\.\.\/)*\.\./
170 | }))
171 | .pipe(gulp.dest('app/templates'));
172 | });
173 |
174 | gulp.task('build', ['html', 'images', 'fonts', 'extras', 'templates', 'libs-py'], () => {
175 | return gulp.src('dist/**/*').pipe($.size({title: 'build', gzip: true}));
176 | })
177 | ;
178 |
179 | gulp.task('default', ['clean'], () => {
180 | gulp.start('build');
181 | })
182 | ;
183 |
184 | gulp.task('serve:gae', function () {
185 | // on Windows run dev_appserver.py manually, see README.md
186 | if(process.platform !== "win32") {
187 | gulp.src('dist/app.yaml')
188 | .pipe($.plumber())
189 | .pipe(gae('dev_appserver.py', [], {
190 | port: 8081,
191 | host: '0.0.0.0',
192 | admin_port: 8001,
193 | admin_host: '0.0.0.0'
194 | }));
195 | }
196 | gulp.watch('app/templates/*.html', ['templates']);
197 | gulp.watch('app/static/*.html', ['html']);
198 | gulp.watch('app/static/styles/**/*.scss', ['styles-scss']);
199 | gulp.watch('app/static/styles/**/*.css', ['styles']);
200 | gulp.watch('app/static/scripts/**/*.js', ['scripts', 'libs']);
201 | gulp.watch('app/static/fonts/**/*', ['fonts']);
202 | gulp.watch('bower.json', ['wiredep', 'fonts']);
203 | gulp.watch('app/*.{py,yaml}', ['extras']);
204 | });
205 |
206 | gulp.task('libs-py', () => {
207 | return gulp.src(['libs/**/*']).pipe(gulp.dest('dist'));
208 | });
209 |
210 | gulp.task('gae-deploy', function () {
211 | gulp.src('app/app.yaml')
212 | .pipe(gae('appcfg.py"', ['update'], {
213 | version: 'dev',
214 | oauth2: undefined // for value-less parameters
215 | }));
216 | });
217 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/app/static/scripts/shore-chart.js:
--------------------------------------------------------------------------------
1 | function createShoreChart(feature, futureFeature) {
2 | console.log('feature shore', feature, futureFeature)
3 | var elementId = 'chart-container';
4 |
5 | // data from global shore json file (single location)
6 | var properties = feature.properties;
7 | var intercept = properties.intercept;
8 | var dt = properties.dt;
9 | var dist = properties.distances;
10 |
11 | var data = [];
12 | $.each(dist, function (index, value) {
13 | var item = {};
14 | item.value = dist[index] - intercept;
15 | item.dt = dt[index];
16 | item.date = new Date(1984 + dt[index], 0, 1);
17 | if (dist[index] === -999) {
18 | return;
19 | }
20 | data.push(item);
21 | });
22 |
23 | // D3js time series chart
24 | var margin = {top: 20, right: 20, bottom: 40, left: 60},
25 | width = 600 - margin.left - margin.right,
26 | height = 400 - margin.top - margin.bottom;
27 |
28 | // Set the ranges
29 | var x = d3.time.scale().range([0, width]);
30 | var y = d3.scale.linear().range([height, 0]);
31 |
32 | // Define the axes
33 | var xAxis = d3.svg.axis().scale(x)
34 | .orient("bottom").ticks(10);
35 |
36 | var yAxis = d3.svg.axis().scale(y)
37 | .orient("left").ticks(5);
38 |
39 | // Define the regression
40 | var abLine = d3.svg.line()
41 | .x(function (d) {
42 | return x(d.date);
43 | })
44 | .y(function (d) {
45 | return y(d.dt * feature.properties.change_rate);
46 | });
47 |
48 | // clear content
49 | d3.select('#' + elementId)
50 | .selectAll('*')
51 | .remove();
52 |
53 | // Adds the svg canvas
54 | var svg = d3.select("#" + elementId)
55 | .append("svg")
56 | .classed({'query-chart': true})
57 | .attr("width", width + margin.left + margin.right)
58 | .attr("height", height + margin.top + margin.bottom)
59 | .append("g")
60 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
61 |
62 | var focus = svg.append("g")
63 | .style("display", "none");
64 |
65 | // Scale the range of the data
66 | var xExtent = d3.extent(data, function (d) {
67 | return d.date;
68 | });
69 |
70 | if (futureFeature) {
71 | // extent to future
72 | xExtent[1] = new Date(2100, 1, 1)
73 |
74 | }
75 |
76 | var yExtent = d3.extent(data, function (d) {
77 | return d.value;
78 | });
79 |
80 | if (futureFeature) {
81 | // update y Extent
82 | let properties = futureFeature.properties
83 | let allProps = Object.keys(properties).filter(function (key) {
84 | return key.includes('sl')
85 | })
86 | let allValues = allProps.map(function (key) {
87 | return properties[key]
88 | })
89 | allValues.push(0)
90 |
91 | let ymin = Math.min(...allValues, yExtent[0])
92 | let ymax = Math.max(...allValues, yExtent[1])
93 | // update yExtent
94 | yExtent = [ymin, ymax]
95 | }
96 |
97 | x.domain(xExtent);
98 | y.domain(yExtent);
99 |
100 |
101 | if (futureFeature) {
102 | extendWithFuture(svg, feature, futureFeature, x, y)
103 | }
104 |
105 |
106 | var lineGroup = svg.append("g");
107 | lineGroup
108 | .attr("class", "abline")
109 | .append("path")
110 | .attr('d', abLine(data));
111 |
112 | if (futureFeature) {
113 | var dataFuture = [
114 | {date: new Date(2020, 1, 1), dt: 2020 - 1984},
115 | {date: new Date(2100, 1, 1), dt: 2100 - 1984}
116 | ];
117 | var futureLineGroup = svg.append('g')
118 | futureLineGroup
119 | .attr("class", "abline future")
120 | .append("path")
121 | .attr('d', abLine(dataFuture));
122 | }
123 |
124 | // Add the dotsh.
125 | svg.append("g")
126 | .attr("class", "dots")
127 | .selectAll("path")
128 | .data(data)
129 | .enter()
130 | .append("path")
131 | .attr("transform", function (d) {
132 | return "translate(" + x(d.date) + "," + y(d.value) + ")";
133 | })
134 | .attr("d", d3.svg.symbol()
135 | .size(40));
136 |
137 |
138 | // Add the X Axis
139 | svg.append("g")
140 | .attr("class", "x axis")
141 | .attr("transform", "translate(0," + height + ")")
142 | .call(xAxis)
143 | .append("text")
144 | .attr("x", width)
145 | .attr("y", -3)
146 | .attr("dy", "-.35em")
147 | .style("font-weight", "bold")
148 | .style("text-anchor", "middle")
149 | .text("time");
150 |
151 | // Add the Y Axis
152 | svg.append("g")
153 | .attr("class", "y axis")
154 | .call(yAxis)
155 | .append("text")
156 | .attr("x", 6)
157 | .attr("dy", ".35em")
158 | .style("font-weight", "bold")
159 | .text("shoreline position [m]")
160 |
161 | // append the x line
162 | focus.append("line")
163 | .attr("class", "x")
164 | .style("stroke", "blue")
165 | .style("stroke-dasharray", "3,3")
166 | .style("opacity", 0.5)
167 | .attr("y1", 0)
168 | .attr("y2", height);
169 |
170 | // append the y line
171 | focus.append("line")
172 | .attr("class", "y")
173 | .style("stroke", "blue")
174 | .style("stroke-dasharray", "3,3")
175 | .style("opacity", 0.5)
176 | .attr("x1", width)
177 | .attr("x2", width);
178 | }
179 |
180 | function extendWithFuture(svg, feature, futureFeature, xScale, yScale) {
181 | var properties = futureFeature.properties
182 | var area = d3.svg.area()
183 | .interpolate("linear")
184 | .x(function (d) {
185 | return xScale(d.x)
186 | })
187 | .y0(function (d) {
188 | return yScale(d.y0)
189 | })
190 | .y1(function (d) {
191 | return yScale(d.y1)
192 | });
193 |
194 | var line = d3.svg.line()
195 | .interpolate("linear")
196 | .x(function (d) {
197 | return xScale(d.x)
198 | })
199 | .y(function (d) {
200 | return yScale(d.y)
201 | });
202 |
203 | var changeRate = feature.properties.change_rate
204 |
205 | function shoreline(year) {
206 | return (year - 1984) * changeRate
207 | }
208 |
209 | var shoreline2020 = shoreline(2020)
210 | var sl45perc50 = [
211 | {x: new Date(2020, 1, 1), y: shoreline2020},
212 | {x: new Date(2050, 1, 1), y: properties['50sl452050'] + shoreline(2050)},
213 | {x: new Date(2100, 1, 1), y: properties['50sl452100'] + shoreline(2100)}
214 | ]
215 | var sl85perc50 = [
216 | {x: new Date(2020, 1, 1), y: shoreline2020},
217 | {x: new Date(2050, 1, 1), y: properties['50sl852050'] + shoreline(2050)},
218 | {x: new Date(2100, 1, 1), y: properties['50sl852100'] + shoreline(2100)}
219 | ]
220 | var sl45perc90 = [
221 | {x: new Date(2020, 1, 1), y0: shoreline2020, y1: shoreline2020},
222 | {
223 | x: new Date(2050, 1, 1),
224 | y0: properties['5sl452050'] + shoreline(2050),
225 | y1: properties['95sl452050'] + shoreline(2050)
226 | },
227 | {
228 | x: new Date(2100, 1, 1),
229 | y0: properties['5sl452100'] + shoreline(2100),
230 | y1: properties['95sl452100'] + shoreline(2100)
231 | },
232 | ]
233 | var sl85perc90 = [
234 | {x: new Date(2020, 1, 1), y0: shoreline2020, y1: shoreline2020},
235 | {
236 | x: new Date(2050, 1, 1),
237 | y0: properties['5sl852050'] + shoreline(2050),
238 | y1: properties['95sl852050'] + shoreline(2050)
239 | },
240 | {
241 | x: new Date(2100, 1, 1),
242 | y0: properties['5sl852100'] + shoreline(2100),
243 | y1: properties['95sl852100'] + shoreline(2100)
244 | },
245 | ]
246 |
247 | var ys = [
248 | ...yScale.domain(),
249 |
250 | sl45perc50[0].y,
251 | sl45perc50[1].y,
252 | sl45perc50[2].y,
253 |
254 | sl85perc50[0].y,
255 | sl85perc50[1].y,
256 | sl85perc50[2].y,
257 |
258 | sl45perc90[0].y0,
259 | sl45perc90[1].y0,
260 | sl45perc90[2].y0,
261 |
262 | sl45perc90[0].y1,
263 | sl45perc90[1].y1,
264 | sl45perc90[2].y1,
265 |
266 | sl85perc90[0].y0,
267 | sl85perc90[1].y0,
268 | sl85perc90[2].y0,
269 |
270 | sl85perc90[0].y1,
271 | sl85perc90[1].y1,
272 | sl85perc90[2].y1
273 | ];
274 |
275 | yScale.domain(d3.extent(ys));
276 |
277 | svg.append("g")
278 | .attr("class", "ci sl45")
279 | .append('path')
280 | .datum(sl45perc90)
281 | .attr('class', 'area')
282 | .attr('d', area);
283 | svg.append("g")
284 | .attr("class", "ci sl85")
285 | .append('path')
286 | .datum(sl85perc90)
287 | .attr('class', 'area')
288 | .attr('d', area);
289 | svg.append("g")
290 | .attr("class", "ci sl45")
291 | .append('path')
292 | .datum(sl45perc50)
293 | .attr('class', 'line')
294 | .attr('d', line);
295 | svg.append("g")
296 | .attr("class", "ci sl85")
297 | .append('path')
298 | .datum(sl85perc50)
299 | .attr('class', 'line')
300 | .attr('d', line);
301 |
302 | // append the vertical line on 2020
303 | // var allProps = Object.keys(properties).filter(function (key) {
304 | // return key.includes('lt')
305 | // });
306 | //
307 | // var allValues = allProps.map(function (key) {
308 | // return properties[key]
309 | // });
310 | // allValues.push(0)
311 | //
312 | // var ymin = Math.min(...allValues);
313 | // var ymax = Math.max(...allValues);
314 | // // y.domain([ymin, ymax]);
315 |
316 | var tStart = new Date(2020, 1, 1);
317 |
318 | var yRange = yScale.domain();
319 | // var yMargin = 0.05 * (yRange[1] - yRange[0])
320 |
321 | svg.append("line")
322 | .attr("x1", xScale(tStart))
323 | .attr("y1", yScale(yRange[0]) - 20) // bottom
324 | .attr("x2", xScale(tStart))
325 | .attr("y2", yScale(yRange[1]) + 20) // top
326 | .style("stroke-width", 2)
327 | .style("stroke", "red")
328 | .style("fill", "none");
329 | }
330 |
--------------------------------------------------------------------------------
/app/static/libs/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
428 |
--------------------------------------------------------------------------------
/app/static/styles/main.scss:
--------------------------------------------------------------------------------
1 | $icon-font-path: '../fonts/';
2 |
3 | $white: #fff;
4 | $gray-light: #ccc;
5 | $black: #000;
6 | $button-shadow: rgba($black, .3) 0 1px 4px -1px;
7 | // bower:scss
8 | // endbower
9 |
10 |
11 | #map .gm-style {
12 | // OverlayMapPanes for these layers
13 | div:first-child > div:first-child {
14 | // change layers
15 | .change,
16 | .change-heatmap,
17 | .change-upscaled-300m,
18 | .dynamic-change,
19 | .dynamic-change-refined {
20 | img {
21 | // a bit nicer colors
22 | // TODO: replace once corrected in the source
23 | filter: hue-rotate(15deg) saturate(80%) !important;
24 | }
25 | }
26 | }
27 | }
28 |
29 | #map.styled .gm-style {
30 | // our custom layers
31 | div:first-child > div:first-child {
32 |
33 | // after layers
34 | .after-percentile,
35 | .after-percentile-sharpened {
36 | img {
37 | // a bit newer
38 | filter: sepia(50%) !important;
39 | }
40 | }
41 | // before layers
42 | .before-percentile,
43 | .before-percentile-sharpened {
44 | img {
45 | // a bit older
46 | filter: sepia(50%) !important;
47 | }
48 | }
49 | // all other layers
50 | div {
51 | img {
52 | // used as background, not too much color
53 | filter: saturate(20%);
54 | }
55 | }
56 | }
57 | }
58 |
59 | #map .gm-style {
60 | .gm-aqua-control {
61 | align-items: center;
62 | background-color: $white;
63 | border-radius: 2px;
64 | box-shadow: $button-shadow;
65 | display: flex;
66 | height: 28px;
67 | justify-content: center;
68 | margin-right: 10px;
69 | margin-top: 10px;
70 | // width: 28px;
71 |
72 | &:hover {
73 | border-radius: 2px;
74 | }
75 |
76 | .ui.icon.button {
77 | border-radius: 2px;
78 | box-shadow: none;
79 | height: 100%;
80 | margin-right: 0;
81 | padding: 0;
82 | width: 100%;
83 | &:hover {
84 | border-radius: 2px;
85 | }
86 | }
87 | i.icon {
88 | // reset
89 | height: initial;
90 | //margin: initial;
91 | }
92 |
93 | }
94 | }
95 |
96 |
97 |
98 | .browserupgrade {
99 | background: $gray-light;
100 | color: $white;
101 | margin: .2em 0;
102 | padding: .2em 0;
103 | }
104 |
105 | /* Custom page header */
106 | .header {
107 |
108 | /* Make the masthead heading the same height as the navigation */
109 | h3 {
110 | margin-top: 0;
111 | margin-bottom: 0;
112 | line-height: 40px;
113 | padding-bottom: 19px;
114 | }
115 | }
116 |
117 | /* Custom page footer */
118 | .footer {
119 | padding-top: 19px;
120 | color: #777;
121 | border-top: 1px solid #e5e5e5;
122 | }
123 |
124 | .container-narrow > hr {
125 | margin: 30px 0;
126 | }
127 |
128 | .controls {
129 | margin-top: 10px;
130 | border: 0px solid transparent;
131 | box-sizing: border-box;
132 | -moz-box-sizing: border-box;
133 | height: 32px;
134 | outline: none;
135 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
136 | }
137 |
138 | #pac-input {
139 | font-size: 15px;
140 | font-weight: 300;
141 | margin-left: 12px;
142 | text-overflow: ellipsis;
143 | padding: 0 11px 0 13px;
144 | width: 320px;
145 | background: rgba(250, 250, 255, 0.78);
146 | &::placeholder {
147 | color: rgba(10, 10, 10, 0.8);
148 | }
149 | &:focus {
150 | border-color: #4d90fe;
151 | }
152 | }
153 |
154 | #share-button {
155 | display: none;
156 | position: absolute;
157 | padding: 0;
158 | bottom: 205px;
159 | right: 10px;
160 | width: 28px;
161 | height: 28px;
162 | border-radius: 2px;
163 | margin: 0;
164 | padding: 5px;
165 | background: ghostwhite;
166 |
167 | .icon {
168 | margin: 0;
169 | }
170 | }
171 |
172 | #info-button {
173 | display: none;
174 | position: absolute;
175 | bottom: 185px;
176 | right: 10px;
177 | width: 28px;
178 | height: 28px;
179 | border-radius: 2px;
180 | margin: 0;
181 | padding: 5px;
182 | background: ghostwhite;
183 |
184 | .icon {
185 | margin: 0;
186 | }
187 | }
188 |
189 | #datasets-button {
190 | display: none;
191 | position: absolute;
192 | padding: 0;
193 | bottom: 165px;
194 | right: 10px;
195 | //width: 120px;
196 | height: 28px;
197 | border-radius: 2px;
198 | margin: 0;
199 | padding: 5px;
200 | background: ghostwhite;
201 |
202 | .icon {
203 | margin-left: 1px;
204 | }
205 | }
206 |
207 |
208 | #twitter-timeline-box {
209 | display: none;
210 | position: absolute;
211 | top: 50px;
212 | width: 320px;
213 | left: 12px;
214 | background: rgba(250, 250, 255, 0.78);
215 | padding: 0;
216 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
217 | border-radius: 2px;
218 |
219 | .info-close-button {
220 | background: transparent;
221 | position: absolute;
222 | right: 0px;
223 | }
224 | }
225 |
226 | #info-box {
227 | display: none;
228 | position: absolute;
229 | top: 50px;
230 | width: 320px;
231 | left: 12px;
232 | background: rgba(250, 250, 255, 0.78);
233 | /* don't grow too big */
234 | overflow-y: scroll;
235 | max-height: 80vh;
236 | padding: 0;
237 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
238 | border-radius: 2px;
239 |
240 | .info-text {
241 | padding: 7px;
242 |
243 | h2 {
244 | font-size: 1.35rem;
245 | }
246 |
247 | p {
248 | text-align: justify;
249 | }
250 | }
251 |
252 | .info-buttons-content {
253 | padding: 7px;
254 | display: inherit;
255 |
256 | div {
257 | display: flex;
258 | padding: 0;
259 |
260 | .button {
261 | display: flex;
262 | height: 20px;
263 | margin-right: 3px;
264 | margin-left: 0;
265 | padding: 3px;
266 | border-radius: 2px;
267 | }
268 |
269 | .icon {
270 | margin-left: 0;
271 | margin-right: 0;
272 | }
273 |
274 | #info-close-button {
275 | background: transparent;
276 | position: absolute;
277 | right: 0px;
278 | }
279 | }
280 | }
281 | }
282 |
283 | /* Responsive: Portrait tablets and up */
284 | @media screen and (min-width: 768px) {
285 | .container {
286 | max-width: 730px;
287 | }
288 |
289 | /* Remove the padding we set earlier */
290 | .header,
291 | .marketing,
292 | .footer {
293 | padding-left: 0;
294 | padding-right: 0;
295 | }
296 |
297 | /* Space out the masthead */
298 | .header {
299 | margin-bottom: 30px;
300 | }
301 |
302 | /* Remove the bottom border on the jumbotron for visual effect */
303 | .jumbotron {
304 | border-bottom: 0;
305 | }
306 | }
307 |
308 | #map {
309 | width: 100%;
310 | height: 100%;
311 | }
312 |
313 | .navbar {
314 | margin-bottom: 0;
315 | }
316 |
317 | .no-padding {
318 | padding: 0;
319 | }
320 |
321 | .no-margin {
322 | margin: 0;
323 | }
324 |
325 | #zoom-warning {
326 | position: absolute;
327 | left: 50%;
328 | top: 15vh;
329 | padding: 20px;
330 | width: 660px;
331 | margin-left: -330px;
332 | display: none;
333 | }
334 |
335 | #map-buttons {
336 | position: absolute;
337 | left: 10px;
338 | top: 100px;
339 | display: none;
340 | }
341 |
342 | /*
343 | #button-query {
344 | background-color: white;
345 | }
346 |
347 | #button-download {
348 | background-color: white;
349 | }
350 | */
351 |
352 | #message-query {
353 | display: none;
354 | position: absolute;
355 | left: 50%;
356 | margin-left: -225px;
357 | width: 450px;
358 | bottom: 250px;
359 | padding: 20px;
360 | }
361 |
362 | #message-download {
363 | display: none;
364 | position: absolute;
365 | left: 50%;
366 | margin-left: -225px;
367 | width: 450px;
368 | bottom: 150px;
369 | padding: 20px;
370 | }
371 |
372 | #message-initializing-ee {
373 | position: absolute;
374 | left: 50%;
375 | bottom: 50%;
376 | width: 350px;
377 | margin-left: -175px;
378 | display: none;
379 | }
380 |
381 | #message-initialize-ee-icon {
382 | float: left;
383 | }
384 |
385 | #message-download-icon {
386 | float: left;
387 | }
388 |
389 | #message-download-content {
390 | width: 340px;
391 | float: right;
392 | }
393 |
394 | #sidebar {
395 | display: none;
396 | }
397 |
398 | #button-menu-sidebar {
399 | position: absolute;
400 | left: 10px;
401 | top: 60px;
402 | background-color: white;
403 | display: none;
404 | }
405 |
406 | #deltares-logo {
407 | position: absolute;
408 | left: 50%;
409 | margin-left: -35px;
410 | bottom: 7px;
411 | width: 70px;
412 | height: 17px;
413 | overflow: visible;
414 | float: none;
415 | display: inline;
416 | }
417 |
418 | #deltares-logo-image-div {
419 | width: 87px;
420 | height: 21px;
421 | cursor: pointer;
422 | }
423 |
424 | #deltares-logo-image {
425 | -webkit-user-select: none;
426 | border: 0;
427 | padding: 0;
428 | margin: 0;
429 | -webkit-filter: drop-shadow(3px 3px 3px rgba(0, 0, 0, 0.5));
430 | }
431 |
432 | .slider.slider-vertical {
433 | height: 100%;
434 | }
435 |
436 | .slider.slider-horizontal {
437 | width: 100%;
438 | }
439 |
440 | #layers-toggle-table {
441 | left: 50%;
442 | position: absolute;
443 | bottom: 45px;
444 | background-color: rgba(255, 255, 255, 0.8);
445 | margin-left: -60px;
446 | display: none;
447 | }
448 |
449 | #layers-toggle-table .toggle {
450 | float: bottom;
451 | height: 80%;
452 | margin-left: 10px;
453 | }
454 |
455 | #layers-toggle-table label {
456 | font-size: 12px;
457 | }
458 |
459 | #layers-table {
460 | right: 70px;
461 | position: absolute;
462 | bottom: 25px;
463 | display: none;
464 | }
465 |
466 | #layers-table .slider {
467 | width: 177px;
468 | margin-bottom: 6px;
469 | margin-top: 6px;
470 | margin-left: 15px;
471 | }
472 |
473 | #label-year-before {
474 | opacity: 0;
475 | left: 15px;
476 | position: absolute;
477 | bottom: 38%;
478 | color: $white;
479 | font-size: 30px;
480 | text-shadow: 1px 1px 0 black, -1px -1px 0px black, 1px -1px 0px black, -1px 1px 0px black;
481 | display: none;
482 | }
483 |
484 | #label-year-after {
485 | opacity: 0;
486 | right: 15px;
487 | position: absolute;
488 | bottom: 38%;
489 | color: $white;
490 | font-size: 30px;
491 | text-shadow: 1px 1px 0 black, -1px -1px 0px black, 1px -1px 0px black, -1px 1px 0px black;
492 | display: none;
493 | }
494 |
495 | .slider-label {
496 | text-align: right;
497 | }
498 |
499 | .slider .slider-selection {
500 | background: #BABABA;
501 | }
502 |
503 | .slider-label {
504 | color: $white;
505 | font-size: 11px;
506 | text-shadow: 1px 1px 0px black, -1px -1px 0px black, 1px -1px 0px black, -1px 1px 0px black;
507 | }
508 |
509 |
510 | #chart-modal.ui.modal {
511 | top: 20%;
512 | display: flex;
513 | }
514 |
--------------------------------------------------------------------------------
/app/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """A simple example of connecting to Earth Engine using App Engine."""
3 |
4 |
5 | import os
6 | import datetime
7 | from datetime import timedelta
8 | import logging
9 |
10 | import json
11 |
12 | import oauth2client
13 | import oauth2client.client
14 |
15 | import ee
16 |
17 | import config
18 | import config_web
19 |
20 | import jinja2
21 |
22 | from flask import Flask, jsonify, redirect, request
23 | import flask_cors
24 | import error_handler
25 | from datetime import date
26 |
27 | app = Flask(__name__)
28 | app.register_blueprint(error_handler.error_handler)
29 |
30 | logging.basicConfig()
31 | logger = logging.getLogger(__name__)
32 |
33 | REDUCTION_SCALE_METERS = 30
34 |
35 | # Initialize the EE API.
36 |
37 | # Use our App Engine service account's credentials.
38 | EE_CREDENTIALS = ee.ServiceAccountCredentials(config.EE_ACCOUNT, config.EE_PRIVATE_KEY_FILE)
39 |
40 | ee.Initialize(EE_CREDENTIALS)
41 |
42 | jinja_environment = jinja2.Environment(
43 | loader=jinja2.FileSystemLoader(
44 | os.path.join(os.path.dirname(__file__), 'templates')
45 | )
46 | )
47 |
48 | @app.route('/')
49 | @flask_cors.cross_origin()
50 | def root():
51 | """
52 | Main page handler
53 | :return:
54 | """
55 |
56 | """Request an image from Earth Engine and render it to a web page."""
57 |
58 | # parse request parameters, move to function
59 | view = request.args.get('view', '')
60 | view_json = '{}'
61 |
62 | if view:
63 | try:
64 | # assume we can split in three items
65 | lat, lng, zoom = view.split(',')
66 | # these should be floats
67 | lat = float(lat)
68 | lng = float(lng)
69 | # strip off the z
70 | zoom = int(zoom.rstrip('z'))
71 | # if all succeeds overwrite the string output
72 | view_json = json.dumps({
73 | "lat": lat,
74 | "lng": lng,
75 | "zoom": zoom
76 | })
77 | except ValueError:
78 | # just log, no worries
79 | logger.exception('invalid view input', view)
80 |
81 | expire_time = datetime.datetime.now() + timedelta(
82 | seconds=config_web.EE_TOKEN_EXPIRE_IN_SEC
83 | )
84 |
85 | credentials = oauth2client.client.OAuth2Credentials(
86 | None,
87 | config_web.EE_CLIENT_ID,
88 | config_web.EE_CLIENT_SECRET,
89 | config_web.EE_REFRESH_TOKEN,
90 | expire_time,
91 | 'https://accounts.google.com/o/oauth2/token',
92 | None
93 | )
94 | config_web.EE_ACCESS_TOKEN = credentials.get_access_token().access_token
95 |
96 | template_values = {
97 | 'client_id': config_web.EE_CLIENT_ID,
98 | 'token_type': config_web.EE_TOKEN_TYPE,
99 | 'access_token': config_web.EE_ACCESS_TOKEN,
100 | 'token_expires_in_sec': config_web.EE_TOKEN_EXPIRE_IN_SEC,
101 | 'token_expire_time': expire_time.strftime("%A, %d. %B %Y %I:%M%p:%S"),
102 | 'view': view_json
103 | }
104 |
105 | datasets_string = request.args.get('datasets', '')
106 | if datasets_string:
107 | template_values['datasets'] = datasets_string.split(',')
108 | else:
109 | template_values['datasets'] = ['surface-water']
110 |
111 | percentile = request.args.get('percentile', '')
112 | if percentile:
113 | template_values['percentile'] = int(percentile)
114 | else:
115 | template_values['percentile'] = 20
116 |
117 | debug = request.args.get('debug', '')
118 | if debug == 'true' or debug == '1':
119 | template_values['debug'] = 'true'
120 | else:
121 | template_values['debug'] = 'false'
122 |
123 | mode = request.args.get('mode', '')
124 | if mode == 'dynamic':
125 | template_values['mode'] = 'dynamic'
126 | else:
127 | template_values['mode'] = 'static'
128 |
129 | refine = request.args.get('refine', '')
130 | if refine == 'false' or refine == '0':
131 | template_values['refine'] = 'false'
132 | else:
133 | template_values['refine'] = 'true'
134 |
135 | smoothen = request.args.get('smoothen', '')
136 | if smoothen == 'false' or smoothen == '0':
137 | template_values['smoothen'] = 'false'
138 | else:
139 | template_values['smoothen'] = 'true'
140 |
141 | # index of the current site
142 | site = request.args.get('site', '')
143 | if site:
144 | template_values['site'] = int(site)
145 | else:
146 | template_values['site'] = '-1'
147 |
148 | # slope thresholds
149 | slope_threshold = request.args.get('slope_threshold', '')
150 | if slope_threshold:
151 | template_values['slope_threshold'] = float(slope_threshold)
152 | else:
153 | template_values['slope_threshold'] = '0.03'
154 |
155 | slope_threshold_sensitive = request.args.get('slope_threshold_sensitive', '')
156 | if slope_threshold_sensitive:
157 | template_values['slope_threshold_sensitive'] = float(slope_threshold_sensitive)
158 | else:
159 | template_values['slope_threshold_sensitive'] = '0.02'
160 |
161 | averaging_months1 = request.args.get('averaging_months1', '')
162 | if averaging_months1:
163 | template_values['averaging_months1'] = float(averaging_months1)
164 | else:
165 | template_values['averaging_months1'] = 12
166 |
167 | averaging_months2 = request.args.get('averaging_months2', '')
168 | if averaging_months2:
169 | template_values['averaging_months2'] = float(averaging_months2)
170 | else:
171 | template_values['averaging_months2'] = 12
172 |
173 | all_years = request.args.get('all_years', '')
174 | if all_years == 'true' or all_years == 1:
175 | template_values['all_years'] = 'true'
176 | else:
177 | template_values['all_years'] = 'false'
178 |
179 | all_years_step = request.args.get('all_years_step', '')
180 | if all_years_step:
181 | template_values['all_years_step'] = int(all_years_step)
182 | else:
183 | template_values['all_years_step'] = 1
184 |
185 | min_year_selection = request.args.get('from', '')
186 | if min_year_selection:
187 | template_values['min_year_selection'] = min_year_selection
188 | else:
189 | template_values['min_year_selection'] = 2000
190 |
191 | max_year_selection = request.args.get('to', '')
192 | if max_year_selection:
193 | template_values['max_year_selection'] = max_year_selection
194 | else:
195 | template_values['max_year_selection'] = date.today().year - 1
196 |
197 | min_doy = request.args.get('min_doy', '')
198 | if min_doy:
199 | template_values['min_doy'] = min_doy
200 | else:
201 | template_values['min_doy'] = 0
202 |
203 | max_doy = request.args.get('max_doy', '')
204 | if min_doy:
205 | template_values['max_doy'] = max_doy
206 | else:
207 | template_values['max_doy'] = 365
208 |
209 | min_year = request.args.get('min_year', '')
210 | if min_year:
211 | template_values['min_year'] = min_year
212 | else:
213 | template_values['min_year'] = 1985
214 |
215 | max_year = request.args.get('max_year', '')
216 | if max_year:
217 | template_values['max_year'] = max_year
218 | else:
219 | template_values['max_year'] = date.today().year
220 |
221 | filter_count = request.args.get('filter_count', '')
222 | if filter_count:
223 | template_values['filter_count'] = filter_count
224 | else:
225 | template_values['filter_count'] = 0
226 |
227 | ndvi_filter = request.args.get('ndvi_filter', '')
228 | if ndvi_filter:
229 | template_values['ndvi_filter'] = ndvi_filter
230 | else:
231 | template_values['ndvi_filter'] = -99
232 |
233 | water_slope_opacity = request.args.get('water_slope_opacity', '')
234 | if water_slope_opacity:
235 | template_values['water_slope_opacity'] = water_slope_opacity
236 | else:
237 | template_values['water_slope_opacity'] = 0.4
238 |
239 | mask_water = request.args.get('mask_water', '')
240 | if mask_water == 'true' or mask_water == 1:
241 | template_values['mask_water'] = 'true'
242 | else:
243 | template_values['mask_water'] = 'false'
244 |
245 | template = jinja_environment.get_template('index.html')
246 | logger.info("template: %s", template)
247 |
248 | return template.render(template_values)
249 |
250 |
251 | #
252 | # class MainPageHandler(webapp2.RequestHandler):
253 | # def get(self, name='index.html'): # pylint: disable=g-bad-name
254 | # """Request an image from Earth Engine and render it to a web page."""
255 | #
256 | # if not name:
257 | # name = 'index.html'
258 | #
259 | # # parse request parameters, move to function
260 | # view = self.request.params.get('view', '')
261 | # view_json = '{}'
262 | #
263 | # if view:
264 | # try:
265 | # # assume we can split in three items
266 | # lat, lng, zoom = view.split(',')
267 | # # these should be floats
268 | # lat = float(lat)
269 | # lng = float(lng)
270 | # # strip off the z
271 | # zoom = int(zoom.rstrip('z'))
272 | # # if all succeeds overwrite the string output
273 | # view_json = json.dumps({
274 | # "lat": lat,
275 | # "lng": lng,
276 | # "zoom": zoom
277 | # })
278 | # except ValueError:
279 | # # just log, no worries
280 | # logger.exception('invalid view input', view)
281 | #
282 | # expire_time = datetime.datetime.now() + timedelta(
283 | # seconds=config_web.EE_TOKEN_EXPIRE_IN_SEC
284 | # )
285 | #
286 | # credentials = oauth2client.client.OAuth2Credentials(
287 | # None,
288 | # config_web.EE_CLIENT_ID,
289 | # config_web.EE_CLIENT_SECRET,
290 | # config_web.EE_REFRESH_TOKEN,
291 | # expire_time,
292 | # 'https://accounts.google.com/o/oauth2/token',
293 | # None
294 | # )
295 | # config_web.EE_ACCESS_TOKEN = credentials.get_access_token().access_token
296 | #
297 | # template_values = {
298 | # 'client_id': config_web.EE_CLIENT_ID,
299 | # 'token_type': config_web.EE_TOKEN_TYPE,
300 | # 'access_token': config_web.EE_ACCESS_TOKEN,
301 | # 'token_expires_in_sec': config_web.EE_TOKEN_EXPIRE_IN_SEC,
302 | # 'token_expire_time': expire_time.strftime("%A, %d. %B %Y %I:%M%p:%S"),
303 | # 'view': view_json
304 | # }
305 | #
306 | # datasets_string = self.request.params.get('datasets', '')
307 | # if datasets_string:
308 | # template_values['datasets'] = [x.encode() for x in datasets_string.split(',')]
309 | # else:
310 | # template_values['datasets'] = ['surface-water']
311 | #
312 | # percentile = self.request.params.get('percentile', '')
313 | # if percentile:
314 | # template_values['percentile'] = int(percentile)
315 | # else:
316 | # template_values['percentile'] = 20
317 | #
318 | # debug = self.request.params.get('debug', '')
319 | # if debug == 'true' or debug == '1':
320 | # template_values['debug'] = 'true'
321 | # else:
322 | # template_values['debug'] = 'false'
323 | #
324 | # mode = self.request.params.get('mode', '')
325 | # if mode == 'dynamic':
326 | # template_values['mode'] = 'dynamic'
327 | # else:
328 | # template_values['mode'] = 'static'
329 | #
330 | # refine = self.request.params.get('refine', '')
331 | # if refine == 'false' or refine == '0':
332 | # template_values['refine'] = 'false'
333 | # else:
334 | # template_values['refine'] = 'true'
335 | #
336 | # smoothen = self.request.params.get('smoothen', '')
337 | # if smoothen == 'false' or smoothen == '0':
338 | # template_values['smoothen'] = 'false'
339 | # else:
340 | # template_values['smoothen'] = 'true'
341 | #
342 | # # index of the current site
343 | # site = self.request.params.get('site', '')
344 | # if site:
345 | # template_values['site'] = int(site)
346 | # else:
347 | # template_values['site'] = '-1'
348 | #
349 | # # slope thresholds
350 | # slope_threshold = self.request.params.get('slope_threshold', '')
351 | # if slope_threshold:
352 | # template_values['slope_threshold'] = float(slope_threshold)
353 | # else:
354 | # template_values['slope_threshold'] = '0.03'
355 | #
356 | # slope_threshold_sensitive = self.request.params.get('slope_threshold_sensitive', '')
357 | # if slope_threshold_sensitive:
358 | # template_values['slope_threshold_sensitive'] = float(slope_threshold_sensitive)
359 | # else:
360 | # template_values['slope_threshold_sensitive'] = '0.02'
361 | #
362 | # averaging_months1 = self.request.params.get('averaging_months1', '')
363 | # if averaging_months1:
364 | # template_values['averaging_months1'] = float(averaging_months1)
365 | # else:
366 | # template_values['averaging_months1'] = 12
367 | #
368 | # averaging_months2 = self.request.params.get('averaging_months2', '')
369 | # if averaging_months2:
370 | # template_values['averaging_months2'] = float(averaging_months2)
371 | # else:
372 | # template_values['averaging_months2'] = 12
373 | #
374 | # all_years = self.request.params.get('all_years', '')
375 | # if all_years == 'true' or all_years == 1:
376 | # template_values['all_years'] = 'true'
377 | # else:
378 | # template_values['all_years'] = 'false'
379 | #
380 | # all_years_step = self.request.params.get('all_years_step', '')
381 | # if all_years_step:
382 | # template_values['all_years_step'] = int(all_years_step)
383 | # else:
384 | # template_values['all_years_step'] = 1
385 | #
386 | # min_year_selection = self.request.params.get('from', '')
387 | # if min_year_selection:
388 | # template_values['min_year_selection'] = min_year_selection
389 | # else:
390 | # template_values['min_year_selection'] = 2000
391 | #
392 | # max_year_selection = self.request.params.get('to', '')
393 | # if max_year_selection:
394 | # template_values['max_year_selection'] = max_year_selection
395 | # else:
396 | # template_values['max_year_selection'] = 2017
397 | #
398 | # min_doy = self.request.params.get('min_doy', '')
399 | # if min_doy:
400 | # template_values['min_doy'] = min_doy
401 | # else:
402 | # template_values['min_doy'] = 0
403 | #
404 | # max_doy = self.request.params.get('max_doy', '')
405 | # if min_doy:
406 | # template_values['max_doy'] = max_doy
407 | # else:
408 | # template_values['max_doy'] = 365
409 | #
410 | # min_year = self.request.params.get('min_year', '')
411 | # if min_year:
412 | # template_values['min_year'] = min_year
413 | # else:
414 | # template_values['min_year'] = 1985
415 | #
416 | # max_year = self.request.params.get('max_year', '')
417 | # if max_year:
418 | # template_values['max_year'] = max_year
419 | # else:
420 | # template_values['max_year'] = 2017
421 | #
422 | # filter_count = self.request.params.get('filter_count', '')
423 | # if filter_count:
424 | # template_values['filter_count'] = filter_count
425 | # else:
426 | # template_values['filter_count'] = 0
427 | #
428 | # ndvi_filter = self.request.params.get('ndvi_filter', '')
429 | # if ndvi_filter:
430 | # template_values['ndvi_filter'] = ndvi_filter
431 | # else:
432 | # template_values['ndvi_filter'] = -99
433 | #
434 | # water_slope_opacity = self.request.params.get('water_slope_opacity', '')
435 | # if water_slope_opacity:
436 | # template_values['water_slope_opacity'] = water_slope_opacity
437 | # else:
438 | # template_values['water_slope_opacity'] = 0.4
439 | #
440 | # mask_water = self.request.params.get('mask_water', '')
441 | # if mask_water == 'true' or mask_water == 1:
442 | # template_values['mask_water'] = 'true'
443 | # else:
444 | # template_values['mask_water'] = 'false'
445 | #
446 | # template = jinja_environment.get_template(name)
447 | # logger.info("template: %s", template)
448 | #
449 | # self.response.out.write(template.render(template_values))
450 |
451 | # class GetImageInfoHandler(webapp2.RequestHandler):
452 | # """A servlet to handle requests for details about a set of images."""
453 | #
454 | # def get(self):
455 | # """Returns details about a polygon."""
456 | #
457 | # aoiJson = json.loads(self.request.get('aoi'))
458 | # aoi = ee.Geometry.Polygon(aoiJson['features'][0]['geometry']['coordinates'])
459 | #
460 | # content = GetImageInfoTimeSeries(aoi)
461 | # self.response.headers['Content-Type'] = 'application/json'
462 | # self.response.out.write(json.dumps(content))
463 | #
464 | #
465 | # def GetWetnessTimeSeries(aoi, begin, end):
466 | # collections = [
467 | # ['LANDSAT/LT4_L1T_TOA', ['B5', 'B2']],
468 | # ['LANDSAT/LT5_L1T_TOA', ['B5', 'B2']],
469 | # ['LANDSAT/LE7_L1T_TOA', ['B5', 'B2']],
470 | # ['LANDSAT/LC8_L1T_TOA', ['B6', 'B3']],
471 | # ]
472 | #
473 | # ic = ee.ImageCollection([])
474 | # for n in collections:
475 | # ic = ee.ImageCollection(ic.merge(ee.ImageCollection(n[0]).filterDate(begin, end).filterBounds(aoi) \
476 | # .select(n[1], ['swir', 'green'])))
477 | #
478 | # def ComputeNdwi(img):
479 | # reduction = img \
480 | # .normalizedDifference(['swir', 'green']) \
481 | # .rename(['ndwi']) \
482 | # .reduceRegion(ee.Reducer.first(), aoi, REDUCTION_SCALE_METERS)
483 | #
484 | # return ee.Feature(None, {'ndwi': reduction,
485 | # 'system:time_start': img.get('system:time_start')})
486 | #
487 | # results = ic.map(ComputeNdwi).getInfo()
488 | #
489 | # # Extract the results as a list of lists.
490 | # def ExtractProperties(feature):
491 | # return {'date': feature['properties']['system:time_start'],
492 | # 'value': feature['properties']['ndwi']['ndwi']}
493 | #
494 | # return map(ExtractProperties, results['features'])
495 | #
496 | #
497 | # def GetImageInfoTimeSeries(aoi):
498 | # def GetImageInfo(img):
499 | # return ee.Feature(None, {
500 | # 'id': img.get('system:id'),
501 | # 'time': img.get('system:time_start'),
502 | # 'cloud_cover': img.get('CLOUD_COVER')
503 | # })
504 | #
505 | # def ToFeatureCollection(imageCollectionName):
506 | # return ee.FeatureCollection(ee.ImageCollection(imageCollectionName).filterBounds(aoi).map(GetImageInfo))
507 | #
508 | # collectionNames = [
509 | # 'LANDSAT/LT4_L1T_TOA',
510 | # 'LANDSAT/LT5_L1T_TOA',
511 | # 'LANDSAT/LE7_L1T_TOA',
512 | # 'LANDSAT/LC8_L1T_TOA'
513 | # ]
514 | #
515 | # fc = ee.FeatureCollection([])
516 | # for n in collectionNames:
517 | # fc = fc.merge(ToFeatureCollection(n))
518 | #
519 | # info = fc.sort('time').getInfo()
520 | #
521 | # return [i['properties'] for i in info['features']]
522 | #
523 | # # TODO: move these to tested utility script
524 | # def get_image_list(bounds, collection='LANDSAT/LC8_L1T_TOA'):
525 | # images = ee.ImageCollection(collection).filterBounds(bounds)
526 | # features = ee.FeatureCollection(images)
527 | # image_info = features.map(lambda img: ee.Feature(None, {
528 | # 'id': img.get('system:id'),
529 | # 'time': img.get('system:time_start'),
530 | # 'cloud_cover': img.get('CLOUD_COVER')
531 | # }))
532 | # info = image_info.getInfo()
533 | # return [i['properties'] for i in info['features']]
534 | #
535 | #
536 | # class GetWetnessTimeSeriesHandler(webapp2.RequestHandler):
537 | # """Gets wetness time series for a given location."""
538 | #
539 | # def get(self):
540 | # """Returns details about a polygon."""
541 | # request = self.request
542 | #
543 | # pointJson = json.loads(self.request.get('location'))
544 | # aoi = ee.Geometry.Point(pointJson['lat'], pointJson['lng'])
545 | #
546 | # begin = self.request.get('begin')
547 | # end = self.request.get('end')
548 | #
549 | # content = GetWetnessTimeSeries(aoi, begin, end)
550 | #
551 | # self.response.headers['Content-Type'] = 'application/json'
552 | # self.response.out.write(json.dumps(content))
553 | #
554 | # self.response.headers['Content-Type'] = 'application/json'
555 | # self.response.headers['Content-Disposition'] = 'attachment; filename="data.json"'
556 | #
557 | #
558 | # class RefreshAccessToken(webapp2.RequestHandler):
559 | # def post(self):
560 | # self.redirect('/')
561 |
562 | # app = webapp2.WSGIApplication([
563 | # ('/get_aoi_image_info_time_series', GetImageInfoHandler),
564 | # ('/get_ndwi_time_series', GetWetnessTimeSeriesHandler),
565 | # # html pages in current directory
566 | # ('/(.*\.html)', MainPageHandler),
567 | # ('/', MainPageHandler)
568 | # ], debug=True)
569 |
570 |
571 | if __name__ == '__main__':
572 | app.run(host='127.0.0.1', port=8081, debug=True)
573 |
--------------------------------------------------------------------------------
/app/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Aqua Monitor - monitoring surface water changes from space.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {#
34 |
35 |
36 |
37 |
38 | #}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
91 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
147 |
148 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
177 |
178 |
181 |
182 |
183 |
188 |
189 |
190 |
191 |
196 |
197 |
198 |
199 |
200 |
201 | Warning! Zoom in to see surface water changes
202 | for selected dates.
203 |
204 |
205 |
206 |
207 |
208 |
209 |
212 |
We're fetching that content for you.
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
223 |
Google Earth Engine ...
224 |
225 |
226 |
227 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 | {#
244 |
245 |
246 |
247 |
248 | #}
249 |
254 |
255 |
256 | Datasets
257 |
294 |
295 |
296 |
297 |
298 |
299 |
312 |
313 |
314 |
315 |
316 | Future Shoreline Changes (2020-2100)
317 |
318 |
319 |
320 | The future shoreline dataset is
321 | visible if you zoom into level 12 and
322 | further (2km scale). You can click
323 | on the black circles with white
324 | stroke to show estimates of future
325 | shorelines under different emission
326 | scenario's. Below, the legend for typical historical and predicted shoreline position changes over time is shown.
327 |
328 |
329 |
330 |
331 |
332 | The procedure to produce the dataset
333 | and the findings are discussed in the
334 | following paper: Vousdoukas et. al., 2020, Nature Climate Change .
335 |
336 |
337 |
This dataset is created in collaboration with the Joint Research Centre.
338 |
339 |
340 |
341 |
342 | Methods to derive historical shoreline changes can be found
343 | in:
Luijendijk et. al., 2018, Nature Scientific Reports .
344 | For more information visit:
Deltares ShorelineMonitor. .
345 |
346 |
349 |
350 |
351 |
388 |
389 |
390 |
391 |
392 | Long-term Shoreline Changes (1984-2016)
393 |
394 |
395 |
396 | The bars represent the erosion/accretion along coasts, every 500m, over the period 1984-2016.
397 | Green bars indicate where shoreline accretion has occurred (natural accretion, land reclamation, nourishments).
398 | Red bars indicate erosive shorelines, based on a linear fit through shoreline positions.
399 | If you're zoomed in you can click on a profile to see a time series chart.
400 |
401 |
402 |
403 |
-3m/yr
404 |
405 |
406 |
407 |
408 |
409 |
3m/yr
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 | The results of the global analysis and methods can be found in:
418 | Luijendijk et al., 2018, Scientific Reports .
419 |
420 |
For inquiries please fill in this form .
421 |
422 |
This dataset is created in collaboration with the Delft University of Technology.
423 |
424 |
425 |
426 |
For the estimates of future shorelines please visit: Future Shorelines .
427 |
428 |
429 |
430 |
467 |
468 |
469 |
470 | Surface water changes (1985-2016)
471 |
472 |
473 |
474 | Green and blue colors represent areas where surface water changes occured during the last 30 years.
475 | Green pixels show where surface water has been turned into land (accretion, land reclamation, droughts).
476 | Blue pixels show where land has been changed into surface water (erosion, reservoir construction).
477 |
478 | The results of the analysis are published in: Donchyts et.al, 2016, Nature Climate Change
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
536 |
537 |
538 |
548 |
549 | 1985
550 | 2016
551 |
552 |
572 |
573 |
587 |
588 |
589 |
590 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
632 |
633 |
634 |
637 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
--------------------------------------------------------------------------------
/app/static/scripts/script.js:
--------------------------------------------------------------------------------
1 | // we need a modulus operator that works for negative numbers
2 | Number.prototype.mod = function (n) {
3 | return ((this % n) + n) % n;
4 | };
5 | Number.prototype.squared = function () {
6 | return (this * this);
7 | };
8 |
9 |
10 | function NamedImageMapType() {
11 | // Create an ImageMapType that adds the name as a class to each created div
12 | // The div contains the image
13 |
14 | // ImageMapType.apply(this, arguments) executes every line of code
15 | // in the ImageMapType body where the value of "this" is the new instance
16 | google.maps.ImageMapType.apply(this, arguments);
17 | }
18 |
19 | //Inherit
20 | NamedImageMapType.prototype = Object.create(google.maps.ImageMapType.prototype);
21 |
22 | // Overwrite getTile
23 | NamedImageMapType.prototype.getTile = function (coord, zoom, ownerDocument) {
24 | // bind the original function to this context and execute
25 | // pass along all arguments
26 | var div = google.maps.ImageMapType.prototype.getTile.call(this, coord, zoom, ownerDocument);
27 | // inject the class name if available
28 | div.classList.add(this.name);
29 | return div;
30 | };
31 |
32 |
33 | // These are all the layers that we're rendering but we can't fill them in yet, because we have to define some functions first
34 | var layers = [];
35 |
36 | function getWaterTrendChangeRatio(start, stop) {
37 | // empiricaly found ratio
38 | return (15 / (stop - start));
39 | }
40 |
41 | var waterChangeTrendRatio = getWaterTrendChangeRatio(minYearSelection, maxYearSelection);
42 |
43 | // First, checks if it isn't implemented yet.
44 | if (!String.prototype.format) {
45 | String.prototype.format = function () {
46 | var args = arguments;
47 | return this.replace(/{(\d+)}/g, function (match, number) {
48 | return typeof args[number] != 'undefined'
49 | ? args[number]
50 | : match
51 | ;
52 | });
53 | };
54 | }
55 |
56 | function renderLandsatMosaic(percentile, start, end, sharpen) {
57 | var bands = ['swir1', 'nir', 'green'];
58 |
59 | var l9 = new ee.ImageCollection('LANDSAT/LC09/C02/T1_TOA').filterDate(start, end).select(['B6', 'B5', 'B3'], bands);
60 |
61 | var l8 = new ee.ImageCollection('LANDSAT/LC08/C02/T1_RT_TOA').filterDate(start, end).select(['B6', 'B5', 'B3'], bands);
62 | var l7 = new ee.ImageCollection("LANDSAT/LE07/C02/T1_RT_TOA").filterDate(start, end).select(['B5', 'B4', 'B2'], bands);
63 |
64 | var l5 = new ee.ImageCollection("LANDSAT/LT05/C02/T1_TOA").filterDate(start, end).select(['B5', 'B4', 'B2'], bands);
65 | var l4 = new ee.ImageCollection("LANDSAT/LT04/C02/T1_TOA").filterDate(start, end).select(['B5', 'B4', 'B2'], bands);
66 |
67 | var images = ee.ImageCollection(l9.merge(l8).merge(l7).merge(l5).merge(l4));
68 |
69 | if (minDoy !== 0 && maxDoy !== 365) {
70 | images = images.filter(ee.Filter.dayOfYear(minDoy, maxDoy))
71 | }
72 |
73 | var image = images
74 | .reduce(ee.Reducer.percentile([percentile]))
75 | .rename(bands);
76 |
77 | if (filterCount > 0) {
78 | image = image.mask(images.select(0).count().gt(filterCount));
79 | }
80 |
81 |
82 | if (sharpen) {
83 | image = image.subtract(image.convolve(ee.Kernel.gaussian(30, 20, 'meters')).convolve(ee.Kernel.laplacian8(0.4)));
84 | }
85 |
86 | return image.visualize({min: 0.05, max: [0.5, 0.5, 0.6], gamma: 1.4});
87 | }
88 |
89 | function renderSurfaceWaterChanges(change) {
90 | var scale = ee.Image('users/gena/AquaMonitor/water_changes_1985_240_2013_48');
91 | var land300m = ee.Image('users/gena/AquaMonitor/water_changes_1985_240_2013_48_land_300m');
92 | var water300m = ee.Image('users/gena/AquaMonitor/water_changes_1985_240_2013_48_water_300m');
93 |
94 | // SWBD mask
95 | var swbd = ee.Image('MODIS/MOD44W/MOD44W_005_2000_02_24').select('water_mask');
96 | var swbdMask = swbd.unmask().not()
97 | // .focal_max(60000, 'circle', 'meters').reproject('EPSG:4326', null, 10000);
98 | .focal_max(1000, 'circle', 'meters').reproject('EPSG:4326', null, 1000);
99 |
100 | if (maskWater) {
101 | land300m = land300m.mask(swbdMask);
102 | water300m = water300m.mask(swbdMask);
103 | scale = scale.multiply(swbdMask);
104 | }
105 |
106 | var bg = ee.Image(1).visualize({opacity: 0.9, palette: ['000000'], forceRgbOutput: true});
107 |
108 | var maxArea = 200;
109 |
110 | var landVis = land300m.mask(land300m.divide(maxArea))
111 | .visualize({min: 0, max: maxArea, palette: ['000000', '00ff00']});
112 |
113 | var waterVis = water300m.mask(water300m.divide(maxArea))
114 | .visualize({min: 0, max: maxArea, palette: ['000000', '00d8ff']});
115 |
116 | if (change) {
117 | var changeImage = scale.visualize({
118 | min: -0.02,
119 | max: 0.02,
120 | palette: ['00ff00', '000000', '00d8ff'],
121 | forceRgbOutput: true
122 | });
123 | var images = [
124 | bg
125 | .visualize({opacity: 0.7})
126 | .rename(['r', 'g', 'b']),
127 | changeImage
128 | .rename(['r', 'g', 'b'])
129 | ];
130 | return ee.ImageCollection.fromImages(images)
131 | .mosaic()
132 | .visualize({
133 | forceRgbOutput: true
134 | });
135 | }
136 |
137 | return ee.ImageCollection.fromImages([
138 | bg
139 | .visualize({opacity: 0.7})
140 | .rename(['r', 'g', 'b']),
141 | waterVis
142 | .rename(['r', 'g', 'b']),
143 | landVis
144 | .rename(['r', 'g', 'b']),
145 | ])
146 | .mosaic()
147 | .visualize({
148 | forceRgbOutput: true
149 | });
150 | }
151 |
152 | function renderShorelineProfiles() {
153 | // get the data
154 | var table = ee.FeatureCollection("projects/dgds-gee/shorelines/transects");
155 | // start with numbers
156 | var empty = ee.Image().float();
157 | // draw
158 | var lines = empty.paint({
159 | featureCollection: table,
160 | color: 'change_rat',
161 | // TODO: this should be function of zoom level ~30m
162 | width: 3
163 | });
164 | // color
165 | var rdYlGn = ['#d73027', '#f46d43', '#fdae61', '#fee08b', '#ffffbf', '#d9ef8b', '#a6d96a', '#66bd63', '#1a9850'];
166 | return lines.visualize({
167 |
168 | palette: rdYlGn,
169 | min: -3,
170 | max: 3
171 | })
172 | // to rgb
173 | .rename(['r', 'g', 'b'])
174 | // mosaic not needed, already a single image
175 | .visualize({forceRgbOutput: true});
176 | }
177 |
178 | var isLoading = false;
179 |
180 | function clickShorelineProfile(pt) {
181 | if (!datasets.includes('shoreline')) {
182 | return
183 | }
184 |
185 | // if(isLoading) {
186 | // return;
187 | // }
188 | // $('#inprogress').show();
189 |
190 | // get the data
191 | var table = ee.FeatureCollection("projects/dgds-gee/shorelines/transects");
192 | var featureProxy = ee.Feature(
193 | table
194 | .filterBounds(pt.buffer(250))
195 | .first()
196 | );
197 | var feature = featureProxy.getInfo();
198 |
199 | // d is lost in translation
200 | var id = _.get(feature, 'properties.transect_i');
201 | if (_.isNil(id)) {
202 | return;
203 | }
204 |
205 | var parts = _.split(id, "_");
206 | var box = parts[1];
207 | var section = parts[2];
208 |
209 | var futureFeature = null
210 | if (datasets.includes('future-shoreline')) {
211 | var futureTable = ee.FeatureCollection("projects/dgds-gee/shorelines/future_shorelines_with_duplicates")
212 | futureFeature = futureTable
213 | .filter(ee.Filter.eq('transect_i', feature.properties.transect_i))
214 | .first()
215 | .getInfo()
216 |
217 | }
218 |
219 | var url = _.template(
220 | 'https://storage.googleapis.com/shoreline-monitor/features/<%- box %>/<%- section %>/BOX_<%- box %>_<%- section %>.json'
221 | )({
222 | box: box,
223 | section: section
224 | });
225 | $.getJSON(url, function (data) {
226 | var filteredFeatures = _.filter(data.features, function (feature) {
227 | return _.get(feature, 'properties.transect_id') === id;
228 | });
229 | var seriesFeature = _.first(filteredFeatures);
230 | createShoreChart(seriesFeature, futureFeature);
231 | var tableTemplate = _.template($('#shoreline-chart-template').html());
232 | var rendered = tableTemplate(seriesFeature.properties);
233 | $('#chart-table').html(rendered);
234 | $('#chart-modal')
235 | .show();
236 |
237 | if (datasets.includes('future-shoreline') && futureFeature) {
238 | $("#note-text-row").show();
239 | $("#note-text-row").css({"display": "contents"});
240 | } else {
241 | $("#note-text-row").hide();
242 | }
243 |
244 | // HACK: Bootstrap restyles everything :(
245 | $('.chart-modal-close-button').css({
246 | "position": "absolute",
247 | "padding-left": "5px",
248 | "padding-right": "5px",
249 | "padding-top": "5px",
250 | "padding-bottom": "5px",
251 | "height": "20px",
252 | "width": "20px",
253 | "right": "0px",
254 | "margin": "0px"
255 | });
256 |
257 | // Done loading.
258 | // $('#inprogress').hide()
259 | });
260 | return id;
261 | }
262 |
263 | function renderFutureShorelines() {
264 | // get the data
265 | var table = ee.FeatureCollection("projects/dgds-gee/shorelines/future_shorelines_with_duplicates")
266 | var points = table.style({
267 | color: 'ffffffdd',
268 | pointSize: 5,
269 | pointShape: 'o',
270 | width: 2,
271 | fillColor: '000000aa'
272 | })
273 | return points
274 | }
275 |
276 | // A helper to apply an expression and linearly rescale the output.
277 | var rescale = function (img, thresholds) {
278 | return img
279 | .subtract(
280 | thresholds[0]
281 | )
282 | .divide(
283 | ee.Number(thresholds[1])
284 | .subtract(thresholds[0])
285 | );
286 | };
287 |
288 | function getEdge(i) {
289 | var canny = ee.Algorithms.CannyEdgeDetector(i, 0.99, 0);
290 | canny = canny.mask(canny);
291 | return canny;
292 | }
293 |
294 | function renderWaterTrend(percentile, datesAndPeriods, slopeThreshold, slopeThresholdSensitive, slopeThresholdRatio, sensitive) {
295 | // Add a band containing image date as years since 1991.
296 | function createTimeBand(img) {
297 | var date = ee.Date(img.get('system:time_start'));
298 | var year = date.get('year').subtract(1970);
299 | // add rescaled MNDWI image
300 | return ee.Image(year).byte()
301 | .addBands(rescale(img, [-0.6, 0.6]));
302 | }
303 |
304 | if (ndviFilter > -1) {
305 | var bands = ['green', 'swir1', 'red', 'nir'];
306 | var bands8 = ['B3', 'B6', 'B5', 'B2'];
307 | var bands7 = ['B2', 'B5', 'B4', 'B1'];
308 | } else {
309 | var bands = ['green', 'swir1'];
310 | var bands8 = ['B3', 'B6'];
311 | var bands7 = ['B2', 'B5'];
312 | }
313 |
314 | var images = new ee.ImageCollection([]);
315 |
316 | var images_l9 = new ee.ImageCollection('LANDSAT/LC09/C02/T1_TOA').select(bands8, bands);
317 | images = new ee.ImageCollection(images.merge(images_l9));
318 |
319 | var images_l8 = new ee.ImageCollection('LANDSAT/LC08/C02/T1_RT_TOA').select(bands8, bands);
320 | images = new ee.ImageCollection(images.merge(images_l8));
321 |
322 | var images_l7 = new ee.ImageCollection('LANDSAT/LE07/C02/T1_RT_TOA').select(bands7, bands);
323 | images = new ee.ImageCollection(images.merge(images_l7));
324 |
325 | var images_l5 = new ee.ImageCollection('LANDSAT/LT05/C02/T1_TOA').select(bands7, bands);
326 | images = new ee.ImageCollection(images.merge(images_l5));
327 |
328 | var images_l4 = new ee.ImageCollection('LANDSAT/LT04/C02/T1_TOA').select(bands7, bands);
329 | images = new ee.ImageCollection(images.merge(images_l4));
330 |
331 | var list = ee.List(datesAndPeriods);
332 |
333 | var annualPercentile = ee.ImageCollection(list.map(function (i) {
334 | var l = ee.List(i);
335 | var year = l.get(0);
336 | var months = l.get(1);
337 | var start = ee.Date(year);
338 | var stop = ee.Date(year).advance(months, 'month');
339 |
340 | var filtered = images
341 | .filterDate(start, stop);
342 |
343 | if (smoothen) {
344 | filtered = filtered.map(function (i) {
345 | return i.resample('bicubic');
346 | });
347 | }
348 |
349 | var image = filtered
350 | .reduce(
351 | ee.Reducer
352 | .percentile([percentile])
353 | )
354 | .rename(
355 | bands
356 | );
357 |
358 | var result = image
359 | .normalizedDifference(['green', 'swir1']).rename('water')
360 | .set('system:time_start', start);
361 |
362 | if (ndviFilter > -1) {
363 | var ndvi = image.normalizedDifference(['nir', 'red']).rename('ndvi');
364 | result = result.addBands(ndvi);
365 | }
366 |
367 | if (filterCount > 0) {
368 | result = result.addBands(filtered.select(0).count().rename('count'));
369 | }
370 |
371 | /*
372 | 1) Map a function to update the mask of each image to be the min mask of all bands that you use
373 | (but don't include bands you don't otherwise use)
374 |
375 | 2) clip to a negative buffer of 6km.
376 |
377 | 3) Make sure you're not including nighttime images; limit SUN_ELEVATION to > 0 and maybe > ~30.
378 | */
379 |
380 | return result;
381 | }));
382 |
383 | var mndwi = annualPercentile.select('water');
384 |
385 | if (ndviFilter > -1) {
386 | var ndvi = annualPercentile.select('ndvi');
387 | }
388 |
389 | var fit = mndwi
390 | .map(createTimeBand)
391 | .reduce(ee.Reducer.linearFit().unweighted());
392 |
393 | var scale = fit.select('scale');
394 |
395 | var mndwiMin = mndwi.min();
396 | var mndwiMax = mndwi.max();
397 |
398 | var mask = scale.abs().gt(slopeThresholdRatio * slopeThreshold)
399 | .and(mndwiMax.gt(-0.05)) // at least one value looks like water
400 | .and(mndwiMin.lt(0.1)); // at least one value looks like ground
401 |
402 | var land = ee.FeatureCollection('USDOS/LSIB/2013')
403 | var landImage = ee.Image(0).float().paint(land, 1)
404 | .focal_max(5)
405 |
406 | // mask = mask.multiply(landImage);
407 |
408 | if (filterCount > 0) {
409 | mask = mask
410 | .and(
411 | annualPercentile
412 | .select('count')
413 | .min()
414 | .gt(filterCount)
415 | );
416 | }
417 |
418 |
419 | if (ndviFilter > -1) {
420 | mask = mask.and(
421 | ndvi
422 | .max()
423 | .gt(ndviFilter)
424 | ); // darkest is not vegetation
425 | }
426 |
427 | if (sensitive && refine) {
428 | var maskSensitive = null;
429 |
430 | mask = mask.reproject('EPSG:4326', null, 30);
431 | var maskProjection = mask.projection();
432 |
433 | // more sensitive mask around change
434 | maskSensitive = mask
435 | .reduceResolution(ee.Reducer.max(), true)
436 | .focal_max(6)
437 | .reproject(maskProjection.scale(6, 6))
438 | .focal_median(150, 'circle', 'meters');
439 |
440 | //mask = scale.abs().gt(0.008).mask(maskSensitive.reproject(maskProjection))
441 | mask = scale
442 | .abs()
443 | .gt(slopeThresholdRatio * slopeThresholdSensitive)
444 | .mask(maskSensitive.reproject(maskProjection))
445 | .and(mndwiMax.gt(-0.05)) // at least one value looks like water
446 | .and(mndwiMin.lt(0.1)); // at least one value looks like ground
447 |
448 | if (ndviFilter > -1) {
449 | mask = mask
450 | .and(
451 | ndvi
452 | .max()
453 | .gt(ndviFilter)
454 | ); // darkest is not vegetation
455 | }
456 |
457 | // smoothen scale and mask
458 | if (smoothen === true || smoothen === 1) {
459 | scale = scale
460 | .focal_median(25, 'circle', 'meters', 3);
461 |
462 | mask = mask
463 | .focal_mode(35, 'circle', 'meters', 3);
464 | }
465 | }
466 |
467 | var bg = scale.mask(landImage).visualize({
468 | min: -slopeThreshold * slopeThresholdRatio,
469 | max: slopeThreshold * slopeThresholdRatio,
470 | palette: ['00ff00', '000000', '00d8ff'], opacity: waterSlopeOpacity
471 | });
472 |
473 | if (!sensitive) {
474 | var swbd = ee.Image('MODIS/MOD44W/MOD44W_005_2000_02_24').select('water_mask');
475 | var swbdMask = swbd.unmask().not().focal_median(1).focal_max(5); // .add(0.2);
476 | } else {
477 | bg = bg.mask(
478 | ee.Image(waterSlopeOpacity)
479 | .toFloat()
480 | .multiply(
481 | mndwiMin
482 | .gt(0.4)
483 | .focal_mode(1)
484 | .not()
485 | ) // exclude when both are water
486 | );
487 | }
488 |
489 | if (filterCount > 0) {
490 | bg = bg
491 | .multiply(
492 | annualPercentile
493 | .select('count')
494 | .min()
495 | .gt(filterCount)
496 | );
497 | }
498 |
499 | scale = scale.mask(mask);
500 |
501 | if (sensitive && refine) {
502 | var edgeWater = getEdge(mask.mask(scale.gt(0))).visualize({palette: '00d8ff'});
503 | var edgeLand = getEdge(mask.mask(scale.lt(0))).visualize({palette: '00ff00'});
504 |
505 | var change = scale.visualize({
506 | min: -slopeThreshold * slopeThresholdRatio,
507 | max: slopeThreshold * slopeThresholdRatio,
508 | palette: ['00ff00', '000000', '00d8ff'],
509 | opacity: 0.3
510 | });
511 |
512 | if (debug) {
513 | var maskSensitiveVis = maskSensitive.mask(maskSensitive).visualize({palette: ['ffffff', '000000'], opacity: 0.5});
514 | return ee.ImageCollection.fromImages([bg, maskSensitiveVis, change, edgeWater, edgeLand]).mosaic();
515 | } else {
516 | return ee.ImageCollection.fromImages([bg, change, edgeWater, edgeLand]).mosaic();
517 | }
518 |
519 | } else {
520 | var changeImage = scale.visualize({
521 | min: -slopeThreshold * slopeThresholdRatio,
522 | max: slopeThreshold * slopeThresholdRatio,
523 | palette: ['00ff00', '000000', '00d8ff']
524 | });
525 |
526 | return ee.ImageCollection.fromImages([bg, changeImage]).mosaic();
527 | }
528 | }
529 |
530 | function setLayer(map, layer) {
531 | // add the layer to the map
532 | if (typeof (layer.urls) === 'object') {
533 | // some object that generates urls
534 | addEELayer(layer);
535 | } else {
536 | // layers.urls should be strings
537 | addStaticLayer(layer);
538 | }
539 | }
540 |
541 | function addStaticLayer(layer) {
542 | // use _.assign from options
543 | // The Google Maps API calls getTileUrl() when it tries to display a map
544 | // tile. This is a good place to swap in the MapID and token we got from
545 | // the Python script. The other values describe other properties of the
546 | // custom map type.
547 | var staticOptions = {
548 | getTileUrl: function (tile, zoom) {
549 | if (!_.inRange(zoom, layer.minZoom, layer.maxZoom)) {
550 | return '';
551 | }
552 |
553 | // global mode
554 | if (layer.mode !== mode) {
555 | return '';
556 | }
557 |
558 | // You don't want to know..... Maps bug...
559 | if (map.overlayMapTypes.getAt(layer.index).opacity < 0.01) {
560 | return '';
561 | }
562 |
563 | // TODO: replace tile.x by mod
564 | return layer.urls
565 | .replace('{x}', tile.x.mod(Math.pow(2, zoom)))
566 | .replace('{y}', tile.y)
567 | .replace('{z}', zoom);
568 | },
569 | // this name is available as in the tile
570 | name: layer.name,
571 | alt: layer.name,
572 | tileSize: new google.maps.Size(256, 256)
573 | };
574 |
575 | var mapType = new NamedImageMapType(staticOptions);
576 |
577 | // Add the EE layer to the map.
578 | // Note that the layer.index does not define the position, but the z-index...
579 | map.overlayMapTypes.setAt(layer.index, mapType);
580 | refreshLayerOpacity(map, layer);
581 | }
582 |
583 | function addEELayer(layer) {
584 | // urls is a google earth engine function
585 | layer.urls.getMap({}, function (mapId, error) {
586 | if (error) {
587 | console.log(error);
588 | }
589 |
590 | var id = mapId.mapid;
591 | var token = mapId.token;
592 |
593 | // The Google Maps API calls getTileUrl() when it tries to display a map
594 | // tile. This is a good place to swap in the MapID and token we got from
595 | // the Python script. The other values describe other properties of the
596 | // custom map type.
597 | var eeMapOptions = {
598 | getTileUrl: function (tile, zoom) {
599 |
600 | if (!_.inRange(zoom, layer.minZoom, layer.maxZoom)) {
601 | return '';
602 | }
603 |
604 | // global mode
605 | if (layer.mode !== mode) {
606 | return '';
607 | }
608 |
609 | // You don't want to know..... Maps bug...
610 | if (map.overlayMapTypes.getAt(layer.index).opacity < 0.01) {
611 | return '';
612 | }
613 |
614 | var baseUrl = 'https://earthengine.googleapis.com/v1alpha'
615 | var url = [baseUrl, id, 'tiles', zoom, tile.x, tile.y].join('/');
616 |
617 | return url;
618 | },
619 | name: layer.name,
620 | alt: layer.name,
621 | tileSize: new google.maps.Size(256, 256)
622 | };
623 |
624 | var mapType = new NamedImageMapType(eeMapOptions);
625 |
626 | // Add the EE layer to the map.
627 | map.overlayMapTypes.setAt(layer.index, mapType);
628 | refreshLayerOpacity(map, layer);
629 | });
630 | }
631 |
632 |
633 | var map;
634 |
635 | var shapesJson;
636 |
637 | // Refresh different components from other components.
638 | function refreshGeoJsonFromData(feature) {
639 | //feature.setDraggable(true);
640 | map.data.toGeoJson(function (geoJson) {
641 | shapesJson = JSON.stringify(geoJson, null, 2);
642 | });
643 | }
644 |
645 | // Apply listeners to refresh the GeoJson display on a given data layer.
646 | function bindDataLayerListeners(dataLayer) {
647 | dataLayer.addListener('addfeature', refreshGeoJsonFromData);
648 | dataLayer.addListener('removefeature', refreshGeoJsonFromData);
649 | dataLayer.addListener('setgeometry', refreshGeoJsonFromData);
650 | }
651 |
652 | function updateMapZoomDependencies() {
653 | var minZoomLevel = 7;
654 |
655 | var currentZoom = map.getZoom();
656 |
657 | var zoomDiv = $('#zoom-warning');
658 |
659 | // check if zoom level is sufficient to perform analysis
660 | if (currentZoom < minZoomLevel) {
661 | zoomDiv.text('Warning! Zoom in to level ' + minZoomLevel + ' to see surface water changes for selected dates. Current zoom level: ' + currentZoom);
662 | }
663 |
664 | if (mode === 'dynamic') {
665 | if (currentZoom >= minZoomLevel) {
666 | $('#time-selector-container').fadeIn();
667 | $('#layers-table').fadeIn();
668 | $('#label-year-before').fadeIn();
669 | $('#label-year-after').fadeIn();
670 | zoomDiv.fadeOut();
671 | } else {
672 | $('#time-selector-container').fadeOut();
673 | $('#layers-table').fadeOut();
674 | $('#label-year-before').fadeOut();
675 | $('#label-year-after').fadeOut();
676 | zoomDiv.fadeIn();
677 | }
678 | } else {
679 | $('#time-selector-container').fadeOut();
680 | $('#layers-table').fadeOut();
681 | $('#label-year-before').fadeOut();
682 | $('#label-year-after').fadeOut();
683 | zoomDiv.fadeOut();
684 | }
685 | }
686 |
687 | var locationInfos = [
688 | {center: {lat: 25, lng: 55}, zoom: 11}, // 0 Dubai, constructions
689 | {center: {lat: 17.555599910902977, lng: 96.10637664794922}, zoom: 10}, // 1 Myanmar, reservoirs
690 | {center: {lat: 36.24200388695886, lng: -114.65053558349608}, zoom: 10}, // 2 LasVegas, Colorado River
691 | {center: {lat: -6.115019440922777, lng: -74.90577697753905}, zoom: 11}, // 3 Peru, crazy rivers
692 | {center: {lat: -18.64207976751951, lng: 36.32624626159668}, zoom: 11}, // 4 Zambezi River Delta
693 | {center: {lat: 24.30417608337614, lng: 89.7373104095459}, zoom: 8}, // 5 Brahmaputra River
694 | {center: {lat: 26.246405548914684, lng: 50.58195114135742}, zoom: 12}, // 6 Bahrain
695 | {center: {lat: 21.142727475393187, lng: 31.03641510009766}, zoom: 7}, // 7 Nile
696 | {center: {lat: 52.045428812114544, lng: 4.190292358398442}, zoom: 11}, // 8 Netherlands
697 | {center: {lat: 51.52194707785323, lng: 14.137382507324222}, zoom: 11}, // 9 Mines? Germany
698 | {center: {lat: -12.832119855443182, lng: -70.24379730224612}, zoom: 11}, // 10 Mines, Peru
699 | {center: {lat: -18.725176095360386, lng: -51.2161636352539}, zoom: 12}, // 11 Brazil, reservoirs
700 | {center: {lat: 30.838073102713093, lng: 110.76282501220703}, zoom: 10}, // 12 Three Georges Dam, China
701 | {center: {lat: 5.351403568565903, lng: -53.20623397827148}, zoom: 10}, // 13 French Guiana, mud bank erosion / accretion
702 | {center: {lat: 26.19027242797399, lng: 57.978882789611816}, zoom: 12}, // 14 Iran, reservoir
703 | {center: {lat: 29.711330304705562, lng: 111.12202644348145}, zoom: 13}, // 15 Xieshui, China, dam
704 | {center: {lat: 45.87827701665845, lng: 126.98593020439148}, zoom: 11}, // 16 reservoir somewhere in China
705 | {center: {lat: 18.906385392620926, lng: 32.243194580078125}, zoom: 11}, // 17 Merowe reservoir, Sudan
706 | {center: {lat: 34.91825639532396, lng: 112.40150451660156}, zoom: 12}, // 18 Xiaolangdi Reservoir, Red River, China
707 | {center: {lat: 37.53203099283159, lng: 93.6544406414032}, zoom: 11}, // 19 Taiji Reservoirs China
708 | {center: {lat: 37.44646619568239, lng: 65.75304399726554}, zoom: 10}, // 20 Amurdarya
709 | {center: {lat: 27.633179467080392, lng: 49.069528579711914}, zoom: 12}, // 21 Saudi Arabia
710 | // http://aqua-monitor.appspot.com?view=51.30393503657091,-69.508957862854.9z - strange :)
711 | ];
712 |
713 |
714 | function initializeMap() {
715 |
716 | var locationIndex = Math.round(Math.random() * (locationInfos.length - 1));
717 |
718 | if (site != -1) {
719 | locationIndex = site;
720 | }
721 |
722 | site = locationIndex;
723 |
724 | var location = locationInfos[locationIndex];
725 |
726 | // full map
727 | location.zoom = 3;
728 | location.center.lat = 16.240652044117923;
729 | location.center.lng = -10;
730 |
731 | // overwrite from the filled in template if available
732 | location.zoom = _.get(view, 'zoom', location.zoom);
733 | location.center.lat = _.get(view, 'lat', location.center.lat);
734 | location.center.lng = _.get(view, 'lng', location.center.lng);
735 |
736 | // Set to Texel
737 | // location.center = {lng: 4.778553150000011, lat: 53.07320558366855}
738 | // location.zoom = 12
739 |
740 | var mapOptions = {
741 | center: location.center,
742 | zoom: location.zoom,
743 | minZoom: 1,
744 | maxZoom: 17,
745 | draggable: true,
746 | editable: true,
747 | mapTypeControl: true,
748 | mapTypeControlOptions: {position: google.maps.ControlPosition.TOP_RIGHT},
749 | streetViewControl: true,
750 | scaleControl: true,
751 | scaleControlOptions: {position: google.maps.ControlPosition.BOTTOM_RIGHT}
752 | };
753 |
754 | // Create the base Google Map.
755 | map = new google.maps.Map(document.getElementById('map'), mapOptions);
756 | map.setMapTypeId(google.maps.MapTypeId.SATELLITE);
757 |
758 | map.setOptions({draggableCursor:'crosshair'});
759 |
760 |
761 |
762 | map.addListener('zoom_changed', function () {
763 | console.log('Map zoom: ' + map.getZoom());
764 | updateMapZoomDependencies();
765 | });
766 |
767 | // button change style
768 | var styleControlDiv = document.createElement('div');
769 | $(styleControlDiv).addClass('gm-aqua-control');
770 | styleControlDiv.innerHTML = ' ';
771 | styleControlDiv.style.width = '28px';
772 | map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(styleControlDiv);
773 | $(styleControlDiv).children('button').on('click', function (evt, el) {
774 | $('#map').toggleClass('styled');
775 | $(evt.currentTarget).toggleClass('active');
776 | });
777 |
778 | // switch mode between dynamic / static
779 | var modeControlDiv = document.createElement('div');
780 | $(modeControlDiv).addClass('gm-aqua-control');
781 | modeControlDiv.style.width = '28px';
782 | modeControlDiv.innerHTML = ' ';
783 |
784 | if (mode === 'dynamic') {
785 | $(modeControlDiv).children('button').addClass('active')
786 | }
787 |
788 | map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(modeControlDiv);
789 |
790 | $(modeControlDiv).children('button').on('click', function (evt, el) {
791 | toggleMode();
792 | $(evt.currentTarget).toggleClass('active');
793 | });
794 |
795 | // component (surface water change, coastline, etc.)
796 | map.controls[google.maps.ControlPosition.TOP_RIGHT].push($('#datasets-button')[0]);
797 |
798 | var dropdown = $('#datasets-button')
799 | .dropdown();
800 | dropdown.dropdown('set value', datasets);
801 | dropdown.on('change', function () {
802 | var selectedDatasets = $(this).dropdown('get value').split(',');
803 | console.log('datasets changed', selectedDatasets);
804 | });
805 |
806 |
807 | // hide all boxes
808 | $('#info-box .info-text').hide();
809 | $('#info-box .extra.content').hide();
810 |
811 | // show the relevant ones
812 | _.each(datasets || ['surface-water'], function (dataset) {
813 | if (datasets.length > 1) {
814 | $('#info-box .original-only').hide();
815 | }
816 |
817 | $('*[data-dataset=' + '"' + dataset + '"' + ']').show();
818 | });
819 |
820 |
821 | // info button
822 | if ($('body').width() >= 1024) {
823 | map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push($('#info-button')[0]);
824 | }
825 |
826 | map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push($('#share-button')[0]);
827 |
828 |
829 | // strange, style changes for one button (GMaps bugs?)
830 | $('#share-button').css('padding', 0)
831 |
832 | $('#share-button').on('click', function (evt, el) {
833 | var count = 0
834 | var url = ''
835 |
836 | function delimiter() {
837 | return count ? '&' : '?'
838 | }
839 |
840 | if (mode != 'static') {
841 | url += delimiter() + 'mode=dynamic';
842 | count++;
843 | }
844 |
845 | url += delimiter() + 'from=' + minYearSelection
846 | count++;
847 |
848 | url += delimiter() + 'to=' + maxYearSelection
849 | count++;
850 |
851 | url += delimiter() + 'view=' + map.getCenter().lat() + ',' + map.getCenter().lng() + ',' + map.getZoom() + 'z'
852 | count++;
853 |
854 | if (minDoy !== 0) {
855 | url += delimiter() + 'min_doy=' + minDoy;
856 | count++;
857 | }
858 |
859 | if (minDoy !== 365) {
860 | url += delimiter() + 'max_doy=' + maxDoy;
861 | count++;
862 | }
863 |
864 | if (percentile != 20) { // default
865 | url += delimiter() + 'percentile=' + percentile
866 | count++;
867 | }
868 |
869 | window.history.replaceState('', '', url);
870 | });
871 |
872 | google.maps.event.addDomListener(document, 'keyup', function (e) {
873 | var code = (e.keyCode ? e.keyCode : e.which);
874 |
875 | if (code === 46) { // Delete
876 | map.data.forEach(function (feature) {
877 | //filter...
878 | map.data.remove(feature);
879 |
880 | $('#chart-dashboard').css('visibility', 'hidden');
881 | });
882 | }
883 | });
884 |
885 | // call when map is ready.
886 | initAutocomplete(map);
887 |
888 | return map;
889 | }
890 |
891 | function addLayers() {
892 |
893 | function afterLayersAdded() {
894 |
895 | function handleLayerClick(evt) {
896 | var pt = ee.Geometry.Point([evt.latLng.lng(), evt.latLng.lat()]);
897 | _.each(layers, function (layer) {
898 | // check if layer is in current loaded layers
899 | if (!datasets.includes(layer.dataset)) {
900 | return
901 | }
902 | if (_.has(layer, 'handlers.click')) {
903 | layer.handlers.click(pt);
904 | }
905 | });
906 |
907 | }
908 |
909 | map.addListener('click', handleLayerClick);
910 |
911 | var sliderDefaults = {
912 | min: 0,
913 | max: 100,
914 | orientation: 'horizontal',
915 | tooltip_position: 'left',
916 | touchCapable: true
917 | };
918 | var sliders = [
919 | {
920 | sliderSelector: '#slider-change',
921 | toggleSelector: '#toggle-change',
922 | properties: {
923 | value: layerByName('dynamic-change').opacity
924 | },
925 | layers: [
926 | 'dynamic-change',
927 | 'dynamic-change-refined'
928 | ],
929 | afterSlide: function (evt) {
930 | // nothing to do
931 | }
932 | },
933 | {
934 | sliderSelector: '#slider-after',
935 | toggleSelector: '#toggle-after',
936 | properties: {
937 | value: layerByName('after-percentile').opacity
938 | },
939 | layers: [
940 | 'after-percentile',
941 | 'after-percentile-sharpened'
942 | ],
943 | afterSlide: function (evt) {
944 | $('#label-year-after').css({opacity: evt.value / 100.0});
945 | var before = $('#slider-before').slider('getValue');
946 | $('#label-year-before').css({opacity: (before - evt.value) / 100.0});
947 | }
948 | },
949 | {
950 | sliderSelector: '#slider-before',
951 | toggleSelector: '#toggle-before',
952 | properties: {
953 | value: layerByName('before-percentile').opacity
954 | },
955 | layers: [
956 | 'before-percentile',
957 | 'before-percentile-sharpened'
958 | ],
959 | afterSlide: function (evt) {
960 | var after = $('#slider-after').slider('getValue');
961 | $('#label-year-before').css({opacity: (evt.value - after) / 100.0});
962 | }
963 | }
964 | ];
965 |
966 | // update sliders
967 | _.each(sliders, function (slider) {
968 | function updateSlider(evt) {
969 | _.each(slider.layers, function (layerName) {
970 | var layer = layerByName(layerName);
971 | layer.opacity = evt.value;
972 | refreshLayerOpacity(map, layer);
973 | slider.afterSlide(evt);
974 | });
975 | }
976 |
977 | // apply defaults and update sliders
978 | $(slider.sliderSelector)
979 | .slider(_.defaults(slider.properties, sliderDefaults))
980 | .on('slide', updateSlider)
981 | .on('slideStop', updateSlider);
982 | });
983 |
984 |
985 | // update toggles
986 | _.each(sliders, function (slider) {
987 | $(slider.toggleSelector).checkbox();
988 | $(slider.toggleSelector).change(function () {
989 | var checked = $('#toggle-change').is(':checked');
990 | _.each(slider.layers, function (layerName) {
991 | var layer = layerByName(layerName);
992 | if (checked) {
993 | layer.opacity = 100;
994 | } else {
995 | layer.opacity = 0;
996 | }
997 | refreshLayerOpacity(map, layer);
998 | });
999 | });
1000 |
1001 | });
1002 |
1003 | $('.tooltip-main').removeClass('top').addClass('right');
1004 |
1005 | // this one is reused....
1006 | function fixTooltips() {
1007 | $('.tooltip-main').css('margin-left', '10px');
1008 | }
1009 |
1010 | fixTooltips();
1011 |
1012 | // hide for now
1013 | $('.tooltip-main').css('display', 'none');
1014 |
1015 | $('#message-initializing-ee').fadeOut();
1016 |
1017 | function layout() {
1018 | if ($('body').width() < 845) {
1019 | $('#time-selector-container').css('width', '70%');
1020 | $('#time-selector-container').css('margin-left', '-35%');
1021 |
1022 | $('#deltares-logo').css('bottom', '20px')
1023 |
1024 | $('#layers-table').css('right', '10vw');
1025 | $('#layers-table').css('left', '10px');
1026 | $('#layers-table').css('right', '20px');
1027 | $('#layers-table').css('bottom', '130px');
1028 | $('.slider').css('width', '60vw');
1029 | } else {
1030 | $('#time-selector-container').css('width', '40%');
1031 | $('#time-selector-container').css('margin-left', '-20%');
1032 |
1033 | $('#deltares-logo').css('bottom', '7px');
1034 |
1035 | $('#layers-table').css('bottom', '25px');
1036 | $('#layers-table').css('right', '70px');
1037 | }
1038 |
1039 | if ($('body').height() < 800) {
1040 | if ($('#info-box').is(':visible')) {
1041 | $('#twitter-timeline-box').hide();
1042 | }
1043 | } else {
1044 | if ($('#info-box').is(':visible')) {
1045 | $('#twitter-timeline-box').show();
1046 | }
1047 | }
1048 | }
1049 |
1050 | layout();
1051 |
1052 | $(window).on('resize', function () {
1053 | // some layout hacks....
1054 | layout();
1055 |
1056 | if (+$('#slider-div-before').width() < 60) {
1057 | $('.tooltip-main').css('display', 'none');
1058 | }
1059 |
1060 | fixTooltips();
1061 |
1062 | });
1063 | }
1064 |
1065 | function fmt(date) {
1066 | return date.format('YYYY-MM-DD');
1067 | }
1068 |
1069 | var before1 = moment(['{0}-01-01'.format(minYearSelection)]);
1070 | var before2 = moment(['{0}-01-01'.format(minYearSelection)]).add(averagingMonths1, 'month');
1071 |
1072 | var after1 = moment(['{0}-01-01'.format(maxYearSelection)]);
1073 | var after2 = moment(['{0}-01-01'.format(maxYearSelection)]).add(averagingMonths2, 'month');
1074 |
1075 |
1076 | // add dynamic changes layers
1077 | var yearsAndPeriods = ee.List([[fmt(before1), averagingMonths1], [fmt(after1), averagingMonths2]]);
1078 |
1079 | if (allYears) {
1080 | var years = ee.List.sequence(minYearSelection, maxYearSelection, allYearsStep).map(function (y) {
1081 | return ee.String(y).slice(0, 4).cat('-01-01');
1082 | });
1083 | var months = ee.List.repeat(averagingMonths1, maxYear - minYear + 1);
1084 | yearsAndPeriods = years.zip(months);
1085 | }
1086 |
1087 | // layer counter
1088 | var nLayers = 1;
1089 | layers = [
1090 | // static layers
1091 | {
1092 | name: 'change-heatmap',
1093 | urls: 'https://storage.googleapis.com/aqua-monitor/AquaMonitor_1_with_heatmap_2016_08_27/{z}/{x}/{y}.png',
1094 | index: nLayers++,
1095 | minZoom: 0,
1096 | maxZoom: 5,
1097 | mode: 'static',
1098 | dataset: 'surface-water',
1099 | opacity: 80
1100 | },
1101 | {
1102 | name: 'change-upscaled-300m',
1103 | urls: 'https://storage.googleapis.com/aqua-monitor/AquaMonitor_2_300m_2016_08_27/{z}/{x}/{y}.png',
1104 | index: nLayers++,
1105 | minZoom: 5,
1106 | maxZoom: 10,
1107 | mode: 'static',
1108 | dataset: 'surface-water',
1109 | opacity: 80
1110 | },
1111 | {
1112 | name: 'change',
1113 | urls: renderSurfaceWaterChanges(true),
1114 | index: nLayers++,
1115 | minZoom: 10,
1116 | maxZoom: 22,
1117 | mode: 'static',
1118 | dataset: 'surface-water',
1119 | opacity: 80
1120 | },
1121 | {
1122 | name: 'shoreline-profiles',
1123 | urls: 'https://storage.googleapis.com/shoreline-monitor/shoreline-500m-z0-11/{z}/{x}/{y}.png',
1124 | index: nLayers++,
1125 | minZoom: 0,
1126 | maxZoom: 11,
1127 | mode: 'static',
1128 | dataset: 'shoreline',
1129 | opacity: 80
1130 | },
1131 | {
1132 | name: 'shoreline-heatmap',
1133 | urls: 'https://storage.googleapis.com/shoreline-monitor/shoreline-heatmap-z0-5/{z}/{x}/{y}.png',
1134 | index: nLayers++,
1135 | minZoom: 0,
1136 | maxZoom: 5,
1137 | mode: 'static',
1138 | dataset: 'shoreline',
1139 | opacity: 30
1140 | },
1141 | {
1142 | name: 'shoreline-profiles',
1143 | urls: renderShorelineProfiles(),
1144 | index: nLayers++,
1145 | minZoom: 10,
1146 | maxZoom: 22,
1147 | mode: 'static',
1148 | dataset: 'shoreline',
1149 | opacity: 100,
1150 | handlers: {
1151 | click: clickShorelineProfile
1152 | }
1153 | },
1154 | {
1155 | name: 'future-shoreline-points',
1156 | urls: renderFutureShorelines(),
1157 | index: nLayers++,
1158 | minZoom: 11,
1159 | maxZoom: 22,
1160 | mode: 'static',
1161 | dataset: 'future-shoreline',
1162 | opacity: 100
1163 | },
1164 | // dynamic mode layers
1165 | {
1166 | name: 'before-percentile',
1167 | urls: renderLandsatMosaic(percentile, fmt(before1), fmt(before2)),
1168 | index: nLayers++,
1169 | minZoom: 7,
1170 | maxZoom: 12,
1171 | mode: 'dynamic',
1172 | dataset: 'surface-water',
1173 | opacity: 100
1174 | },
1175 | {
1176 | name: 'after-percentile',
1177 | urls: renderLandsatMosaic(percentile, fmt(after1), fmt(after2)),
1178 | index: nLayers++,
1179 | minZoom: 7,
1180 | maxZoom: 12,
1181 | mode: 'dynamic',
1182 | dataset: 'surface-water',
1183 | opacity: 0
1184 | },
1185 | {
1186 | name: 'dynamic-change',
1187 | urls: renderWaterTrend(percentile, yearsAndPeriods, waterSlopeThreshold, waterSlopeThresholdSensitive, waterChangeTrendRatio),
1188 | index: nLayers++,
1189 | minZoom: 7,
1190 | maxZoom: 12,
1191 | mode: 'dynamic',
1192 | dataset: 'surface-water',
1193 | opacity: 100
1194 | },
1195 | {
1196 | name: 'before-percentile-sharpened',
1197 | urls: renderLandsatMosaic(percentile, fmt(before1), fmt(before2), true),
1198 | index: nLayers++,
1199 | minZoom: 12,
1200 | maxZoom: 22,
1201 | mode: 'dynamic',
1202 | dataset: 'surface-water',
1203 | opacity: 100
1204 | },
1205 | {
1206 | name: 'after-percentile-sharpened',
1207 | urls: renderLandsatMosaic(percentile, fmt(after1), fmt(after2), true),
1208 | index: nLayers++,
1209 | minZoom: 12,
1210 | maxZoom: 22,
1211 | mode: 'dynamic',
1212 | dataset: 'surface-water',
1213 | opacity: 0
1214 | },
1215 | {
1216 | name: 'dynamic-change-refined',
1217 | urls: renderWaterTrend(percentile, yearsAndPeriods, waterSlopeThreshold, waterSlopeThresholdSensitive, waterChangeTrendRatio, true),
1218 | index: nLayers++,
1219 | minZoom: 12,
1220 | maxZoom: 22,
1221 | mode: 'dynamic',
1222 | dataset: 'surface-water',
1223 | opacity: 100
1224 | }
1225 |
1226 | ];
1227 | // add all layers
1228 | _.each(layers, function (layer) {
1229 | // if datasets is not specified (Show all) or if dataset is in datasets
1230 | if (_.isEmpty(datasets) || _.includes(datasets, layer.dataset)) {
1231 | setLayer(map, layer);
1232 | }
1233 | // hide and show layers based on datasets-menu
1234 | });
1235 | afterLayersAdded();
1236 |
1237 |
1238 | // HACK: reposition control, Google Maps options are very limited
1239 | var positionInitialized = false; // used in a HACK to
1240 | google.maps.event.addDomListener(map, 'tilesloaded', function () {
1241 | // We only want to wrap once!
1242 | if (!positionInitialized) {
1243 | positionInitialized = true;
1244 |
1245 | updateMapZoomDependencies();
1246 | }
1247 | });
1248 | }
1249 |
1250 | function layerByName(name) {
1251 | return _.find(layers, ['name', name]);
1252 | }
1253 |
1254 | var currentYear = new Date().getFullYear();
1255 |
1256 | function toggleMode() {
1257 | // change the global mode
1258 | mode = (mode == 'dynamic') ? 'static' : 'dynamic';
1259 | console.log('mode change to', mode);
1260 |
1261 | if (mode === 'dynamic') {
1262 | $('#info-text-title').text('Surface water changes (1985-' + currentYear + ')');
1263 | $('#info-text-body').html('Green and blue colors represent areas where surface water changes occured during the last 30 years. Green pixels show where surface water has been turned into land (accretion, land reclamation, droughts). Blue pixels show where land has been changed into surface water (erosion, reservoir construction). Note, it may take some time for results to appear, because the analysis is performed on-the-fly. The results of the analysis are published in: Donchyts et.al, 2016, Nature Climate Change ');
1264 |
1265 | $('#map').addClass('dynamic');
1266 | $('#map').removeClass('static');
1267 |
1268 | var after = $('#slider-after').slider('getValue');
1269 | $('#label-year-after').css({opacity: after / 100.0});
1270 | var before = $('#slider-before').slider('getValue');
1271 | $('#label-year-before').css({opacity: (before - after) / 100.0});
1272 | } else {
1273 | $('#info-text-title').text('Surface water changes (1985-2017)');
1274 | $('#info-text-body').html('Green and blue colors represent areas where surface water changes occured during the last 30 years. Green pixels show where surface water has been turned into land (accretion, land reclamation, droughts). Blue pixels show where land has been changed into surface water (erosion, reservoir construction). The results of the analysis are published in: Donchyts et.al, 2016, Nature Climate Change ');
1275 |
1276 | $('#map').addClass('static');
1277 | $('#map').removeClass('dynamic');
1278 |
1279 | $('#label-year-after').css({opacity: 0});
1280 | $('#label-year-before').css({opacity: 0});
1281 | }
1282 |
1283 | updateMapZoomDependencies();
1284 |
1285 | _.each(layers, function (layer) {
1286 | //refreshLayerOpacity(map, layer)
1287 |
1288 | // reset all overlays so that url's are reevaluated (based on zoom)
1289 | var overlay = map.overlayMapTypes.getAt(layer.index);
1290 | map.overlayMapTypes.removeAt(layer.index);
1291 | map.overlayMapTypes.insertAt(layer.index, overlay);
1292 | });
1293 | }
1294 |
1295 | function refreshLayerOpacity(map, layer) {
1296 | var overlay = map.overlayMapTypes.getAt(layer.index);
1297 | if (overlay === undefined) {
1298 | console.log('trying to set opacity for undefined overlay', layer, overlay);
1299 | return;
1300 | }
1301 |
1302 | if (layer.mode === 'static') {
1303 | if (mode === 'static') {
1304 | overlay.setOpacity(layer.opacity / 100.0);
1305 | } else {
1306 | overlay.setOpacity(0);
1307 | }
1308 | }
1309 |
1310 | var appear = layer.opacity / 100 > 0.01 && overlay.getOpacity() < 0.01;
1311 | overlay.setOpacity(layer.opacity / 100.0);
1312 |
1313 | // appear hack (is visible, was hidden)
1314 | if (appear) {
1315 | map.overlayMapTypes.removeAt(layer.index);
1316 | map.overlayMapTypes.insertAt(layer.index, overlay);
1317 | }
1318 |
1319 | }
1320 |
--------------------------------------------------------------------------------