diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..8ed41f353 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ +## Overview + +Brief description of what this PR does, and why it is needed. + +Connects #XXX + +### Demo + +Optional. Screenshots, `curl` examples, etc. + +### Notes + +Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. + +## Testing Instructions + + * How to test this PR + * Prefer bulleted description + * Start after checking out this branch + * Include any setup required, such as bundling scripts, restarting services, etc. + * Include test case, and expected output diff --git a/Vagrantfile b/Vagrantfile index 4923dd2fe..e2ebfa8c2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -90,9 +90,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| worker.vm.synced_folder "src/mmw", "/opt/app/" - if ENV["VAGRANT_ENV"].nil? || ENV["VAGRANT_ENV"] != "TEST" - worker.vm.synced_folder "/opt/rwd-data", "/opt/rwd-data" - end + # Path to RWD data (ex. /media/passport/rwd-nhd) + worker.vm.synced_folder ENV.fetch("RWD_DATA", "/tmp"), "/opt/rwd-data" # Docker worker.vm.network "forwarded_port", { diff --git a/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml b/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml index 78e38ac8e..ca67b1610 100644 --- a/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml +++ b/deployment/ansible/roles/model-my-watershed.rwd/defaults/main.yml @@ -2,4 +2,8 @@ rwd_data_path: "/opt/rwd-data" rwd_host: "localhost" rwd_port: 5000 -rwd_docker_image: "quay.io/wikiwatershed/rwd:1.1.1" +rwd_docker_image: "quay.io/wikiwatershed/rwd:1.2.1" + +app_config: + RWD_HOST: "{{ rwd_host }}" + RWD_PORT: "{{ rwd_port }}" diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml new file mode 100644 index 000000000..8bf935507 --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/app.yml @@ -0,0 +1,16 @@ +--- +- name: Create RWD data directory + file: path="{{ rwd_data_path }}" + state=directory + +- name: Pull RWD docker container image + command: /usr/bin/docker pull {{ rwd_docker_image }} + +- name: Configure RWD service definition + template: src=upstart-mmw-rwd.conf.j2 + dest=/etc/init/mmw-rwd.conf + notify: + - Restart mmw-rwd + +- name: Ensure service is running + service: name=mmw-rwd state=started diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml new file mode 100644 index 000000000..c8126fe8e --- /dev/null +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/configuration.yml @@ -0,0 +1,10 @@ +--- +- name: Configure application + copy: content="{{ item.value }}" + dest="{{ envdir_home }}/{{ item.key }}" + owner=root + group=mmw + mode=0750 + with_dict: "{{ app_config }}" + notify: + - Restart Celery diff --git a/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml b/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml index 8bf935507..2fe6804c3 100644 --- a/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml +++ b/deployment/ansible/roles/model-my-watershed.rwd/tasks/main.yml @@ -1,16 +1,3 @@ --- -- name: Create RWD data directory - file: path="{{ rwd_data_path }}" - state=directory - -- name: Pull RWD docker container image - command: /usr/bin/docker pull {{ rwd_docker_image }} - -- name: Configure RWD service definition - template: src=upstart-mmw-rwd.conf.j2 - dest=/etc/init/mmw-rwd.conf - notify: - - Restart mmw-rwd - -- name: Ensure service is running - service: name=mmw-rwd state=started +- { include: configuration.yml } +- { include: app.yml } diff --git a/deployment/cfn/application.py b/deployment/cfn/application.py index f22920ee4..73ca5006e 100644 --- a/deployment/cfn/application.py +++ b/deployment/cfn/application.py @@ -173,7 +173,7 @@ def set_up_stack(self): self.app_server_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'AppServerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Application server ASG schedule end recurrence' ), 'AppServerAutoScalingScheduleEndRecurrence') diff --git a/deployment/cfn/tiler.py b/deployment/cfn/tiler.py index 97a068f88..ce4992421 100644 --- a/deployment/cfn/tiler.py +++ b/deployment/cfn/tiler.py @@ -164,7 +164,7 @@ def set_up_stack(self): self.tile_server_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'TileServerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Tile server ASG schedule end recurrence' ), 'TileServerAutoScalingScheduleEndRecurrence') diff --git a/deployment/cfn/utils/constants.py b/deployment/cfn/utils/constants.py index d7999a1f1..7b1f93fcd 100644 --- a/deployment/cfn/utils/constants.py +++ b/deployment/cfn/utils/constants.py @@ -2,7 +2,8 @@ 't2.micro', 't2.small', 't2.medium', - 't2.large' + 't2.large', + 'r4.large' ] RDS_INSTANCE_TYPES = [ diff --git a/deployment/cfn/worker.py b/deployment/cfn/worker.py index dff482f1f..db458bde4 100644 --- a/deployment/cfn/worker.py +++ b/deployment/cfn/worker.py @@ -170,7 +170,7 @@ def set_up_stack(self): self.worker_auto_scaling_schedule_end_recurrence = self.add_parameter( # NOQA Parameter( 'WorkerAutoScalingScheduleEndRecurrence', Type='String', - Default='0 23 * * *', + Default='0 1 * * *', Description='Worker ASG schedule end recurrence' ), 'WorkerAutoScalingScheduleEndRecurrence') @@ -331,6 +331,7 @@ def create_auto_scaling_resources(self, worker_security_group, worker_lb): worker_launch_config = self.add_resource( asg.LaunchConfiguration( worker_launch_config_name, + EbsOptimized=True, ImageId=Ref(self.worker_ami), IamInstanceProfile=Ref(self.worker_instance_profile), InstanceType=Ref(self.worker_instance_type), @@ -416,7 +417,10 @@ def get_cloud_config(self): ' - path: /etc/mmw.d/env/ROLLBAR_SERVER_SIDE_ACCESS_TOKEN\n', ' permissions: 0750\n', ' owner: root:mmw\n', - ' content: ', self.get_input('RollbarServerSideAccessToken')] # NOQA + ' content: ', self.get_input('RollbarServerSideAccessToken'), # NOQA + '\n', + 'runcmd:\n', + ' - /opt/model-my-watershed/scripts/aws/ebs-warmer.sh'] def create_cloud_watch_resources(self, worker_auto_scaling_group): self.add_resource(cw.Alarm( diff --git a/deployment/default.yml.example b/deployment/default.yml.example index a9924aca5..36364e840 100644 --- a/deployment/default.yml.example +++ b/deployment/default.yml.example @@ -29,8 +29,8 @@ AppServerAutoScalingScheduleStartCapacity: '1' # 8AM ET AppServerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' AppServerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -AppServerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +AppServerAutoScalingScheduleEndRecurrence: '0 1 * * *' SSLCertificateARN: 'arn:aws:iam...' BackwardCompatSSLCertificateARN: 'arn:aws:iam...' TileServerInstanceType: 't2.micro' @@ -45,8 +45,8 @@ TileServerAutoScalingScheduleStartCapacity: '1' # 8AM ET TileServerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' TileServerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -TileServerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +TileServerAutoScalingScheduleEndRecurrence: '0 1 * * *' WorkerInstanceType: 't2.micro' # Leaving this commented dynamically looks up the # most recent AMI for this type. @@ -59,8 +59,8 @@ WorkerAutoScalingScheduleStartCapacity: '2' # 8AM ET WorkerAutoScalingScheduleStartRecurrence: '0 13 * * 1-5' WorkerAutoScalingScheduleEndCapacity: '0' -# 6PM ET -WorkerAutoScalingScheduleEndRecurrence: '0 23 * * *' +# 8PM ET +WorkerAutoScalingScheduleEndRecurrence: '0 1 * * *' ITSIBaseURL: '' ITSISecretKey: '' RollbarServerSideAccessToken: '' diff --git a/deployment/packer/template.js b/deployment/packer/template.js index e7feeb50d..6ca73e0b4 100644 --- a/deployment/packer/template.js +++ b/deployment/packer/template.js @@ -63,7 +63,7 @@ "ami_block_device_mappings": [ { "device_name": "/dev/sdf", - "snapshot_id": "snap-090ac799996dba0a4", + "snapshot_id": "snap-0211cbbff8a81266f", "volume_type": "gp2", "delete_on_termination": true } diff --git a/scripts/aws/ebs-warmer.sh b/scripts/aws/ebs-warmer.sh new file mode 100755 index 000000000..bfc4eb2a1 --- /dev/null +++ b/scripts/aws/ebs-warmer.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Ref: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-initialize.html + +function log() { + echo "[$(date --rfc-3339=seconds)] $1" +} + +log "Warming up RWD EBS volume..." + +find /opt/rwd-data/nhd/Main_Watershed/* -type f -print0 \ + | xargs -0 -P0 -L1 -t cat >/dev/null + +find /opt/rwd-data/drb/Main_Watershed/* -type f -print0 \ + | xargs -0 -P0 -L1 -t cat >/dev/null + +log "Done" diff --git a/scripts/aws/setupdb.sh b/scripts/aws/setupdb.sh index 000bbe908..0a9dc5cfc 100755 --- a/scripts/aws/setupdb.sh +++ b/scripts/aws/setupdb.sh @@ -15,6 +15,7 @@ where: \n -m load/reload mapshed data\n -p load/reload DEP data\n -q load/reload water quality data\n + -x purge s3 cache for given path\n " # HTTP accessible storage for initial app data @@ -25,7 +26,7 @@ load_stream=false load_mapshed=false load_water_quality=false -while getopts ":hbsdpmqf:" opt; do +while getopts ":hbsdpmqf:x:" opt; do case $opt in h) echo -e $usage @@ -44,6 +45,8 @@ while getopts ":hbsdpmqf:" opt; do load_water_quality=true ;; f) file_to_load=$OPTARG ;; + x) + path_to_purge=$OPTARG ;; \?) echo "invalid option: -$OPTARG" exit ;; @@ -81,6 +84,11 @@ if [ ! -z "$file_to_load" ] ; then download_and_load $FILES fi +if [ ! -z "$path_to_purge" ] ; then + PATHS=("$path_to_purge") + purge_tile_cache $PATHS +fi + if [ "$load_dep" = "true" ] ; then # Fetch DEP layers FILES=("dep_urban_areas.sql.gz" "dep_municipalities.sql.gz") diff --git a/src/mmw/apps/core/templates/base.html b/src/mmw/apps/core/templates/base.html index 344752258..ce66d94fe 100644 --- a/src/mmw/apps/core/templates/base.html +++ b/src/mmw/apps/core/templates/base.html @@ -1,31 +1,6 @@ {% include 'head.html' %} {% load staticfiles %} - - - - {% block metatitle %} - Model My Watershed - {% endblock metatitle %} - - - - - - - - - - {% block header %} {% endblock header %} diff --git a/src/mmw/apps/home/views.py b/src/mmw/apps/home/views.py index 43b537116..be7f0f5e2 100644 --- a/src/mmw/apps/home/views.py +++ b/src/mmw/apps/home/views.py @@ -135,6 +135,8 @@ def get_client_settings(request): 'draw_tools': settings.DRAW_TOOLS, 'map_controls': settings.MAP_CONTROLS, 'vizer_urls': settings.VIZER_URLS, + 'vizer_ignore': settings.VIZER_IGNORE, + 'vizer_names': settings.VIZER_NAMES, 'model_packages': get_model_packages(), 'mapshed_max_area': settings.GWLFE_CONFIG['MaxAoIArea'] }), diff --git a/src/mmw/apps/modeling/tasks.py b/src/mmw/apps/modeling/tasks.py index 4923dc97e..8fe24403e 100644 --- a/src/mmw/apps/modeling/tasks.py +++ b/src/mmw/apps/modeling/tasks.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from __future__ import absolute_import +import os import logging import json import requests @@ -30,6 +31,9 @@ DRB = 'drb' +RWD_HOST = os.environ.get('RWD_HOST', 'localhost') +RWD_PORT = os.environ.get('RWD_PORT', '5000') + @shared_task def start_rwd_job(location, snapping, data_source): @@ -42,7 +46,8 @@ def start_rwd_job(location, snapping, data_source): location = json.loads(location) lat, lng = location end_point = 'rwd' if data_source == DRB else 'rwd-nhd' - rwd_url = 'http://localhost:5000/%s/%f/%f' % (end_point, lat, lng) + rwd_url = 'http://%s:%s/%s/%f/%f' % (RWD_HOST, RWD_PORT, end_point, + lat, lng) # The Webserver defaults to enable snapping, uses 1 (true) 0 (false) if not snapping: diff --git a/src/mmw/apps/user/middleware.py b/src/mmw/apps/user/middleware.py index 581e74154..addce43fe 100644 --- a/src/mmw/apps/user/middleware.py +++ b/src/mmw/apps/user/middleware.py @@ -1,34 +1,18 @@ # -*- coding: utf-8 -*- from django.conf import settings -from django.core.urlresolvers import reverse -from django.shortcuts import redirect -from apps.user.views import itsi_login EMBED_FLAG = settings.ITSI['embed_flag'] -LOGIN_URL = reverse(itsi_login) class ItsiAuthenticationMiddleware(object): """ - Middleware for automatically logging in ITSI users - and setting relevant flags. + Middleware for setting relevant ITSI flags. """ def process_request(self, request): - """ - Check if ITSI EMBED FLAG is set, and if so attempt to log - the user in with their ITSI credentials and redirect them - to current page. - """ - - # If flag is not set then return None so Django can proceed - # with the request as-is - if request.GET.get(EMBED_FLAG, 'false') != 'true': - return None - - # Flag is set. Assume user is not logged in. Set session flag and - # Redirect them to ITSI LOGIN URL, with the current URL to return to - request.session[EMBED_FLAG] = True - return redirect('{0}?next={1}'.format(LOGIN_URL, request.path)) + # If flag is set then set a session variable so it can be passed to + # front-end + if request.GET.get(EMBED_FLAG, 'false') == 'true': + request.session[EMBED_FLAG] = True diff --git a/src/mmw/img/stroud-logo.png b/src/mmw/img/stroud-logo.png index 164681bad..2ece95fbb 100644 Binary files a/src/mmw/img/stroud-logo.png and b/src/mmw/img/stroud-logo.png differ diff --git a/src/mmw/js/src/analyze/templates/catchmentWaterQualityTable.html b/src/mmw/js/src/analyze/templates/catchmentWaterQualityTable.html index 96643d586..80123b88c 100644 --- a/src/mmw/js/src/analyze/templates/catchmentWaterQualityTable.html +++ b/src/mmw/js/src/analyze/templates/catchmentWaterQualityTable.html @@ -11,7 +11,7 @@ Total P (kg/ha) - Total TSS (kg/ha) + Total SS (kg/ha) Avg TN (mg/l) diff --git a/src/mmw/js/src/analyze/templates/resultsWindow.html b/src/mmw/js/src/analyze/templates/resultsWindow.html index 62da0184c..fb2ad6c38 100644 --- a/src/mmw/js/src/analyze/templates/resultsWindow.html +++ b/src/mmw/js/src/analyze/templates/resultsWindow.html @@ -9,7 +9,7 @@
  • diff --git a/src/mmw/js/src/analyze/tests.js b/src/mmw/js/src/analyze/tests.js index c0abad626..5adf0e3a6 100644 --- a/src/mmw/js/src/analyze/tests.js +++ b/src/mmw/js/src/analyze/tests.js @@ -172,7 +172,7 @@ var tableHeaders = { animals: ['Animal', 'Count'], pointsource: ['NPDES Code', 'City', 'Discharge (MGD)', 'TN Load (kg/yr)', 'TP Load (kg/yr)'], catchment_water_quality: ['Id', 'Area (ha)', 'Total N (kg/ha)', 'Total P (kg/ha)', - 'Total TSS (kg/ha)', 'Avg TN (mg/l)', 'Avg TP (mg/l)', 'Avg TSS (mg/l)'], + 'Total SS (kg/ha)', 'Avg TN (mg/l)', 'Avg TP (mg/l)', 'Avg TSS (mg/l)'], }; function tableRows(type, result) { diff --git a/src/mmw/js/src/analyze/views.js b/src/mmw/js/src/analyze/views.js index a4e8127fc..9e5cc7b13 100644 --- a/src/mmw/js/src/analyze/views.js +++ b/src/mmw/js/src/analyze/views.js @@ -746,7 +746,9 @@ var PointSourceResultView = AnalyzeResultView.extend({ var CatchmentWaterQualityResultView = AnalyzeResultView.extend({ onShow: function() { - var desc = 'Delaware River Basin only: Stream Reach Assessment Tool model estimates', + var desc = 'Delaware River Basin only: ' + + 'Stream Reach Assessment Tool model estimates', chart = null; this.showAnalyzeResults(coreModels.CatchmentWaterQualityCensusCollection, CatchmentWaterQualityTableView, chart, desc); diff --git a/src/mmw/js/src/app.js b/src/mmw/js/src/app.js index 7c3df8d31..4fa0296ce 100644 --- a/src/mmw/js/src/app.js +++ b/src/mmw/js/src/app.js @@ -24,6 +24,7 @@ var App = new Marionette.Application({ // Initialize embed interface if in activity mode if (activityMode) { this.itsi = new itsi.ItsiEmbed(this); + this.itsi.getAuthInfo(); } // This view is intentionally not attached to any region. @@ -86,6 +87,12 @@ var App = new Marionette.Application({ return this._mapView._leafletMap; }, + getUserOrShowLoginIfNotItsiEmbed: function() { + if (!settings.get('itsi_embed')) { + this.getUserOrShowLogin(); + } + }, + getUserOrShowLogin: function() { this.user.fetch().always(function() { if (App.user.get('guest')) { @@ -115,18 +122,61 @@ function RestAPI() { } function initializeShutterbug() { + var googleTileLayerSelector = '#map > .leaflet-google-layer > div > div > div:nth-child(1) > div:nth-child(1)'; + $(window) .on('shutterbug-saycheese', function() { // Set fixed width before screenshot to constrain width to viewport $('#model-output-wrapper, body > .map-container').css({ 'width': window.innerWidth }); + + var mapView = App.getMapView(), + activeBaseLayer = mapView.baseLayers[mapView.getActiveBaseLayerName()], + googleLayerVisible = !!activeBaseLayer._google; + + if (googleLayerVisible) { + // Convert Google Maps CSS Transforms to Left / Right + var $googleTileLayer = $(googleTileLayerSelector), + transform = $googleTileLayer.css('transform').split(','), + left = parseFloat(transform[4]), + top = parseFloat(transform[5]); + + $googleTileLayer.css({ + transform: 'none', + left: left, + top: top, + }); + } + + // Fix Firefox screenshots by adding a '#' to the URL. + // Setting to empty string does nothing, so we first set to + // '/' then to empty string, which leaves a '#' in the URL. + document.location.hash = '/'; + document.location.hash = ''; }) .on('shutterbug-asyouwere', function() { // Reset after screenshot has been taken $('#model-output-wrapper, body > .map-container').css({ 'width': '' }); + + var mapView = App.getMapView(), + activeBaseLayer = mapView.baseLayers[mapView.getActiveBaseLayerName()], + googleLayerVisible = !!activeBaseLayer._google; + + if (googleLayerVisible) { + var $googleTileLayer = $(googleTileLayerSelector), + left = parseFloat($googleTileLayer.css('left')), + top = parseFloat($googleTileLayer.css('top')), + transform = 'matrix(1, 0, 0, 1, ' + left + ', ' + top + ')'; + + $googleTileLayer.css({ + transform: transform, + left: '', + top: '', + }); + } }); shutterbug.enable('body'); diff --git a/src/mmw/js/src/core/itsiEmbed.js b/src/mmw/js/src/core/itsiEmbed.js index 92d69a617..f78d332d2 100644 --- a/src/mmw/js/src/core/itsiEmbed.js +++ b/src/mmw/js/src/core/itsiEmbed.js @@ -40,9 +40,11 @@ var ItsiEmbed = function(App) { this.loadInteractive = function(interactiveState) { // Only redirect if route specified and different + // and user is logged in if (interactiveState && interactiveState.route && - interactiveState.route !== Backbone.history.getFragment()) { + interactiveState.route !== Backbone.history.getFragment() && + !App.user.get('guest')) { App.currentProject = null; router.navigate(interactiveState.route, { trigger: true }); @@ -53,10 +55,22 @@ var ItsiEmbed = function(App) { this.phone.post('extendedSupport', this.extendedSupport); }; + this.getAuthInfo = function() { + this.phone.post('getAuthInfo'); + }; + + this.authInfo = function(info) { + if (info && info.loggedIn && App.user.get('guest')) { + window.location.href = '/user/itsi/login?itsi_embed=true&next=/' + + Backbone.history.getFragment(); + } + }; + this.phone.addListener('getExtendedSupport', _.bind(this.getExtendedSupport, this)); this.phone.addListener('getLearnerUrl', _.bind(this.sendLearnerUrlOnlyFromProjectView, this)); this.phone.addListener('getInteractiveState', _.bind(this.sendLearnerUrlOnlyFromProjectView, this)); this.phone.addListener('loadInteractive', _.bind(this.loadInteractive, this)); + this.phone.addListener('authInfo', _.bind(this.authInfo, this)); this.phone.initialize(); }; diff --git a/src/mmw/js/src/core/latLngControl.js b/src/mmw/js/src/core/latLngControl.js new file mode 100644 index 000000000..e9f77663d --- /dev/null +++ b/src/mmw/js/src/core/latLngControl.js @@ -0,0 +1,50 @@ +'use strict'; + +var L = require('leaflet'); + +var PRECISION = 5; + +module.exports = L.Control.extend({ + options: { + position: 'bottomleft' + }, + + onAdd: function(map) { + var el = L.DomUtil.create('span'); + el.className = 'leaflet-latlng-control'; + + L.DomEvent.disableClickPropagation(el); + + map.on('mousemove', this.update, this); + map.on('click', this.log, this); + + this.el = el; + return el; + }, + + onRemove: function(map) { + map.off('mousemove', this.update, this); + map.off('click', this.log, this); + }, + + log: function(e) { + if (console && console.debug) { + console.debug(e.latlng.toString(PRECISION)); + } + }, + + update: function(e) { + this.latlng = e.latlng; + this.render(); + }, + + getLatLngString: function() { + var lat = L.Util.formatNum(this.latlng.lat, PRECISION), + lng = L.Util.formatNum(this.latlng.lng, PRECISION); + return 'Lat: ' + lat + ' Lng: ' + lng; + }, + + render: function() { + this.el.innerText = this.getLatLngString(); + } +}); diff --git a/src/mmw/js/src/core/layerControl.js b/src/mmw/js/src/core/layerControl.js index 01fbb4e11..295f55551 100644 --- a/src/mmw/js/src/core/layerControl.js +++ b/src/mmw/js/src/core/layerControl.js @@ -44,8 +44,11 @@ module.exports = L.Control.Layers.extend({ for (var i in observationLayers) { self._addLayer(observationLayers[i], i, 'observation'); } + + var num_points = JSON.parse(pointSourceData[0]).features.length; self._addLayer(pointSourceLayer.Layer.createLayer(pointSourceData[0], - self._map), 'DRB Point Source', 'observation'); + self._map), 'EPA Permitted Point Sources (' + num_points + ')', + 'observation'); self._update(); }) .fail(function() { diff --git a/src/mmw/js/src/core/models.js b/src/mmw/js/src/core/models.js index c6a4760c7..7af2816aa 100644 --- a/src/mmw/js/src/core/models.js +++ b/src/mmw/js/src/core/models.js @@ -4,7 +4,9 @@ var Backbone = require('../../shim/backbone'), $ = require('jquery'), _ = require('lodash'), turfArea = require('turf-area'), - utils = require('./utils'); + utils = require('./utils'), + drawUtils = require('../draw/utils'); + var MapModel = Backbone.Model.extend({ defaults: { @@ -257,10 +259,17 @@ var GeoModel = Backbone.Model.extend({ shape: null, // GeoJSON area: '0', units: 'm2', + isValidForAnalysis: true }, initialize: function() { + this.update(); + this.listenTo(this, 'change:shape', this.update); + }, + + update: function() { this.setDisplayArea(); + this.setValidForAnalysis(); }, setDisplayArea: function(shapeAttr, areaAttr, unitsAttr) { @@ -280,6 +289,11 @@ var GeoModel = Backbone.Model.extend({ this.set(area, areaInMeters / this.M_IN_KM); this.set(units, 'km2'); } + }, + + setValidForAnalysis: function() { + var shape = this.get('shape'); + this.set('isValidForAnalysis', drawUtils.isValidForAnalysis(shape)); } }); diff --git a/src/mmw/js/src/core/settings.js b/src/mmw/js/src/core/settings.js index 73c6cd9ae..8d481423a 100644 --- a/src/mmw/js/src/core/settings.js +++ b/src/mmw/js/src/core/settings.js @@ -9,6 +9,8 @@ var defaultSettings = { draw_tools: [], map_controls: [], vizer_urls: {}, + vizer_ignore: [], + vizer_names: {}, model_packages: [] }; diff --git a/src/mmw/js/src/core/templates/areaOfInterestHeader.html b/src/mmw/js/src/core/templates/areaOfInterestHeader.html index ddb26123e..079a401c8 100644 --- a/src/mmw/js/src/core/templates/areaOfInterestHeader.html +++ b/src/mmw/js/src/core/templates/areaOfInterestHeader.html @@ -8,11 +8,15 @@

    {{ place }}

    + {% if area > 0 %} + {% endif %}
    + {% if isValidForAnalysis %}
    + {% endif %} {% endif %} diff --git a/src/mmw/js/src/core/templates/layerControlList.html b/src/mmw/js/src/core/templates/layerControlList.html index 2b5936d90..ae2925862 100644 --- a/src/mmw/js/src/core/templates/layerControlList.html +++ b/src/mmw/js/src/core/templates/layerControlList.html @@ -5,12 +5,15 @@
    -
    +
    +

    Delaware River Basin only

    +
    +
    diff --git a/src/mmw/js/src/core/templates/observationPopup.html b/src/mmw/js/src/core/templates/observationPopup.html index d933796c3..199c41d7e 100644 --- a/src/mmw/js/src/core/templates/observationPopup.html +++ b/src/mmw/js/src/core/templates/observationPopup.html @@ -14,7 +14,7 @@

    {{ name }}

    - Data from
    {% if url %}{% endif %}{{ provider }}{% if url %}{% endif %} + Data from
    {% if url %}{% endif %}{{ providerName }}{% if url %}{% endif %}
    diff --git a/src/mmw/js/src/core/views.js b/src/mmw/js/src/core/views.js index 5465c5839..566be10b9 100644 --- a/src/mmw/js/src/core/views.js +++ b/src/mmw/js/src/core/views.js @@ -616,18 +616,11 @@ var MapView = Marionette.ItemView.extend({ if (additionalShapes) { _.each(additionalShapes.features, function(geoJSONpoint) { - function createMarkerIcon(iconName) { - return L.divIcon({ - className: 'marker-rwd marker-rwd-' + iconName, - iconSize: [16,16] - }); - } - var newLayer = L.geoJson(geoJSONpoint, { pointToLayer: function(feature, latLngForPoint) { var customIcon = feature.properties.original ? - createMarkerIcon('original-point') : - createMarkerIcon('nearest-stream-point'); + drawUtils.createRwdMarkerIcon('original-point') : + drawUtils.createRwdMarkerIcon('nearest-stream-point'); return L.marker(latLngForPoint, { icon: customIcon }); }, onEachFeature: function(feature, layer) { @@ -872,13 +865,16 @@ var AreaOfInterestView = Marionette.ItemView.extend({ template: areaOfInterestTmpl, initialize: function() { this.map = this.options.App.map; - this.listenTo(this.map, 'change areaOfInterest', this.syncArea); + this.listenTo(this.map, 'change:areaOfInterest', this.syncArea); }, - modelEvents: { 'change shape': 'render' }, + modelEvents: { 'change:shape': 'render' }, syncArea: function() { - this.model.set('shape', this.map.get('areaOfInterest')); + this.model.set({ + 'shape': this.map.get('areaOfInterest'), + 'place': this.map.get('areaOfInterestName'), + }); } }); diff --git a/src/mmw/js/src/core/vizerLayers.js b/src/mmw/js/src/core/vizerLayers.js index eaa067d67..ed4690aeb 100644 --- a/src/mmw/js/src/core/vizerLayers.js +++ b/src/mmw/js/src/core/vizerLayers.js @@ -10,13 +10,15 @@ var $ = require('jquery'), measurementTmpl = require('./templates/measurement.html'), measurementsTmpl = require('./templates/measurements.html'), popupTmpl = require('./templates/observationPopup.html'), - vizerUrls = require('./settings').get('vizer_urls'); + vizerUrls = require('./settings').get('vizer_urls'), + vizerIgnore = require('./settings').get('vizer_ignore'), + vizerNames = require('./settings').get('vizer_names'); // These are likely temporary until we develop custom icons for each type var platformIcons = { 'River Guage': '#F44336', - 'Weather Station': '#2196F3', - 'Fixed Shore Platform': '#4CAF50', + 'Weather Station': '#4CAF50', + 'Fixed Shore Platform': '#2196F3', 'Soil Pit': '#FFEB3B', 'Well': '#795548' }, @@ -41,7 +43,7 @@ function VizerLayers() { // A list of all assets (typically sensor devices) and the variables // they manage, along with various meta data grouped by the data // provider which acts as the "layer" which can be toggled on/off - var layerAssets = _.groupBy(assets.result, 'provider'); + var layerAssets = _.omit(_.groupBy(assets.result, 'provider'), vizerIgnore); var layers = _.map(layerAssets, function(assets, provider) { // Create a marker for each asset point in this layer and @@ -78,7 +80,7 @@ function VizerLayers() { function makeProviderLabel(provider, assets) { // Create a label for the layer selector. - return provider + ' (' + assets.length + ')'; + return (vizerNames[provider] || provider) + ' (' + assets.length + ')'; } function attachPopups(featureGroup) { @@ -182,9 +184,15 @@ var ObservationPopupView = Marionette.LayoutView.extend({ return time; } return latestTime; - }); + }), + provider = this.model.get('provider'), + providerName = + provider === "NOS/CO-OPS" ? + provider : + vizerNames[provider]; return { - lastUpdated: moment(latestTime).fromNow() + lastUpdated: moment(latestTime).fromNow(), + providerName: providerName, }; }, diff --git a/src/mmw/js/src/draw/controllers.js b/src/mmw/js/src/draw/controllers.js index 6ed395cbb..83c4f5529 100644 --- a/src/mmw/js/src/draw/controllers.js +++ b/src/mmw/js/src/draw/controllers.js @@ -32,20 +32,21 @@ var DrawController = { enableSingleProjectModeIfActivity(); - if (App.map.get('areaOfInterest')) { - var aoiView = new coreViews.AreaOfInterestView({ - id: 'aoi-header-wrapper', - App: App, - model: new coreModels.AreaOfInterestModel({ - can_go_back: false, - next_label: 'Analyze', - url: 'analyze', - shape: App.map.get('areaOfInterest'), - place: App.map.get('areaOfInterestName') - }) - }); + var aoiView = new coreViews.AreaOfInterestView({ + id: 'aoi-header-wrapper', + App: App, + model: new coreModels.AreaOfInterestModel({ + can_go_back: false, + next_label: 'Analyze', + url: 'analyze', + shape: App.map.get('areaOfInterest'), + place: App.map.get('areaOfInterestName') + }) + }); - App.rootView.footerRegion.show(aoiView); + App.rootView.footerRegion.show(aoiView); + + if (App.map.get('areaOfInterest')) { App.map.setDrawWithBarSize(true); } diff --git a/src/mmw/js/src/draw/models.js b/src/mmw/js/src/draw/models.js index ec6135035..8c6f2d57e 100644 --- a/src/mmw/js/src/draw/models.js +++ b/src/mmw/js/src/draw/models.js @@ -20,6 +20,13 @@ var ToolbarModel = Backbone.Model.extend({ disableTools: function() { this.set('toolsEnabled', false); + }, + + clearRwdClickedPoint: function(map) { + if (map && this.has('rwd-original-point')) { + map.removeLayer(this.get('rwd-original-point')); + this.unset('rwd-original-point'); + } } }); diff --git a/src/mmw/js/src/draw/templates/delineationOptions.html b/src/mmw/js/src/draw/templates/delineationOptions.html index b340e3bf8..a2657ec57 100644 --- a/src/mmw/js/src/draw/templates/delineationOptions.html +++ b/src/mmw/js/src/draw/templates/delineationOptions.html @@ -19,7 +19,7 @@
  • - Mid-Atlantic Medium Resolution Continental US Medium Resolution
  • diff --git a/src/mmw/js/src/draw/tests.js b/src/mmw/js/src/draw/tests.js index 9e1154ae2..eb17723bd 100644 --- a/src/mmw/js/src/draw/tests.js +++ b/src/mmw/js/src/draw/tests.js @@ -16,9 +16,15 @@ var $ = require('jquery'), var sandboxId = 'sandbox', sandboxSelector = '#' + sandboxId, + // City Hall TEST_SHAPE = { 'type': 'MultiPolygon', - 'coordinates': [[[-5e6, -1e6], [-4e6, 1e6], [-3e6, -1e6]]] + 'coordinates': [[[ + [-75.16472339630127, 39.953446247674904], + [-75.16255617141724, 39.95311727224624], + [-75.16287803649902, 39.95161218948083], + [-75.16518473625183, 39.95194939669509], + [-75.16472339630127, 39.953446247674904]]]] }; var SandboxRegion = Marionette.Region.extend({ diff --git a/src/mmw/js/src/draw/utils.js b/src/mmw/js/src/draw/utils.js index d3488d376..31f4f927c 100644 --- a/src/mmw/js/src/draw/utils.js +++ b/src/mmw/js/src/draw/utils.js @@ -2,7 +2,12 @@ var $ = require('jquery'), L = require('leaflet'), - _ = require('lodash'); + _ = require('lodash'), + turfArea = require('turf-area'), + coreUtils = require('../core/utils'); + +// Keep in sync with src/api/main.py in rapid-watershed-delineation. +var MAX_AREA = 112700; // About the size of a large state (in km^2) var polygonDefaults = { fillColor: '#E77471', @@ -67,16 +72,41 @@ function placeMarker(map, drawOpts) { return defer.promise(); } +function createRwdMarkerIcon(iconName) { + return L.divIcon({ + className: 'marker-rwd marker-rwd-' + iconName, + iconSize: [16,16] + }); +} + // Cancel any previous draw action in progress. function cancelDrawing(map) { map.fire('draw:drawstop'); } +// Return shape area in km2. +function shapeArea(shape) { + return coreUtils.changeOfAreaUnits(turfArea(shape), + 'm2', 'km2'); +} + +function isValidForAnalysis(shape) { + if (shape) { + var area = shapeArea(shape); + return area > 0 && area <= MAX_AREA; + } + return false; +} + module.exports = { drawPolygon: drawPolygon, placeMarker: placeMarker, + createRwdMarkerIcon: createRwdMarkerIcon, cancelDrawing: cancelDrawing, polygonDefaults: polygonDefaults, + shapeArea: shapeArea, + isValidForAnalysis: isValidForAnalysis, NHD: 'nhd', DRB: 'drb', + MAX_AREA: MAX_AREA }; diff --git a/src/mmw/js/src/draw/views.js b/src/mmw/js/src/draw/views.js index 052d08c2e..177fcfe0e 100644 --- a/src/mmw/js/src/draw/views.js +++ b/src/mmw/js/src/draw/views.js @@ -4,7 +4,6 @@ var $ = require('jquery'), _ = require('lodash'), L = require('leaflet'), Marionette = require('../../shim/backbone.marionette'), - turfArea = require('turf-area'), turfBboxPolygon = require('turf-bbox-polygon'), turfDestination = require('turf-destination'), turfIntersect = require('turf-intersect'), @@ -14,6 +13,7 @@ var $ = require('jquery'), utils = require('./utils'), models = require('./models'), coreUtils = require('../core/utils'), + drawUtils = require('../draw/utils'), toolbarTmpl = require('./templates/toolbar.html'), loadingTmpl = require('./templates/loading.html'), selectTypeTmpl = require('./templates/selectType.html'), @@ -24,9 +24,18 @@ var $ = require('jquery'), modalModels = require('../core/modals/models'), modalViews = require('../core/modals/views'); -var MAX_AREA = 112700; // About the size of a large state (in km^2) var codeToLayer = {}; // code to layer mapping +function displayAlert(message, alertType) { + var alertView = new modalViews.AlertView({ + model: new modalModels.AlertModel({ + alertMessage: message, + alertType: alertType + }) + }); + alertView.render(); +} + function actOnUI(datum, bool) { var code = datum.code, $el = $('[data-layer-code="' + code + '"]'); @@ -49,7 +58,8 @@ function validateRwdShape(result) { var d = new $.Deferred(); if (result.watershed) { if (result.watershed.features[0].geometry.type === 'MultiPolygon') { - d.reject('Unable to generate a valid watershed area at this location'); + d.reject('Unfortunately, the watershed generated at this ' + + 'location is not available for analysis'); } validateShape(result.watershed) .done(function() { @@ -64,36 +74,21 @@ function validateRwdShape(result) { } function validateShape(polygon) { - var area = coreUtils.changeOfAreaUnits(turfArea(polygon), 'm2', 'km2'), - d = new $.Deferred(); - var selfIntersectingShape = turfKinks(polygon).features.length > 0; - var alertView; + var d = new $.Deferred(), + selfIntersectingShape = turfKinks(polygon).features.length > 0; if (selfIntersectingShape) { var errorMsg = 'This watershed shape is invalid because it intersects ' + 'itself. Try drawing the shape again without crossing ' + 'over its own border.'; - alertView = new modalViews.AlertView({ - model: new modalModels.AlertModel({ - alertMessage: errorMsg, - alertType: modalModels.AlertTypes.warn - }) - }); - - alertView.render(); d.reject(errorMsg); - } else if (area > MAX_AREA) { - var message = 'Sorry, your Area of Interest is too large.\n\n' + - Math.floor(area).toLocaleString() + ' km² were selected, ' + - 'but the maximum supported size is currently ' + - MAX_AREA.toLocaleString() + ' km².'; - alertView = new modalViews.AlertView({ - model: new modalModels.AlertModel({ - alertMessage: message, - alertType: modalModels.AlertTypes.warn - }) - }); - alertView.render(); + } else if (!utils.isValidForAnalysis(polygon)) { + var maxArea = utils.MAX_AREA.toLocaleString(), + selectedArea = Math.floor(utils.shapeArea(polygon)).toLocaleString(), + message = 'Sorry, the area you have delineated is too large ' + + 'to analyze or model. ' + selectedArea + ' km² were ' + + 'selected, but the maximum supported size is ' + + 'currently ' + maxArea + ' km².'; d.reject(message); } else { d.resolve(polygon); @@ -107,21 +102,21 @@ function validatePointWithinDataSourceBounds(latlng, dataSource) { perimeter = null, point_outside_message = null; - switch (dataSource) { - case utils.DRB: - var streamLayers = settings.get('stream_layers'); - perimeter = _.findWhere(streamLayers, {code:'drb_streams_v2'}).perimeter; - point_outside_message = 'Selected point is outside the Delaware River Basin'; - break; - case utils.NHD: - perimeter = settings.get('nhd_perimeter'); - point_outside_message = 'Selected point is outside the NHD Mid-Atlantic Region'; - break; - default: - var message = 'Not a valid data source'; - d.reject(message); - return d.promise(); - } + switch (dataSource) { + case utils.DRB: + var streamLayers = settings.get('stream_layers'); + perimeter = _.findWhere(streamLayers, {code:'drb_streams_v2'}).perimeter; + point_outside_message = 'Selected point is outside the Delaware River Basin'; + break; + case utils.NHD: + // Bounds checking disabled until #1656 is complete. + d.resolve(latlng); + return d.promise(); + default: + var message = 'Not a valid data source'; + d.reject(message); + return d.promise(); + } if (turfIntersect(point, perimeter)) { d.resolve(latlng); @@ -330,8 +325,9 @@ var DrawView = Marionette.ItemView.extend({ .then(function(shape) { addLayer(shape); navigateToAnalyze(); - }).fail(function() { + }).fail(function(message) { revertLayer(); + displayAlert(message, modalModels.AlertTypes.error); }).always(function() { self.model.enableTools(); }); @@ -376,8 +372,9 @@ var DrawView = Marionette.ItemView.extend({ addLayer(box, '1 Square Km'); navigateToAnalyze(); - }).fail(function() { + }).fail(function(message) { revertLayer(); + displayAlert(message, modalModels.AlertTypes.error); }).always(function() { self.model.enableTools(); }); @@ -435,6 +432,16 @@ var WatershedDelineationView = Marionette.ItemView.extend({ .then(function(latlng) { return validatePointWithinDataSourceBounds(latlng, dataSource); }) + .then(function(latlng) { + // Set `rwd-original-point` in the model so it can be + // removed when the deferred chain completes. + var rwdClickedPoint = L.marker(latlng, { + icon: drawUtils.createRwdMarkerIcon('original-point') + }).bindPopup('Original clicked outlet point'); + self.model.set('rwd-original-point', rwdClickedPoint); + rwdClickedPoint.addTo(map); + return latlng; + }) .then(function(latlng) { return self.delineateWatershed(latlng, snappingOn, dataSource); }) @@ -446,19 +453,11 @@ var WatershedDelineationView = Marionette.ItemView.extend({ navigateToAnalyze(); }) .fail(function(message) { - clearAoiLayer(); - if (message) { - var alertView = new modalViews.AlertView({ - model: new modalModels.AlertModel({ - alertMessage: message, - alertType: modalModels.AlertTypes.error - }) - }); - - alertView.render(); - } + displayAlert(message, modalModels.AlertTypes.warn); }) .always(function() { + self.model.clearRwdClickedPoint(map); + self.model.enableTools(); }); }, @@ -475,9 +474,18 @@ var WatershedDelineationView = Marionette.ItemView.extend({ pollSuccess: function(response) { self.model.set('polling', false); - var result = JSON.parse(response.result); - // Convert watershed to MultiPolygon to pass shape validation. - result.watershed = coreUtils.toMultiPolygon(result.watershed); + + var result; + try { + result = JSON.parse(response.result); + } catch (ex) { + return this.pollFailure(); + } + + if (result.watershed) { + // Convert watershed to MultiPolygon to pass shape validation. + result.watershed = coreUtils.toMultiPolygon(result.watershed); + } deferred.resolve(result); }, @@ -559,6 +567,7 @@ var ResetDrawView = Marionette.ItemView.extend({ }, resetDrawingState: function() { + this.model.clearRwdClickedPoint(App.getLeafletMap()); this.rwdTaskModel.reset(); this.model.set({ polling: false, @@ -643,15 +652,22 @@ function getShapeAndAnalyze(e, model, ofg, grid, layerCode, layerName) { function clearAoiLayer() { var projectNumber = App.projectNumber, - previousShape = App.map.get('areaOfInterest'); + previousShape = App.map.get('areaOfInterest'), + previousShapeName = App.map.get('areaOfInterestName'); - App.map.set('areaOfInterest', null); + App.map.set({ + 'areaOfInterest': null, + 'areaOfInterestName': '' + }); App.projectNumber = undefined; App.map.setDrawSize(false); App.clearAnalyzeCollection(); return function revertLayer() { - App.map.set('areaOfInterest', previousShape); + App.map.set({ + 'areaOfInterest': previousShape, + 'areaOfInterestName': previousShapeName + }); App.projectNumber = projectNumber; }; } diff --git a/src/mmw/js/src/main.js b/src/mmw/js/src/main.js index 5bce7c235..b858449f6 100644 --- a/src/mmw/js/src/main.js +++ b/src/mmw/js/src/main.js @@ -23,7 +23,7 @@ App.on('start', function() { // Show login modal only if landing on home page if (Backbone.history.getFragment() === '') { - this.getUserOrShowLogin(); + this.getUserOrShowLoginIfNotItsiEmbed(); } else { this.user.fetch(); } diff --git a/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html b/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html index 06d8a9895..7f1c21e39 100644 --- a/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html +++ b/src/mmw/js/src/modeling/gwlfe/quality/templates/result.html @@ -1 +1,6 @@ +

    + Average annual loads + from 30-years of daily fluxes + simulated by the GWLF-E (MapShed) model. +

    diff --git a/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html b/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html index 233917277..ad0ebb5d6 100644 --- a/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html +++ b/src/mmw/js/src/modeling/gwlfe/runoff/templates/result.html @@ -1,3 +1,8 @@ +

    + Average monthly water fluxes in centimeters + from 30-years of daily water balance + simulated by the GWLF-E (MapShed) model. +

    diff --git a/src/mmw/js/src/modeling/tests.js b/src/mmw/js/src/modeling/tests.js index 9837ef573..add0276e0 100644 --- a/src/mmw/js/src/modeling/tests.js +++ b/src/mmw/js/src/modeling/tests.js @@ -675,14 +675,14 @@ describe('Modeling', function() { }); model.updateModificationHash(); - assert.equal(model.get('modification_hash'), '65af065d7205cd998aeb0bf15c41f256'); + assert.equal(model.get('modification_hash'), '582b6178186440159bd4ea25f0260892'); var mod = new models.ModificationModel(mocks.modifications.sample2); model.get('modifications').add(mod); - assert.equal(model.get('modification_hash'), 'ae69e823f926824a2fc22d9a5f1ea62c'); + assert.equal(model.get('modification_hash'), 'd95d0c983f0e3d0b171aaa0a84540205'); model.get('modifications').remove(mod); - assert.equal(model.get('modification_hash'), '65af065d7205cd998aeb0bf15c41f256'); + assert.equal(model.get('modification_hash'), '582b6178186440159bd4ea25f0260892'); }); it('is called when the modifications for a scenario changes', function() { @@ -963,6 +963,49 @@ describe('Modeling', function() { assert.isTrue(spyAlert.calledOnce); assert.equal(collection.at(1).get('name'), 'New Scenario 1'); }); + + it('will not show error when trying to save name as is', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text('New Scenario 1'); + childViews[1].ui.nameField.trigger('blur'); + + assert.isFalse(spyAlert.calledOnce); + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); + + it('resets name if set to empty string', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text(''); + childViews[1].ui.nameField.trigger('blur'); + + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); + + it('resets name if set to just spaces', function() { + var collection = getTestScenarioCollection(), + view = new views.ScenarioTabPanelsView({ collection: collection }); + + view.render(); + var childViews = _.values(view.children._views); + + childViews[1].ui.rename.trigger('click'); + childViews[1].ui.nameField.text(' '); + childViews[1].ui.nameField.trigger('blur'); + + assert.equal(collection.at(1).get('name'), 'New Scenario 1'); + }); }); describe('#duplicateScenario', function() { diff --git a/src/mmw/js/src/modeling/tr55/quality/templates/result.html b/src/mmw/js/src/modeling/tr55/quality/templates/result.html index 1b9351752..72808f68d 100644 --- a/src/mmw/js/src/modeling/tr55/quality/templates/result.html +++ b/src/mmw/js/src/modeling/tr55/quality/templates/result.html @@ -1,2 +1,6 @@ +

    + Total loads delivered in a 24-hour hypothetical storm event + as simulated by EPA's STEP-L model algorithms. +

    diff --git a/src/mmw/js/src/modeling/tr55/runoff/templates/result.html b/src/mmw/js/src/modeling/tr55/runoff/templates/result.html index 011f052a6..658e05d86 100644 --- a/src/mmw/js/src/modeling/tr55/runoff/templates/result.html +++ b/src/mmw/js/src/modeling/tr55/runoff/templates/result.html @@ -1,2 +1,6 @@ +

    + Results of a 24-hour hypothetical storm event + as simulated by SLAMM and TR-55 model algorithms. +

    diff --git a/src/mmw/js/src/modeling/views.js b/src/mmw/js/src/modeling/views.js index dd0a5cc1b..92bc3aaaf 100644 --- a/src/mmw/js/src/modeling/views.js +++ b/src/mmw/js/src/modeling/views.js @@ -196,7 +196,7 @@ var ProjectMenuView = Marionette.ItemView.extend({ .fail(function() { var alertView = new modalViews.AlertView({ model: new modalModels.AlertModel({ - alertMessage: 'Could not delete this project.', + alertMessage: 'Could not delete this project.', alertType: modalModels.AlertTypes.error }) }); @@ -399,7 +399,9 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ return model.get('name').toLowerCase() === newName.toLowerCase(); }); - if (match) { + if (model.get('name') === newName || !newName){ + return false; + } else if (match) { console.log('This name is already in use.'); var alertView = new modalViews.AlertView({ model: new modalModels.AlertModel({ @@ -411,17 +413,27 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ alertView.render(); return false; - } else if (model.get('name') !== newName) { + } else { return model.set('name', newName); } }, - setScenarioName = function(name) { - if (!updateScenarioName(self.model, name)) { - self.render(); // resets view state - } - }; + setScenarioName = function(name) { + if (!updateScenarioName(self.model, name)) { + self.render(); // resets view state + } + }, + + selectScenarioTabText = function() { + // Select scenario name's text + var range = document.createRange(), + selection = window.getSelection(); + range.selectNodeContents(self.ui.nameField[0]); + selection.removeAllRanges(); + selection.addRange(range); + }; + selectScenarioTabText(); this.ui.nameField.attr('contenteditable', true).focus(); this.ui.nameField.on('keyup', function(e) { @@ -441,6 +453,12 @@ var ScenarioTabPanelView = Marionette.ItemView.extend({ } }); + this.ui.nameField.on('click', function(e) { + // Don't let the outer swallow clicks and exit rename mode + // Allows selecting text on double click + e.stopImmediatePropagation(); + }); + this.ui.nameField.on('blur', function() { setScenarioName($(this).text()); }); diff --git a/src/mmw/js/src/user/views.js b/src/mmw/js/src/user/views.js index 6d6339b9e..8941397f6 100644 --- a/src/mmw/js/src/user/views.js +++ b/src/mmw/js/src/user/views.js @@ -246,7 +246,7 @@ var LoginModalView = ModalBaseView.extend({ var loginURL = '/user/itsi/login?next=/' + Backbone.history.getFragment(); if (window.clientSettings.itsi_embed) { - loginURL = '/?itsi_embed=true&next=/' + Backbone.history.getFragment(); + loginURL += '&itsi_embed=true'; } window.location.href = loginURL; } diff --git a/src/mmw/mmw/settings/base.py b/src/mmw/mmw/settings/base.py index 7d0095383..3c088e0c6 100644 --- a/src/mmw/mmw/settings/base.py +++ b/src/mmw/mmw/settings/base.py @@ -12,7 +12,8 @@ from os.path import abspath, basename, dirname, join, normpath from sys import path -from layer_settings import LAYERS, VIZER_URLS, NHD_REGION2_PERIMETER, DRB_PERIMETER # NOQA +from layer_settings import (LAYERS, VIZER_URLS, VIZER_IGNORE, VIZER_NAMES, + NHD_REGION2_PERIMETER, DRB_PERIMETER) # NOQA from gwlfe_settings import (GWLFE_DEFAULTS, GWLFE_CONFIG, SOIL_GROUP, # NOQA SOILP, CURVE_NUMBER) # NOQA diff --git a/src/mmw/mmw/settings/layer_settings.py b/src/mmw/mmw/settings/layer_settings.py index 7c6b074cb..edb5806c9 100644 --- a/src/mmw/mmw/settings/layer_settings.py +++ b/src/mmw/mmw/settings/layer_settings.py @@ -41,6 +41,7 @@ NHD_REGION2_PERIMETER = json.load(nhd_region2_simple_perimeter_file) +NEW_LINE_AND_TAB = '
         ' LAYERS = [ { @@ -144,7 +145,7 @@ { 'code': 'nhd_streams_v2', 'display': ('Continental US Medium Resolution' + - '
         Stream Network'), + NEW_LINE_AND_TAB + 'Stream Network'), 'table_name': 'nhdflowline', 'stream': True, 'overlay': True, @@ -153,7 +154,7 @@ { 'code': 'drb_streams_v2', 'display': ('Delaware River Basin High Resolution' + - '
         Stream Network'), + NEW_LINE_AND_TAB + 'Stream Network'), 'table_name': 'drb_streams_50', 'stream': True, 'overlay': True, @@ -162,24 +163,27 @@ # overlaps with perimeter polygon. }, { - 'code': 'nhd_quality_tp', - 'display': 'Delaware River Basin TP Concentration', - 'table_name': 'nhd_quality_tp', + 'code': 'nhd_quality_tn', + 'display': ('Delaware River Basin TN Concentration' + + NEW_LINE_AND_TAB + 'From SRAT'), + 'table_name': 'nhd_quality_tn', 'stream': True, 'overlay': True, 'minZoom': 3 }, { - 'code': 'nhd_quality_tn', - 'display': 'Delaware River Basin TN Concentration', - 'table_name': 'nhd_quality_tn', + 'code': 'nhd_quality_tp', + 'display': ('Delaware River Basin TP Concentration' + + NEW_LINE_AND_TAB + 'From SRAT'), + 'table_name': 'nhd_quality_tp', 'stream': True, 'overlay': True, 'minZoom': 3 }, { 'code': 'nhd_quality_tss', - 'display': 'Delaware River Basin TSS Concentration', + 'display': ('Delaware River Basin TSS Concentration' + + NEW_LINE_AND_TAB + 'From SRAT'), 'table_name': 'nhd_quality_tss', 'stream': True, 'overlay': True, @@ -199,7 +203,7 @@ 'has_opacity_slider': True }, { - 'display': 'Hydrologic Soil Groups', + 'display': 'Hydrologic Soil Groups From gSSURGO', 'short_display': 'SSURGO', 'helptext': 'Soils are classified by the Natural Resource Conservation ' 'Service into four Hydrologic Soil Groups based on the ' @@ -278,7 +282,7 @@ { 'code': 'drb_catchment_water_quality_tn', 'display': ('DRB Catchment Water Quality Data' + - '
         TN Loading Rates'), + NEW_LINE_AND_TAB + 'TN Loading Rates from SRAT Catchments'), 'table_name': 'drb_catchment_water_quality_tn', 'raster': True, 'overlay': True, @@ -290,7 +294,7 @@ { 'code': 'drb_catchment_water_quality_tp', 'display': ('DRB Catchment Water Quality Data' + - '
         TP Loading Rates'), + NEW_LINE_AND_TAB + 'TP Loading Rates from SRAT Catchments'), 'table_name': 'drb_catchment_water_quality_tp', 'raster': True, 'overlay': True, @@ -302,7 +306,7 @@ { 'code': 'drb_catchment_water_quality_tss', 'display': ('DRB Catchment Water Quality Data' + - '
         TSS Loading Rates'), + NEW_LINE_AND_TAB + 'TSS Loading Rates from SRAT Catchments'), 'table_name': 'drb_catchment_water_quality_tss', 'raster': True, 'overlay': True, @@ -327,3 +331,17 @@ 'variable': VIZER_ROOT + 'opt=data&asset_id={{asset_id}}&var_id={{var_id}}' + VIZER_TYPE_PARAM, # NOQA 'recent': VIZER_ROOT + 'opt=recent_values&asset_id={{asset_id}}&var_id=all' + VIZER_TYPE_PARAM # NOQA } + +# To hide elements from the UI +VIZER_IGNORE = [ + 'CRBCZO', + 'SWRC', + 'USCRN' +] + +# To give friendly names in the UI +VIZER_NAMES = { + 'DEOS': 'Delaware Environmental Observing System', + 'NOS/CO-OPS': 'NOAA Tides and Currents', + 'USGS': 'USGS National Water Information System' +} diff --git a/src/mmw/sass/pages/_model.scss b/src/mmw/sass/pages/_model.scss index e88d62127..4a73733b8 100644 --- a/src/mmw/sass/pages/_model.scss +++ b/src/mmw/sass/pages/_model.scss @@ -49,6 +49,11 @@ height: 100%; width: 100%; + .result-text { + font-size: 0.8rem; + margin-bottom: 20px; + } + .mean-flow { font-size: 16px; } diff --git a/src/mmw/sass/pages/_search-map.scss b/src/mmw/sass/pages/_search-map.scss index 6a1986d8e..65695a193 100644 --- a/src/mmw/sass/pages/_search-map.scss +++ b/src/mmw/sass/pages/_search-map.scss @@ -2,7 +2,6 @@ #search-map { position: absolute; top: 0; - left: 30px; z-index: 100; padding: 1rem; diff --git a/src/tiler/server.js b/src/tiler/server.js index a5a12676f..ba313ef4a 100644 --- a/src/tiler/server.js +++ b/src/tiler/server.js @@ -103,6 +103,8 @@ var interactivity = { stream_order = 6; } else if (zoom <= 8) { stream_order = 5; + } else if (zoom <= 9) { + stream_order = 4; } } diff --git a/src/tiler/styles.mss b/src/tiler/styles.mss index 3b55d8a90..4fbd55b51 100644 --- a/src/tiler/styles.mss +++ b/src/tiler/styles.mss @@ -155,7 +155,7 @@ } } -#drb_streams_50[zoom<=4], + #nhdflowline[zoom<=4], #nhd_quality_tp[zoom<=4], #nhd_quality_tn[zoom<=4], @@ -171,7 +171,7 @@ } } -#drb_streams_50[zoom>=5][zoom<=6], + #nhdflowline[zoom>=5][zoom<=6], #nhd_quality_tp[zoom>=5][zoom<=6], #nhd_quality_tn[zoom>=5][zoom<=6], @@ -187,7 +187,7 @@ } } -#drb_streams_50[zoom>=7][zoom<=8], + #nhdflowline[zoom>=7][zoom<=8], #nhd_quality_tp[zoom>=7][zoom<=8], #nhd_quality_tn[zoom>=7][zoom<=8], @@ -206,7 +206,7 @@ } } -#drb_streams_50[zoom>=9][zoom<=10], + #nhdflowline[zoom>=9][zoom<=10], #nhd_quality_tp[zoom>=9][zoom<=10], #nhd_quality_tn[zoom>=9][zoom<=10], @@ -221,11 +221,11 @@ line-width: 5.0 * @zoomBase; } [stream_order=3] { - line-width: 4.0 * @zoomBase; + line-width: 3.0 * @zoomBase; } } -#drb_streams_50[zoom>=11][zoom<=12], + #nhdflowline[zoom>=11][zoom<=12], #nhd_quality_tp[zoom>=11][zoom<=12], #nhd_quality_tn[zoom>=11][zoom<=12], @@ -251,7 +251,7 @@ } -#drb_streams_50[zoom>=13], + #nhdflowline[zoom>=13], #nhd_quality_tp[zoom>=13], #nhd_quality_tn[zoom>=13], @@ -272,3 +272,98 @@ line-width: 5.0 * @zoomBase; } } + +#drb_streams_50[zoom<=4]{ + [stream_order=10] { + line-width: 3.0 * @zoomBase; + } + [stream_order=9] { + line-width: 3.0 * @zoomBase; + } + [stream_order<=8] { + line-width: 2.0 * @zoomBase; + } +} + +#drb_streams_50[zoom>=5][zoom<=6]{ + [stream_order=10] { + line-width: 4.0 * @zoomBase; + } + [stream_order=9] { + line-width: 3.0 * @zoomBase; + } + [stream_order<=8] { + line-width: 2.0 * @zoomBase; + } +} + +#drb_streams_50[zoom>=7][zoom<=8]{ + [stream_order>=9] { + line-width: 7.0 * @zoomBase; + } + [stream_order<=8][stream_order>=7] { + line-width: 4.0 * @zoomBase; + } + [stream_order<=6] { + line-width: 3.0 * @zoomBase; + } + [stream_order=5] { + line-width: 2.0 * @zoomBase; + } +} + +#drb_streams_50[zoom>=9][zoom<=10]{ + [stream_order>=9] { + line-width: 10.0 * @zoomBase; + } + [stream_order<=8][stream_order>=6] { + line-width: 5.0 * @zoomBase; + } + [stream_order<=5][stream_order>=4] { + line-width: 3.0 * @zoomBase; + } + [stream_order=3] { + line-width: 2.0 * @zoomBase; + } +} + +#drb_streams_50[zoom>=11][zoom<=12] { + [stream_order>=9] { + line-width: 12.0 * @zoomBase; + } + [stream_order<=8][stream_order>=6] { + line-width: 10.0 * @zoomBase; + } + [stream_order<=5][stream_order>=4] { + line-width: 6.0 * @zoomBase; + } + [stream_order=3] { + line-width: 5.0 * @zoomBase; + } + [stream_order=2] { + line-width: 4.0 * @zoomBase; + } + [stream_order<=1][stream_order>=0] { + line-width: 3.0 * @zoomBase; + } +} +#drb_streams_50[zoom>=13]{ + [stream_order>=9] { + line-width: 14.0 * @zoomBase; + } + [stream_order<=8][stream_order>=6] { + line-width: 11.0 * @zoomBase; + } + [stream_order<=5][stream_order>=4] { + line-width: 8.0 * @zoomBase; + } + [stream_order=3] { + line-width: 5.0 * @zoomBase; + } + [stream_order=2] { + line-width: 4.0 * @zoomBase; + } + [stream_order<=1][stream_order>=0] { + line-width: 3.0 * @zoomBase; + } +}