├── .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 | 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 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 | 11 | 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 | 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 | 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 |
31 | 32 |
33 |
34 |
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 | 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 |
174 |
175 |
176 |
177 | 178 | 181 | 182 |
183 | 188 | 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 |
210 | Just one second 211 |
212 |

We're fetching that content for you.

213 |
214 |
215 | 216 |
217 | 218 | 219 |
220 |
221 | Initializing 222 |
223 |

Google Earth Engine ...

224 |
225 |
226 | 227 | 234 | 235 | 238 | 239 | 242 | 243 | {# 244 | 247 | 248 | #} 249 | 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 |
352 | 381 | 382 |
383 | 384 | 385 | 386 |
387 |
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 |
431 | 460 | 461 |
462 | 463 | 464 | 465 |
466 |
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 |
487 | 516 | 517 |
518 | 519 | 520 | 521 |
522 |
523 |
524 | 525 | 526 | 527 | 538 | 548 | 549 |
1985
550 |
2016
551 | 552 | 553 | 554 | 557 | 558 | 559 | 560 | 563 | 564 | 565 | 566 | 569 | 570 | 571 |
555 |
Changes
556 |
561 |
2016
562 |
567 |
2000
568 |
572 | 573 |
574 |
575 | 576 | 577 |
578 |
579 | 580 | 581 |
582 |
583 | 584 | 585 |
586 |
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 | --------------------------------------------------------------------------------