diff --git a/.gitignore b/.gitignore index 48fb168f..7af77682 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,27 @@ # Ignore bundler config. /.bundle +vendor/bundle +coverage/ # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep +.DS_Store # Ignore Byebug command history file. .byebug_history + +# Ignore coverage reports +coverage + +# Ignore public compiled assets +public/assets/ + +# Ignore .env configuration +.env* + +# Ignore schema +db/schema.rb diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..279202f2 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,27 @@ +AllCops: + Exclude: + - 'db/schema.rb' + +Metrics/LineLength: + Max: 199 + +Style/BlockComments: + Enabled: false + +Style/EmptyMethod: + Enabled: false + +Style/Encoding: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/LeadingCommentSpace: + Enabled: false + +Style/SpaceInsideBrackets: + Enabled: false + +Style/StringLiterals: + Enabled: false diff --git a/.ruby-version b/.ruby-version index 2bf1c1cc..0bee604d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.3.3 diff --git a/Gemfile b/Gemfile index 4c7e5d3b..32fbd40e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,20 +1,25 @@ source 'https://rubygems.org' - gem 'coffee-rails', '~> 4.2' # Use CoffeeScript for .coffee assets and views +gem 'dotenv-rails' # Use dotenv to load environment variables +gem 'excon-rails' # Use excon rails for http requests +gem 'font-awesome-rails', '>= 4.7.0' # Use Font Awesome for CSS Icons +gem 'i18n-js', ">= 3.0.0.rc15" #extend i18n support directly into JS gem 'jbuilder', '~> 2.5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jquery-rails' # Use jquery as the JavaScript library gem 'pg' # Use PostgreSQL as the database for Active Record gem 'puma', '~> 3.0' # Use Puma as the app server -gem 'rails', '~> 5.0.0', '>= 5.0.0.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 5.0.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rb-readline' # Why is this suddenly such a problem? gem 'sass-rails', '~> 5.0' # Use SCSS for stylesheets -gem 'turbolinks', '~> 5' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'uglifier', '>= 1.3.0' # Use Uglifier as compressor for JavaScript assets +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +# gem 'turbolinks', '~> 5' + # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby - # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password @@ -24,12 +29,24 @@ gem 'uglifier', '>= 1.3.0' # Use Uglifier as compressor for JavaScript assets # gem 'capistrano-rails', group: :development group :development, :test do + gem "brakeman", require: false # code analysis (http://brakemanscanner.org) gem 'byebug', platform: :mri # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'rspec-rails', '~> 3.5' + gem 'capybara' # allow interaction with DOM in tests + gem 'chromedriver-helper' + # gem 'chunky_png' # read png images + gem 'jasmine-rails' # JavaScript testing + gem 'jshint' + gem 'launchy' + gem 'poltergeist' + gem 'rspec-rails', '~> 3.5' # Use RSpec for tests + gem 'rubocop' # Enforce ruby code style + gem 'selenium-webdriver' + gem 'simplecov', require: false # determine code coverage of tests + gem 'teaspoon-jasmine' end group :development do - gem 'better_errors' + gem 'better_errors' # show a much more friendly error page with a command line REPL gem 'binding_of_caller' # used by better_errors to provide a REPL in the error window gem 'listen', '~> 3.0.5' gem 'spring' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring diff --git a/Gemfile.lock b/Gemfile.lock index 1d36497c..2bf38bc8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,52 +1,71 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.0.0.1) - actionpack (= 5.0.0.1) + actioncable (5.0.1) + actionpack (= 5.0.1) nio4r (~> 1.2) websocket-driver (~> 0.6.1) - actionmailer (5.0.0.1) - actionpack (= 5.0.0.1) - actionview (= 5.0.0.1) - activejob (= 5.0.0.1) + actionmailer (5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.0.1) - actionview (= 5.0.0.1) - activesupport (= 5.0.0.1) + actionpack (5.0.1) + actionview (= 5.0.1) + activesupport (= 5.0.1) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.0.1) - activesupport (= 5.0.0.1) + actionview (5.0.1) + activesupport (= 5.0.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (5.0.0.1) - activesupport (= 5.0.0.1) + activejob (5.0.1) + activesupport (= 5.0.1) globalid (>= 0.3.6) - activemodel (5.0.0.1) - activesupport (= 5.0.0.1) - activerecord (5.0.0.1) - activemodel (= 5.0.0.1) - activesupport (= 5.0.0.1) + activemodel (5.0.1) + activesupport (= 5.0.1) + activerecord (5.0.1) + activemodel (= 5.0.1) + activesupport (= 5.0.1) arel (~> 7.0) - activesupport (5.0.0.1) + activesupport (5.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - arel (7.1.2) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + archive-zip (0.7.0) + io-like (~> 0.3.0) + arel (7.1.4) + ast (2.3.0) better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - builder (3.2.2) + brakeman (3.5.0) + builder (3.2.3) byebug (9.0.6) + capybara (2.11.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + childprocess (0.5.9) + ffi (~> 1.0, >= 1.0.11) + chromedriver-helper (1.0.0) + archive-zip (~> 0.7.0) + nokogiri (~> 1.6) + cliver (0.3.2) coderay (1.1.1) coffee-rails (4.2.1) coffee-script (>= 2.2.0) @@ -54,23 +73,52 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) - concurrent-ruby (1.0.2) + coffee-script-source (1.12.2) + concurrent-ruby (1.0.4) debug_inspector (0.0.2) - diff-lcs (1.2.5) + diff-lcs (1.3) + docile (1.1.5) + dotenv (2.1.2) + dotenv-rails (2.1.2) + dotenv (= 2.1.2) + railties (>= 3.2, < 5.1) erubis (2.7.0) + excon (0.54.0) + excon-rails (1.0.0) + activesupport (>= 3.0) + excon (>= 0.18.0) + sweet_notifications (~> 1.0) execjs (2.7.0) - ffi (1.9.14) + ffi (1.9.17) + font-awesome-rails (4.7.0.1) + railties (>= 3.2, < 5.1) globalid (0.3.7) activesupport (>= 4.1.0) i18n (0.7.0) - jbuilder (2.6.0) + i18n-js (3.0.0.rc15) + i18n (~> 0.6, >= 0.6.6) + io-like (0.3.0) + jasmine-core (2.5.2) + jasmine-rails (0.14.1) + jasmine-core (>= 1.3, < 3.0) + phantomjs (>= 1.9) + railties (>= 3.2.0) + sprockets-rails + jbuilder (2.6.1) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.2.1) + jquery-rails (4.2.2) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + jshint (1.5.0) + execjs (>= 1.4.0) + multi_json (~> 1.0) + therubyracer (~> 0.12.1) + json (2.0.3) + launchy (2.4.3) + addressable (~> 2.3) + libv8 (3.16.14.17) listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -83,45 +131,56 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) - minitest (5.9.1) + minitest (5.10.1) multi_json (1.12.1) nio4r (1.2.1) - nokogiri (1.6.8) + nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) - pkg-config (~> 1.1.7) + parser (2.3.3.1) + ast (~> 2.2) pg (0.19.0) - pkg-config (1.1.7) - puma (3.6.0) + phantomjs (2.1.1.0) + poltergeist (1.12.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) + powerpack (0.1.1) + public_suffix (2.0.5) + puma (3.6.2) rack (2.0.1) rack-test (0.6.3) rack (>= 1.0) - rails (5.0.0.1) - actioncable (= 5.0.0.1) - actionmailer (= 5.0.0.1) - actionpack (= 5.0.0.1) - actionview (= 5.0.0.1) - activejob (= 5.0.0.1) - activemodel (= 5.0.0.1) - activerecord (= 5.0.0.1) - activesupport (= 5.0.0.1) + rails (5.0.1) + actioncable (= 5.0.1) + actionmailer (= 5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + activemodel (= 5.0.1) + activerecord (= 5.0.1) + activesupport (= 5.0.1) bundler (>= 1.3.0, < 2.0) - railties (= 5.0.0.1) + railties (= 5.0.1) sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.1) + rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (5.0.0.1) - actionpack (= 5.0.0.1) - activesupport (= 5.0.0.1) + railties (5.0.1) + actionpack (= 5.0.1) + activesupport (= 5.0.1) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.3.0) - rb-fsevent (0.9.7) + rainbow (2.2.1) + rake (12.0.0) + rb-fsevent (0.9.8) rb-inotify (0.9.7) ffi (>= 0.5.0) + rb-readline (0.5.3) + ref (2.0.0) + request_store (1.3.2) rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) @@ -139,43 +198,72 @@ GEM rspec-mocks (~> 3.5.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) - sass (3.4.22) + rubocop (0.47.1) + parser (>= 2.3.3.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + ruby-progressbar (1.8.1) + rubyzip (1.2.0) + sass (3.4.23) sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + selenium-webdriver (3.0.5) + childprocess (~> 0.5) + rubyzip (~> 1.0) + websocket (~> 1.0) + simplecov (0.12.0) + docile (~> 1.1.0) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) spring (2.0.0) activesupport (>= 4.2) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (3.7.0) + sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.0) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - thor (0.19.1) + sweet_notifications (1.0.0) + activesupport (>= 4.0, < 6) + railties (>= 4.0, < 6) + request_store (~> 1.0) + teaspoon (1.1.5) + railties (>= 3.2.5, < 6) + teaspoon-jasmine (2.3.4) + teaspoon (>= 1.0.0) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref + thor (0.19.4) thread_safe (0.3.5) tilt (2.0.5) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.0) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.2) + uglifier (3.0.4) execjs (>= 0.3.0, < 3) - web-console (3.3.1) + unicode-display_width (1.1.3) + web-console (3.4.0) actionview (>= 5.0) activemodel (>= 5.0) debug_inspector railties (>= 5.0) + websocket (1.2.3) websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -183,21 +271,36 @@ PLATFORMS DEPENDENCIES better_errors binding_of_caller + brakeman byebug + capybara + chromedriver-helper coffee-rails (~> 4.2) + dotenv-rails + excon-rails + font-awesome-rails (>= 4.7.0) + i18n-js (>= 3.0.0.rc15) + jasmine-rails jbuilder (~> 2.5) jquery-rails + jshint + launchy listen (~> 3.0.5) pg + poltergeist puma (~> 3.0) - rails (~> 5.0.0, >= 5.0.0.1) + rails (~> 5.0.1) + rb-readline rspec-rails (~> 3.5) + rubocop sass-rails (~> 5.0) + selenium-webdriver + simplecov spring spring-watcher-listen (~> 2.0.0) - turbolinks (~> 5) + teaspoon-jasmine uglifier (>= 1.3.0) web-console BUNDLED WITH - 1.12.5 + 1.13.6 diff --git a/app/assets/images/geolocation-marker.svg b/app/assets/images/geolocation-marker.svg new file mode 100644 index 00000000..ba92af72 --- /dev/null +++ b/app/assets/images/geolocation-marker.svg @@ -0,0 +1,17 @@ + + + + My Location + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/hubzone-map-marker.svg b/app/assets/images/hubzone-map-marker.svg new file mode 100644 index 00000000..af4b605e --- /dev/null +++ b/app/assets/images/hubzone-map-marker.svg @@ -0,0 +1,13 @@ + + + + hubzone-map-marker + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/57x57.png b/app/assets/images/icons/57x57.png new file mode 100644 index 00000000..4dd59b9a Binary files /dev/null and b/app/assets/images/icons/57x57.png differ diff --git a/app/assets/images/sba-logo.png b/app/assets/images/sba-logo.png new file mode 100644 index 00000000..7613a3b5 Binary files /dev/null and b/app/assets/images/sba-logo.png differ diff --git a/app/assets/javascripts/HZ.js.erb b/app/assets/javascripts/HZ.js.erb new file mode 100644 index 00000000..b330fd75 --- /dev/null +++ b/app/assets/javascripts/HZ.js.erb @@ -0,0 +1,28 @@ +//this is our base level hubzone app namespace +/* exported HZApp */ +window.HZApp = { + Autocomplete: {}, + config: { + wmsEnabled: ("<%= MAP_CONFIG[:geom_wms_settings][:wms_enabled] %>" === "true") + }, + Constructors: {}, + GA: {}, + GeoLocation: {}, + HZQuery: {}, + Layers: { + LayerUtils: {}, + LayerDefs: {} + }, + Legend: {}, + map: {}, + MapUtils: {}, + Markers: {}, + Print: {}, + Report: {}, + SidebarUtils: {}, + Styles: {}, + WMSUtils: {} +}; + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c5c65d51..1897c863 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,6 +12,6 @@ // //= require jquery //= require jquery_ujs -//= require turbolinks //= require uswds -//= require_tree . +//= require HZ +//= require i18n/translations diff --git a/app/assets/javascripts/hzmap.js b/app/assets/javascripts/hzmap.js new file mode 100644 index 00000000..b92f6382 --- /dev/null +++ b/app/assets/javascripts/hzmap.js @@ -0,0 +1,13 @@ +// This is a manifest file that'll be compiled into hzmap.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require_tree ./hzmap diff --git a/app/assets/javascripts/hzmap/autocomplete.js b/app/assets/javascripts/hzmap/autocomplete.js new file mode 100644 index 00000000..48340889 --- /dev/null +++ b/app/assets/javascripts/hzmap/autocomplete.js @@ -0,0 +1,21 @@ +// autocomplete contrustructor +HZApp.Autocomplete = { + searchInput: function(){ + return document.getElementById('search-field-small'); + }, + options: { + types: [] + }, + autocomplete: {}, + createAutocomplete: function(){ + this.autocomplete = new google.maps.places.Autocomplete(this.searchInput(), this.options); + }, + createListener: function(autocompleteObject){ + autocompleteObject.addListener('place_changed', this.triggerSearch($('.usa-search')) ); + }, + triggerSearch: function($selector) { + return function() { + $selector.submit(); + }; + } +}; diff --git a/app/assets/javascripts/hzmap/ga.js b/app/assets/javascripts/hzmap/ga.js new file mode 100644 index 00000000..d77020a0 --- /dev/null +++ b/app/assets/javascripts/hzmap/ga.js @@ -0,0 +1,39 @@ +/* exported GAs */ +HZApp.GA = (function() { + + window.console = window.console || { }; + if ( window.console.log === null ) { + window.console.log = function() { }; + } + + return { + navigateToPage: function(url){ + return (document.location = url); + }, + openLink : function( url, category, action, label, value ) { + // console.log( "GA.openLink: ", url, category, action, label, value ); + if ( typeof ga === "function" ) { + var timeout = setTimeout( function() { HZApp.GA.navigateToPage(url); }, 500 ); + ga( 'send', 'event', category || "outbound", action || "click", label || url, value, { + hitCallback : function() { + clearTimeout( timeout ); + HZApp.GA.navigateToPage(url); + } + }); + } else { + HZApp.GA.navigateToPage(url); + } + }, + track : function( category, action, label, value ) { + //console.log( "GA.track: ", category, action, label, value ); + if ( typeof ga === "function" ) { + ga( "send", "event", category, action, label, value ); + } + }, + trackSubmit : function(action, inputId) { + var term = $(inputId).val(); + this.track( 'map', action, term ); + } + }; +})(); + diff --git a/app/assets/javascripts/hzmap/geolocation.js b/app/assets/javascripts/hzmap/geolocation.js new file mode 100644 index 00000000..2c767258 --- /dev/null +++ b/app/assets/javascripts/hzmap/geolocation.js @@ -0,0 +1,45 @@ +/* exported GeoLocation */ +HZApp.GeoLocation = (function() { + // GeoLocation button listener + $(function() { + $('#geolocation').click(HZApp.GeoLocation.catchGeoLocationButtonClick); + }); + + return { + catchGeoLocationButtonClick: function(){ + HZApp.GeoLocation.getUserLocation(window.navigator.geolocation); + }, + getUserLocation: function(navLocation){ + //grab users location and set map around it + $('#geolocation i').css("display", "none"); + $('.geolocation-loading').css("display", "block"); + if (navLocation) { + navLocation.getCurrentPosition(this.moveMapToUserLocation, this.geolocationError); + return navLocation; + } else { + //browser doesn't support Geolocation + $('#geolocation i').css("display", "block"); + $('.geolocation-loading').css("display", "none"); + // window.console.warn('browser does not support geolocation'); + return null; + } + }, + geolocationError: function() { + $('#geolocation i').css("display", "block"); + $('.geolocation-loading').css("display", "none"); + // window.console.log("unable to retrieve user location"); + }, + moveMapToUserLocation: function(position){ + var pos = { + lat: position.coords.latitude, + lng: position.coords.longitude + }; + //set map to that location + HZApp.map.setCenter(pos); + HZApp.map.setZoom(15); + HZApp.Markers.hzUserLocation.updateMarkers(pos); + $('#geolocation i').css("display", "block"); + $('.geolocation-loading').css("display", "none"); + } + }; +})(); diff --git a/app/assets/javascripts/hzmap/hz-query.js b/app/assets/javascripts/hzmap/hz-query.js new file mode 100644 index 00000000..c1f78769 --- /dev/null +++ b/app/assets/javascripts/hzmap/hz-query.js @@ -0,0 +1,61 @@ +// utils for handling queries +HZApp.HZQuery = { + query:{ + latlng: null, + q: null, + zoom: null, + }, + response: {}, + parseResponse: function(response) { + //set the response for later + this.response = response; + this.response.geocodeLocation = null; + this.query.latlng = null; + this.query.q = null; + + // handle bad responses + this.handleBadResponses(response.status); + + // get all the desired geometry and attributes out of the response + this.parseResponseGeometry(this.response); + + //finally, update the map with the new response + this.updateMap(); + }, + handleBadResponses: function(responseStatus){ + if (responseStatus === 'ZERO_RESULTS' || responseStatus === 'INVALID_REQUEST'){ + $('.sidebar-card.map-report').hide(); + $('#sidebar-content').addClass('zero-results'); + $('#legend').addClass('zero-results'); + } else { + $('.sidebar-card.map-report').show(); + $('#sidebar-content').removeClass('zero-results'); + $('#legend').removeClass('zero-results'); + } + }, + parseResponseGeometry: function(response){ + if (HZApp.HZQuery.response.geometry){ + HZApp.MapUtils.jumpToLocation({ + viewport: response.geometry.viewport, + location: response.geometry.location + }); + + this.response.geocodeLocation = response.geometry.location; + + if (response.place_id){ + this.query.q = response.formatted_address; + this.query.latlng = null; + } else { + this.query.q = null; + this.query.latlng = [response.geocodeLocation.lat, response.geocodeLocation.lng ].join(','); + } + } + }, + updateMap: function(){ + HZApp.SidebarUtils.sidebar.open(); + HZApp.Markers.hzQueryMarker.updateMarkers(this.response.geocodeLocation); + }, + resetStreetView: function (elem){ + elem.click(); + } +}; diff --git a/app/assets/javascripts/hzmap/layer-defs.js b/app/assets/javascripts/hzmap/layer-defs.js new file mode 100644 index 00000000..c853e62b --- /dev/null +++ b/app/assets/javascripts/hzmap/layer-defs.js @@ -0,0 +1,43 @@ +// hubzone data layer style definitions + +// this object holds the current google overlays in the .overlay array +// the layerIndex prop is used for order +// the object that is stuck into overlay will have a setOpacity which is used for toggling + +HZApp.Layers.LayerDefs = (function(){ + + return { + hzWMSOverlays: { + indian_lands: { + layerIndex: 0, + layerGroup: 'indian_lands', + isVisible: true, + overlay: [], + }, + qnmc_e: { + layerIndex: 1, + layerGroup: 'qnmc', + isVisible: true, + overlay:[], + }, + redesignated_lg: { + layerIndex: 2, + layerGroup: 'redesignated', + isVisible: true, + overlay:[], + }, + brac_lg: { + layerIndex: 3, + layerGroup: 'brac', + isVisible: true, + overlay:[], + }, + qct_e: { + layerIndex: 4, + layerGroup: 'qct', + isVisible: true, + overlay:[], + } + } + }; +})(); diff --git a/app/assets/javascripts/hzmap/legend-defs.js b/app/assets/javascripts/hzmap/legend-defs.js new file mode 100644 index 00000000..d8fb4a70 --- /dev/null +++ b/app/assets/javascripts/hzmap/legend-defs.js @@ -0,0 +1,134 @@ +// hubzone data layer style definitions + +// this object holds the current google overlays in the .overlay array and the per layer styles +// order in this object defines draw order on the map: +// first object is drawn first, then next on top of that, etc. +HZApp.Legend.LegendDefs = (function(){ + var legendDefaults = { + circleFillColor: '#FFFFFF', + circleFillOpacity: 0.5, + circleStrokeColor: '#CCCCCC', + circleStrokeOpacity: 1, + circleStrokeWidth: 1, + displacementX: 0, + displacementY: 0, + fillColor: '#CCCCCC', + fillOpacity: 0.5, + graphicSpacing: 10, + lineStrokeColor: '#fff', + lineStrokeOpacity: 1, + lineStrokeWidth: 1, + lineRotation: 0, + strokeColor: '#CCCCCC', + strokeOpacity: 1, + strokeWidth: 1.25, + tileSize: 10, + + // // USWDS Alt 1 + // qctColor: '#2E8540', + // qnmcColor: '#0071BB', + // indianLandsColor: '#4C2C92' + + // Tyler 1 + qctColor: '#0D465C', + qnmcColor: '#BA233F', + indianLandsColor: '#009DCD' + }; + + var legendKeys = { + qct: { + title: "Census Tract", + svg: [], + canToggle: true, + layerGroup: 'qct', + styleOptions: [ + { + type: 'polygon', + fillColor: legendDefaults.qctColor, + fillOpacity: legendDefaults.fillOpacity, + strokeColor: legendDefaults.qctColor, + strokeOpacity: legendDefaults.strokeOpacity, + strokeWidth: legendDefaults.strokeWidth + } + ] + }, + qnmc: { + title: "County", + svg: [], + canToggle: true, + layerGroup: 'qnmc', + styleOptions: [ + { + type: 'polygon', + fillColor: legendDefaults.qnmcColor, + fillOpacity: legendDefaults.fillOpacity, + strokeColor: legendDefaults.qnmcColors, + strokeOpacity: legendDefaults.strokeOpacity, + strokeWidth: legendDefaults.strokeWidth + } + ] + }, + indian_lands: { + title: "Indian Land", + svg: [], + canToggle: true, + layerGroup: 'indian_lands', + styleOptions: [ + { + type: 'polygon', + fillColor: legendDefaults.indianLandsColor, + fillOpacity: legendDefaults.fillOpacity, + strokeColor: legendDefaults.indianLandsColor, + strokeOpacity: legendDefaults.strokeOpacity, + strokeWidth: legendDefaults.strokeWidth + } + ] + }, + redesignated: { + title: "Redesignated", + svg: [], + canToggle: true, + layerGroup: 'redesignated', + styleOptions: [ + { + type: 'horline', + lineStrokeColor: legendDefaults.lineStrokeColor, + lineStrokeWidth: 5, + lineStrokeOpacity: legendDefaults.fillOpacity, + strokeWidth: legendDefaults.strokeWidth, + strokeColor: legendDefaults.lineStrokeColor, + strokeOpacity: legendDefaults.strokeOpacity, + tileSize: 30, + lineRotation: 0 + } + ] + }, + brac: { + title: "Base Closure Area", + svg: [], + canToggle: true, + layerGroup: 'brac', + styleOptions: [ + { + type: 'circle', + circleFillColor: '#fff', + circleFillOpacity: legendDefaults.fillOpacity, + circleStrokeColor: '#fff', + circleStrokeOpacity: legendDefaults.strokeOpacity, + circleStrokeWidth: legendDefaults.strokeWidth, + strokeColor: '#000000', + strokeOpacity: legendDefaults.strokeOpacity, + strokeWidth: legendDefaults.strokeWidth, + tileSize: 15, + graphicSpacing: legendDefaults.graphicSpacing + } + ] + }, + }; + + return { + legendDefaults: legendDefaults, + legend: legendKeys + }; + +})(); diff --git a/app/assets/javascripts/hzmap/legend.js.erb b/app/assets/javascripts/hzmap/legend.js.erb new file mode 100644 index 00000000..49ca049f --- /dev/null +++ b/app/assets/javascripts/hzmap/legend.js.erb @@ -0,0 +1,171 @@ +// legend utilties +HZApp.Legend = (function(){ + return { + legend: HZApp.Legend.LegendDefs.legend, + buildLegend: function(layers){ + Object.keys(layers).map(function(layer){ + var legendConfig = HZApp.Legend.getConfigFromLayerStyle(layers[layer]); + HZApp.Legend.legend[legendConfig.layerGroup].svg.push(HZApp.Legend.svgFromStyle(legendConfig)); + }); + + Object.keys(this.legend).map(HZApp.Legend.insertLegendItem); + + this.addLegendButtonListeners(); + + this.setLegendState(window.innerWidth); + + this.addLayerToggleListeners(); + + }, + addLegendButtonListeners: function(){ + $('#legend-header').click(function(event) { + HZApp.Legend.toggleLegendVisibility(event.currentTarget.className); + }); + }, + toggleLegendVisibility: function(legendState) { + if(legendState === 'open') { + HZApp.Legend.hideLegend(); + $('#legend-header').removeClass('open'); + } else { + HZApp.Legend.showLegend(); + $('#legend-header').addClass('open'); + } + }, + getConfigFromLayerStyle: function(layer){ + return { + layerGroup: layer.layerGroup, + styleType: layer.styleOptions[0].type, + styleColor: layer.styleOptions[0][HZApp.Legend.legendTypeToColorType[layer.styleOptions[0].type]] + }; + }, + svgFromStyle: function(style){ + var width = 34, height = 29; + var svg = this.svgHeader(width, height); + svg += 'hubzone legend'; + var svg_fn_name = "svg_" + style.styleType; + svg += HZApp.Legend[svg_fn_name](style,width, height); + svg += ''; + return svg; + }, + svgHeader: function(width, height){ + return ('' + ); + }, + svg_polygon: function(style, width, height){ + return ( '' + + '' + + '' + ); + }, + svg_horline: function(style, width, height){ + return ( + // '' + + '' + + '' + + '' + + '' + + // '' + + '' + + '' + + '' + + '' + + '' + ); + }, + svg_circle: function(style, width, height){ + return ( + // '' + + '' + + '' + + '' + + '' + + // '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); + }, + insertLegendItem: function(legendItem) { + HZApp.Legend.legend[legendItem].canToggle ? HZApp.Legend.insertLegendItemToggle(legendItem) : HZApp.Legend.insertLegendItemNoToggle(legendItem); + }, + insertLegendItemToggle: function(legendItem){ + var li = '
  • '; + li += ''; + li += ''; + li += '
  • '; + $('#legend > ul').append(li); + }, + // insertLegendItemNoToggle: function(legendItem){ + // var li = '
  • '; + // HZApp.Legend.legend[legendItem].svg.map(function(svg){ li += svg; }); + // li += '' + I18n.t('legend.' + legendItem) + ''; + // li += '
  • '; + // $('#legend > ul').append(li); + // }, + legendTypeToColorType: { //mapping style type to css/svg color parameter + 'polygon': 'fillColor', + 'circle': 'circleFillColor', + 'horline': 'lineStrokeColor' + }, + hideLegend: function(){ + $('#legend li.legend-item').hide(); + $('#hide-legend-button').hide(); + $('#legend-header-title-expanded').hide(); + $('#legend-header-title-hidden').show(); + $('#show-legend-button').show(); + }, + showLegend: function(){ + $('#legend li.legend-item').show(); + $('#hide-legend-button').show(); + $('#legend-header-title-expanded').show(); + $('#legend-header-title-hidden').hide(); + $('#show-legend-button').hide(); + }, + setLegendState: function(windowWidth) { + if (windowWidth > 950) { + HZApp.Legend.toggleLegendVisibility(''); + } else { + HZApp.Legend.toggleLegendVisibility('open'); + } + }, + addLayerToggleListeners: function() { + $('input[type="checkbox"]').click( function(event){ + HZApp.Legend.setLayerGroups(event.currentTarget.value, HZApp.Layers.LayerDefs.hzWMSOverlays); + }); + }, + setLayerGroups: function(selectedLayer, wmsOverlays) { + Object.keys(wmsOverlays).map(function(layer) { + if(wmsOverlays[layer].layerGroup === selectedLayer){ + HZApp.Legend.toggleLayerGroup(wmsOverlays[layer]); + } else { + } + }); + }, + toggleLayerGroup: function(layer) { + if(layer.isVisible){ + layer.overlay.setOpacity(0); + layer.isVisible = false; + } else { + layer.overlay.setOpacity(1); + layer.isVisible = true; + } + } + }; +})(); diff --git a/app/assets/javascripts/hzmap/map-utils.js.erb b/app/assets/javascripts/hzmap/map-utils.js.erb new file mode 100644 index 00000000..16ea6fba --- /dev/null +++ b/app/assets/javascripts/hzmap/map-utils.js.erb @@ -0,0 +1,45 @@ +HZApp.MapUtils = (function(){ + return { + // turn latlng object into url + catchMapClick: function(clickEvent){ + var clicklng = clickEvent.latLng.lng(); + var clicklat = clickEvent.latLng.lat(); + var date = HZApp.MapUtils.parseDate(new Date()); + var locale = document.documentElement.lang || 'en'; + // Log the click + HZApp.GA.track( 'map', 'click', clicklat + ',' + clicklng ); + var url = "<%= search_path %>?latlng=" + clicklat + ',' + clicklng + '&query_date=' + date + '&locale=' + locale; + $.ajax({ + url: url + }); + return url; + }, + //helper to parse a javascript date because, why? + parseDate: function(date){ + var mm = date.getMonth() + 1; + var dd = date.getDate(); + return [date.getFullYear(), + (mm>9 ? '' : '0') + mm, + (dd>9 ? '' : '0') + dd + ].join('-'); + }, + //jump to location on the map based on the geocode viewport object + jumpToLocation: function(geocodeLocation){ + if (geocodeLocation.viewport){ + var newBounds = this.createGoogleLatLngBounds( + geocodeLocation.viewport.southwest.lng, + geocodeLocation.viewport.southwest.lat, + geocodeLocation.viewport.northeast.lng, + geocodeLocation.viewport.northeast.lat + ); + HZApp.map.fitBounds(newBounds); + } + }, + createGoogleLatLngBounds: function(SWLng, SWLat, NELng, NELat){ + return new google.maps.LatLngBounds( + new google.maps.LatLng(SWLat, SWLng), + new google.maps.LatLng(NELat, NELng) + ); + }, + }; +})(); diff --git a/app/assets/javascripts/hzmap/map.js.erb b/app/assets/javascripts/hzmap/map.js.erb new file mode 100644 index 00000000..305d049d --- /dev/null +++ b/app/assets/javascripts/hzmap/map.js.erb @@ -0,0 +1,62 @@ +//create the map on load, when idle, jump to updateMap to get features +/* exported initMap */ +function initMap() { + + HZApp.map = new google.maps.Map(document.getElementById('map'), { + center: {lat: 39.8282, lng: -98.5795}, + zoom: 5, + zoomControl: true, + zoomControlOptions: { + position: google.maps.ControlPosition.RIGHT_BOTTOM + }, + streetViewControlOptions: { + position: google.maps.ControlPosition.RIGHT_BOTTOM + }, + mapTypeControlOptions: { + mapTypeIds: ['roadmap', 'hz_map', 'hybrid' ], + position: google.maps.ControlPosition.BOTTOM_CENTER + } + }); + + //adds in the hz style into the basemap picker + var hzStyledMap = new google.maps.StyledMapType(HZApp.Styles.hzBaseMapStyle, {name: 'Gray'}); + HZApp.map.mapTypes.set('hz_map', hzStyledMap); + + //adds the map legend and geolocation button + HZApp.map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(document.getElementById('legend')); + HZApp.map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(document.getElementById('geolocation')); + + //add in the WMS tiles + HZApp.WMTSUtils.initializeTiles(); + + //add listener on the map for clciks + HZApp.WMTSUtils.addClickListeners(HZApp.map); + + //adds autocomplete and click listener + HZApp.Autocomplete.createAutocomplete(); + HZApp.Autocomplete.createListener(HZApp.Autocomplete.autocomplete); + + // build out the markers + /* jshint ignore:start */ + HZApp.Markers.hzQueryMarker = new HZApp.Constructors.HubzoneMapMarker({ + icon: '<%= image_path 'hubzone-map-marker.svg' %>', + scaledSize: 40 + }); + HZApp.Markers.hzUserLocation = new HZApp.Constructors.HubzoneMapMarker({ + icon: '<%= image_path 'geolocation-marker.svg' %>', + scaledSize: 17 + }); + /* jshint ignore:end */ + + //initialize user location + HZApp.GeoLocation.getUserLocation(navigator.geolocation); + + //build the legend + HZApp.Legend.buildLegend(HZApp.Legend.legend); + + // build the sidebar + HZApp.SidebarUtils.buildSidebar(); + + //returns the map + return HZApp.map; +} diff --git a/app/assets/javascripts/hzmap/markers.js.erb b/app/assets/javascripts/hzmap/markers.js.erb new file mode 100644 index 00000000..7df6589c --- /dev/null +++ b/app/assets/javascripts/hzmap/markers.js.erb @@ -0,0 +1,33 @@ +// // code for handling marker behavior + +/* exported HubzoneMapMarker */ +HZApp.Constructors.HubzoneMapMarker = function(options) { + this.markers = []; + this.clearMarkers = function() { + this.setMapOnAll(null); + this.markers = []; + }; + this.setMapOnAll = function(map) { + for (var i = 0; i < this.markers.length; i++) { + this.markers[i].setMap(map); + } + }; + + this.updateMarkers = function(geocodeLocation){ + this.clearMarkers(); + if (geocodeLocation !== null && geocodeLocation !== undefined){ + var marker = new google.maps.Marker({ + position: geocodeLocation, + map: HZApp.map, + icon: { + url: options.icon, + scaledSize: new google.maps.Size(options.scaledSize,options.scaledSize) + }, + optimized: false + }); + google.maps.event.addListener(marker, 'click', HZApp.MapUtils.catchMapClick); + this.markers.push(marker); + } + }; + +}; diff --git a/app/assets/javascripts/hzmap/report.js.erb b/app/assets/javascripts/hzmap/report.js.erb new file mode 100644 index 00000000..121b5b75 --- /dev/null +++ b/app/assets/javascripts/hzmap/report.js.erb @@ -0,0 +1,91 @@ +// reporting utility +HZApp.Report = (function(){ + //adds listeners + $(function() { + $(document).bind("keydown", HZApp.Report.catchKeyStrokeToPrint); + $(document).on('click','#map-report', HZApp.Report.requestReport); + }); + + return { + catchKeyStrokeToPrint: function(e){ + if((e.ctrlKey || e.metaKey) && e.keyCode === 80){ + e.preventDefault(); + HZApp.Report.requestReport(); + } else { + return; + } + }, + + requestReport: function(event, request_type_arg) { // fetch the app state to send off to the report + HZApp.HZQuery.query.zoom = HZApp.map.getZoom(); + + var url = "<%= MAP_CONFIG[:hubzone_report_host] + MAP_CONFIG[:hubzone_api_report_path] %>"; //jshint ignore:line + url += HZApp.Report.getReportRequestParams(HZApp.HZQuery.query); + + var request_type = request_type_arg || "<%= MAP_CONFIG[:hubzone_report_request_type] %>" //jshint ignore:line + if (request_type === "window_open"){ + window.open(url, '_blank'); + } else { + var req = new XMLHttpRequest(); + HZApp.Report.buildAndRunRequest(req, url); + } + }, + buildAndRunRequest: function(req, url){ + HZApp.Report.showReportWaiting(); + req.open("GET", url, true); + req.responseType = "blob"; + req.onerror = HZApp.Report.requestReportError; + req.onload = HZApp.Report.handleReportResponse; + req.send(); + }, + handleReportResponse: function handleReportResponse(event){ + HZApp.Report.hideReportWaiting(); + var link = HZApp.Report.generateDownloadLink(event.currentTarget.response); + HZApp.Report.downloadReport(link); + }, + generateDownloadLink: function(blob){ + var link = document.createElement('a'); + link.target = '_blank'; + link.href = window.URL.createObjectURL(blob); + link.download = ("hz_report" + "_address_" + + HZApp.HZQuery.query.q.replace(' ', '_') + ".pdf"); + return link; + }, + downloadReport: function(link){ + link.click(); + window.URL.revokeObjectURL(link.href); + }, + //takes a hzQuery object as input and parses it for a report GET + getReportRequestParams:function(hzQuery){ + var params = ""; + params += "?latlng=" + encodeURIComponent(hzQuery.latlng); + params.zoom = hzQuery.zoom; + if (hzQuery.q){ + params += "&q=" + encodeURIComponent(hzQuery.q); + } + params += "&zoom=" + hzQuery.zoom; + params += "&locale=" + (document.documentElement.lang || "en"); + + return params; + }, + requestReportError: function requestReportError(){ + + $('#report-waiting').html(I18n.t('report.error')); + HZApp.Report.clearReportWaiting(7500); + }, + showReportWaiting: function(){ + $('#report-waiting').html(I18n.t('report.generating')); + $('#report-waiting').show(); + }, + hideReportWaiting: function(){ + $('#report-waiting').html(I18n.t('report.created')); + HZApp.Report.clearReportWaiting(5000); + }, + clearReportWaiting: function(timeout){ + setTimeout(function(){ + $('#report-waiting').html(); + $('#report-waiting').hide(); + }, timeout); + } + }; +})(); diff --git a/app/assets/javascripts/hzmap/sidebar.js b/app/assets/javascripts/hzmap/sidebar.js new file mode 100644 index 00000000..df5af5c0 --- /dev/null +++ b/app/assets/javascripts/hzmap/sidebar.js @@ -0,0 +1,62 @@ +//Sidebar utils +HZApp.SidebarUtils = (function(){ + // extend jquery with our sidebar function + $.fn.sidebar = function() { + var $sidebar = this; + $sidebar.currentClass = 'hidden'; + + $sidebar.update = function() { + if (!$sidebar.hasClass('on')) { + $sidebar.addClass('on'); + $('#legend').addClass('legend-mobile'); + $sidebar.removeClass('hidden'); + $('#sidebar-button').html(''); + $('div.gmnoprint[controlheight="55"], div.gmnoprint[controlheight="66"], .gm-svpc').addClass('gm-sidebar-on'); + $('#geolocation').addClass('geolocation-sidebar-on'); + $sidebar.currentClass = 'on'; + + } else { + $sidebar.removeClass('on'); + $('#legend').removeClass('legend-mobile'); + $('#sidebar-button').html(''); + $('div.gmnoprint[controlheight="55"], div.gmnoprint[controlheight="66"], .gm-svpc').removeClass('gm-sidebar-on'); + $('#geolocation').removeClass('geolocation-sidebar-on'); + $sidebar.currentClass = 'hidden'; + $('#hubzone-qualifications').attr("aria-live", "off"); + } + }; + /*** Open the sidebar ***/ + $sidebar.open = function() { + if (!$sidebar.hasClass('on')) { + $sidebar.update(); + } + }; + /*** Close the sidebar ***/ + $sidebar.close = function() { + if ($sidebar.hasClass('on')) { + $sidebar.update(); + } + }; + return $sidebar; + }; + + return { + triggerSidebar: function(){ + $('#sidebar').hasClass('on') ? HZApp.GA.track( 'map', 'sidebar', 'hide' ) : + HZApp.GA.track( 'map', 'sidebar', 'show' ); + $('#sidebar').hasClass('on') ? HZApp.SidebarUtils.sidebar.close() : HZApp.SidebarUtils.sidebar.open(); + }, + sidebar: {}, + buildSidebar: function(){ + HZApp.SidebarUtils.sidebar = $('#sidebar').sidebar(); + $('#sidebar-button').click(HZApp.SidebarUtils.triggerSidebar); + }, + //update settings on qualifications to force the screen reader to the qualifications tab + updateA11yFocus: function(elem){ + elem.attr("aria-live", "rude"); + elem.attr("tabindex", "-1"); + elem.focus(); + } + }; +})(); + diff --git a/app/assets/javascripts/hzmap/styles.js b/app/assets/javascripts/hzmap/styles.js new file mode 100644 index 00000000..afa9ad83 --- /dev/null +++ b/app/assets/javascripts/hzmap/styles.js @@ -0,0 +1,202 @@ +// hubzone google basemap vector tile styling array +/* jshint unused: false */ +HZApp.Styles.hzBaseMapStyle = [ + { + "featureType": "all", + "elementType": "geometry", + "stylers": [ + { + "color": "#f5f5f5" + } + ] + }, + { + "featureType": "all", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#616161" + } + ] + }, + { + "featureType": "all", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#f5f5f5" + } + ] + }, + { + "featureType": "all", + "elementType": "labels.icon", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "administrative.country", + "elementType": "geometry.stroke", + "stylers": [ + { + "visibility": "on" + }, + { + "color": "#c9c9c9" + } + ] + }, + { + "featureType": "administrative.province", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#adb0b5" + }, + { + "visibility": "on" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#bdbdbd" + } + ] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [ + { + "color": "#eeeeee" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#757575" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { + "color": "#e5e5e5" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9e9e9e" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#ffffff" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#dadada" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#616161" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.icon", + "stylers": [ + { + "saturation": -100 + }, + { + "visibility": "on" + } + ] + }, + { + "featureType": "road.arterial", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#757575" + } + ] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9e9e9e" + } + ] + }, + { + "featureType": "transit.line", + "elementType": "geometry", + "stylers": [ + { + "color": "#e5e5e5" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [ + { + "color": "#eeeeee" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#c9c9c9" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9e9e9e" + } + ] + } +]; diff --git a/app/assets/javascripts/hzmap/wmts-utils.js.erb b/app/assets/javascripts/hzmap/wmts-utils.js.erb new file mode 100644 index 00000000..5208c923 --- /dev/null +++ b/app/assets/javascripts/hzmap/wmts-utils.js.erb @@ -0,0 +1,94 @@ +// web map tile service utilites +HZApp.WMTSUtils = (function(){ + + Math.sinh = Math.sinh || function(angle){ + return (Math.exp(angle) - Math.exp(-angle)) / 2; + }; + + return { + tileSize: 256, + initializeTiles: function(){ + //for each layer defined in the hzWMSOverlays create the tile overlay + if (HZApp.config.wmsEnabled){ + Object.keys(HZApp.Layers.LayerDefs.hzWMSOverlays).map(function(layer){ + HZApp.WMTSUtils.addLayerTiles(layer, HZApp.Layers.LayerDefs.hzWMSOverlays[layer].layerIndex); + }); + } else { + //wms is not enabled, do not fetch fetch layers + } + }, + addLayerTiles: function(layer, layerIndex){ + var layerTiles = { + getTileUrl: function(coord, zoom){ + return HZApp.WMTSUtils.buildTileUrl(coord, zoom, layer); + }, + tileSize: new google.maps.Size(this.tileSize, this.tileSize), + isPng: true, + opacity: 1.0 + }; + + var customMapType = new google.maps.ImageMapType(layerTiles); + HZApp.Layers.LayerDefs.hzWMSOverlays[layer].overlay = customMapType; + HZApp.map.overlayMapTypes.insertAt(layerIndex, customMapType); + }, + tile2Bbox: function(x, y, zoom){ + var bb = {}; + bb.n = this.tile2Lat(y,zoom); + bb.s = this.tile2Lat(y+1, zoom); + bb.w = this.tile2Lng(x, zoom); + bb.e = this.tile2Lng(x+1, zoom); + + var webMSW = this.toWebMercator(bb.w, bb.s); + var webMNE = this.toWebMercator(bb.e, bb.n); + return [webMSW[0], webMSW[1], webMNE[0], webMNE[1]].join(','); + }, + toWebMercator: function(x_lon, y_lat){ + if ((Math.abs(x_lon) > 180) || (Math.abs(y_lat) > 90)){ + return []; + } else { + var semimajor_axis = 6378137.0; + var east = x_lon * 0.017453292519943295; + var north = y_lat * 0.017453292519943295; + var northing = 3189068.5 * Math.log((1.0 + Math.sin(north)) / (1.0 - Math.sin(north))); + var easting = semimajor_axis * east; + return [easting, northing]; + } + }, + tile2Lng: function(x, z){ + return (x / Math.pow(2.0, z) * 360.0 - 180); + }, + tile2Lat: function(y,z) { + var n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z); + return this.toDegrees(Math.atan(Math.sinh(n))); + }, + toDegrees: function(angle){ + return angle * 180.0 / Math.PI; + }, + toRadians: function(angle){ + return angle * Math.PI / 180.0; + }, + buildTileUrl: function(coord, zoom, layer){ + var url = "<%= MAP_CONFIG[:geom_wms_settings][:url_root] %>"; + url += "&LAYERS=" + "<%= MAP_CONFIG[:geom_wms_settings][:workspace] %>:" + layer; + url += "&SERVICE=WMS"; + url += "&VERSION=1.1.1"; + url += "&REQUEST=GetMap"; + url += "&SRS=EPSG:900913"; + url += "&bbox=" + this.tile2Bbox(coord.x, coord.y, zoom); + url += "&width=" + this.tileSize; + url += "&height=" + this.tileSize; + url += "&format=image/png"; + return url; + }, + addClickListeners: function(overlay) { + overlay.addListener('click', this.handleSingleClick); + overlay.addListener('dblclick', this.handleDblClick); + }, + handleSingleClick: function(event) { + this.clickEventTimer = setTimeout(function() { HZApp.MapUtils.catchMapClick(event); }, 400); + }, + handleDblClick: function() { + clearTimeout(this.clickEventTimer); + } + }; +})(); diff --git a/app/assets/stylesheets/_globals.scss b/app/assets/stylesheets/_globals.scss new file mode 100644 index 00000000..f4d336fd --- /dev/null +++ b/app/assets/stylesheets/_globals.scss @@ -0,0 +1,3 @@ +.hidden { + display: none; +} diff --git a/app/assets/stylesheets/_map_body.scss b/app/assets/stylesheets/_map_body.scss new file mode 100644 index 00000000..d3dee09d --- /dev/null +++ b/app/assets/stylesheets/_map_body.scss @@ -0,0 +1,20 @@ +/* Map body */ +.map-body { + position: absolute; + width: 100%; + height: calc(100% - 87px); +} +@media only screen and (max-width: 950px) { + .map-body { + height: calc(100% - 40px); + } +} +@media only screen and (max-width: 480px) { + .map-body { + height: calc(100% - 40px); + } +} +#map { + height: 100%; + width: 100%; +} diff --git a/app/assets/stylesheets/_map_controls.scss b/app/assets/stylesheets/_map_controls.scss new file mode 100644 index 00000000..1cc3121a --- /dev/null +++ b/app/assets/stylesheets/_map_controls.scss @@ -0,0 +1,108 @@ +@import 'variables'; + +/* Google Maps Controls */ +.gm-svpc { + transition-duration: 400ms; + border-radius: 3px !important; +} +.gm-tilt { + transition-duration: 400ms; + border-radius: 3px !important; +} +.gmnoprint { + transition-duration: 400ms; + border-radius: 3px !important; +} +.gmnoprint > div { + border-radius: 3px !important; +} +.gm-sidebar-on { + left: -34rem !important; +} +.gm-iv-address-link a { + font-size: 9px !important; +} +div[dir="ltr"] { + margin-top: 5rem; +} +.gm-iv-address { + height: auto !important; +} +/* Geolocation Loading Spinner */ +.geolocation-loading { + width: 16px; + height: 16px; + position: relative; + display: none; + border-radius: 2px; +} +.circle1, .circle2 { + width: 100%; + height: 100%; + border-radius: 50%; + background-color: $gray-dark; + opacity: 0.6; + position: absolute; + -webkit-animation: sk-bounce 2.0s infinite ease-in-out; + animation: sk-bounce 2.0s infinite ease-in-out; +} +.circle2 { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; +} +@-webkit-keyframes sk-bounce { + 0%, 100% { -webkit-transform: scale(0.0) } + 50% { -webkit-transform: scale(1.0) } +} +@keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + -webkit-transform: scale(0.0); + } 50% { + transform: scale(1.0); + -webkit-transform: scale(1.0); + } +} +/* Geolocation Button */ +#geolocation { + width: 28px; + height: 28px; + background-color: $white; + border-radius: 3px; + -webkit-box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + -moz-box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + cursor: pointer; + margin-right: 10px; + padding: 6px 6px 5px 6px; +} +#geolocation .fa-location-arrow { + font-size: 1.8rem; + color: $gray; +} +.geolocation-sidebar-on { + right: 34rem !important; +} +@media only screen and (max-width: 950px) { + #geolocation { + display: block; + top: 50px; + width: 40px; + height: 40px; + } + #geolocation .fa-location-arrow { + font-size: 3rem; + margin-left: 2px; + margin-top: -1px; + } + .geolocation-loading { + width: 28px; + height: 28px; + } + .geolocation-sidebar-on { + right: 0rem !important; + } + .gmnoprint { + display: none; + } +} diff --git a/app/assets/stylesheets/_map_header.scss b/app/assets/stylesheets/_map_header.scss new file mode 100644 index 00000000..239d9cab --- /dev/null +++ b/app/assets/stylesheets/_map_header.scss @@ -0,0 +1,82 @@ +@import 'variables'; + +#sba-logo { + max-width: 8.5rem; + float: left; + padding-top: .7rem; +} +.usa-header { + border-bottom: none; +} +.usa-logo-text a > h1 { + font-family: $font-sans; + font-size: 3.6rem; + font-weight: 700; + color: $gray-dark; + float: left; + clear: none; + margin: 0 0 0 2rem; + padding: 0; +} +.usa-nav-secondary { + position: fixed; + top: -4.5rem; + right: 2rem; +} +.usa-nav-secondary-links li:not(:last-child)::after { + content: none; +} +.usa-nav-secondary-links > li { + padding-left: 2.5rem; +} +.usa-banner-inner { + max-width: 100%; + padding: 0 2rem 0 2rem; +} +.usa-header-extended .usa-logo { + margin-top: 1rem; + margin-bottom: 1rem; +} +.usa-header-extended .usa-nav { + border-top: none; +} +.usa-header-extended .usa-navbar { + max-width: 100%; + padding: 0 2rem 0 2rem; + border-bottom: 2px solid $gray-lighter; +} +@media only screen and (max-width: 950px) { + #sba-logo { + max-width: 6rem; + padding-top: 1rem; + } + #map-search button { + background-size: 3.5rem; + } + .usa-navbar { + border-bottom: none; + } + .usa-banner { + display: none; + } + .usa-logo { + margin-left: .5rem; + } + .usa-header-extended .usa-navbar { + padding-left: 0rem; + border: none; + } + .usa-header-extended .usa-logo { + margin-top: 0rem; + } + .usa-logo-text a > h1 { + margin: 0.7rem 0 0 1rem; + font-size: 2.2rem; + font-weight: 400; + } +} +@media only screen and (max-width: 480px) { + .usa-banner { + display: none; + } +} diff --git a/app/assets/stylesheets/_map_legend.scss b/app/assets/stylesheets/_map_legend.scss new file mode 100644 index 00000000..3c35b0e3 --- /dev/null +++ b/app/assets/stylesheets/_map_legend.scss @@ -0,0 +1,69 @@ +@import 'variables'; + +#legend { + font-family: $font-sans; + font-size: 1.5rem; + background-color: $white; + -webkit-box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + -moz-box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + box-shadow: 0px 1px 4px -1px rgba(0,0,0,0.29); + border-radius: 3px; + margin: 0 0 0 1rem; + padding: 1rem; +} +#legend label { + margin-top: 0.3rem; +} +#legend ul { + list-style-type: none; + padding-left: 0; + margin: 0; +} +#legend li svg:not(:root) { + margin-bottom: -0.9rem; +} +#legend-header { + margin-bottom: 0; + display: block; + cursor: pointer; +} +.legend-title { + color: $gray-dark; + font-size: 1.8rem; + font-weight: bold; + padding-right: 0; +} +#legend-button-div { + color: $gray-dark; + float: right; + padding-top: 0; + padding-left: .5rem; + padding-right: .5rem; + font-size: 1.7rem; +} +.no-toggle { + padding-left: 2.7rem; +} +[type="checkbox"]:checked + label::before, +[type="radio"]:checked + label::before { + background-color: $gray-dark; + box-shadow: 0 0 0 1px $gray-dark; +} + +[type="radio"]:checked + label::before { + box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px $gray-dark; +} + +[type="checkbox"]:focus + label::before { + box-shadow: 0 0 0 1px #ffffff, 0 0 0 3px $gray-dark; +} + +@media only screen and (max-width: 950px) { + .legend-mobile { + bottom: 11rem !important; + } + + #legend.zero-results{ + bottom: 6rem !important; + } +} diff --git a/app/assets/stylesheets/_map_print.scss b/app/assets/stylesheets/_map_print.scss new file mode 100644 index 00000000..7b25f4bb --- /dev/null +++ b/app/assets/stylesheets/_map_print.scss @@ -0,0 +1,51 @@ +@import 'variables'; + +.map-body.printable-map { + width: 450px; + height: 450px; +} + +@media print { + .usa-banner { + display: none; + } + .usa-navbar { + margin-left: 0; + margin-bottom: 1rem; + border-bottom: 1px solid $gray; + } + .hubzone-search { + display: none; + } + .usa-nav-primary > li { + border: 0; + } + .usa-nav-primary > li:first-child { + height: auto; + } + section.map-header { + margin: 0; + padding: 0; + width: 500px; + left: 0; + } + .map-print { + display: none; + } + .usa-grid { + padding: 1em 0 1em 0; + margin: 0; + } + .usa-menu-btn { + display: none; + } + .pac-container { + display: none; + } + #map-print { + display: none; + } + #sidebar-button { + display: none; + } +} diff --git a/app/assets/stylesheets/_map_search.scss b/app/assets/stylesheets/_map_search.scss new file mode 100644 index 00000000..72d74b81 --- /dev/null +++ b/app/assets/stylesheets/_map_search.scss @@ -0,0 +1,128 @@ +@import 'variables'; + +.hubzone-search { + position: absolute; + top: 10px; + z-index: 3; + left: 1.6rem; +} +#search-field-small { + background-color: $white; + border: .5px solid $gray; + border-right: none; + height: 4rem; + font-size: 2rem; + color: $gray-dark; + padding: 1rem 2em 1rem 0.7rem; +} +#search-field-small::-webkit-input-placeholder { + color: $gray-dark; +} +#search-field-small::-moz-placeholder { + color: $gray-dark; + opacity: 1; +} +#search-field-small:-ms-input-placeholder { + color: $gray-dark; +} +#hubzone-search-button { + background-color: $gray-dark; + background-size: 4rem; + height: 4rem; + float: left; +} +#search-field-small:not(:valid) ~ .clear-search { + display: none; +} +button.clear-search { + background-color: transparent !important; + padding: 0 !important; + margin: 0 !important; + position: absolute; + text-align: right; + right: 7rem; + top: .8rem; + color: $gray-dark; + font-size: 2.4rem; +} +button.clear-search:hover { + color: $gray-dark; +} +.usa-search.usa-search-small [type="search"], +.usa-search.usa-search-small .usa-search-input { + max-width: 100%; +} +.pac-container { + background-color: $white; + color: $gray-dark; + border-top: none; + border-radius: 0; + margin-left: 1px; +} +.pac-icon { + display: none; +} +.pac-item { + font-family: $font-sans; + color: $gray-dark; + border: 0; + padding: .4em 0 .4em 1.25em; + font-size: 1.7rem; +} +.pac-matched { + color: $gray-dark; + font-weight: bold; + font-size: 1.8rem; +} +.pac-item-query { + color: $gray-dark; + font-size: 1.8rem; +} +.pac-item-selected { + background-color: $gray-lightest; + color: $gray-dark; +} +.pac-item:hover { + background-color: $gray-lightest; + color: $gray-dark; +} +@media only screen and (max-width: 950px) { + .hubzone-search { + top: 0; + left: 0; + width: 100%; + border-left: none; + } + #search-field-small { + border-left: none; + padding-left: 0.5rem; + } + #map-search button { + border-radius: 0; + } + .pac-container { + margin-left: 0; + border: none; + } + .pac-item { + padding-left: .25rem; + } + .usa-menu-btn { + display: none; + } + button.clear-search { + right: 5.6rem; + } +} + +@media screen and (min-width: 951px) { + .usa-search.usa-search-small [type="search"], + .usa-search.usa-search-small .usa-search-input { + max-width: 100%; + width: 50rem; + } + #hubzone-search-button{ + border-radius: 0 3px 3px 0; + width: 6rem; + } +} diff --git a/app/assets/stylesheets/_map_sidebar.scss b/app/assets/stylesheets/_map_sidebar.scss new file mode 100644 index 00000000..515c7ece --- /dev/null +++ b/app/assets/stylesheets/_map_sidebar.scss @@ -0,0 +1,228 @@ +@import 'variables'; + +#sidebar { + position: absolute; + background-color: rgba(255, 255, 255, 0.80); + right: -34rem; + height: 100%; + width: 34rem; + transition-duration: 400ms; + z-index: 2; +} +#sidebar.on { + right: 0; +} +#sidebar-button { + position: absolute; + margin-left: -2.9rem; + margin-top: 2.3rem; + padding: 2rem .3rem 2rem 0.5rem; + z-index: 2; + border-radius: 3px 0 0 3px; + font-size: 3rem; + background-color: rgba(255, 255, 255, 0.80); +} +#sidebar i.fa-chevron-right, i.fa-chevron-left { + color: $gray-dark; +} +.sidebar-card { + position: relative; + width: 90%; + min-height: 4em; + margin: 1rem auto 1rem auto; + padding: 1.5rem 0 1.5rem 0; + background-color: $white; + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.25); + border-radius: 3px; + display: table; +} + +.hubzone-sidebar-cell { + display: table-cell; +} + +.hubzone-sidebar-row { + display: table-row; +} + +.hubzone-sidebar-cell.text { + padding-left: 3rem; + vertical-align: middle; +} + +.sidebar-card h2 { + display: inline; + vertical-align: middle; + top: 1rem; + max-width: 11rem; + margin: 0; + padding-right: 1rem; + font-family: $font-sans; + font-size: 2rem; + font-weight: normal; + color: $gray-dark; +} + +.sidebar-card h4 { + display: inline; + vertical-align: middle; + top: 1rem; + max-width: 11rem; + margin: 0; + padding-right: 1rem; + font-family: $font-sans; + font-size: 1.5rem; + font-weight: normal; + color: $gray-light; +} +.sidebar-card .fa.fa-map-marker { + position: relative; + left: 2rem; + font-size: 4.5rem; + color: $gray-dark; +} +#hubzone-status { + float: left; + font-size: 2.4rem; + font-weight: normal; + padding-left: 0.8rem; +} +.qualified-hubzone { + background-color: $green-light; + color: $white; +} +.non-qualified-hubzone { + background-color: $gray-dark; + color: $white; +} +.hubzone-status-indicator .fa.fa-times-circle-o, .fa.fa-check-circle-o { + float: left; + font-size: 4rem; +} +.sidebar-qualifications { + width: 90%; + min-width: auto; + margin: 0 auto; + border-radius: 3px; + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.25); + border-collapse: separate; + overflow: hidden; +} +table.sidebar-qualifications th { + border: none; +} +.sidebar-qualifications thead th { + border: none; + padding: 1rem 1.5rem 1rem 1.5rem; +} +.sidebar-qualifications tbody th { + background-color: $white; + border-top: 1px solid $gray-lightest; + color: $gray-dark; + font-size: 1.5rem; + font-weight: 400; +} +.sidebar-qualifications th > span { + float: none; + font-weight: bold; +} +.sidebar-qualifications tfoot tr td { + border-left: 1px solid $gray-lightest; + border-right: 1px solid $gray-lightest; + border-bottom: 1px solid $gray-lightest; + border-top: none; + border-radius: 0 0 3px 3px; + font-size: 1.5rem; +} +.hubzone-status-date { + font-weight: normal; + font-size: 1.3rem; + padding-left: 2px; +} +i.fa.fa-print { + font-size: 2.5rem; + color: $gray-dark; + padding: .5rem 2rem 0 2rem; +} +#map-report { + cursor: pointer; + display: inline; +} +i.fa-file-pdf-o { + font-size: 2.5rem; + color: $gray-dark; + padding: 0; +} +#report-waiting { + display: none; +} +button#map-report { + background: none; + margin: 0.5rem 0.5rem .5rem 1.5rem; + padding: 0rem; +} +.create-report { + font-size: 1.5rem; + font-weight: normal; + padding-top: 0.5rem; + color: $gray-dark; +} + +@media only screen and (max-width: 950px) { + #sidebar-button { + display: none; + } + #sidebar { + width: 100%; + height: auto; + top: 100%; + background-color: transparent; + } + + #sidebar-content.zero-results { + margin-top: 0; + position: absolute; + bottom: 0px; + } + + #sidebar-content { + margin-top: -100px; + } + .sidebar-card.search-result { + min-height: 5.1rem; + } + .sidebar-card { + width: 100%; + border-radius: 0; + margin: 0; + padding: 0.5rem; + min-height: auto; + } + .sidebar-qualifications { + width: 100%; + border: none; + border-radius: 0; + } + .sidebar-card > h2 { + font-size: 1em; + width: 100%; + padding-left: 3.3rem; + } + .sidebar-card .fa.fa-map-marker { + font-size: 3.5rem; + } + .sidebar-qualifications thead th { + padding: 0.5rem 1.5rem 0.5rem 2rem; + } + .hubzone-status-indicator .fa.fa-times-circle-o, .fa.fa-check-circle-o { + font-size: 3.5rem; + } + #hubzone-status { + font-size: 1em; + } + button#map-report { + text-align: left; + margin: .5rem 0 .5rem 2rem; + cursor: pointer; + } +} diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss new file mode 100644 index 00000000..0c4d1ca0 --- /dev/null +++ b/app/assets/stylesheets/_variables.scss @@ -0,0 +1,53 @@ +// These variables drive the HUBZone interface look and feel +// and are used in conjunction with the US Web Design Standards. + +// Typography +$font-sans: "Source Sans Pro", Helvetica Neue, Arial, sans-serif; +$font-serif: "Merriweather", "Georgia", "Cambria", "Times New Roman", "Times", serif; + + +// Colors +$black: #000000; +$white: #ffffff; +$base: #212121; + +$primary: #0071bc; +$primary-darker: #205493; +$primary-darkest: #112e51; +$primary-alt: #02bfe7; +$primary-alt-dark: #00a6d2; +$primary-alt-darkest: #046b99; +$primary-alt-light: #9bdaf1; +$primary-alt-lightest: #e1f3f8; + +$secondary: #e31c3d; +$secondary-dark: #cd2026; +$secondary-darkest: #981b1e; +$secondary-light: #e59393; +$secondary-lightest: #f9dede; + +$gray-dark: #323a45; +$gray: #5b616b; +$gray-medium: #757575; +$gray-light: #aeb0b5; +$gray-lighter: #d6d7d9; +$gray-lightest: #f1f1f1; +$gray-warm-dark: #494440; +$gray-warm-light: #e4e2e0; +$gray-cool-light: #dce4ef; + +$gold: #fdb81e; +$gold-light: #f9c642; +$gold-lighter: #fad980; +$gold-lightest: #fff1d2; + +$green: #2e8540; +$green-light: #4aa564; +$green-lighter: #94bfa2; +$green-lightest: #e7f4e4; + +$cool-blue: #205493; +$cool-blue-light: #4773aa; +$cool-blue-lighter: #8ba6ca; +$cool-blue-lightest: #dce4ef; + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6ccd766a..7918898a 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -10,7 +10,8 @@ * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * + *= require uswds + *= require font-awesome *= require_tree . *= require_self - *= require uswds - */ + */ \ No newline at end of file diff --git a/app/assets/stylesheets/map.scss b/app/assets/stylesheets/map.scss index 69273757..94d49139 100644 --- a/app/assets/stylesheets/map.scss +++ b/app/assets/stylesheets/map.scss @@ -1,38 +1,14 @@ -html, body { - height: 100%; - margin: 0; - padding: 0; -} - -#map { - height: 80%; -} - -#header { - height: 15%; - width: 100%; -} - -#geocodingResultContainer { - display: inline-block; - overflow-y: auto; - height: 100%; -} - -#search-control-ui, -#results-control-ui { - float: left; - background-color: white; - font-size: 12px; - margin: 5px; - max-height: 200px; - overflow: auto; - width: 380px; - -webkit-user-select: none; - padding: 8px; - border-radius: 2px; - -webkit-background-clip: padding-box; - box-shadow: rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px; - min-width: 40px; - background-clip: padding-box; -} +// Globals & Variables +@import 'variables'; +@import 'globals'; + +// Partials +@import 'map_header'; +@import 'map_body'; +@import 'map_search'; +@import 'map_sidebar'; +@import 'map_controls'; +@import 'map_legend'; + +// Responsive +@import 'map_print'; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1c07694e..1157e883 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,10 @@ +# Parent class of all controllers in the application class ApplicationController < ActionController::Base protect_from_forgery with: :exception + + before_action :set_locale + + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end end diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb new file mode 100644 index 00000000..08ab942f --- /dev/null +++ b/app/controllers/health_check_controller.rb @@ -0,0 +1,6 @@ +# Provides a route for the AWS health check +class HealthCheckController < ApplicationController + def status + render plain: "I'm OK" + end +end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb new file mode 100644 index 00000000..37d74477 --- /dev/null +++ b/app/controllers/main_controller.rb @@ -0,0 +1,6 @@ +# Handles root access +class MainController < ApplicationController + def index + redirect_to "#{ENV['HUBZONE_MAP_HOST']}#{map_path}" + end +end diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index e65e1809..be7118bf 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -1,5 +1,42 @@ +require 'excon' +require 'uri' + +# Provides access to the main page with the HUBZone map class MapController < ApplicationController - def fake - render layout: false + def index + end + + def search + query = format_query params + path = "#{MAP_CONFIG[:hubzone_api_search_path]}?#{query}" + response = connection.request(method: :get, + path: path) + @body = response.data[:body] + respond_to do |format| + format.js {} + end + end + + def format_query(params) + query = parse_search_query params + return query if query.nil? + + # Add in the query date if present + query += "&" + URI.encode_www_form("query_date" => params[:query_date] ||= ' ') if params[:query_date].present? + query + end + + def parse_search_query(params) + if params[:search].present? + URI.encode_www_form("q" => params[:search] ||= ' ') + elsif params[:latlng].present? + URI.encode_www_form("latlng" => params[:latlng] ||= ' ') + end + end + + private + + def connection + Excon.new(MAP_CONFIG[:hubzone_api_host]) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be794..7b49f816 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,3 @@ +# Helper methods for the application module ApplicationHelper end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 286b2239..6c066cad 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,4 @@ +# Default mailer base class class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..c8f60890 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,4 @@ +# Created by Rails class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3f9b72b3..fc19df09 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,14 +1,39 @@ - + - HubzoneMap + <%= t('hubzone_map') %> <%= csrf_meta_tags %> + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_include_tag 'application' %> + <%= javascript_include_tag 'hzmap' %> + "> + + + - - - <%= yield %> - + + <%= render partial: "map_header", locals: {body: nil} %> +
    +
    + <%= yield %> +
    + diff --git a/app/views/map/_map_body.html.erb b/app/views/map/_map_body.html.erb new file mode 100644 index 00000000..5a93ef7f --- /dev/null +++ b/app/views/map/_map_body.html.erb @@ -0,0 +1,33 @@ +
    + + + +
    + <%= render partial: "map_legend", locals: {body: nil} %> +
    +
    + +
    +
    +
    +
    +
    + +
    diff --git a/app/views/map/_map_header.html.erb b/app/views/map/_map_header.html.erb new file mode 100644 index 00000000..101cde72 --- /dev/null +++ b/app/views/map/_map_header.html.erb @@ -0,0 +1,52 @@ + diff --git a/app/views/map/_map_legend.html.erb b/app/views/map/_map_legend.html.erb new file mode 100644 index 00000000..9443f0b6 --- /dev/null +++ b/app/views/map/_map_legend.html.erb @@ -0,0 +1,12 @@ + diff --git a/app/views/map/_map_sidebar.html.erb b/app/views/map/_map_sidebar.html.erb new file mode 100644 index 00000000..b40065f4 --- /dev/null +++ b/app/views/map/_map_sidebar.html.erb @@ -0,0 +1,79 @@ +<% + # expecting body to be a hash that has either a message or a formatted_address and hubzones from hzutil search + if body + address = body['formatted_address'].nil? ? t(body['message']) : body['formatted_address'] + if !body['address_components'].nil? + coordinates = [format('%.5f', body['geometry']['location']['lat']) + "\xC2\xB0", format('%.5f', body['geometry']['location']['lng']) + "\xC2\xB0"].join(', ') + else + coordinates = nil + end + date = if body['query_date'].present? + date = Date.parse(body['query_date']) + date = I18n.l date, format: :full + else + body['query_date'] + end + if body['formatted_address'].nil? + sidebar_display = 'hidden' + hubzone_status_indicator = 'fa fa-times-circle-o' + hubzone_status_style = 'qualified-hubzone' + elsif body['hubzone'].size > 0 + sidebar_display = 'sidebar-qualifications' + hubzone_status_indicator = 'fa fa-check-circle-o qualified-hubzone' + hubzone_status_style = 'qualified-hubzone' + hubzone_status = t('hubzone_assertions.qualified') + aria_hubzone_status = t('hubzone_assertions.qualified_aria') + hubzones = body['hubzone'] + else + sidebar_display = 'sidebar-qualifications' + hubzone_status_style = 'non-qualified-hubzone' + hubzone_status_indicator ='fa fa-times-circle-o non-qualified-hubzone' + hubzone_status = t('hubzone_assertions.not_qualified') + aria_hubzone_status = t('hubzone_assertions.not_qualified_aria') + end + end +%> + + + + + + + + + + + <%= render partial: "qualification", collection: hubzones %> + + + + diff --git a/app/views/map/_qualification.html.erb b/app/views/map/_qualification.html.erb new file mode 100644 index 00000000..978a6109 --- /dev/null +++ b/app/views/map/_qualification.html.erb @@ -0,0 +1,13 @@ +<% + unless qualification["expires"].nil? + expires_text = qualification["expires"].to_date < Date.today ? "expired" : "expires" + end +%> + + +
    <%= t(('hubzone_assertions.' + qualification["hz_type"]).to_s) %>
    + <% unless qualification["expires"].nil? %> + <%= t('hubzone_assertions.' + expires_text) %> <%= qualification["expires"].to_s %> + <% end %> + + diff --git a/app/views/map/fake.html.erb b/app/views/map/fake.html.erb deleted file mode 100644 index a51ef45a..00000000 --- a/app/views/map/fake.html.erb +++ /dev/null @@ -1,38 +0,0 @@ - - - - GoogleMaps API Demo - - <%= javascript_include_tag "google-maps-api.js" %> - <%# - - %> - - <%= stylesheet_link_tag "application" %> - <%# - - - %> - - - - -
    - - <%= javascript_include_tag "application" %> - <%# - - %> - - diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb new file mode 100644 index 00000000..238985dd --- /dev/null +++ b/app/views/map/index.html.erb @@ -0,0 +1 @@ +<%= render "map_body" %> diff --git a/app/views/map/search.js.erb b/app/views/map/search.js.erb new file mode 100644 index 00000000..ddeb75c6 --- /dev/null +++ b/app/views/map/search.js.erb @@ -0,0 +1,4 @@ +$('#sidebar-content').html("<%= j render partial: "map_sidebar", locals: {body: JSON.parse(@body)} %>"); // jshint ignore:line +HZApp.HZQuery.parseResponse(JSON.parse('<%= @body.html_safe %>')); +HZApp.SidebarUtils.updateA11yFocus($('#hubzone-qualifications')); +HZApp.HZQuery.resetStreetView($('div[jsaction="closeControl.click"]')); diff --git a/config/boot.rb b/config/boot.rb index 8cdd9812..4f2240ef 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -4,8 +4,10 @@ require 'rails/commands/server' -module Rails +module Rails + # Created by Rails class Server + # rubocop:disable Style/Alias alias :default_options_bk :default_options def default_options default_options_bk.merge!(Host: '0.0.0.0') diff --git a/config/cable.yml b/config/cable.yml index 0bbde6f7..8863ba6c 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -4,6 +4,15 @@ development: test: adapter: async -production: +production: &prd adapter: redis url: redis://localhost:6379/1 + +demo: + <<: *prd + +staging: + <<: *prd + +dev: + adapter: async diff --git a/config/database.yml b/config/database.yml index 6257a94e..92aa2dca 100644 --- a/config/database.yml +++ b/config/database.yml @@ -24,6 +24,25 @@ default: &default password: <%= ENV['HUBZONE_MAP_DB_PASSWORD'] %> host: <%= ENV.fetch('HUBZONE_MAP_DB_HOST') { 'localhost' } %> +production: + <<: *default + database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_prd' } %> + +staging: + <<: *default + database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_stg' } %> + host: <%= ENV.fetch('HUBZONE_MAP_DB_HOST') { 'hz-hzstg.cyy8xym5djtg.us-east-1.rds.amazonaws.com' } %> + +demo: + <<: *default + database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_demo' } %> + host: <%= ENV.fetch('HUBZONE_MAP_DB_HOST') { 'db1.map.dev.sba-one.net' } %> + +dev: + <<: *default + database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_dev' } %> + host: <%= ENV.fetch('HUBZONE_MAP_DB_HOST') { 'db1.map.dev.sba-one.net' } %> + development: <<: *default database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_dev' } %> @@ -34,7 +53,3 @@ development: test: <<: *default database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_test' } %> - -production: - <<: *default - database: <%= ENV.fetch('HUBZONE_MAP_DB_NAME') { 'hzmap_prd' } %> diff --git a/config/environments/demo.rb b/config/environments/demo.rb new file mode 100644 index 00000000..d208ae04 --- /dev/null +++ b/config/environments/demo.rb @@ -0,0 +1,2 @@ +# Same settings as production +require Rails.root.join('config', 'environments', 'production') diff --git a/config/environments/dev.rb b/config/environments/dev.rb new file mode 100644 index 00000000..bbbba0b4 --- /dev/null +++ b/config/environments/dev.rb @@ -0,0 +1,9 @@ +# Same settings as production +require Rails.root.join("config/environments/development") + +Rails.application.configure do + #Serve up compiled assets + config.assets.compile = false + config.assets.debug = false + config.assets.digest = true +end diff --git a/config/environments/production.rb b/config/environments/production.rb index cdf5da29..432d13e4 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -16,6 +16,8 @@ # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. + # + # This should be false on production, i.e., do not set this at all config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. @@ -24,6 +26,8 @@ # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false + config.assets.debug = false + config.assets.digest = true # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb @@ -44,7 +48,8 @@ # Use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug + config.log_level = :info + config.logger = Logger.new(File.join('/', 'var', 'log', 'hubzone-webmap', "#{Rails.env}.log")) # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] diff --git a/config/environments/staging.rb b/config/environments/staging.rb new file mode 100644 index 00000000..d208ae04 --- /dev/null +++ b/config/environments/staging.rb @@ -0,0 +1,2 @@ +# Same settings as production +require Rails.root.join('config', 'environments', 'production') diff --git a/config/environments/test.rb b/config/environments/test.rb index 30587ef6..b727de48 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -39,4 +39,6 @@ # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + config.assets.debug = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 678302ad..b6825719 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -7,10 +7,17 @@ # Rails.application.config.assets.paths << Emoji.images_path %w(js css img fonts).each do |p| Rails.application.config.assets.paths << - Rails.root.join("vendor", "assets", "uswds-0.12.1", p) + Rails.root.join("vendor", "assets", "uswds-0.13.1", p) end +Rails.application.config.assets.paths << + Rails.root.join("vendor", "assets", "uswds-0.13.1", "img", "favicons") # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) -Rails.application.config.assets.precompile += %w( google-maps-api.js ) +Rails.application.config.assets.precompile += %w(google-maps-api.js) +Rails.application.config.assets.precompile += %w(hzmap.js) +Rails.application.config.assets.precompile += %w(*.png *.jpg *.jpeg *.gif *.svg) + +# Allow path helpers in JavaScript assets. +# http://stackoverflow.com/questions/7451517/using-a-rails-helper-method-within-a-javascript-asset +Sprockets::Context.send :include, Rails.application.routes.url_helpers diff --git a/config/initializers/maps.rb b/config/initializers/maps.rb new file mode 100644 index 00000000..ac294c07 --- /dev/null +++ b/config/initializers/maps.rb @@ -0,0 +1,4 @@ +# rubocop:disable Security/YAMLLoad +MAP_CONFIG = YAML.load(ERB.new(File.read(File.expand_path('../../map.yml', __FILE__))).result) +MAP_CONFIG.merge! MAP_CONFIG.fetch(Rails.env, {}) +MAP_CONFIG.deep_symbolize_keys! diff --git a/config/jshint.yml b/config/jshint.yml new file mode 100644 index 00000000..e091c17e --- /dev/null +++ b/config/jshint.yml @@ -0,0 +1,40 @@ +files: ["**/*.js", "**/*.js.erb"] # No need to put app/assets/ or vendor/assets here +exclude_paths: ["vendor/assets/*", "**/helpers/"] +include_paths: ["app/views/", "spec/javascripts"] +options: + boss: true + browser: true + curly: true + eqeqeq: true + eqnull: true + expr: true + immed: true + indent: 2 + latedef: nofunc + maxcomplexity: 5 + newcap: true + noarg: true + sub: true + trailing: true + undef: true + unused: true + globals: + "$": true + ActionCable: true + App: true + console: true + constructSLDXML: true + ga: true + GeoLocation: true + google: true + HZ: true + HZApp: true + hzBaseMapStyle: true + hzQueryMarker: true + hzUserLocation: true + hzWMSOverlays: true + I18n: true + jumpToLocation: true + map: true + updateAccordions: true + updateMarkers: true diff --git a/config/locales/dev.yml b/config/locales/dev.yml new file mode 100644 index 00000000..faa25327 --- /dev/null +++ b/config/locales/dev.yml @@ -0,0 +1,91 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +dev: + hubzone_map: "Mapa de HUBZone" + hubzone_map_aria_label: "Mapa de HUBZone" + usa_banner: + title: "Un sitio web oficial del gobierno de los Estados Unidos" + date: + month_names: [~, Enero, Febrero, Marzo, Abril, Mayo, Junio, Julio, Agosto, Septiembre, Octubre, Noviembre, Diciembre] + abbr_month_names: [~, Ene, Feb, Mar, Abr, May, Jun, Jul, Ago, Sep, Oct, Nov, Dic] + formats: + full: " %B %d %Y" + menu: + hubzone_program: "Programa HUBZone" + hubzone_program_aria_label: "Programa hubzone" + accessible_version: "Versión accesible" + help: "Ayuda" + map: + enter_address: "Ingresa la direccion" + filter: "Filtrar" + search: + label: "Mapa de HUBZone Search Buscar" + sidebar: + button: "Botón de la barra lateral" + api: + success: "Éxito" + error: + zero_results: "Lo sentimos. No se pudo completar la búsqueda. Compruebe la ortografía o incluya más información de dirección." + invalid_request: "Lo sentimos. La solicitud ha fallado debido a un error interno. Vuelve a intentarlo." + over_query_limit: "Lo sentimos. La solicitud ha fallado debido a un error interno. Vuelve a intentarlo." + request_denied: "Lo sentimos. La solicitud ha fallado debido a un error interno. Vuelve a intentarlo." + unknown_error: "Lo sentimos. La solicitud ha fallado debido a un error interno. Vuelve a intentarlo." + hubzone_assertions: + qualified_by: "Calificado por: " + qualifications_effective: "A partir de" + qualified: "HUBZone Calificado" + qualified_aria: "hubzone Calificado" + qualified_short: "Calificado" + not_qualified: "HUBZone No Calificado" + not_qualified_aria: "Hubzone No Calificado" + expired: "Muerto" + expiring: "Caducando" + expires: "Caduca" + qct: "Zona censal" + qct_e: "Zona censal" + qct_b: "BRAC Zona censal" + qct_r: "Rediseñado Zona censal" + qnmc: "Condado" + qnmc_a: "Condado" + qnmc_ab: "Condado" + qnmc_b: "Condado" + qnmc_c: "Condado" + qnmc_dda: "Condado" + qnmc_e: "Condado" + qnmc_brac: "BRAC Condado" + qnmc_r: "Rediseñado Condado" + indian_lands: "Tierra Indígena" + brac: "rediseñado" + disaster_area: "Área de desastre" + legend: + title: "Leyenda" + aria_label: "Leyenda de hubzone" + qct: "Zona censal" + indian_lands: "Tierra Indígena" + redesignated: "Rediseñado" + qnmc: "Condado" + brac: "Antigua base militare" + brac_base: "Antigua base militare" + report: + error: "Error al generar el informe" + generating: "Generar informe..." + created: "Informe creado" diff --git a/config/locales/en.yml b/config/locales/en.yml index 06539571..06b0e0d5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,4 +20,72 @@ # available at http://guides.rubyonrails.org/i18n.html. en: - hello: "Hello world" + hubzone_map: " HUBZone Map" + hubzone_map_aria_label: "hubzone map" + usa_banner: + title: "An official website of the United States government" + date: + month_names: [~, January, Febuary, March, April, May, June, July, August, September, October, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] + formats: + full: " %B %d %Y" + menu: + hubzone_program: "HUBZone Program" + hubzone_program_aria_label: "hubzone program" + accessible_version: "Accessible Version" + help: "Help" + map: + enter_address: "Enter address" + filter: "Filter" + search: + label: "hubzone map search" + sidebar: + button: "Sidebar Button" + api: + success: "Success" + error: + zero_results: "We're sorry. Your search could not be completed. Please check spelling or include more address information." + invalid_request: "We're sorry. The request failed due to an internal error. Please try again." + over_query_limit: "We're sorry. The request failed due to an internal error. Please try again." + request_denied: "We're sorry. The request failed due to an internal error. Please try again." + unknown_error: "We're sorry. The request failed due to an internal error. Please try again." + hubzone_assertions: + qualified_by: "Qualified by: " + qualifications_effective: "As of" + qualified: "Qualified HUBZone" + qualified_aria: "qualified hubzone" + qualified_short: "Qualified" + not_qualified: "Not Qualified" + not_qualified_aria: "Not Qualified" + expiring: "Expiring" + expires: "Expires" + expired: "Expired" + qct: "Census Tract" + qct_e: "Census Tract" + qct_b: "Base Closure Census Tract" + qct_r: "Redesignated Census Tract" + qnmc: "County" + qnmc_a: "County" + qnmc_ab: "County" + qnmc_b: "County" + qnmc_c: "County" + qnmc_dda: "County" + qnmc_e: "County" + qnmc_brac: "Base Closure County" + qnmc_r: "Redesignated County" + indian_lands: "Indian Land" + brac: "Base Closure" + disaster_area: "Disaster Area" + legend: + title: "Legend" + aria_label: "hubzone legend" + qct: "Census Tract" + indian_lands: "Indian Land" + redesignated: "Redesignated" + qnmc: "County" + brac: "Base Closure Area" + brac_base: "Closed Base" + report: + error: "Error Generating Report" + generating: "Generating Report..." + created: "Report Created" diff --git a/config/map.yml b/config/map.yml new file mode 100644 index 00000000..ceb0c1cf --- /dev/null +++ b/config/map.yml @@ -0,0 +1,67 @@ +default: &defaults + hubzone_map_host: <%= ENV.fetch('HUBZONE_MAP_HOST') { 'http://localhost:3001' } %> + hubzone_api_host: <%= ENV.fetch('HUBZONE_API_HOST') { 'http://localhost:3001' } %> + hubzone_api_search_path: '/api/search' + hubzone_report_host: <%= ENV.fetch('HUBZONE_REPORT_API_HOST') { 'http://localhost:3002' } %> + hubzone_api_report_path: '/report' + hubzone_report_request_type: <%= ENV.fetch('HUBZONE_REPORT_REQUEST_TYPE') { 'window_open' } %> + google_api_key: <%= ENV.fetch('HUBZONE_GOOGLE_API_KEY') { 'AIzaSyBsR78bM2H5vMlO60MAtaL9FVtPGWGyQ7c' } %> + google_analytics_tracking_id: <%= ENV.fetch('GOOGLE_ANALYTICS_TRACKING_ID') { 'UA-89111383-1' } %> + hotjar_tracking_id: <%= ENV.fetch('HUBZONE_HOTJAR_TRACKING_ID') { '' } %> + geom_wms_settings: &default_wms + url_root: <%= ENV.fetch('HUBZONE_WMS_URL_ROOT') { 'http://localhost:8080/geoserver/gwc/service/wms?' } %> + workspace: <%= ENV.fetch('HUBZONE_WMS_WORKSPACE') { 'hubzone' } %> + srs: 4326 + wms_enabled: true + + +production: + <<: *defaults + google_analytics_tracking_id: <%= ENV.fetch('GOOGLE_ANALYTICS_TRACKING_ID') { 'UA-19362636-24' } %> + geom_wms_settings: + <<: *default_wms + db: hzgeo_prd + +staging: + <<: *defaults + hubzone_map_host: <%= ENV.fetch('HUBZONE_MAP_HOST') { 'http://maps.staging.certify.sba.gov/' } %> + hubzone_api_host: <%= ENV.fetch('HUBZONE_API_HOST') { 'http://maps.staging.certify.sba.gov' } %> + hubzone_report_host: <%= ENV.fetch('HUBZONE_REPORT_HOST') { 'http://maps.staging.certify.sba.gov' } %> + hotjar_tracking_id: <%= ENV.fetch('HUBZONE_HOTJAR_TRACKING_ID') { '435148' } %> + geom_wms_settings: + <<: *default_wms + url_root: <%= ENV.fetch('HUBZONE_WMS_URL_ROOT') { 'http://maps.staging.certify.sba.gov/geoserver/gwc/service/wms?' } %> + db: hzgeo_stg + +demo: + <<: *defaults + hubzone_map_host: <%= ENV.fetch('HUBZONE_MAP_HOST') { 'https://hz-demo.sba-one.net' } %> + hubzone_api_host: <%= ENV.fetch('HUBZONE_API_HOST') { 'https://hz-demo.sba-one.net' } %> + hubzone_report_host: <%= ENV.fetch('HUBZONE_REPORT_HOST') { 'https://hz-demo.sba-one.net' } %> + hotjar_tracking_id: <%= ENV.fetch('HUBZONE_HOTJAR_TRACKING_ID') { '428263' } %> + geom_wms_settings: + <<: *default_wms + url_root: <%= ENV.fetch('HUBZONE_WMS_URL_ROOT') { 'https://hz-demo.sba-one.net/geoserver/gwc/service/wms?' } %> + db: hzgeo_demo + +dev: + <<: *defaults + hubzone_map_host: <%= ENV.fetch('HUBZONE_MAP_HOST') { 'https://map.dev.sba-one.net' } %> + hubzone_api_host: <%= ENV.fetch('HUBZONE_API_HOST') { 'https://map.dev.sba-one.net' } %> + hubzone_report_host: <%= ENV.fetch('HUBZONE_REPORT_HOST') { 'https://map.dev.sba-one.net' } %> + geom_wms_settings: + <<: *default_wms + url_root: <%= ENV.fetch('HUBZONE_WMS_URL_ROOT') { 'https://map.dev.sba-one.net/geoserver/gwc/service/wms?' } %> + db: hzgeo_dev + +development: + <<: *defaults + geom_wms_settings: + <<: *default_wms + db: hzgeo_dev + +test: + <<: *defaults + geom_wms_settings: + <<: *default_wms + db: why_are_you_hitting_the_WMS_server_in_test diff --git a/config/puma.rb b/config/puma.rb index c7f311f8..61b82220 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -44,4 +44,4 @@ # end # Allow puma to be restarted by `rails restart` command. -plugin :tmp_restart +plugin :tmp_restart unless Rails.env.test? diff --git a/config/routes.rb b/config/routes.rb index 7445121c..8e607d4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,17 @@ Rails.application.routes.draw do - get 'map/fake' - # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + + root to: 'main#index' + + mount JasmineRails::Engine => '/specs' if defined?(JasmineRails) + + app_scope = Rails.env.production? ? '/hubzone/map' : '/map' + + scope app_scope do + get 'search', to: 'map#search' + get 'translate', to: 'map#translate' + get 'aws-hc', to: 'health_check#status' + get 'report', to: 'report#report' + get '/', to: 'map#index', as: :map + end end diff --git a/config/secrets.yml b/config/secrets.yml index 305b28d5..dd52b8e5 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -18,5 +18,17 @@ test: # Do not keep production secrets in the repository, # instead read values from the environment. -production: +default: &defaults secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + +production: + <<: *defaults + +staging: + <<: *defaults + +demo: + <<: *defaults + +dev: + <<: *defaults diff --git a/example.env b/example.env new file mode 100644 index 00000000..3f45c1da --- /dev/null +++ b/example.env @@ -0,0 +1,47 @@ +################################################################################ +# # +# Note: None of the keys in this file are to be used in a # +# staging/production environment # +# # +################################################################################ + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Rails Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# Only set the RAILS_ENV if you want/need to force the system into a specific +# environment. Do not set this for daily development of the system, or else +# your tests will run in the development environment (and will blow away your +# development database!). +# +# RAILS_ENV=development + +# You can generate a new key via `rake secret` +# +SECRET_KEY_BASE='9a8e6968d809c05d559a8d09fae041ee6ed736f21c1a9240efd3192373effa77befb8c640fb31151b2e4b9ebd3a7a26442bf8fdb25f1d4311a1a07b600d6a6c9' + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HUBZone Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# HUBZone Map Settings --------------------------------------------------------- + +# You should set your postgresql user/password. Only uncomment and set the +# other variables if you need to override the defaults. + +HUBZONE_MAP_DB_USER= +HUBZONE_MAP_DB_PASSWORD= +# HUBZONE_MAP_DB_HOST=localhost + +# Google Keys ------------------------------------------------------------------ + +HUBZONE_GOOGLE_API_KEY='AIzaSyBsR78bM2H5vMlO60MAtaL9FVtPGWGyQ7c' # Doug's +HUBZONE_GOOGLE_ANALYTICS_TRACKING_ID='UA-89111383-1' # Doug's + +# *OPTIONAL* HUBZone Override Settings ------------------------------------------- + +# These vars are set in either database.yml or map.yml and should work for +# local development. + +# HUBZONE_MAP_DB_NAME=hzmap_dev +# HUBZONE_API_HOST=http://localhost:3001 +# HUBZONE_REPORT_API_HOST=http://localhost:3002 +# HUBZONE_REPORT_REQUEST_TYPE=window_open +# HUBZONE_WMS_WORKSPACE=hubzone +# HUBZONE_WMS_URL_ROOT=http://localhost:8080/geoserver/gwc/service/wms? diff --git a/public/404.html b/public/404.html index b612547f..0005ef55 100644 --- a/public/404.html +++ b/public/404.html @@ -3,65 +3,23 @@ The page you were looking for doesn't exist (404) - - -
    -
    -

    The page you were looking for doesn't exist.

    -

    You may have mistyped the address or the page may have moved.

    -
    -

    If you are the application owner check the logs for more information.

    -
    + + + + + + + + + +
    certify. SBA .gov
    +

    + The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +









    +
    diff --git a/public/422.html b/public/422.html index a21f82b3..aee37ec7 100644 --- a/public/422.html +++ b/public/422.html @@ -3,65 +3,23 @@ The change you wanted was rejected (422) - - -
    -
    -

    The change you wanted was rejected.

    -

    Maybe you tried to change something you didn't have access to.

    -
    -

    If you are the application owner check the logs for more information.

    -
    + + + + + + + + + +
    certify. SBA .gov
    +

    + The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +









    +
    diff --git a/public/500.html b/public/500.html index 061abc58..5d3228b0 100644 --- a/public/500.html +++ b/public/500.html @@ -3,64 +3,25 @@ We're sorry, but something went wrong (500) - - -
    -
    -

    We're sorry, but something went wrong.

    -
    -

    If you are the application owner check the logs for more information.

    -
    + + + + + + + + + +
    certify. SBA .gov
    +

    + We're sorry, but something went wrong in processing your request.

    +

    Please retry. If the problem persists, contact us at certify@sba.gov

    +









    +
    + + diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb new file mode 100644 index 00000000..45072963 --- /dev/null +++ b/spec/controllers/health_check_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe HealthCheckController do + describe 'GET :status' do + it "should succeed" do + get :status + expect(response).to have_http_status(:ok) + end + it "should return simple text" do + get :status + expect(response.body).to eql("I'm OK") + end + end +end diff --git a/spec/controllers/main_controller_spec.rb b/spec/controllers/main_controller_spec.rb new file mode 100644 index 00000000..0913d597 --- /dev/null +++ b/spec/controllers/main_controller_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +RSpec.describe MainController do + describe "root path" do + before do + ENV['HUBZONE_MAP_HOST'] = 'http://foo.bar' + end + subject { get :index } + + it "should redirect to the map path" do + url = "#{ENV['HUBZONE_MAP_HOST']}#{map_path}" + expect(subject).to redirect_to(url) + end + end +end diff --git a/spec/controllers/map_controller_spec.rb b/spec/controllers/map_controller_spec.rb index 492984f5..9c76bacd 100644 --- a/spec/controllers/map_controller_spec.rb +++ b/spec/controllers/map_controller_spec.rb @@ -1,12 +1,18 @@ -require 'rails_helper' +require "rails_helper" -RSpec.describe MapController, type: :controller do - - describe "GET #fake" do - it "returns http success" do - get :fake - expect(response).to have_http_status(:success) +RSpec.describe MapController do + describe "parses latlng queries" do + context "when given a latlng request" + query = MapController.new.parse_search_query(latlng: "35.86456960744962,-97.87994384765625") + it "should be URI parsed" do + expect(query).to eq "latlng=35.86456960744962%2C-97.87994384765625" + end + end + describe "parse text search" do + context "when given a text request" + query = MapController.new.parse_search_query(search: "8 market place baltimore md") + it "should be URI parsed" do + expect(query).to eq "q=8+market+place+baltimore+md" end end - end diff --git a/spec/features/ga_spec.rb b/spec/features/ga_spec.rb new file mode 100644 index 00000000..04eebcc2 --- /dev/null +++ b/spec/features/ga_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe "Google Analytics", type: :feature do + before do + visit map_path + end + + context "Header links" do + links = { title: { selector: '#logo a.title-link', + description: 'Title link' }, + logo: { selector: '#logo a.logo-link', + description: 'Logo link' }, + program: { selector: 'a#hubzone-program-link', + description: 'Program link' } } + + links.each do |_key, info| + it "should be ready to send an event for the #{info[:description]} link" do + link = find(:css, info[:selector]) + expect(link[:onclick]).to match(/HZApp.GA.openLink/) + end + end + end + + context "Searching" do + it "should be ready to send an event when a user searches an address" do + form = find(:css, 'form.usa-search') + expect(form[:onsubmit]).to match(/HZApp.GA.trackSubmit/) + end + end +end diff --git a/spec/features/map_header_spec.rb b/spec/features/map_header_spec.rb new file mode 100644 index 00000000..3b5ad827 --- /dev/null +++ b/spec/features/map_header_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe "The Header", type: :feature do + before do + visit('/map') + end + it "should have Official U.S. Gov website copy" do + expect(page).to have_content 'An official website of the United States government' + end + it "should have SBA Logo with alt and link to SBA.gov" do + expect(page.find('#sba-logo')['src']).to have_content 'sba-logo' + expect(page).to have_css('.logo-link') + expect(page.find('#sba-logo')['alt']).to have_content 'U.S. Small Business Administration logo' + end + it "should have HUBZone Map title and aria label" do + expect(page).to have_content 'HUBZone Map' + expect(page.find('.title-link')['aria-label']).to have_content 'hubzone map home' + end + it "should have HUBZone Program link and aria label" do + expect(page).to have_content 'HUBZone Program' + expect(page.find('#hubzone-program-link')['aria-label']).to have_content 'hubzone program' + end +end diff --git a/spec/features/map_search_spec.rb b/spec/features/map_search_spec.rb new file mode 100644 index 00000000..9cd7a94f --- /dev/null +++ b/spec/features/map_search_spec.rb @@ -0,0 +1,161 @@ +require 'rails_helper' + +# rubocop:disable Metrics/BlockLength +describe 'The Search', type: :feature, js: true do + context 'before a search performed' do + before do + visit map_path + end + it "should have aria labels" do + expect(page.find('#search-field-small')['aria-labelledby']).to have_content('hubzone-search') + end + it "should have autofocus" do + expect(page.find('#search-field-small')['autofocus']).to be_truthy + end + it "should have a tab index" do + expect(page.find('#search-field-small')['tabindex']).to eq('1') + end + end + + test_queries = { + qualified_single: { + search: 'navajo', + response: { + formatted_address: 'Yup', + http_status: 200, + hubzone: [ + { + hz_type: "indian_lands", + expires: nil + } + ], + geometry: { + location: { + lat: 0, + lng: 0 + } + }, + query_date: Date.today + }, + status: "hubzone_assertions.qualified" + }, + qualified_expired: { + search: 'rockcastle, ky', + response: { + formatted_address: 'Rockcastle County, KY, USA', + http_status: 200, + hubzone: [ + { + hz_type: "qct_e", + expires: Date.tomorrow + }, + { + hz_type: "qnmc_r", + expires: Date.today.last_week + } + ], + geometry: { + location: { + lat: 0, + lng: 0 + } + }, + query_date: Date.today + }, + status: "hubzone_assertions.qualified" + }, + not_qualified: { + search: "banana", + response: { + formatted_address: "Banana QLD 4702, Australia", + http_status: 200, + hubzone: [], + geometry: { + location: { + lat: 0, + lng: 0 + } + }, + query_date: Date.today + }, + status: "hubzone_assertions.not_qualified" + }, + qualified_multiple: { + search: "tiffany peak, co", + response: { + formatted_address: "Tiffany Peak, Colorado 81137, USA", + http_status: 200, + hubzone: [ + { + hz_type: "indian_lands", + expires: nil + }, + { + hz_type: "qct", + expires: nil + } + ], + geometry: { + location: { + lat: 0, + lng: 0 + } + }, + query_date: Date.today + }, + status: "hubzone_assertions.qualified" + } + } + + %w(en dev).each do |locale| + context "in the #{locale} locale" do + before do + I18n.locale = locale + visit map_path(locale: locale) + end + + test_queries.map do |hztype, tquery| + context "with #{hztype} query" do + before do + Excon.stub({}, body: tquery[:response].to_json) + fill_in 'search', with: tquery[:search] + click_button 'hubzone-search-button' + end + + after(:each) do + Excon.stubs.clear + end + + it "should show the correct designation status" do + expect(page).to have_content(t(tquery[:status])) + end + + it "should have the correct formatted_address" do + expect(page).to have_content(tquery[:formatted_address]) + end + + it "should provide a clear search button" do + expect(page).to have_css(".clear-search") + end + + it "should display the date of the search" do + expect(page).to have_content(t('hubzone_assertions.qualifications_effective') + I18n.l(Date.today, format: :full)) + end + + context "for any hubzone designations" do + tquery[:response][:hubzone].each do |hubzone| + it "should contain the correct hubzone assertions" do + expect(page).to have_content(t("hubzone_assertions." + hubzone[:hz_type].to_s)) + end + + next unless hubzone[:expires] + it "should show the correct language for expires or expired if expiration date is present" do + expect(page).to have_content(hubzone[:expires] < Date.today ? t('hubzone_assertions.expired') : t('hubzone_assertions.expires')) + end + end + end + end + end + end + end +end diff --git a/spec/features/map_sidebar_spec.rb b/spec/features/map_sidebar_spec.rb new file mode 100644 index 00000000..7523f91a --- /dev/null +++ b/spec/features/map_sidebar_spec.rb @@ -0,0 +1,116 @@ +require 'rails_helper' + +# rubocop:disable Metrics/BlockLength +describe "The Sidebar", type: :feature do + queries = { qualified_multiple: 'navajo', + qualified_single: 'tiffany peak, co', + non_qualified: 'banana', + intersection: '25th & st. paul, baltimore' } + + responses = { qualified_multiple: { formatted_address: 'Yup', + http_status: 200, + hubzone: [ + { + hz_type: "indian_lands" + }, + { + hz_type: "qct" + } + ], + geometry: { + location: { + lat: 0, + lng: 0 + } + } }, + non_qualified: { formatted_address: "Nope", + http_status: 200, + hubzone: [], + geometry: { + location: { + lat: 0, + lng: 0 + } + } }, + intersection: { formatted_address: + 'St Paul St & E 25th St, Baltimore, MD 21218, USA', + http_status: 200, + hubzone: [], + geometry: { + location: { + lat: 0, + lng: 0 + } + } } } + + before do + visit('/map') + end + + after(:each) do + Excon.stubs.clear + end + + context "before any interactions" do + it "should exist and be hidden" do + expect(page).to have_css("#sidebar.hidden") + end + it "should show hubzone qualification with aria label and tab index" do + expect(page).to have_css("#hubzone-status") + expect(page.find('#hubzone-status')['aria-label']).to be_truthy + expect(page.find('#hubzone-status')['tabindex']).to be_truthy + end + it "should have as-of date with tab index" do + expect(page).to have_css(".hubzone-status-date") + expect(page.find('.hubzone-status-date')['tabindex']).to be_truthy + end + end + + context "after a search performed", js: true do + before do + Excon.stub({}, + body: responses[:non_qualified].to_json) + end + it "should have a report button" do + fill_in 'search', with: queries[:non_qualified] + click_button "hubzone-search-button" + expect(page).to have_css('#map-report') + end + it "should be visible" do + click_button "hubzone-search-button" + expect(page).not_to have_css("#sidebar.hidden") + end + it "should move the zoom controls out" do + fill_in 'search', with: queries[:non_qualified] + click_button "hubzone-search-button" + expect(page).to have_css(".gm-sidebar-on") + end + it "...and back in" do + expect(page).not_to have_css(".gm-sidebar-on") + end + end + + context "with a non-qualified address", js: true do + before do + Excon.stub({}, + body: responses[:non_qualified].to_json) + end + it "should show no qualifications" do + fill_in 'search', with: queries[:non_qualified] + click_button 'hubzone-search-button' + expect(page).not_to have_css("#qct_e", visible: false) + end + end + + context "with a qualified address", js: true do + before do + Excon.stub({}, + body: responses[:qualified_multiple].to_json) + end + it "should show one qualification" do + fill_in 'search', with: queries[:qualified_single] + click_button 'hubzone-search-button' + expect(page).to have_css("#indian_lands", visible: false) + end + end +end diff --git a/spec/features/map_spec.rb b/spec/features/map_spec.rb new file mode 100644 index 00000000..457523f4 --- /dev/null +++ b/spec/features/map_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe "The HUBZone Map", type: :feature do + before do + visit('/map') + end + it "should have header section" do + expect(page).to have_selector('header') + end + it "should have body section" do + expect(page).to have_selector('body') + end + it "should have a search bar" do + expect(page).to have_selector('#search-field-small') + end + it "should have a map div" do + expect(page).to have_selector('#map') + end + it "should have a sidebar" do + expect(page).to have_selector('#sidebar') + end + it "should have a legend" do + expect(page).to have_css("#legend") + end +end diff --git a/spec/hubzone_helper.rb b/spec/hubzone_helper.rb new file mode 100644 index 00000000..fb9467e8 --- /dev/null +++ b/spec/hubzone_helper.rb @@ -0,0 +1,5 @@ +module HubzoneHelper + def t(arg) + I18n.translate arg + end +end diff --git a/spec/javascripts/basic_spec.js b/spec/javascripts/basic_spec.js new file mode 100644 index 00000000..125d7895 --- /dev/null +++ b/spec/javascripts/basic_spec.js @@ -0,0 +1,5 @@ +// describe ('This is how jasmine works', function() { +// it ("indexOf to work", function(){ +// expect([1].indexOf(1) !== -1).toBe(true); +// }) +// }); diff --git a/spec/javascripts/fixtures/hz_mock_page.html b/spec/javascripts/fixtures/hz_mock_page.html new file mode 100644 index 00000000..192bcb43 --- /dev/null +++ b/spec/javascripts/fixtures/hz_mock_page.html @@ -0,0 +1,31 @@ +
    + + + +
    + +
    + + + + diff --git a/spec/javascripts/helpers/hz-jasmine.js b/spec/javascripts/helpers/hz-jasmine.js new file mode 100644 index 00000000..d3d96179 --- /dev/null +++ b/spec/javascripts/helpers/hz-jasmine.js @@ -0,0 +1,334 @@ +/* jshint unused: false */ +/* jshint undef: false */ + +// helper for running hz jasmine tests +var HZSpecHelper = (function(){ + //polyfills + if (!String.prototype.includes) { + String.prototype.includes = function(search, start) { + 'use strict'; + if (typeof start !== 'number') { + start = 0; + } + + if (start + search.length > this.length) { + return false; + } else { + return this.indexOf(search, start) !== -1; + } + }; + } + + return { + //build a dummy sidebar and mapbody + mockPage: { + build: function(){ + + //add map + var mapBodyDiv = document.createElement('div'); + $(mapBodyDiv).addClass('map-body mock-page'); + + //add header and search + $('body').append(''); + $('#header').append('') + + //add legend + $('body').append('
    '); + + //add geolocation button + $('body').append(''); + $('#geolocation').append('') + $('#geolocation').append('