├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── .buildignore ├── .htaccess ├── 404.html ├── angular-leaflet-directive.min.js ├── favicon.ico ├── humans.txt ├── images │ ├── astrodigital.png │ ├── logo_horizontal_white.png │ ├── ndvi.png │ ├── noun_17256.png │ ├── noun_17256_w.png │ ├── noun_2019_cc_noattr.png │ ├── noun_2019_cc_noattr_w.png │ ├── noun_24967.png │ ├── noun_24967_down.png │ ├── noun_24967_up.png │ ├── noun_24967_w.png │ ├── noun_52713_cc.png │ ├── noun_52713_cc_noattr_w.png │ ├── noun_88350_cc_noattr.png │ ├── noun_88350_cc_noattr_w.png │ ├── noun_88351_cc_noattr.png │ ├── noun_88351_cc_noattr_w.png │ ├── satellite.gif │ ├── truecolor.png │ ├── urban.png │ └── yeoman.png ├── index.html ├── jquery.nouislider.all.min.js ├── jquery.nouislider.min.css ├── jquery.nouislider.min.js ├── jquery.nouislider.pips.min.css ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ ├── advanced.js │ │ └── main.js │ └── filters │ │ └── suffix.js ├── styles │ └── main.scss └── views │ ├── advanced.html │ ├── main.html │ └── modal.html ├── attributions.md ├── bower.json ├── package.json └── test ├── .jshintrc ├── karma.conf.js ├── mock └── landsatAPI.json └── spec └── controllers └── main.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | bower_components 5 | .project 6 | .resources/ 7 | dist 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false, 22 | "d3": false, 23 | "jQuery": false, 24 | "Spinner": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | 5 | env: 6 | global: 7 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 8 | 9 | before_script: 10 | - npm install -g bower grunt-cli 11 | - bower install 12 | - gem install sass --version "=3.4.9" 13 | - gem install compass --version "=1.0.1" 14 | 15 | script: grunt build 16 | 17 | deploy: 18 | provider: s3 19 | access_key_id: AKIAJR4CUXWRKZDB6RAA 20 | secret_access_key: 21 | secure: cYYpx+LVDFLQqMGDuUNODU6kqEFsr4C58wSah4gmHjnK5FrZIcNF6E7A4qEu/yVI3sch6+hZBT4ckprQF0rGQd4EB2graXjmd47hALjSF3srXlOmg2BpOYai/yC08Bj48mzjwQGEW2rBhWSu3aFDgzCuq/K9CMuGXep1AaUWGTM= 22 | bucket: libra.developmentseed.org 23 | local-dir: dist 24 | skip_cleanup: true 25 | acl: public_read 26 | on: 27 | repo: AstroDigital/libra 28 | branch: master 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #Contribution guidelines 2 | 3 | There are many ways to contribute to a project, below are some examples: 4 | 5 | - Report bugs, ideas, requests for features by creating “Issues” in the project repository. 6 | - Fork the code and play with it, whether you later choose to make a pull request or not. 7 | - Create pull requests of changes that you think are laudatory. From typos to major design flaws, you will find a target-rich environment for improvements. 8 | 9 | ## Style 10 | There is no set style for this project, but please try to match existing coding styles as closely as possible. 11 | 12 | ## Tests 13 | If you're going to add new features, please make sure they come along with tests to make sure everything works as expected. Outside minor changes, Pull Requests will not be accepted without associated tests. 14 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2015-01-05 using generator-angular 0.10.0 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Configurable paths for the application 19 | var appConfig = { 20 | app: require('./bower.json').appPath || 'app', 21 | dist: 'dist' 22 | }; 23 | 24 | // Define the configuration for all the tasks 25 | grunt.initConfig({ 26 | 27 | // Project settings 28 | yeoman: appConfig, 29 | 30 | // Watches files for changes and runs tasks based on the changed files 31 | watch: { 32 | bower: { 33 | files: ['bower.json'], 34 | tasks: ['wiredep'] 35 | }, 36 | js: { 37 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 38 | tasks: ['newer:jshint:all'], 39 | options: { 40 | livereload: '<%= connect.options.livereload %>' 41 | } 42 | }, 43 | jsTest: { 44 | files: ['test/spec/{,*/}*.js'], 45 | tasks: ['newer:jshint:test', 'karma'] 46 | }, 47 | compass: { 48 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 49 | tasks: ['compass:server', 'autoprefixer'] 50 | }, 51 | gruntfile: { 52 | files: ['Gruntfile.js'] 53 | }, 54 | livereload: { 55 | options: { 56 | livereload: '<%= connect.options.livereload %>' 57 | }, 58 | files: [ 59 | '<%= yeoman.app %>/{,*/}*.html', 60 | '.tmp/styles/{,*/}*.css', 61 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 62 | ] 63 | } 64 | }, 65 | 66 | // The actual grunt server settings 67 | connect: { 68 | options: { 69 | port: 9090, 70 | // Change this to '0.0.0.0' to access the server from outside. 71 | hostname: 'localhost', 72 | livereload: 35729 73 | }, 74 | livereload: { 75 | options: { 76 | open: true, 77 | middleware: function (connect) { 78 | return [ 79 | connect.static('.tmp'), 80 | connect().use( 81 | '/bower_components', 82 | connect.static('./bower_components') 83 | ), 84 | connect.static(appConfig.app) 85 | ]; 86 | } 87 | } 88 | }, 89 | test: { 90 | options: { 91 | port: 9001, 92 | middleware: function (connect) { 93 | return [ 94 | connect.static('.tmp'), 95 | connect.static('test'), 96 | connect().use( 97 | '/bower_components', 98 | connect.static('./bower_components') 99 | ), 100 | connect.static(appConfig.app) 101 | ]; 102 | } 103 | } 104 | }, 105 | dist: { 106 | options: { 107 | open: true, 108 | base: '<%= yeoman.dist %>' 109 | } 110 | } 111 | }, 112 | 113 | // Make sure code styles are up to par and there are no obvious mistakes 114 | jshint: { 115 | options: { 116 | jshintrc: '.jshintrc', 117 | reporter: require('jshint-stylish') 118 | }, 119 | all: { 120 | src: [ 121 | 'Gruntfile.js', 122 | '<%= yeoman.app %>/scripts/{,*/}*.js' 123 | ] 124 | }, 125 | test: { 126 | options: { 127 | jshintrc: 'test/.jshintrc' 128 | }, 129 | src: ['test/spec/{,*/}*.js'] 130 | } 131 | }, 132 | 133 | // Empties folders to start fresh 134 | clean: { 135 | dist: { 136 | files: [{ 137 | dot: true, 138 | src: [ 139 | '.tmp', 140 | '<%= yeoman.dist %>/{,*/}*', 141 | '!<%= yeoman.dist %>/.git{,*/}*' 142 | ] 143 | }] 144 | }, 145 | server: '.tmp' 146 | }, 147 | 148 | // Add vendor prefixed styles 149 | autoprefixer: { 150 | options: { 151 | browsers: ['last 1 version'] 152 | }, 153 | dist: { 154 | files: [{ 155 | expand: true, 156 | cwd: '.tmp/styles/', 157 | src: '{,*/}*.css', 158 | dest: '.tmp/styles/' 159 | }] 160 | } 161 | }, 162 | 163 | // Automatically inject Bower components into the app 164 | wiredep: { 165 | app: { 166 | src: ['<%= yeoman.app %>/index.html'], 167 | ignorePath: /\.\.\// 168 | }, 169 | sass: { 170 | src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 171 | ignorePath: /(\.\.\/){1,2}bower_components\// 172 | } 173 | }, 174 | 175 | // Compiles Sass to CSS and generates necessary files if requested 176 | compass: { 177 | options: { 178 | sassDir: '<%= yeoman.app %>/styles', 179 | cssDir: '.tmp/styles', 180 | generatedImagesDir: '.tmp/images/generated', 181 | imagesDir: '<%= yeoman.app %>/images', 182 | javascriptsDir: '<%= yeoman.app %>/scripts', 183 | fontsDir: '<%= yeoman.app %>/styles/fonts', 184 | importPath: './bower_components', 185 | httpImagesPath: '/images', 186 | httpGeneratedImagesPath: '/images/generated', 187 | httpFontsPath: '/styles/fonts', 188 | relativeAssets: false, 189 | assetCacheBuster: false, 190 | raw: 'Sass::Script::Number.precision = 10\n' 191 | }, 192 | dist: { 193 | options: { 194 | generatedImagesDir: '<%= yeoman.dist %>/images/generated' 195 | } 196 | }, 197 | server: { 198 | options: { 199 | debugInfo: true 200 | } 201 | } 202 | }, 203 | 204 | // Renames files for browser caching purposes 205 | filerev: { 206 | dist: { 207 | src: [ 208 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 209 | '<%= yeoman.dist %>/styles/{,*/}*.css', 210 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 211 | '<%= yeoman.dist %>/styles/fonts/*' 212 | ] 213 | } 214 | }, 215 | 216 | // Reads HTML for usemin blocks to enable smart builds that automatically 217 | // concat, minify and revision files. Creates configurations in memory so 218 | // additional tasks can operate on them 219 | useminPrepare: { 220 | html: '<%= yeoman.app %>/index.html', 221 | options: { 222 | dest: '<%= yeoman.dist %>', 223 | flow: { 224 | html: { 225 | steps: { 226 | js: ['concat', 'uglifyjs'], 227 | css: ['cssmin'] 228 | }, 229 | post: {} 230 | } 231 | } 232 | } 233 | }, 234 | 235 | // Performs rewrites based on filerev and the useminPrepare configuration 236 | usemin: { 237 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 238 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 239 | options: { 240 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] 241 | } 242 | }, 243 | 244 | // The following *-min tasks will produce minified files in the dist folder 245 | // By default, your `index.html`'s will take care of 246 | // minification. These next options are pre-configured if you do not wish 247 | // to use the Usemin blocks. 248 | // cssmin: { 249 | // dist: { 250 | // files: { 251 | // '<%= yeoman.dist %>/styles/main.css': [ 252 | // '.tmp/styles/{,*/}*.css' 253 | // ] 254 | // } 255 | // } 256 | // }, 257 | // uglify: { 258 | // dist: { 259 | // files: { 260 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 261 | // '<%= yeoman.dist %>/scripts/scripts.js' 262 | // ] 263 | // } 264 | // } 265 | // }, 266 | // concat: { 267 | // dist: {} 268 | // }, 269 | 270 | imagemin: { 271 | dist: { 272 | files: [{ 273 | expand: true, 274 | cwd: '<%= yeoman.app %>/images', 275 | src: '{,*/}*.{png,jpg,jpeg,gif}', 276 | dest: '<%= yeoman.dist %>/images' 277 | }] 278 | } 279 | }, 280 | 281 | svgmin: { 282 | dist: { 283 | files: [{ 284 | expand: true, 285 | cwd: '<%= yeoman.app %>/images', 286 | src: '{,*/}*.svg', 287 | dest: '<%= yeoman.dist %>/images' 288 | }] 289 | } 290 | }, 291 | 292 | htmlmin: { 293 | dist: { 294 | options: { 295 | collapseWhitespace: true, 296 | conservativeCollapse: true, 297 | collapseBooleanAttributes: true, 298 | removeCommentsFromCDATA: true, 299 | removeOptionalTags: true 300 | }, 301 | files: [{ 302 | expand: true, 303 | cwd: '<%= yeoman.dist %>', 304 | src: ['*.html', 'views/{,*/}*.html'], 305 | dest: '<%= yeoman.dist %>' 306 | }] 307 | } 308 | }, 309 | 310 | // ng-annotate tries to make the code safe for minification automatically 311 | // by using the Angular long form for dependency injection. 312 | ngAnnotate: { 313 | dist: { 314 | files: [{ 315 | expand: true, 316 | cwd: '.tmp/concat/scripts', 317 | src: ['*.js', '!oldieshim.js'], 318 | dest: '.tmp/concat/scripts' 319 | }] 320 | } 321 | }, 322 | 323 | // Replace Google CDN references 324 | cdnify: { 325 | dist: { 326 | html: ['<%= yeoman.dist %>/*.html'] 327 | } 328 | }, 329 | 330 | // Copies remaining files to places other tasks can use 331 | copy: { 332 | dist: { 333 | files: [{ 334 | expand: true, 335 | dot: true, 336 | cwd: '<%= yeoman.app %>', 337 | dest: '<%= yeoman.dist %>', 338 | src: [ 339 | '*.{ico,png,txt}', 340 | '.htaccess', 341 | '*.html', 342 | 'views/{,*/}*.html', 343 | 'images/{,*/}*.{webp}', 344 | 'fonts/{,*/}*.*' 345 | ] 346 | }, { 347 | expand: true, 348 | cwd: '.tmp/images', 349 | dest: '<%= yeoman.dist %>/images', 350 | src: ['generated/*'] 351 | }, { 352 | expand: true, 353 | cwd: '.', 354 | src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', 355 | dest: '<%= yeoman.dist %>' 356 | }] 357 | }, 358 | styles: { 359 | expand: true, 360 | cwd: '<%= yeoman.app %>/styles', 361 | dest: '.tmp/styles/', 362 | src: '{,*/}*.css' 363 | } 364 | }, 365 | 366 | // Run some tasks in parallel to speed up the build process 367 | concurrent: { 368 | server: [ 369 | 'compass:server' 370 | ], 371 | test: [ 372 | 'compass' 373 | ], 374 | dist: [ 375 | 'compass:dist', 376 | 'imagemin', 377 | 'svgmin' 378 | ] 379 | }, 380 | 381 | // Test settings 382 | karma: { 383 | unit: { 384 | configFile: 'test/karma.conf.js', 385 | singleRun: true 386 | } 387 | } 388 | }); 389 | 390 | 391 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 392 | if (target === 'dist') { 393 | return grunt.task.run(['build', 'connect:dist:keepalive']); 394 | } 395 | 396 | grunt.task.run([ 397 | 'clean:server', 398 | 'wiredep', 399 | 'concurrent:server', 400 | 'autoprefixer', 401 | 'connect:livereload', 402 | 'watch' 403 | ]); 404 | }); 405 | 406 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 407 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 408 | grunt.task.run(['serve:' + target]); 409 | }); 410 | 411 | grunt.registerTask('test', [ 412 | 'clean:server', 413 | 'concurrent:test', 414 | 'autoprefixer', 415 | 'connect:test', 416 | 'karma' 417 | ]); 418 | 419 | grunt.registerTask('build', [ 420 | 'clean:dist', 421 | 'wiredep', 422 | 'useminPrepare', 423 | 'concurrent:dist', 424 | 'autoprefixer', 425 | 'concat', 426 | 'ngAnnotate', 427 | 'copy:dist', 428 | 'cdnify', 429 | 'cssmin', 430 | 'uglify', 431 | 'filerev', 432 | 'usemin', 433 | 'htmlmin' 434 | ]); 435 | 436 | grunt.registerTask('default', [ 437 | 'newer:jshint', 438 | 'test', 439 | 'build' 440 | ]); 441 | }; 442 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Development Seed 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Libra [![Build Status](https://travis-ci.org/AstroDigital/libra.svg)](https://travis-ci.org/AstroDigital/libra) 2 | 3 | ## Overview 4 | 5 | Libra is an open-source, Landsat-8 imagery browser. It relies on [landsat-api](https://github.com/developmentseed/landsat-api) and an [AngularJS](https://angularjs.org/)-designed GUI to allow users to browse, sort, and download more than 275 Terabytes of open Landsat imagery. 6 | 7 | See [here](https://developmentseed.org/blog/2015/01/15/astro-digital-image-search/), 8 | [here](https://medium.com/@astrodigital/browsing-large-sets-of-satellite-imagery-7096db1a807f), and [here](https://developmentseed.org/blog/2015/01/22/announcing-libra/) for more information. 9 | 10 | ## Setting up your development environment 11 | To set up the development environment for this app, you'll need to install the following on your system: 12 | 13 | - [npm](https://www.npmjs.com/) 14 | - [Compass](http://compass-style.org/) & [Sass](http://sass-lang.com/) 15 | - [Grunt](http://gruntjs.com/) ( $ npm install -g grunt-cli ) 16 | - [Bower](http://bower.io/) ($ npm install -g bower) 17 | 18 | After these basic requirements are met, run the following commands in the root project folder: 19 | ``` 20 | $ npm install 21 | $ bower install 22 | ``` 23 | 24 | ## Running the app 25 | To start the app running, run the following command in the root project folder. 26 | 27 | ``` 28 | $ grunt serve 29 | ``` 30 | Serves the site at: `http://localhost:9090` (should automatically open in 31 | your browser) 32 | 33 | ## Deploying the app 34 | To deploy the app, run the command below to create a `dist` directory in your project root 35 | 36 | ``` 37 | $ grunt build 38 | ``` 39 | 40 | `dist` is a directory of static HTML, CSS and JS files and can be served via any traditional web-serving mechanism. 41 | 42 | ## Future improvements 43 | - Determine a good way to show the total number of results being displayed 44 | - Add animations where applicable 45 | - When switching into the single result pane 46 | - When the top filters drop down 47 | - Make the scroll bar look a bit nicer 48 | - Add a way to toggle between various basemaps 49 | - Implement lazy loading for the results pane so not all images are loaded at 50 | the same time 51 | - Place name search 52 | - Different cluster sizes at different zoom levels 53 | - Client side caching of results (TBD) 54 | - Improved stack icons (2-3 circles for multiple results) 55 | 56 | ## Known issues 57 | 58 | - Histograms disappear when opening modal (close filters on modal open for now) 59 | - Date filter clicks back one day when opening for the first time 60 | - When over water where no scenes are returned, error message says '...you zoomed in too much' which isn't technically the exact error 61 | - We currently have an issue when drawing the histograms where we get the dreaded ```Error: $digest already in progress``` in the console. While this doesn't cause any visual issues, it does mean we can't run the test suite. 62 | 63 | ## Where to go from here? 64 | 65 | Now that you have access to all this wonderful imagery, you may be wondering what do next. There are a number of open-source tools you can use to dive into the imagery on a deeper level. A few of them are listed below: 66 | 67 | - [landsat-util](https://github.com/developmentseed/landsat-util) 68 | - [GDAL](http://www.gdal.org/) [[Landsat-8 specific tutorial](https://www.mapbox.com/blog/processing-landsat-8/)] 69 | - [QGIS](http://qgis.org) [[Tutorials](http://www.qgistutorials.com)] 70 | - [rasterio](https://github.com/mapbox/rasterio) 71 | - [Mapbox guide](https://www.mapbox.com/guides/processing-satellite-imagery/) 72 | -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache Configuration File 2 | 3 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have access 4 | # to the main server config file (usually called `httpd.conf`), you should add 5 | # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. 6 | 7 | # ############################################################################## 8 | # # CROSS-ORIGIN RESOURCE SHARING (CORS) # 9 | # ############################################################################## 10 | 11 | # ------------------------------------------------------------------------------ 12 | # | Cross-domain AJAX requests | 13 | # ------------------------------------------------------------------------------ 14 | 15 | # Enable cross-origin AJAX requests. 16 | # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity 17 | # http://enable-cors.org/ 18 | 19 | # 20 | # Header set Access-Control-Allow-Origin "*" 21 | # 22 | 23 | # ------------------------------------------------------------------------------ 24 | # | CORS-enabled images | 25 | # ------------------------------------------------------------------------------ 26 | 27 | # Send the CORS header for images when browsers request it. 28 | # https://developer.mozilla.org/en/CORS_Enabled_Image 29 | # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html 30 | # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ 31 | 32 | 33 | 34 | 35 | SetEnvIf Origin ":" IS_CORS 36 | Header set Access-Control-Allow-Origin "*" env=IS_CORS 37 | 38 | 39 | 40 | 41 | # ------------------------------------------------------------------------------ 42 | # | Web fonts access | 43 | # ------------------------------------------------------------------------------ 44 | 45 | # Allow access from all domains for web fonts 46 | 47 | 48 | 49 | Header set Access-Control-Allow-Origin "*" 50 | 51 | 52 | 53 | 54 | # ############################################################################## 55 | # # ERRORS # 56 | # ############################################################################## 57 | 58 | # ------------------------------------------------------------------------------ 59 | # | 404 error prevention for non-existing redirected folders | 60 | # ------------------------------------------------------------------------------ 61 | 62 | # Prevent Apache from returning a 404 error for a rewrite if a directory 63 | # with the same name does not exist. 64 | # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews 65 | # http://www.webmasterworld.com/apache/3808792.htm 66 | 67 | Options -MultiViews 68 | 69 | # ------------------------------------------------------------------------------ 70 | # | Custom error messages / pages | 71 | # ------------------------------------------------------------------------------ 72 | 73 | # You can customize what Apache returns to the client in case of an error (see 74 | # http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: 75 | 76 | ErrorDocument 404 /404.html 77 | 78 | 79 | # ############################################################################## 80 | # # INTERNET EXPLORER # 81 | # ############################################################################## 82 | 83 | # ------------------------------------------------------------------------------ 84 | # | Better website experience | 85 | # ------------------------------------------------------------------------------ 86 | 87 | # Force IE to render pages in the highest available mode in the various 88 | # cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. 89 | 90 | 91 | Header set X-UA-Compatible "IE=edge" 92 | # `mod_headers` can't match based on the content-type, however, we only 93 | # want to send this header for HTML pages and not for the other resources 94 | 95 | Header unset X-UA-Compatible 96 | 97 | 98 | 99 | # ------------------------------------------------------------------------------ 100 | # | Cookie setting from iframes | 101 | # ------------------------------------------------------------------------------ 102 | 103 | # Allow cookies to be set from iframes in IE. 104 | 105 | # 106 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" 107 | # 108 | 109 | # ------------------------------------------------------------------------------ 110 | # | Screen flicker | 111 | # ------------------------------------------------------------------------------ 112 | 113 | # Stop screen flicker in IE on CSS rollovers (this only works in 114 | # combination with the `ExpiresByType` directives for images from below). 115 | 116 | # BrowserMatch "MSIE" brokenvary=1 117 | # BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 118 | # BrowserMatch "Opera" !brokenvary 119 | # SetEnvIf brokenvary 1 force-no-vary 120 | 121 | 122 | # ############################################################################## 123 | # # MIME TYPES AND ENCODING # 124 | # ############################################################################## 125 | 126 | # ------------------------------------------------------------------------------ 127 | # | Proper MIME types for all files | 128 | # ------------------------------------------------------------------------------ 129 | 130 | 131 | 132 | # Audio 133 | AddType audio/mp4 m4a f4a f4b 134 | AddType audio/ogg oga ogg 135 | 136 | # JavaScript 137 | # Normalize to standard type (it's sniffed in IE anyways): 138 | # http://tools.ietf.org/html/rfc4329#section-7.2 139 | AddType application/javascript js jsonp 140 | AddType application/json json 141 | 142 | # Video 143 | AddType video/mp4 mp4 m4v f4v f4p 144 | AddType video/ogg ogv 145 | AddType video/webm webm 146 | AddType video/x-flv flv 147 | 148 | # Web fonts 149 | AddType application/font-woff woff 150 | AddType application/vnd.ms-fontobject eot 151 | 152 | # Browsers usually ignore the font MIME types and sniff the content, 153 | # however, Chrome shows a warning if other MIME types are used for the 154 | # following fonts. 155 | AddType application/x-font-ttf ttc ttf 156 | AddType font/opentype otf 157 | 158 | # Make SVGZ fonts work on iPad: 159 | # https://twitter.com/FontSquirrel/status/14855840545 160 | AddType image/svg+xml svg svgz 161 | AddEncoding gzip svgz 162 | 163 | # Other 164 | AddType application/octet-stream safariextz 165 | AddType application/x-chrome-extension crx 166 | AddType application/x-opera-extension oex 167 | AddType application/x-shockwave-flash swf 168 | AddType application/x-web-app-manifest+json webapp 169 | AddType application/x-xpinstall xpi 170 | AddType application/xml atom rdf rss xml 171 | AddType image/webp webp 172 | AddType image/x-icon ico 173 | AddType text/cache-manifest appcache manifest 174 | AddType text/vtt vtt 175 | AddType text/x-component htc 176 | AddType text/x-vcard vcf 177 | 178 | 179 | 180 | # ------------------------------------------------------------------------------ 181 | # | UTF-8 encoding | 182 | # ------------------------------------------------------------------------------ 183 | 184 | # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. 185 | AddDefaultCharset utf-8 186 | 187 | # Force UTF-8 for certain file formats. 188 | 189 | AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml 190 | 191 | 192 | 193 | # ############################################################################## 194 | # # URL REWRITES # 195 | # ############################################################################## 196 | 197 | # ------------------------------------------------------------------------------ 198 | # | Rewrite engine | 199 | # ------------------------------------------------------------------------------ 200 | 201 | # Turning on the rewrite engine and enabling the `FollowSymLinks` option is 202 | # necessary for the following directives to work. 203 | 204 | # If your web host doesn't allow the `FollowSymlinks` option, you may need to 205 | # comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the 206 | # performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks 207 | 208 | # Also, some cloud hosting services require `RewriteBase` to be set: 209 | # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site 210 | 211 | 212 | Options +FollowSymlinks 213 | # Options +SymLinksIfOwnerMatch 214 | RewriteEngine On 215 | # RewriteBase / 216 | 217 | 218 | # ------------------------------------------------------------------------------ 219 | # | Suppressing / Forcing the "www." at the beginning of URLs | 220 | # ------------------------------------------------------------------------------ 221 | 222 | # The same content should never be available under two different URLs especially 223 | # not with and without "www." at the beginning. This can cause SEO problems 224 | # (duplicate content), therefore, you should choose one of the alternatives and 225 | # redirect the other one. 226 | 227 | # By default option 1 (no "www.") is activated: 228 | # http://no-www.org/faq.php?q=class_b 229 | 230 | # If you'd prefer to use option 2, just comment out all the lines from option 1 231 | # and uncomment the ones from option 2. 232 | 233 | # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! 234 | 235 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 236 | 237 | # Option 1: rewrite www.example.com → example.com 238 | 239 | 240 | RewriteCond %{HTTPS} !=on 241 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] 242 | RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] 243 | 244 | 245 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 246 | 247 | # Option 2: rewrite example.com → www.example.com 248 | 249 | # Be aware that the following might not be a good idea if you use "real" 250 | # subdomains for certain parts of your website. 251 | 252 | # 253 | # RewriteCond %{HTTPS} !=on 254 | # RewriteCond %{HTTP_HOST} !^www\..+$ [NC] 255 | # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 256 | # 257 | 258 | 259 | # ############################################################################## 260 | # # SECURITY # 261 | # ############################################################################## 262 | 263 | # ------------------------------------------------------------------------------ 264 | # | Content Security Policy (CSP) | 265 | # ------------------------------------------------------------------------------ 266 | 267 | # You can mitigate the risk of cross-site scripting and other content-injection 268 | # attacks by setting a Content Security Policy which whitelists trusted sources 269 | # of content for your site. 270 | 271 | # The example header below allows ONLY scripts that are loaded from the current 272 | # site's origin (no inline scripts, no CDN, etc). This almost certainly won't 273 | # work as-is for your site! 274 | 275 | # To get all the details you'll need to craft a reasonable policy for your site, 276 | # read: http://html5rocks.com/en/tutorials/security/content-security-policy (or 277 | # see the specification: http://w3.org/TR/CSP). 278 | 279 | # 280 | # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" 281 | # 282 | # Header unset Content-Security-Policy 283 | # 284 | # 285 | 286 | # ------------------------------------------------------------------------------ 287 | # | File access | 288 | # ------------------------------------------------------------------------------ 289 | 290 | # Block access to directories without a default document. 291 | # Usually you should leave this uncommented because you shouldn't allow anyone 292 | # to surf through every directory on your server (which may includes rather 293 | # private places like the CMS's directories). 294 | 295 | 296 | Options -Indexes 297 | 298 | 299 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 300 | 301 | # Block access to hidden files and directories. 302 | # This includes directories used by version control systems such as Git and SVN. 303 | 304 | 305 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 306 | RewriteCond %{SCRIPT_FILENAME} -f 307 | RewriteRule "(^|/)\." - [F] 308 | 309 | 310 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 311 | 312 | # Block access to backup and source files. 313 | # These files may be left by some text editors and can pose a great security 314 | # danger when anyone has access to them. 315 | 316 | 317 | Order allow,deny 318 | Deny from all 319 | Satisfy All 320 | 321 | 322 | # ------------------------------------------------------------------------------ 323 | # | Secure Sockets Layer (SSL) | 324 | # ------------------------------------------------------------------------------ 325 | 326 | # Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: 327 | # prevent `https://www.example.com` when your certificate only allows 328 | # `https://secure.example.com`. 329 | 330 | # 331 | # RewriteCond %{SERVER_PORT} !^443 332 | # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] 333 | # 334 | 335 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 336 | 337 | # Force client-side SSL redirection. 338 | 339 | # If a user types "example.com" in his browser, the above rule will redirect him 340 | # to the secure version of the site. That still leaves a window of opportunity 341 | # (the initial HTTP connection) for an attacker to downgrade or redirect the 342 | # request. The following header ensures that browser will ONLY connect to your 343 | # server via HTTPS, regardless of what the users type in the address bar. 344 | # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ 345 | 346 | # 347 | # Header set Strict-Transport-Security max-age=16070400; 348 | # 349 | 350 | # ------------------------------------------------------------------------------ 351 | # | Server software information | 352 | # ------------------------------------------------------------------------------ 353 | 354 | # Avoid displaying the exact Apache version number, the description of the 355 | # generic OS-type and the information about Apache's compiled-in modules. 356 | 357 | # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! 358 | 359 | # ServerTokens Prod 360 | 361 | 362 | # ############################################################################## 363 | # # WEB PERFORMANCE # 364 | # ############################################################################## 365 | 366 | # ------------------------------------------------------------------------------ 367 | # | Compression | 368 | # ------------------------------------------------------------------------------ 369 | 370 | 371 | 372 | # Force compression for mangled headers. 373 | # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping 374 | 375 | 376 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding 377 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding 378 | 379 | 380 | 381 | # Compress all output labeled with one of the following MIME-types 382 | # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` 383 | # and can remove the `` and `` lines 384 | # as `AddOutputFilterByType` is still in the core directives). 385 | 386 | AddOutputFilterByType DEFLATE application/atom+xml \ 387 | application/javascript \ 388 | application/json \ 389 | application/rss+xml \ 390 | application/vnd.ms-fontobject \ 391 | application/x-font-ttf \ 392 | application/x-web-app-manifest+json \ 393 | application/xhtml+xml \ 394 | application/xml \ 395 | font/opentype \ 396 | image/svg+xml \ 397 | image/x-icon \ 398 | text/css \ 399 | text/html \ 400 | text/plain \ 401 | text/x-component \ 402 | text/xml 403 | 404 | 405 | 406 | 407 | # ------------------------------------------------------------------------------ 408 | # | Content transformations | 409 | # ------------------------------------------------------------------------------ 410 | 411 | # Prevent some of the mobile network providers from modifying the content of 412 | # your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. 413 | 414 | # 415 | # Header set Cache-Control "no-transform" 416 | # 417 | 418 | # ------------------------------------------------------------------------------ 419 | # | ETag removal | 420 | # ------------------------------------------------------------------------------ 421 | 422 | # Since we're sending far-future expires headers (see below), ETags can 423 | # be removed: http://developer.yahoo.com/performance/rules.html#etags. 424 | 425 | # `FileETag None` is not enough for every server. 426 | 427 | Header unset ETag 428 | 429 | 430 | FileETag None 431 | 432 | # ------------------------------------------------------------------------------ 433 | # | Expires headers (for better cache control) | 434 | # ------------------------------------------------------------------------------ 435 | 436 | # The following expires headers are set pretty far in the future. If you don't 437 | # control versioning with filename-based cache busting, consider lowering the 438 | # cache time for resources like CSS and JS to something like 1 week. 439 | 440 | 441 | 442 | ExpiresActive on 443 | ExpiresDefault "access plus 1 month" 444 | 445 | # CSS 446 | ExpiresByType text/css "access plus 1 year" 447 | 448 | # Data interchange 449 | ExpiresByType application/json "access plus 0 seconds" 450 | ExpiresByType application/xml "access plus 0 seconds" 451 | ExpiresByType text/xml "access plus 0 seconds" 452 | 453 | # Favicon (cannot be renamed!) 454 | ExpiresByType image/x-icon "access plus 1 week" 455 | 456 | # HTML components (HTCs) 457 | ExpiresByType text/x-component "access plus 1 month" 458 | 459 | # HTML 460 | ExpiresByType text/html "access plus 0 seconds" 461 | 462 | # JavaScript 463 | ExpiresByType application/javascript "access plus 1 year" 464 | 465 | # Manifest files 466 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" 467 | ExpiresByType text/cache-manifest "access plus 0 seconds" 468 | 469 | # Media 470 | ExpiresByType audio/ogg "access plus 1 month" 471 | ExpiresByType image/gif "access plus 1 month" 472 | ExpiresByType image/jpeg "access plus 1 month" 473 | ExpiresByType image/png "access plus 1 month" 474 | ExpiresByType video/mp4 "access plus 1 month" 475 | ExpiresByType video/ogg "access plus 1 month" 476 | ExpiresByType video/webm "access plus 1 month" 477 | 478 | # Web feeds 479 | ExpiresByType application/atom+xml "access plus 1 hour" 480 | ExpiresByType application/rss+xml "access plus 1 hour" 481 | 482 | # Web fonts 483 | ExpiresByType application/font-woff "access plus 1 month" 484 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month" 485 | ExpiresByType application/x-font-ttf "access plus 1 month" 486 | ExpiresByType font/opentype "access plus 1 month" 487 | ExpiresByType image/svg+xml "access plus 1 month" 488 | 489 | 490 | 491 | # ------------------------------------------------------------------------------ 492 | # | Filename-based cache busting | 493 | # ------------------------------------------------------------------------------ 494 | 495 | # If you're not using a build process to manage your filename version revving, 496 | # you might want to consider enabling the following directives to route all 497 | # requests such as `/css/style.12345.css` to `/css/style.css`. 498 | 499 | # To understand why this is important and a better idea than `*.css?v231`, read: 500 | # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring 501 | 502 | # 503 | # RewriteCond %{REQUEST_FILENAME} !-f 504 | # RewriteCond %{REQUEST_FILENAME} !-d 505 | # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] 506 | # 507 | 508 | # ------------------------------------------------------------------------------ 509 | # | File concatenation | 510 | # ------------------------------------------------------------------------------ 511 | 512 | # Allow concatenation from within specific CSS and JS files, e.g.: 513 | # Inside of `script.combined.js` you could have 514 | # 515 | # 516 | # and they would be included into this single file. 517 | 518 | # 519 | # 520 | # Options +Includes 521 | # AddOutputFilterByType INCLUDES application/javascript application/json 522 | # SetOutputFilter INCLUDES 523 | # 524 | # 525 | # Options +Includes 526 | # AddOutputFilterByType INCLUDES text/css 527 | # SetOutputFilter INCLUDES 528 | # 529 | # 530 | 531 | # ------------------------------------------------------------------------------ 532 | # | Persistent connections | 533 | # ------------------------------------------------------------------------------ 534 | 535 | # Allow multiple requests to be sent over the same TCP connection: 536 | # http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. 537 | 538 | # Enable if you serve a lot of static content but, be aware of the 539 | # possible disadvantages! 540 | 541 | # 542 | # Header set Connection Keep-Alive 543 | # 544 | -------------------------------------------------------------------------------- /app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /app/angular-leaflet-directive.min.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * The MIT License 3 | * 4 | * Copyright (c) 2013 the angular-leaflet-directive Team, http://tombatossals.github.io/angular-leaflet-directive 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | * angular-leaflet-directive 25 | * https://github.com/tombatossals/angular-leaflet-directive 26 | * 27 | * @authors https://github.com/tombatossals/angular-leaflet-directive/graphs/contributors 28 | */ 29 | 30 | /*! angular-leaflet-directive 29-12-2014 */ 31 | !function(){"use strict";angular.module("leaflet-directive",[]).directive("leaflet",["$q","leafletData","leafletMapDefaults","leafletHelpers","leafletEvents",function(a,b,c,d,e){var f;return{restrict:"EA",replace:!0,scope:{center:"=",defaults:"=",maxbounds:"=",bounds:"=",markers:"=",legend:"=",geojson:"=",paths:"=",tiles:"=",layers:"=",controls:"=",decorations:"=",eventBroadcast:"="},transclude:!0,template:'
',controller:["$scope",function(b){f=a.defer(),this.getMap=function(){return f.promise},this.getLeafletScope=function(){return b}}],link:function(a,g,h){function i(){isNaN(h.width)?g.css("width",h.width):g.css("width",h.width+"px")}function j(){isNaN(h.height)?g.css("height",h.height):g.css("height",h.height+"px")}var k=d.isDefined,l=c.setDefaults(a.defaults,h.id),m=e.genDispatchMapEvent,n=e.getAvailableMapEvents();k(h.width)&&(i(),a.$watch(function(){return g[0].getAttribute("width")},function(){i(),o.invalidateSize()})),k(h.height)&&(j(),a.$watch(function(){return g[0].getAttribute("height")},function(){j(),o.invalidateSize()}));var o=new L.Map(g[0],c.getMapCreationDefaults(h.id));if(f.resolve(o),k(h.center)||o.setView([l.center.lat,l.center.lng],l.center.zoom),!k(h.tiles)&&!k(h.layers)){var p=L.tileLayer(l.tileLayer,l.tileLayerOptions);p.addTo(o),b.setTiles(p,h.id)}if(k(o.zoomControl)&&k(l.zoomControlPosition)&&o.zoomControl.setPosition(l.zoomControlPosition),k(o.zoomControl)&&l.zoomControl===!1&&o.zoomControl.removeFrom(o),k(o.zoomsliderControl)&&k(l.zoomsliderControl)&&l.zoomsliderControl===!1&&o.zoomsliderControl.removeFrom(o),!k(h.eventBroadcast))for(var q="broadcast",r=0;rp.center.zoom?{setView:!0,maxZoom:b.zoom}:j(p.maxZoom)?{setView:!0,maxZoom:p.maxZoom}:{setView:!0})):void(u&&l(b,f)||(r.settingCenterFromScope=!0,f.setView([b.lat,b.lng],b.zoom),h.notifyCenterChangedToBounds(r,f),d(function(){r.settingCenterFromScope=!1}))):void a.warn("[AngularJS - Leaflet] invalid 'center'")},!0),f.whenReady(function(){u=!0}),f.on("moveend",function(){i.resolve(),h.notifyCenterUrlHashChanged(r,f,o,c.search()),l(s,f)||b.settingCenterFromScope||m(r,function(a){r.settingCenterFromScope||(a.center={lat:f.getCenter().lat,lng:f.getCenter().lng,zoom:f.getZoom(),autoDiscover:!1}),h.notifyCenterChangedToBounds(r,f)})}),s.autoDiscover===!0&&f.on("locationerror",function(){a.warn("[AngularJS - Leaflet] The Geolocation API is unauthorized on this page."),n(s)?(f.setView([s.lat,s.lng],s.zoom),h.notifyCenterChangedToBounds(r,f)):(f.setView([p.center.lat,p.center.lng],p.center.zoom),h.notifyCenterChangedToBounds(r,f))})})}}}]),angular.module("leaflet-directive").directive("tiles",["$log","leafletData","leafletMapDefaults","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i=d.isDefined,j=h.getLeafletScope(),k=j.tiles;return i(k)||i(k.url)?void h.getMap().then(function(a){var d,e=c.getDefaults(g.id);j.$watch("tiles",function(c){var f=e.tileLayerOptions,h=e.tileLayer;return!i(c.url)&&i(d)?void a.removeLayer(d):i(d)?i(c.url)&&i(c.options)&&!angular.equals(c.options,f)?(a.removeLayer(d),f=e.tileLayerOptions,angular.copy(c.options,f),h=c.url,d=L.tileLayer(h,f),d.addTo(a),void b.setTiles(d,g.id)):void(i(c.url)&&d.setUrl(c.url)):(i(c.options)&&angular.copy(c.options,f),i(c.url)&&(h=c.url),d=L.tileLayer(h,f),d.addTo(a),void b.setTiles(d,g.id))},!0)}):void a.warn("[AngularJS - Leaflet] The 'tiles' definition doesn't have the 'url' property.")}}}]),angular.module("leaflet-directive").directive("legend",["$log","$http","leafletHelpers","leafletLegendHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i,j,k,l,m=c.isArray,n=c.isDefined,o=c.isFunction,p=h.getLeafletScope(),q=p.legend;p.$watch("legend",function(a){n(a)&&(i=a.legendClass?a.legendClass:"legend",j=a.position||"bottomright",l=a.type||"arcgis")},!0),h.getMap().then(function(c){p.$watch("legend",function(b){return n(b)?n(b.url)||"arcgis"!==l||m(b.colors)&&m(b.labels)&&b.colors.length===b.labels.length?n(b.url)?void a.info("[AngularJS - Leaflet] loading legend service."):(n(k)&&(k.removeFrom(c),k=null),k=L.control({position:j}),"arcgis"===l&&(k.onAdd=d.getOnAddArrayLegend(b,i)),void k.addTo(c)):void a.warn("[AngularJS - Leaflet] legend.colors and legend.labels must be set."):void(n(k)&&(k.removeFrom(c),k=null))}),p.$watch("legend.url",function(e){n(e)&&b.get(e).success(function(a){n(k)?d.updateLegend(k.getContainer(),a,l,e):(k=L.control({position:j}),k.onAdd=d.getOnAddLegend(a,i,l,e),k.addTo(c)),n(q.loadedData)&&o(q.loadedData)&&q.loadedData()}).error(function(){a.warn("[AngularJS - Leaflet] legend.url not loaded.")})})})}}}]),angular.module("leaflet-directive").directive("geojson",["$log","$rootScope","leafletData","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(a,e,f,g){var h=d.safeApply,i=d.isDefined,j=g.getLeafletScope(),k={};g.getMap().then(function(a){j.$watch("geojson",function(e){if(i(k)&&a.hasLayer(k)&&a.removeLayer(k),i(e)&&i(e.data)){var g,l=e.resetStyleOnMouseout;g=e.onEachFeature?e.onEachFeature:function(a,c){d.LabelPlugin.isLoaded()&&i(e.label)&&c.bindLabel(a.properties.description),c.on({mouseover:function(c){h(j,function(){b.$broadcast("leafletDirectiveMap.geojsonMouseover",a,c)})},mouseout:function(a){l&&k.resetStyle(a.target),h(j,function(){b.$broadcast("leafletDirectiveMap.geojsonMouseout",a)})},click:function(c){h(j,function(){b.$broadcast("leafletDirectiveMap.geojsonClick",a,c)})}})},e.options={style:e.style,filter:e.filter,onEachFeature:g,pointToLayer:e.pointToLayer},k=L.geoJson(e.data,e.options),c.setGeoJSON(k,f.id),k.addTo(a)}},!0)})}}}]),angular.module("leaflet-directive").directive("layers",["$log","$q","leafletData","leafletHelpers","leafletLayerHelpers","leafletControlHelpers",function(a,b,c,d,e,f){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",controller:["$scope",function(a){a._leafletLayers=b.defer(),this.getLayers=function(){return a._leafletLayers.promise}}],link:function(a,b,g,h){var i=d.isDefined,j={},k=h.getLeafletScope(),l=k.layers,m=e.createLayer,n=f.updateLayersControl,o=!1;h.getMap().then(function(b){a._leafletLayers.resolve(j),c.setLayers(j,g.id),j.baselayers={},j.overlays={};var d=g.id,e=!1;for(var f in l.baselayers){var h=m(l.baselayers[f]);i(h)?(j.baselayers[f]=h,l.baselayers[f].top===!0&&(b.addLayer(j.baselayers[f]),e=!0)):delete l.baselayers[f]}!e&&Object.keys(j.baselayers).length>0&&b.addLayer(j.baselayers[Object.keys(l.baselayers)[0]]);for(f in l.overlays){"cartodb"===l.overlays[f].type;var p=m(l.overlays[f]);i(p)?(j.overlays[f]=p,l.overlays[f].visible===!0&&b.addLayer(j.overlays[f])):delete l.overlays[f]}k.$watch("layers.baselayers",function(a){for(var c in j.baselayers)i(a[c])||(b.hasLayer(j.baselayers[c])&&b.removeLayer(j.baselayers[c]),delete j.baselayers[c]);for(var e in a)if(i(j.baselayers[e]))a[e].top!==!0||b.hasLayer(j.baselayers[e])?a[e].top===!1&&b.hasLayer(j.baselayers[e])&&b.removeLayer(j.baselayers[e]):b.addLayer(j.baselayers[e]);else{var f=m(a[e]);i(f)&&(j.baselayers[e]=f,a[e].top===!0&&b.addLayer(j.baselayers[e]))}var g=!1;for(var h in j.baselayers)if(b.hasLayer(j.baselayers[h])){g=!0;break}!g&&Object.keys(l.baselayers).length>0&&b.addLayer(j.baselayers[Object.keys(l.baselayers)[0]]),o=n(b,d,o,a,l.overlays,j)},!0),k.$watch("layers.overlays",function(a){for(var c in j.overlays)i(a[c])||(b.hasLayer(j.overlays[c])&&b.removeLayer(j.overlays[c]),delete j.overlays[c]);for(var e in a){if(!i(j.overlays[e])){var f=m(a[e]);i(f)&&(j.overlays[e]=f,a[e].visible===!0&&b.addLayer(j.overlays[e]))}a[e].visible&&!b.hasLayer(j.overlays[e])?b.addLayer(j.overlays[e]):a[e].visible===!1&&b.hasLayer(j.overlays[e])&&b.removeLayer(j.overlays[e]),a[e].visible&&b._loaded&&a[e].data&&"heatmap"===a[e].type&&(j.overlays[e].setData(a[e].data),j.overlays[e].update())}o=n(b,d,o,l.baselayers,a,j)},!0)})}}}]),angular.module("leaflet-directive").directive("bounds",["$log","$timeout","leafletHelpers","leafletBoundsHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","center"],link:function(e,f,g,h){var i=c.isDefined,j=d.createLeafletBounds,k=h[0].getLeafletScope(),l=h[0],m=function(a){return 0===a._southWest.lat&&0===a._southWest.lng&&0===a._northEast.lat&&0===a._northEast.lng};l.getMap().then(function(c){k.$on("boundsChanged",function(a){var b=a.currentScope,d=c.getBounds();if(!m(d)&&!b.settingBoundsFromScope){var e={northEast:{lat:d._northEast.lat,lng:d._northEast.lng},southWest:{lat:d._southWest.lat,lng:d._southWest.lng}};angular.equals(b.bounds,e)||(b.bounds=e)}}),k.$watch("bounds",function(d){if(!i(d))return void a.error("[AngularJS - Leaflet] Invalid bounds");var f=j(d);f&&!c.getBounds().equals(f)&&(e.settingBoundsFromScope=!0,c.fitBounds(f),b(function(){e.settingBoundsFromScope=!1}))},!0)})}}}]),angular.module("leaflet-directive").directive("markers",["$log","$rootScope","$q","leafletData","leafletHelpers","leafletMapDefaults","leafletMarkersHelpers","leafletEvents",function(a,b,c,d,e,f,g,h){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","?layers"],link:function(b,f,i,j){var k=j[0],l=e,m=e.isDefined,n=e.isString,o=k.getLeafletScope(),p=g.deleteMarker,q=g.addMarkerWatcher,r=g.listenMarkerEvents,s=g.addMarkerToGroup,t=h.bindMarkerEvents,u=g.createMarker;k.getMap().then(function(b){var e,f={};e=m(j[1])?j[1].getLayers:function(){var a=c.defer();return a.resolve(),a.promise},e().then(function(c){d.setMarkers(f,i.id),o.$watch("markers",function(d){for(var e in f)m(d)&&m(d[e])||(p(f[e],b,c),delete f[e]);for(var h in d)if(-1===h.search("-")){var j=!m(i.watchMarkers)||"true"===i.watchMarkers;if(!m(f[h])){var k=d[h],v=u(k);if(!m(v)){a.error("[AngularJS - Leaflet] Received invalid data on the marker "+h+".");continue}if(f[h]=v,m(k.message)&&v.bindPopup(k.message,k.popupOptions),m(k.group)){var w=m(k.groupOption)?k.groupOption:null;s(v,k.group,w,b)}if(l.LabelPlugin.isLoaded()&&m(k.label)&&m(k.label.message)&&v.bindLabel(k.label.message,k.label.options),m(k)&&m(k.layer)){if(!n(k.layer)){a.error("[AngularJS - Leaflet] A layername must be a string");continue}if(!m(c)){a.error("[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.");continue}if(!m(c.overlays)||!m(c.overlays[k.layer])){a.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"');continue}var x=c.overlays[k.layer];if(!(x instanceof L.LayerGroup||x instanceof L.FeatureGroup)){a.error('[AngularJS - Leaflet] Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"');continue}x.addLayer(v),!j&&b.hasLayer(v)&&k.focus===!0&&g.manageOpenPopup(v,k)}else m(k.group)||(b.addLayer(v),j||k.focus!==!0||g.manageOpenPopup(v,k));j&&(q(v,h,o,c,b),r(v,k,o)),t(v,h,k,o)}}else a.error('The marker can\'t use a "-" on his key name: "'+h+'".')},!0)})})}}}]),angular.module("leaflet-directive").directive("paths",["$log","$q","leafletData","leafletMapDefaults","leafletHelpers","leafletPathsHelpers","leafletEvents",function(a,b,c,d,e,f,g){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","?layers"],link:function(h,i,j,k){var l=k[0],m=e.isDefined,n=e.isString,o=l.getLeafletScope(),p=o.paths,q=f.createPath,r=g.bindPathEvents,s=f.setPathOptions;l.getMap().then(function(f){var g,h=d.getDefaults(j.id);g=m(k[1])?k[1].getLayers:function(){var a=b.defer();return a.resolve(),a.promise},m(p)&&g().then(function(b){var d={};c.setPaths(d,j.id);var g=function(a,c){var d=o.$watch("paths."+c,function(c,e){if(!m(c)){if(m(e.layer))for(var g in b.overlays){var h=b.overlays[g];h.removeLayer(a)}return f.removeLayer(a),void d()}s(a,c.type,c)},!0)};o.$watch("paths",function(c){for(var i in d)m(c[i])||delete d[i];for(var j in c)if(0!==j.search("\\$"))if(-1===j.search("-")){if(!m(d[j])){var k=c[j],l=q(j,c[j],h);if(m(l)&&m(k.message)&&l.bindPopup(k.message),e.LabelPlugin.isLoaded()&&m(k.label)&&m(k.label.message)&&l.bindLabel(k.label.message,k.label.options),m(k)&&m(k.layer)){if(!n(k.layer)){a.error("[AngularJS - Leaflet] A layername must be a string");continue}if(!m(b)){a.error("[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.");continue}if(!m(b.overlays)||!m(b.overlays[k.layer])){a.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"');continue}var p=b.overlays[k.layer];if(!(p instanceof L.LayerGroup||p instanceof L.FeatureGroup)){a.error('[AngularJS - Leaflet] Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"');continue}d[j]=l,p.addLayer(l),g(l,j)}else m(l)&&(d[j]=l,f.addLayer(l),g(l,j));r(l,j,k,o)}}else a.error('[AngularJS - Leaflet] The path name "'+j+'" is not valid. It must not include "-" and a number.')},!0)})})}}}]),angular.module("leaflet-directive").directive("controls",["$log","leafletHelpers",function(a,b){return{restrict:"A",scope:!1,replace:!1,require:"?^leaflet",link:function(a,c,d,e){if(e){var f=b.isDefined,g=e.getLeafletScope(),h=g.controls;e.getMap().then(function(a){if(f(L.Control.Draw)&&f(h.draw)){f(h.edit)||(h.edit={featureGroup:new L.FeatureGroup},a.addLayer(h.edit.featureGroup));var b=new L.Control.Draw(h);a.addControl(b)}if(f(h.custom))for(var c in h.custom)a.addControl(h.custom[c])})}}}}]),angular.module("leaflet-directive").directive("eventBroadcast",["$log","$rootScope","leafletHelpers","leafletEvents",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(b,e,f,g){var h=c.isObject,i=g.getLeafletScope(),j=i.eventBroadcast,k=d.getAvailableMapEvents(),l=d.genDispatchMapEvent;g.getMap().then(function(b){var c,d,e=[],f="broadcast";if(h(j)){if(void 0===j.map||null===j.map)e=k;else if("object"!=typeof j.map)a.warn("[AngularJS - Leaflet] event-broadcast.map must be an object check your model.");else{void 0!==j.map.logic&&null!==j.map.logic&&("emit"!==j.map.logic&&"broadcast"!==j.map.logic?a.warn("[AngularJS - Leaflet] Available event propagation logic are: 'emit' or 'broadcast'."):"emit"===j.map.logic&&(f="emit"));var g=!1,m=!1;if(void 0!==j.map.enable&&null!==j.map.enable&&"object"==typeof j.map.enable&&(g=!0),void 0!==j.map.disable&&null!==j.map.disable&&"object"==typeof j.map.disable&&(m=!0),g&&m)a.warn("[AngularJS - Leaflet] can not enable and disable events at the time");else if(g||m)if(g)for(c=0;c=1+e&&b<=d.overlaysArray.length+e){var f;for(var h in d.layers.overlays)if(d.layers.overlays[h].index===b){f=d.layers.overlays[h];break}f&&g(d,function(){f.index=a.index,a.index=b})}c.stopPropagation(),c.preventDefault()},initIndex:function(a,b){var c=Object.keys(d.layers.baselayers).length;a.index=h(a.index)?a.index:b+c+1},toggleOpacity:function(b,c){if(a.debug("Event",b),c.visible){var e=angular.element(b.currentTarget);e.toggleClass(d.icons.close+" "+d.icons.open),e=e.parents(".lf-row").find(".lf-opacity"),e.toggle("fast",function(){g(d,function(){c.opacityControl=!c.opacityControl})})}b.stopPropagation(),b.preventDefault()},unsafeHTML:function(a){return f.trustAsHtml(a)}});var i=e.get(0);L.Browser.touch?L.DomEvent.on(i,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(i),L.DomEvent.on(i,"mousewheel",L.DomEvent.stopPropagation))}],template:'
',link:function(d,e,f,g){var h=c.isDefined,i=g.getLeafletScope(),j=i.layers;f.order=!h(f.order)||"normal"!==f.order&&"reverse"!==f.order?"normal":f.order,d.order="normal"===f.order,d.orderNumber="normal"===f.order?-1:1,d.layers=j,g.getMap().then(function(c){i.$watch("layers.baselayers",function(a){b.getLayers().then(function(b){var e;for(e in a)a[e].icon=c.hasLayer(b.baselayers[e])?d.icons.radio:d.icons.unradio})}),i.$watch("layers.overlays",function(f){var g=[];b.getLayers().then(function(a){for(var b in f)f[b].icon=d.icons[f[b].visible?"uncheck":"check"],g.push(f[b]),h(f[b].index)&&a.overlays[b].setZIndex&&a.overlays[b].setZIndex(f[b].index)});var i=d.$watch(function(){return e.children().size()>1?(e.find(".lf-overlays").trigger("resize"),e.find(".lf-opacity").size()===Object.keys(j.overlays).length):void 0},function(f){f===!0&&(h(e.find(".lf-opacity-control").ionRangeSlider)?e.find(".lf-opacity-control").each(function(a,e){var f,g=Object.keys(j.baselayers).length;for(var i in d.overlaysArray)d.overlaysArray[i].index===a+g+1&&(f=d.overlaysArray[i]);var k=angular.element(e),l=h(f)&&h(f.layerOptions)?f.layerOptions.opacity:void 0;k.ionRangeSlider({min:0,from:h(l)?Math.ceil(100*l):100,step:1,max:100,prettify:!1,hasGrid:!1,hideMinMax:!0,onChange:function(a){b.getLayers().then(function(b){var d,e,f=a.input.data().key;for(var g in j.overlays)if(j.overlays[g].index===f){d=b.overlays[g],e=j.overlays[g];break}c.hasLayer(d)&&(e.layerOptions=h(e.layerOptions)?e.layerOptions:{},e.layerOptions.opacity=a.input.val()/100,d.setOpacity&&d.setOpacity(a.input.val()/100),d.getLayers&&d.eachLayer&&d.eachLayer(function(b){b.setOpacity&&b.setOpacity(a.input.val()/100)}))})}})}):a.warn("[AngularJS - Leaflet] Ion Slide Range Plugin is not loaded"),i())});d.overlaysArray=g},!0)})}}}]),angular.module("leaflet-directive").service("leafletData",["$log","$q","leafletHelpers",function(a,b,c){var d=c.getDefer,e=c.getUnresolvedDefer,f=c.setResolvedDefer,g={},h={},i={},j={},k={},l={},m={},n={};this.setMap=function(a,b){var c=e(g,b);c.resolve(a),f(g,b)},this.getMap=function(a){var b=d(g,a);return b.promise},this.unresolveMap=function(a){var b=c.obtainEffectiveMapId(g,a);g[b]=void 0,h[b]=void 0,i[b]=void 0,j[b]=void 0,k[b]=void 0,l[b]=void 0,m[b]=void 0,n[b]=void 0},this.getPaths=function(a){var b=d(j,a);return b.promise},this.setPaths=function(a,b){var c=e(j,b);c.resolve(a),f(j,b)},this.getMarkers=function(a){var b=d(k,a);return b.promise},this.setMarkers=function(a,b){var c=e(k,b);c.resolve(a),f(k,b)},this.getLayers=function(a){var b=d(i,a);return b.promise},this.setLayers=function(a,b){var c=e(i,b);c.resolve(a),f(i,b)},this.getUTFGrid=function(a){var b=d(m,a);return b.promise},this.setUTFGrid=function(a,b){var c=e(m,b);c.resolve(a),f(m,b)},this.setTiles=function(a,b){var c=e(h,b);c.resolve(a),f(h,b)},this.getTiles=function(a){var b=d(h,a);return b.promise},this.setGeoJSON=function(a,b){var c=e(l,b);c.resolve(a),f(l,b)},this.getGeoJSON=function(a){var b=d(l,a);return b.promise},this.setDecorations=function(a,b){var c=e(n,b);c.resolve(a),f(n,b)},this.getDecorations=function(a){var b=d(n,a);return b.promise}}]),angular.module("leaflet-directive").factory("leafletMapDefaults",["$q","leafletHelpers",function(a,b){function c(){return{keyboard:!0,dragging:!0,worldCopyJump:!1,doubleClickZoom:!0,scrollWheelZoom:!0,touchZoom:!0,zoomControl:!0,zoomsliderControl:!1,zoomControlPosition:"topleft",attributionControl:!0,controls:{layers:{visible:!0,position:"topright",collapsed:!0}},crs:L.CRS.EPSG3857,tileLayer:"//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",tileLayerOptions:{attribution:'© OpenStreetMap contributors'},path:{weight:10,opacity:1,color:"#0000ff"},center:{lat:0,lng:0,zoom:1}}}var d=b.isDefined,e=b.isObject,f=b.obtainEffectiveMapId,g={};return{getDefaults:function(a){var b=f(g,a);return g[b]},getMapCreationDefaults:function(a){var b=f(g,a),c=g[b],e={maxZoom:c.maxZoom,keyboard:c.keyboard,dragging:c.dragging,zoomControl:c.zoomControl,doubleClickZoom:c.doubleClickZoom,scrollWheelZoom:c.scrollWheelZoom,touchZoom:c.touchZoom,attributionControl:c.attributionControl,worldCopyJump:c.worldCopyJump,crs:c.crs};if(d(c.minZoom)&&(e.minZoom=c.minZoom),d(c.zoomAnimation)&&(e.zoomAnimation=c.zoomAnimation),d(c.fadeAnimation)&&(e.fadeAnimation=c.fadeAnimation),d(c.markerZoomAnimation)&&(e.markerZoomAnimation=c.markerZoomAnimation),c.map)for(var h in c.map)e[h]=c.map[h];return e},setDefaults:function(a,b){var h=c();d(a)&&(h.doubleClickZoom=d(a.doubleClickZoom)?a.doubleClickZoom:h.doubleClickZoom,h.scrollWheelZoom=d(a.scrollWheelZoom)?a.scrollWheelZoom:h.doubleClickZoom,h.touchZoom=d(a.touchZoom)?a.touchZoom:h.doubleClickZoom,h.zoomControl=d(a.zoomControl)?a.zoomControl:h.zoomControl,h.zoomsliderControl=d(a.zoomsliderControl)?a.zoomsliderControl:h.zoomsliderControl,h.attributionControl=d(a.attributionControl)?a.attributionControl:h.attributionControl,h.tileLayer=d(a.tileLayer)?a.tileLayer:h.tileLayer,h.zoomControlPosition=d(a.zoomControlPosition)?a.zoomControlPosition:h.zoomControlPosition,h.keyboard=d(a.keyboard)?a.keyboard:h.keyboard,h.dragging=d(a.dragging)?a.dragging:h.dragging,d(a.controls)&&angular.extend(h.controls,a.controls),e(a.crs)?h.crs=a.crs:d(L.CRS[a.crs])&&(h.crs=L.CRS[a.crs]),d(a.center)&&angular.copy(a.center,h.center),d(a.tileLayerOptions)&&angular.copy(a.tileLayerOptions,h.tileLayerOptions),d(a.maxZoom)&&(h.maxZoom=a.maxZoom),d(a.minZoom)&&(h.minZoom=a.minZoom),d(a.zoomAnimation)&&(h.zoomAnimation=a.zoomAnimation),d(a.fadeAnimation)&&(h.fadeAnimation=a.fadeAnimation),d(a.markerZoomAnimation)&&(h.markerZoomAnimation=a.markerZoomAnimation),d(a.worldCopyJump)&&(h.worldCopyJump=a.worldCopyJump),d(a.map)&&(h.map=a.map));var i=f(g,b);return g[i]=h,h}}}]),angular.module("leaflet-directive").factory("leafletEvents",["$rootScope","$q","$log","leafletHelpers",function(a,b,c,d){var e=d.safeApply,f=d.isDefined,g=d.isObject,h=d,i=function(){return["click","dblclick","mousedown","mouseover","mouseout","contextmenu"]},j=function(a,b,c,d){for(var e=i(),f="markers."+d,g=0;g1},i=function(a){var b=d.getDefaults(a),c={collapsed:b.controls.layers.collapsed,position:b.controls.layers.position};angular.extend(c,b.controls.layers.options);var e;return e=b.controls.layers&&g(b.controls.layers.control)?b.controls.layers.control.apply(this,[[],[],c]):new L.control.layers([],[],c)};return{layersControlMustBeVisible:h,updateLayersControl:function(a,b,c,d,f,j){var k,l=h(d,f,b);if(g(e)&&c){for(k in j.baselayers)e.removeLayer(j.baselayers[k]);for(k in j.overlays)e.removeLayer(j.overlays[k]);e.removeFrom(a)}if(l){e=i(b);for(k in d)g(j.baselayers[k])&&e.addBaseLayer(j.baselayers[k],d[k].name);for(k in f)g(j.overlays[k])&&e.addOverlay(j.overlays[k],f[k].name);e.addTo(a)}return l}}}]),angular.module("leaflet-directive").factory("leafletLegendHelpers",function(){var a=function(a,b,c,d){if(a.innerHTML="",b.error)a.innerHTML+='
'+b.error.message+"
";else if("arcgis"===c)for(var e=0;e'+f.layerName+"";for(var g=0;g
'+h.label+"
"}}else"image"===c&&(a.innerHTML='')},b=function(b,c,d,e){return function(){var f=L.DomUtil.create("div",c);return L.Browser.touch?L.DomEvent.on(f,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(f),L.DomEvent.on(f,"mousewheel",L.DomEvent.stopPropagation)),a(f,b,d,e),f}},c=function(a,b){return function(){for(var c=L.DomUtil.create("div",b),d=0;d
'+a.labels[d]+"
";return L.Browser.touch?L.DomEvent.on(c,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(c),L.DomEvent.on(c,"mousewheel",L.DomEvent.stopPropagation)),c}};return{getOnAddLegend:b,getOnAddArrayLegend:c,updateLegend:a}}),angular.module("leaflet-directive").factory("leafletPathsHelpers",["$rootScope","$log","leafletHelpers",function(a,b,c){function d(a){return a.filter(function(a){return k(a)}).map(function(a){return e(a)})}function e(a){return i(a)?new L.LatLng(a[0],a[1]):new L.LatLng(a.lat,a.lng)}function f(a){return a.map(function(a){return d(a)})}function g(a,b){for(var c={},d=0;d-1?a.$on("$includeContentLoaded",function(a,b){f.getContent().indexOf(b)>-1&&g(f)}):g(f)}k.LabelPlugin.isLoaded()&&e(c.label)&&e(c.label.options)&&c.label.options.noHide===!0&&(d(b.label._container)(a),b.showLabel())};return{resetMarkerGroup:q,resetMarkerGroups:r,deleteMarker:s,manageOpenPopup:t,createMarker:function(a){if(!e(a))return void c.error("[AngularJS - Leaflet] The marker definition is not valid.");var b={icon:p(a.icon),title:e(a.title)?a.title:"",draggable:e(a.draggable)?a.draggable:!1,clickable:e(a.clickable)?a.clickable:!0,riseOnHover:e(a.riseOnHover)?a.riseOnHover:!1,zIndexOffset:e(a.zIndexOffset)?a.zIndexOffset:0,iconAngle:e(a.iconAngle)?a.iconAngle:0};for(var d in a)a.hasOwnProperty(d)&&!b.hasOwnProperty(d)&&(b[d]=a[d]);var f=new L.marker(a,b);return l(a.message)||f.unbindPopup(),f},addMarkerToGroup:function(a,b,d,g){return l(b)?f.isLoaded()?(e(o[b])||(o[b]=new L.MarkerClusterGroup(d),g.addLayer(o[b])),void o[b].addLayer(a)):void c.error("[AngularJS - Leaflet] The MarkerCluster plugin is not loaded."):void c.error("[AngularJS - Leaflet] The marker group you have specified is invalid.")},listenMarkerEvents:function(a,b,c){a.on("popupopen",function(){j(c,function(){b.focus=!0})}),a.on("popupclose",function(){j(c,function(){b.focus=!1})})},addMarkerWatcher:function(a,b,d,f,g){var h=d.$watch('markers["'+b+'"]',function(b,d){if(!e(b))return s(a,g,f),void h();if(e(d)){if(!m(b.lat)||!m(b.lng))return c.warn("There are problems with lat-lng data, please verify your marker model"),void s(a,g,f);var i=b===d;if(e(b.iconAngle)&&d.iconAngle!==b.iconAngle&&a.setIconAngle(b.iconAngle),l(b.layer)||l(d.layer)&&(e(f.overlays[d.layer])&&f.overlays[d.layer].hasLayer(a)&&(f.overlays[d.layer].removeLayer(a),a.closePopup()),g.hasLayer(a)||g.addLayer(a)),l(b.layer)&&d.layer!==b.layer){if(l(d.layer)&&e(f.overlays[d.layer])&&f.overlays[d.layer].hasLayer(a)&&f.overlays[d.layer].removeLayer(a),a.closePopup(),g.hasLayer(a)&&g.removeLayer(a),!e(f.overlays[b.layer]))return void c.error("[AngularJS - Leaflet] You must use a name of an existing layer");var j=f.overlays[b.layer];if(!(j instanceof L.LayerGroup||j instanceof L.FeatureGroup))return void c.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group" or "featureGroup"');j.addLayer(a),g.hasLayer(a)&&b.focus===!0&&t(a,b)}if(b.draggable!==!0&&d.draggable===!0&&e(a.dragging)&&a.dragging.disable(),b.draggable===!0&&d.draggable!==!0&&(a.dragging?a.dragging.enable():L.Handler.MarkerDrag&&(a.dragging=new L.Handler.MarkerDrag(a),a.options.draggable=!0,a.dragging.enable())),n(b.icon)||n(d.icon)&&(a.setIcon(p()),a.closePopup(),a.unbindPopup(),l(b.message)&&a.bindPopup(b.message,b.popupOptions)),n(b.icon)&&n(d.icon)&&!angular.equals(b.icon,d.icon)){var o=!1;a.dragging&&(o=a.dragging.enabled()),a.setIcon(p(b.icon)),o&&a.dragging.enable(),a.closePopup(),a.unbindPopup(),l(b.message)&&a.bindPopup(b.message,b.popupOptions)}!l(b.message)&&l(d.message)&&(a.closePopup(),a.unbindPopup()),k.LabelPlugin.isLoaded()&&e(b.label)&&e(b.label.message)&&!angular.equals(b.label.message,d.label.message)&&a.updateLabelContent(b.label.message),l(b.message)&&!l(d.message)&&a.bindPopup(b.message,b.popupOptions),l(b.message)&&l(d.message)&&b.message!==d.message&&a.setPopupContent(b.message);var q=!1;b.focus!==!0&&d.focus===!0&&(a.closePopup(),q=!0),(b.focus===!0&&d.focus===!1||i&&b.focus===!0)&&(t(a,b),q=!0),d.zIndexOffset!==b.zIndexOffset&&a.setZIndexOffset(b.zIndexOffset);var r=a.getLatLng(),u=l(b.layer)&&k.MarkerClusterPlugin.is(f.overlays[b.layer]);u?q?(b.lat!==d.lat||b.lng!==d.lng)&&(f.overlays[b.layer].removeLayer(a),a.setLatLng([b.lat,b.lng]),f.overlays[b.layer].addLayer(a)):r.lat!==b.lat||r.lng!==b.lng?(f.overlays[b.layer].removeLayer(a),a.setLatLng([b.lat,b.lng]),f.overlays[b.layer].addLayer(a)):b.lat!==d.lat||b.lng!==d.lng?(f.overlays[b.layer].removeLayer(a),a.setLatLng([b.lat,b.lng]),f.overlays[b.layer].addLayer(a)):n(b.icon)&&n(d.icon)&&!angular.equals(b.icon,d.icon)&&(f.overlays[b.layer].removeLayer(a),f.overlays[b.layer].addLayer(a)):(r.lat!==b.lat||r.lng!==b.lng)&&a.setLatLng([b.lat,b.lng])}},!0)}}}]),angular.module("leaflet-directive").factory("leafletHelpers",["$q","$log",function(a,b){function c(a,c){var d,e;if(angular.isDefined(c))d=c;else if(0===Object.keys(a).length)d="main";else if(Object.keys(a).length>=1)for(e in a)a.hasOwnProperty(e)&&(d=e);else 0===Object.keys(a).length?d="main":b.error("[AngularJS - Leaflet] - You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call");return d}function d(b,d){var e,f=c(b,d);return angular.isDefined(b[f])&&b[f].resolvedDefer!==!0?e=b[f].defer:(e=a.defer(),b[f]={defer:e,resolvedDefer:!1}),e}return{isEmpty:function(a){return 0===Object.keys(a).length},isUndefinedOrEmpty:function(a){return angular.isUndefined(a)||null===a||0===Object.keys(a).length},isDefined:function(a){return angular.isDefined(a)&&null!==a},isNumber:function(a){return angular.isNumber(a)},isString:function(a){return angular.isString(a)},isArray:function(a){return angular.isArray(a)},isObject:function(a){return angular.isObject(a)},isFunction:function(a){return angular.isFunction(a)},equals:function(a,b){return angular.equals(a,b)},isValidCenter:function(a){return angular.isDefined(a)&&angular.isNumber(a.lat)&&angular.isNumber(a.lng)&&angular.isNumber(a.zoom)},isValidPoint:function(a){return angular.isDefined(a)?angular.isArray(a)?2===a.length&&angular.isNumber(a[0])&&angular.isNumber(a[1]):angular.isNumber(a.lat)&&angular.isNumber(a.lng):!1},isSameCenterOnMap:function(a,b){var c=b.getCenter(),d=b.getZoom();return a.lat&&a.lng&&c.lat.toFixed(4)===a.lat.toFixed(4)&&c.lng.toFixed(4)===a.lng.toFixed(4)&&d===a.zoom?!0:!1},safeApply:function(a,b){var c=a.$root.$$phase;"$apply"===c||"$digest"===c?a.$eval(b):a.$apply(b)},obtainEffectiveMapId:c,getDefer:function(a,b){var e,f=c(a,b);return e=angular.isDefined(a[f])&&a[f].resolvedDefer!==!1?a[f].defer:d(a,b)},getUnresolvedDefer:d,setResolvedDefer:function(a,b){var d=c(a,b);a[d].resolvedDefer=!0},AwesomeMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.AwesomeMarkers)&&angular.isDefined(L.AwesomeMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.AwesomeMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},PolylineDecoratorPlugin:{isLoaded:function(){return angular.isDefined(L.PolylineDecorator)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.PolylineDecorator:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},MakiMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.MakiMarkers)&&angular.isDefined(L.MakiMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.MakiMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},ExtraMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.ExtraMarkers)&&angular.isDefined(L.ExtraMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.ExtraMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},LabelPlugin:{isLoaded:function(){return angular.isDefined(L.Label)},is:function(a){return this.isLoaded()?a instanceof L.MarkerClusterGroup:!1}},MarkerClusterPlugin:{isLoaded:function(){return angular.isDefined(L.MarkerClusterGroup)},is:function(a){return this.isLoaded()?a instanceof L.MarkerClusterGroup:!1}},GoogleLayerPlugin:{isLoaded:function(){return angular.isDefined(L.Google)},is:function(a){return this.isLoaded()?a instanceof L.Google:!1}},ChinaLayerPlugin:{isLoaded:function(){return angular.isDefined(L.tileLayer.chinaProvider)}},HeatMapLayerPlugin:{isLoaded:function(){return angular.isDefined(L.TileLayer.WebGLHeatMap)}},BingLayerPlugin:{isLoaded:function(){return angular.isDefined(L.BingLayer)},is:function(a){return this.isLoaded()?a instanceof L.BingLayer:!1}},WFSLayerPlugin:{isLoaded:function(){return void 0!==L.GeoJSON.WFS},is:function(a){return this.isLoaded()?a instanceof L.GeoJSON.WFS:!1}},AGSLayerPlugin:{isLoaded:function(){return void 0!==lvector&&void 0!==lvector.AGS},is:function(a){return this.isLoaded()?a instanceof lvector.AGS:!1}},YandexLayerPlugin:{isLoaded:function(){return angular.isDefined(L.Yandex)},is:function(a){return this.isLoaded()?a instanceof L.Yandex:!1}},DynamicMapLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.dynamicMapLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.dynamicMapLayer:!1}},GeoJSONPlugin:{isLoaded:function(){return angular.isDefined(L.TileLayer.GeoJSON)},is:function(a){return this.isLoaded()?a instanceof L.TileLayer.GeoJSON:!1}},UTFGridPlugin:{isLoaded:function(){return angular.isDefined(L.UtfGrid)},is:function(a){return this.isLoaded()?a instanceof L.UtfGrid:(b.error("[AngularJS - Leaflet] No UtfGrid plugin found."),!1)}},CartoDB:{isLoaded:function(){return cartodb},is:function(){return!0}},Leaflet:{DivIcon:{is:function(a){return a instanceof L.DivIcon},equal:function(a,b){return this.is(a)?angular.equals(a,b):!1}},Icon:{is:function(a){return a instanceof L.Icon},equal:function(a,b){return this.is(a)?angular.equals(a,b):!1}}}}}])}(); -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/favicon.ico -------------------------------------------------------------------------------- /app/humans.txt: -------------------------------------------------------------------------------- 1 | ______ _ _ _____ _ 2 | | _ \ | | | | / ___| | | 3 | | | | |_____ _____| | ___ _ __ _ __ ___ ___ _ __ | |_ \ `--. ___ ___ __| | 4 | | | | / _ \ \ / / _ \ |/ _ \| '_ \| '_ ` _ \ / _ \ '_ \| __| `--. \/ _ \/ _ \/ _` | 5 | | |/ / __/\ V / __/ | (_) | |_) | | | | | | __/ | | | |_ /\__/ / __/ __/ (_| | 6 | |___/ \___| \_/ \___|_|\___/| .__/|_| |_| |_|\___|_| |_|\__| \____/ \___|\___|\__,_| 7 | | | 8 | |_| 9 | =================================================================================== 10 | 11 | 2015 original site and app development: 12 | 13 | Drew Bollinger 14 | Daniel da Silva 15 | Joe Flasher 16 | 17 | Heavily reliant on landsat-util and landsat-api work done by: 18 | 19 | Alireza 20 | -------------------------------------------------------------------------------- /app/images/astrodigital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/astrodigital.png -------------------------------------------------------------------------------- /app/images/logo_horizontal_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/logo_horizontal_white.png -------------------------------------------------------------------------------- /app/images/ndvi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/ndvi.png -------------------------------------------------------------------------------- /app/images/noun_17256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_17256.png -------------------------------------------------------------------------------- /app/images/noun_17256_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_17256_w.png -------------------------------------------------------------------------------- /app/images/noun_2019_cc_noattr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_2019_cc_noattr.png -------------------------------------------------------------------------------- /app/images/noun_2019_cc_noattr_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_2019_cc_noattr_w.png -------------------------------------------------------------------------------- /app/images/noun_24967.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_24967.png -------------------------------------------------------------------------------- /app/images/noun_24967_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_24967_down.png -------------------------------------------------------------------------------- /app/images/noun_24967_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_24967_up.png -------------------------------------------------------------------------------- /app/images/noun_24967_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_24967_w.png -------------------------------------------------------------------------------- /app/images/noun_52713_cc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_52713_cc.png -------------------------------------------------------------------------------- /app/images/noun_52713_cc_noattr_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_52713_cc_noattr_w.png -------------------------------------------------------------------------------- /app/images/noun_88350_cc_noattr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_88350_cc_noattr.png -------------------------------------------------------------------------------- /app/images/noun_88350_cc_noattr_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_88350_cc_noattr_w.png -------------------------------------------------------------------------------- /app/images/noun_88351_cc_noattr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_88351_cc_noattr.png -------------------------------------------------------------------------------- /app/images/noun_88351_cc_noattr_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/noun_88351_cc_noattr_w.png -------------------------------------------------------------------------------- /app/images/satellite.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/satellite.gif -------------------------------------------------------------------------------- /app/images/truecolor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/truecolor.png -------------------------------------------------------------------------------- /app/images/urban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/urban.png -------------------------------------------------------------------------------- /app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDigital/libra/58ca231719eddb9af5cf92022484540367faf844/app/images/yeoman.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Libra 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 | :   
37 | :
38 | :
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 | 102 |
103 | 104 |
105 | 106 | 107 | 108 |
109 | 110 | 111 | 112 |

113 |
114 |
115 | 116 |
117 | 118 |
119 |
120 |
121 | 122 |
123 |
124 |
125 |
126 | 128 | 129 | 130 | 139 | 140 | 141 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /app/jquery.nouislider.all.min.js: -------------------------------------------------------------------------------- 1 | /*! noUiSlider - 7.0.10 - 2014-12-27 14:50:47 */ 2 | 3 | !function(){"use strict";function a(a){return a.split("").reverse().join("")}function b(a,b){return a.substring(0,b.length)===b}function c(a,b){return a.slice(-1*b.length)===b}function d(a,b,c){if((a[b]||a[c])&&a[b]===a[c])throw new Error(b)}function e(a){return"number"==typeof a&&isFinite(a)}function f(a,b){var c=Math.pow(10,b);return(Math.round(a*c)/c).toFixed(b)}function g(b,c,d,g,h,i,j,k,l,m,n,o){var p,q,r,s=o,t="",u="";return i&&(o=i(o)),e(o)?(b!==!1&&0===parseFloat(o.toFixed(b))&&(o=0),0>o&&(p=!0,o=Math.abs(o)),b!==!1&&(o=f(o,b)),o=o.toString(),-1!==o.indexOf(".")?(q=o.split("."),r=q[0],d&&(t=d+q[1])):r=o,c&&(r=a(r).match(/.{1,3}/g),r=a(r.join(a(c)))),p&&k&&(u+=k),g&&(u+=g),p&&l&&(u+=l),u+=r,u+=t,h&&(u+=h),m&&(u=m(u,s)),u):!1}function h(a,d,f,g,h,i,j,k,l,m,n,o){var p,q="";return n&&(o=n(o)),o&&"string"==typeof o?(k&&b(o,k)&&(o=o.replace(k,""),p=!0),g&&b(o,g)&&(o=o.replace(g,"")),l&&b(o,l)&&(o=o.replace(l,""),p=!0),h&&c(o,h)&&(o=o.slice(0,-1*h.length)),d&&(o=o.split(d).join("")),f&&(o=o.replace(f,".")),p&&(q+="-"),q+=o,q=q.replace(/[^0-9\.\-.]/g,""),""===q?!1:(q=Number(q),j&&(q=j(q)),e(q)?q:!1)):!1}function i(a){var b,c,e,f={};for(b=0;b=0&&8>e))throw new Error(c);f[c]=e}else if("encoder"===c||"decoder"===c||"edit"===c||"undo"===c){if("function"!=typeof e)throw new Error(c);f[c]=e}else{if("string"!=typeof e)throw new Error(c);f[c]=e}return d(f,"mark","thousand"),d(f,"prefix","negative"),d(f,"prefix","negativeBefore"),f}function j(a,b,c){var d,e=[];for(d=0;d"),!0):void 0}function d(b){if("string"==typeof b&&0!==b.indexOf("-")){this.method="val";var c=document.createElement("input");return c.name=b,c.type="hidden",this.target=this.el=a(c),!0}}function e(a){return"function"==typeof a?(this.target=!1,this.method=a,!0):void 0}function f(a,c){return b(a)&&!c?(a.is("input, select, textarea")?(this.method="val",this.target=a.on("change.liblink",this.changeHandler)):(this.target=a,this.method="html"),!0):void 0}function g(a,c){return b(a)&&("function"==typeof c||"string"==typeof c&&a[c])?(this.method=c,this.target=a,!0):void 0}function h(b,c,d){var e=this,f=!1;if(this.changeHandler=function(b){var c=e.formatInstance.from(a(this).val());return c===!1||isNaN(c)?(a(this).val(e.lastSetValue),!1):void e.changeHandlerMethod.call("",b,c)},this.el=!1,this.formatInstance=d,a.each(k,function(a,d){return f=d.call(e,b,c),!f}),!f)throw new RangeError("(Link) Invalid Link.")}function i(a){this.items=[],this.elements=[],this.origin=a}function j(b,c,d,e){0===b&&(b=this.LinkDefaultFlag),this.linkAPI||(this.linkAPI={}),this.linkAPI[b]||(this.linkAPI[b]=new i(this));var f=new h(c,d,e||this.LinkDefaultFormatter);f.target||(f.target=a(this)),f.changeHandlerMethod=this.LinkConfirm(b,f.el),this.linkAPI[b].push(f,f.el),this.LinkUpdate(b)}var k=[c,d,e,f,g];h.prototype.set=function(a){var b=Array.prototype.slice.call(arguments),c=b.slice(1);this.lastSetValue=this.formatInstance.to(a),c.unshift(this.lastSetValue),("function"==typeof this.method?this.method:this.target[this.method]).apply(this.target,c)},i.prototype.push=function(a,b){this.items.push(a),b&&this.elements.push(b)},i.prototype.reconfirm=function(a){var b;for(b=0;b1?b[1].length:0}function j(a,b){return 100/(b-a)}function k(a,b){return 100*b/(a[1]-a[0])}function l(a,b){return k(a,a[0]<0?b+Math.abs(a[0]):b-a[0])}function m(a,b){return b*(a[1]-a[0])/100+a[0]}function n(a,b){for(var c=1;a>=b[c];)c+=1;return c}function o(a,b,c){if(c>=a.slice(-1)[0])return 100;var d,e,f,g,h=n(c,a);return d=a[h-1],e=a[h],f=b[h-1],g=b[h],f+l([d,e],c)/j(f,g)}function p(a,b,c){if(c>=100)return a.slice(-1)[0];var d,e,f,g,h=n(c,b);return d=a[h-1],e=a[h],f=b[h-1],g=b[h],m([d,e],(c-f)*j(f,g))}function q(a,b,d,e){if(100===e)return e;var f,g,h=n(e,a);return d?(f=a[h-1],g=a[h],e-f>(g-f)/2?g:f):b[h-1]?a[h-1]+c(e-a[h-1],b[h-1]):e}function r(a,b,c){var e;if("number"==typeof b&&(b=[b]),"[object Array]"!==Object.prototype.toString.call(b))throw new Error("noUiSlider: 'range' contains invalid value.");if(e="min"===a?0:"max"===a?100:parseFloat(a),!d(e)||!d(b[0]))throw new Error("noUiSlider: 'range' value isn't numeric.");c.xPct.push(e),c.xVal.push(b[0]),e?c.xSteps.push(isNaN(b[1])?!1:b[1]):isNaN(b[1])||(c.xSteps[0]=b[1])}function s(a,b,c){return b?void(c.xSteps[a]=k([c.xVal[a],c.xVal[a+1]],b)/j(c.xPct[a],c.xPct[a+1])):!0}function t(a,b,c,d){this.xPct=[],this.xVal=[],this.xSteps=[d||!1],this.xNumSteps=[!1],this.snap=b,this.direction=c;var e,f=[];for(e in a)a.hasOwnProperty(e)&&f.push([a[e],e]);for(f.sort(function(a,b){return a[0]-b[0]}),e=0;e2)throw new Error("noUiSlider: 'start' option is incorrect.");b.handles=c.length,b.start=c}function x(a,b){if(a.snap=b,"boolean"!=typeof b)throw new Error("noUiSlider: 'snap' option must be a boolean.")}function y(a,b){if(a.animate=b,"boolean"!=typeof b)throw new Error("noUiSlider: 'animate' option must be a boolean.")}function z(a,b){if("lower"===b&&1===a.handles)a.connect=1;else if("upper"===b&&1===a.handles)a.connect=2;else if(b===!0&&2===a.handles)a.connect=3;else{if(b!==!1)throw new Error("noUiSlider: 'connect' option doesn't match handle count.");a.connect=0}}function A(a,b){switch(b){case"horizontal":a.ort=0;break;case"vertical":a.ort=1;break;default:throw new Error("noUiSlider: 'orientation' option is invalid.")}}function B(a,b){if(!d(b))throw new Error("noUiSlider: 'margin' option must be numeric.");if(a.margin=a.spectrum.getMargin(b),!a.margin)throw new Error("noUiSlider: 'margin' option is only supported on linear sliders.")}function C(a,b){if(!d(b))throw new Error("noUiSlider: 'limit' option must be numeric.");if(a.limit=a.spectrum.getMargin(b),!a.limit)throw new Error("noUiSlider: 'limit' option is only supported on linear sliders.")}function D(a,b){switch(b){case"ltr":a.dir=0;break;case"rtl":a.dir=1,a.connect=[0,2,1,3][a.connect];break;default:throw new Error("noUiSlider: 'direction' option was not recognized.")}}function E(a,b){if("string"!=typeof b)throw new Error("noUiSlider: 'behaviour' must be a string containing options.");var c=b.indexOf("tap")>=0,d=b.indexOf("drag")>=0,e=b.indexOf("fixed")>=0,f=b.indexOf("snap")>=0;a.events={tap:c||f,drag:d,fixed:e,snap:f}}function F(a,b){if(a.format=b,"function"==typeof b.to&&"function"==typeof b.from)return!0;throw new Error("noUiSlider: 'format' requires 'to' and 'from' methods.")}function G(b){var c,d={margin:0,limit:0,animate:!0,format:Z};return c={step:{r:!1,t:u},start:{r:!0,t:w},connect:{r:!0,t:z},direction:{r:!0,t:D},snap:{r:!1,t:x},animate:{r:!1,t:y},range:{r:!0,t:v},orientation:{r:!1,t:A},margin:{r:!1,t:B},limit:{r:!1,t:C},behaviour:{r:!0,t:E},format:{r:!1,t:F}},b=a.extend({connect:!1,direction:"ltr",behaviour:"tap",orientation:"horizontal"},b),a.each(c,function(a,c){if(void 0===b[a]){if(c.r)throw new Error("noUiSlider: '"+a+"' is required.");return!0}c.t(d,b[a])}),d.style=d.ort?"top":"left",d}function H(a,b,c){var d=a+b[0],e=a+b[1];return c?(0>d&&(e+=Math.abs(d)),e>100&&(d-=e-100),[g(d),g(e)]):[d,e]}function I(a){a.preventDefault();var b,c,d=0===a.type.indexOf("touch"),e=0===a.type.indexOf("mouse"),f=0===a.type.indexOf("pointer"),g=a;return 0===a.type.indexOf("MSPointer")&&(f=!0),a.originalEvent&&(a=a.originalEvent),d&&(b=a.changedTouches[0].pageX,c=a.changedTouches[0].pageY),(e||f)&&(f||void 0!==window.pageXOffset||(window.pageXOffset=document.documentElement.scrollLeft,window.pageYOffset=document.documentElement.scrollTop),b=a.clientX+window.pageXOffset,c=a.clientY+window.pageYOffset),g.points=[b,c],g.cursor=e,g}function J(b,c){var d=a("
").addClass(Y[2]),e=["-lower","-upper"];return b&&e.reverse(),d.children().addClass(Y[3]+" "+Y[3]+e[c]),d}function K(a,b,c){switch(a){case 1:b.addClass(Y[7]),c[0].addClass(Y[6]);break;case 3:c[1].addClass(Y[6]);case 2:c[0].addClass(Y[7]);case 0:b.addClass(Y[6])}}function L(a,b,c){var d,e=[];for(d=0;a>d;d+=1)e.push(J(b,d).appendTo(c));return e}function M(b,c,d){return d.addClass([Y[0],Y[8+b],Y[4+c]].join(" ")),a("
").appendTo(d).addClass(Y[1])}function N(b,c,d){function e(){return C[["width","height"][c.ort]]()}function j(a){var b,c=[E.val()];for(b=0;b1&&(e=1===e?0:1),l(e)}function o(){var a,b;for(a=0;a1),f=v(d[0],c[h],1===d.length),d.length>1&&(f=v(d[1],c[h?0:1],!1)||f),f&&j(["slide"])}function r(b){a("."+Y[15]).removeClass(Y[15]),b.cursor&&a("body").css("cursor","").off(W),U.off(W),E.removeClass(Y[12]),j(["set","change"])}function s(b,c){1===c.handles.length&&c.handles[0].children().addClass(Y[15]),b.stopPropagation(),p(X.move,U,q,{start:b.calcPoint,handles:c.handles,positions:[F[0],F[D.length-1]]}),p(X.end,U,r,null),b.cursor&&(a("body").css("cursor",a(b.target).css("cursor")),D.length>1&&E.addClass(Y[12]),a("body").on("selectstart"+W,!1))}function t(b){var d,g=b.calcPoint,h=0;b.stopPropagation(),a.each(D,function(){h+=this.offset()[c.style]}),h=h/2>g||1===D.length?0:1,g-=C.offset()[c.style],d=100*g/e(),c.events.snap||f(E,Y[14],300),v(D[h],d),j(["slide","set","change"]),c.events.snap&&s(b,{handles:[D[h]]})}function u(a){var b,c;if(!a.fixed)for(b=0;b1&&(b=e?Math.max(b,f):Math.min(b,h)),d!==!1&&c.limit&&D.length>1&&(b=e?Math.min(b,i):Math.max(b,j)),b=G.getStep(b),b=g(parseFloat(b.toFixed(7))),b===F[e]?!1:(a.css(c.style,b+"%"),a.is(":first-child")&&a.toggleClass(Y[17],b>50),F[e]=b,J[e]=G.fromStepping(b),m(N[e]),!0)}function w(a,b){var d,e,f;for(c.limit&&(a+=1),d=0;a>d;d+=1)e=d%2,f=b[e],null!==f&&f!==!1&&("number"==typeof f&&(f=String(f)),f=c.format.from(f),(f===!1||isNaN(f)||v(D[e],G.toStepping(f),d===3-c.dir)===!1)&&m(N[e]))}function x(a){if(E[0].LinkIsEmitting)return this;var b,d=h(a);return c.dir&&c.handles>1&&d.reverse(),c.animate&&-1!==F[0]&&f(E,Y[14],300),b=D.length>1?3:1,1===d.length&&(b=1),w(b,d),j(["set"]),this}function y(){var a,b=[];for(a=0;a=c[1]?c[2]:c[0]||!1;return[[h,f]]});return k(b)}function B(){return d}var C,D,E=a(b),F=[-1,-1],G=c.spectrum,J=[],N=["lower","upper"].slice(0,c.handles);if(c.dir&&N.reverse(),b.LinkUpdate=m,b.LinkConfirm=n,b.LinkDefaultFormatter=c.format,b.LinkDefaultFlag="lower",b.reappend=o,E.hasClass(Y[0]))throw new Error("Slider was already initialized.");C=M(c.dir,c.ort,E),D=L(c.handles,c.dir,C),K(c.connect,E,D),u(c.events),b.vSet=x,b.vGet=y,b.destroy=z,b.getCurrentStep=A,b.getOriginalOptions=B,b.getInfo=function(){return[G,c.style,c.ort]},E.val(c.start)}function O(a){var b=G(a,this);return this.each(function(){N(this,b,a)})}function P(b){return this.each(function(){if(!this.destroy)return void a(this).noUiSlider(b);var c=a(this).val(),d=this.destroy(),e=a.extend({},d,b);a(this).noUiSlider(e),this.reappend(),d.start===e.start&&a(this).val(c)})}function Q(){return this[0][arguments.length?"vSet":"vGet"].apply(this[0],arguments)}function R(b,c,d,e){if("range"===c||"steps"===c)return b.xVal;if("count"===c){var f,g=100/(d-1),h=0;for(d=[];(f=h++*g)<=100;)d.push(f);c="positions"}return"positions"===c?a.map(d,function(a){return b.fromStepping(e?b.getStep(a):a)}):"values"===c?e?a.map(d,function(a){return b.fromStepping(b.getStep(b.toStepping(a)))}):d:void 0}function S(c,d,e,f){var g=c.direction,h={},i=c.xVal[0],j=c.xVal[c.xVal.length-1],k=!1,l=!1,m=0;return c.direction=0,f=b(f.slice().sort(function(a,b){return a-b})),f[0]!==i&&(f.unshift(i),k=!0),f[f.length-1]!==j&&(f.push(j),l=!0),a.each(f,function(b){var g,i,j,n,o,p,q,r,s,t,u=f[b],v=f[b+1];if("steps"===e&&(g=c.xNumSteps[b]),g||(g=v-u),u!==!1&&void 0!==v)for(i=u;v>=i;i+=g){for(n=c.toStepping(i),o=n-m,r=o/d,s=Math.round(r),t=o/s,j=1;s>=j;j+=1)p=m+j*t,h[p.toFixed(5)]=["x",0];q=a.inArray(i,f)>-1?1:"steps"===e?2:0,!b&&k&&(q=0),i===v&&l||(h[n.toFixed(5)]=[i,q]),m=n}}),c.direction=g,h}function T(b,c,d,e,f,g){function h(a){return["-normal","-large","-sub"][a]}function i(a,c,d){return'class="'+c+" "+c+"-"+k+" "+c+h(d[1],d[0])+'" style="'+b+": "+a+'%"'}function j(a,b){d&&(a=100-a),b[1]=b[1]&&f?f(b[0],b[1]):b[1],l.append("
"),b[1]&&l.append("
"+g.to(b[0])+"
")}var k=["horizontal","vertical"][c],l=a("
");return l.addClass("noUi-pips noUi-pips-"+k),a.each(e,j),l}var U=a(document),V=a.fn.val,W=".nui",X=window.navigator.pointerEnabled?{start:"pointerdown",move:"pointermove",end:"pointerup"}:window.navigator.msPointerEnabled?{start:"MSPointerDown",move:"MSPointerMove",end:"MSPointerUp"}:{start:"mousedown touchstart",move:"mousemove touchmove",end:"mouseup touchend"},Y=["noUi-target","noUi-base","noUi-origin","noUi-handle","noUi-horizontal","noUi-vertical","noUi-background","noUi-connect","noUi-ltr","noUi-rtl","noUi-dragable","","noUi-state-drag","","noUi-state-tap","noUi-active","","noUi-stacking"];t.prototype.getMargin=function(a){return 2===this.xPct.length?k(this.xVal,a):!1},t.prototype.toStepping=function(a){return a=o(this.xVal,this.xPct,a),this.direction&&(a=100-a),a},t.prototype.fromStepping=function(a){return this.direction&&(a=100-a),e(p(this.xVal,this.xPct,a))},t.prototype.getStep=function(a){return this.direction&&(a=100-a),a=q(this.xPct,this.xSteps,this.snap,a),this.direction&&(a=100-a),a},t.prototype.getApplicableStep=function(a){var b=n(a,this.xPct),c=100===a?2:1;return[this.xNumSteps[b-2],this.xVal[b-c],this.xNumSteps[b-c]]},t.prototype.convert=function(a){return this.getStep(this.toStepping(a))};var Z={to:function(a){return a.toFixed(2)},from:Number};a.fn.val=function(b){function c(a){return a.hasClass(Y[0])?Q:V}if(!arguments.length){var d=a(this[0]);return c(d).call(d)}var e=a.isFunction(b);return this.each(function(d){var f=b,g=a(this);e&&(f=b.call(this,d,g.val())),c(g).call(g,f)})},a.fn.noUiSlider=function(a,b){switch(a){case"step":return this[0].getCurrentStep();case"options":return this[0].getOriginalOptions()}return(b?P:O).call(this,a)},a.fn.noUiSlider_pips=function(b){var c=b.mode,d=b.density||1,e=b.filter||!1,f=b.values||!1,g=b.format||{to:Math.round},h=b.stepped||!1;return this.each(function(){var b=this.getInfo(),i=R(b[0],c,f,h),j=S(b[0],d,c,i);return a(this).append(T(b[1],b[2],b[0].direction,j,e,g))})}}(window.jQuery||window.Zepto); -------------------------------------------------------------------------------- /app/jquery.nouislider.min.css: -------------------------------------------------------------------------------- 1 | /*! noUiSlider - 7.0.10 - 2014-12-27 14:50:47 */ 2 | 3 | 4 | .noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-user-select:none;-ms-touch-action:none;-ms-user-select:none;-moz-user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative;direction:ltr}.noUi-base{width:100%;height:100%;position:relative}.noUi-origin{position:absolute;right:0;top:0;left:0;bottom:0}.noUi-handle{position:relative;z-index:1}.noUi-stacking .noUi-handle{z-index:10}.noUi-state-tap .noUi-origin{-webkit-transition:left .3s,top .3s;transition:left .3s,top .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-base{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;left:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;left:-6px;top:-17px}.noUi-background{background:#FAFAFA;box-shadow:inset 0 1px 1px #f0f0f0}.noUi-connect{background:#3FB8AF;box-shadow:inset 0 0 3px rgba(51,51,51,.45);-webkit-transition:background 450ms;transition:background 450ms}.noUi-origin{border-radius:2px}.noUi-target{border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-target.noUi-connect{box-shadow:inset 0 0 3px rgba(51,51,51,.45),0 3px 6px -5px #BBB}.noUi-dragable{cursor:w-resize}.noUi-vertical .noUi-dragable{cursor:n-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect,[disabled].noUi-connect{background:#B8B8B8}[disabled] .noUi-handle{cursor:not-allowed} -------------------------------------------------------------------------------- /app/jquery.nouislider.min.js: -------------------------------------------------------------------------------- 1 | /*! noUiSlider - 7.0.10 - 2014-12-27 14:50:47 */ 2 | 3 | !function(a){"use strict";function b(a,b){return Math.round(a/b)*b}function c(a){return"number"==typeof a&&!isNaN(a)&&isFinite(a)}function d(a){var b=Math.pow(10,7);return Number((Math.round(a*b)/b).toFixed(7))}function e(a,b,c){a.addClass(b),setTimeout(function(){a.removeClass(b)},c)}function f(a){return Math.max(Math.min(a,100),0)}function g(b){return a.isArray(b)?b:[b]}function h(a){var b=a.split(".");return b.length>1?b[1].length:0}function i(a,b){return 100/(b-a)}function j(a,b){return 100*b/(a[1]-a[0])}function k(a,b){return j(a,a[0]<0?b+Math.abs(a[0]):b-a[0])}function l(a,b){return b*(a[1]-a[0])/100+a[0]}function m(a,b){for(var c=1;a>=b[c];)c+=1;return c}function n(a,b,c){if(c>=a.slice(-1)[0])return 100;var d,e,f,g,h=m(c,a);return d=a[h-1],e=a[h],f=b[h-1],g=b[h],f+k([d,e],c)/i(f,g)}function o(a,b,c){if(c>=100)return a.slice(-1)[0];var d,e,f,g,h=m(c,b);return d=a[h-1],e=a[h],f=b[h-1],g=b[h],l([d,e],(c-f)*i(f,g))}function p(a,c,d,e){if(100===e)return e;var f,g,h=m(e,a);return d?(f=a[h-1],g=a[h],e-f>(g-f)/2?g:f):c[h-1]?a[h-1]+b(e-a[h-1],c[h-1]):e}function q(a,b,d){var e;if("number"==typeof b&&(b=[b]),"[object Array]"!==Object.prototype.toString.call(b))throw new Error("noUiSlider: 'range' contains invalid value.");if(e="min"===a?0:"max"===a?100:parseFloat(a),!c(e)||!c(b[0]))throw new Error("noUiSlider: 'range' value isn't numeric.");d.xPct.push(e),d.xVal.push(b[0]),e?d.xSteps.push(isNaN(b[1])?!1:b[1]):isNaN(b[1])||(d.xSteps[0]=b[1])}function r(a,b,c){return b?void(c.xSteps[a]=j([c.xVal[a],c.xVal[a+1]],b)/i(c.xPct[a],c.xPct[a+1])):!0}function s(a,b,c,d){this.xPct=[],this.xVal=[],this.xSteps=[d||!1],this.xNumSteps=[!1],this.snap=b,this.direction=c;var e,f=[];for(e in a)a.hasOwnProperty(e)&&f.push([a[e],e]);for(f.sort(function(a,b){return a[0]-b[0]}),e=0;e2)throw new Error("noUiSlider: 'start' option is incorrect.");b.handles=c.length,b.start=c}function w(a,b){if(a.snap=b,"boolean"!=typeof b)throw new Error("noUiSlider: 'snap' option must be a boolean.")}function x(a,b){if(a.animate=b,"boolean"!=typeof b)throw new Error("noUiSlider: 'animate' option must be a boolean.")}function y(a,b){if("lower"===b&&1===a.handles)a.connect=1;else if("upper"===b&&1===a.handles)a.connect=2;else if(b===!0&&2===a.handles)a.connect=3;else{if(b!==!1)throw new Error("noUiSlider: 'connect' option doesn't match handle count.");a.connect=0}}function z(a,b){switch(b){case"horizontal":a.ort=0;break;case"vertical":a.ort=1;break;default:throw new Error("noUiSlider: 'orientation' option is invalid.")}}function A(a,b){if(!c(b))throw new Error("noUiSlider: 'margin' option must be numeric.");if(a.margin=a.spectrum.getMargin(b),!a.margin)throw new Error("noUiSlider: 'margin' option is only supported on linear sliders.")}function B(a,b){if(!c(b))throw new Error("noUiSlider: 'limit' option must be numeric.");if(a.limit=a.spectrum.getMargin(b),!a.limit)throw new Error("noUiSlider: 'limit' option is only supported on linear sliders.")}function C(a,b){switch(b){case"ltr":a.dir=0;break;case"rtl":a.dir=1,a.connect=[0,2,1,3][a.connect];break;default:throw new Error("noUiSlider: 'direction' option was not recognized.")}}function D(a,b){if("string"!=typeof b)throw new Error("noUiSlider: 'behaviour' must be a string containing options.");var c=b.indexOf("tap")>=0,d=b.indexOf("drag")>=0,e=b.indexOf("fixed")>=0,f=b.indexOf("snap")>=0;a.events={tap:c||f,drag:d,fixed:e,snap:f}}function E(a,b){if(a.format=b,"function"==typeof b.to&&"function"==typeof b.from)return!0;throw new Error("noUiSlider: 'format' requires 'to' and 'from' methods.")}function F(b){var c,d={margin:0,limit:0,animate:!0,format:V};return c={step:{r:!1,t:t},start:{r:!0,t:v},connect:{r:!0,t:y},direction:{r:!0,t:C},snap:{r:!1,t:w},animate:{r:!1,t:x},range:{r:!0,t:u},orientation:{r:!1,t:z},margin:{r:!1,t:A},limit:{r:!1,t:B},behaviour:{r:!0,t:D},format:{r:!1,t:E}},b=a.extend({connect:!1,direction:"ltr",behaviour:"tap",orientation:"horizontal"},b),a.each(c,function(a,c){if(void 0===b[a]){if(c.r)throw new Error("noUiSlider: '"+a+"' is required.");return!0}c.t(d,b[a])}),d.style=d.ort?"top":"left",d}function G(a,b,c){var d=a+b[0],e=a+b[1];return c?(0>d&&(e+=Math.abs(d)),e>100&&(d-=e-100),[f(d),f(e)]):[d,e]}function H(a){a.preventDefault();var b,c,d=0===a.type.indexOf("touch"),e=0===a.type.indexOf("mouse"),f=0===a.type.indexOf("pointer"),g=a;return 0===a.type.indexOf("MSPointer")&&(f=!0),a.originalEvent&&(a=a.originalEvent),d&&(b=a.changedTouches[0].pageX,c=a.changedTouches[0].pageY),(e||f)&&(f||void 0!==window.pageXOffset||(window.pageXOffset=document.documentElement.scrollLeft,window.pageYOffset=document.documentElement.scrollTop),b=a.clientX+window.pageXOffset,c=a.clientY+window.pageYOffset),g.points=[b,c],g.cursor=e,g}function I(b,c){var d=a("
").addClass(U[2]),e=["-lower","-upper"];return b&&e.reverse(),d.children().addClass(U[3]+" "+U[3]+e[c]),d}function J(a,b,c){switch(a){case 1:b.addClass(U[7]),c[0].addClass(U[6]);break;case 3:c[1].addClass(U[6]);case 2:c[0].addClass(U[7]);case 0:b.addClass(U[6])}}function K(a,b,c){var d,e=[];for(d=0;a>d;d+=1)e.push(I(b,d).appendTo(c));return e}function L(b,c,d){return d.addClass([U[0],U[8+b],U[4+c]].join(" ")),a("
").appendTo(d).addClass(U[1])}function M(b,c,d){function i(){return C[["width","height"][c.ort]]()}function j(a){var b,c=[E.val()];for(b=0;b1&&(e=1===e?0:1),l(e)}function o(){var a,b;for(a=0;a1),e=v(d[0],c[g],1===d.length),d.length>1&&(e=v(d[1],c[g?0:1],!1)||e),e&&j(["slide"])}function r(b){a("."+U[15]).removeClass(U[15]),b.cursor&&a("body").css("cursor","").off(S),Q.off(S),E.removeClass(U[12]),j(["set","change"])}function s(b,c){1===c.handles.length&&c.handles[0].children().addClass(U[15]),b.stopPropagation(),p(T.move,Q,q,{start:b.calcPoint,handles:c.handles,positions:[F[0],F[D.length-1]]}),p(T.end,Q,r,null),b.cursor&&(a("body").css("cursor",a(b.target).css("cursor")),D.length>1&&E.addClass(U[12]),a("body").on("selectstart"+S,!1))}function t(b){var d,f=b.calcPoint,g=0;b.stopPropagation(),a.each(D,function(){g+=this.offset()[c.style]}),g=g/2>f||1===D.length?0:1,f-=C.offset()[c.style],d=100*f/i(),c.events.snap||e(E,U[14],300),v(D[g],d),j(["slide","set","change"]),c.events.snap&&s(b,{handles:[D[g]]})}function u(a){var b,c;if(!a.fixed)for(b=0;b1&&(b=e?Math.max(b,g):Math.min(b,h)),d!==!1&&c.limit&&D.length>1&&(b=e?Math.min(b,i):Math.max(b,j)),b=I.getStep(b),b=f(parseFloat(b.toFixed(7))),b===F[e]?!1:(a.css(c.style,b+"%"),a.is(":first-child")&&a.toggleClass(U[17],b>50),F[e]=b,M[e]=I.fromStepping(b),m(N[e]),!0)}function w(a,b){var d,e,f;for(c.limit&&(a+=1),d=0;a>d;d+=1)e=d%2,f=b[e],null!==f&&f!==!1&&("number"==typeof f&&(f=String(f)),f=c.format.from(f),(f===!1||isNaN(f)||v(D[e],I.toStepping(f),d===3-c.dir)===!1)&&m(N[e]))}function x(a){if(E[0].LinkIsEmitting)return this;var b,d=g(a);return c.dir&&c.handles>1&&d.reverse(),c.animate&&-1!==F[0]&&e(E,U[14],300),b=D.length>1?3:1,1===d.length&&(b=1),w(b,d),j(["set"]),this}function y(){var a,b=[];for(a=0;a=c[1]?c[2]:c[0]||!1;return[[i,f]]});return k(b)}function B(){return d}var C,D,E=a(b),F=[-1,-1],I=c.spectrum,M=[],N=["lower","upper"].slice(0,c.handles);if(c.dir&&N.reverse(),b.LinkUpdate=m,b.LinkConfirm=n,b.LinkDefaultFormatter=c.format,b.LinkDefaultFlag="lower",b.reappend=o,E.hasClass(U[0]))throw new Error("Slider was already initialized.");C=L(c.dir,c.ort,E),D=K(c.handles,c.dir,C),J(c.connect,E,D),u(c.events),b.vSet=x,b.vGet=y,b.destroy=z,b.getCurrentStep=A,b.getOriginalOptions=B,b.getInfo=function(){return[I,c.style,c.ort]},E.val(c.start)}function N(a){var b=F(a,this);return this.each(function(){M(this,b,a)})}function O(b){return this.each(function(){if(!this.destroy)return void a(this).noUiSlider(b);var c=a(this).val(),d=this.destroy(),e=a.extend({},d,b);a(this).noUiSlider(e),this.reappend(),d.start===e.start&&a(this).val(c)})}function P(){return this[0][arguments.length?"vSet":"vGet"].apply(this[0],arguments)}var Q=a(document),R=a.fn.val,S=".nui",T=window.navigator.pointerEnabled?{start:"pointerdown",move:"pointermove",end:"pointerup"}:window.navigator.msPointerEnabled?{start:"MSPointerDown",move:"MSPointerMove",end:"MSPointerUp"}:{start:"mousedown touchstart",move:"mousemove touchmove",end:"mouseup touchend"},U=["noUi-target","noUi-base","noUi-origin","noUi-handle","noUi-horizontal","noUi-vertical","noUi-background","noUi-connect","noUi-ltr","noUi-rtl","noUi-dragable","","noUi-state-drag","","noUi-state-tap","noUi-active","","noUi-stacking"];s.prototype.getMargin=function(a){return 2===this.xPct.length?j(this.xVal,a):!1},s.prototype.toStepping=function(a){return a=n(this.xVal,this.xPct,a),this.direction&&(a=100-a),a},s.prototype.fromStepping=function(a){return this.direction&&(a=100-a),d(o(this.xVal,this.xPct,a))},s.prototype.getStep=function(a){return this.direction&&(a=100-a),a=p(this.xPct,this.xSteps,this.snap,a),this.direction&&(a=100-a),a},s.prototype.getApplicableStep=function(a){var b=m(a,this.xPct),c=100===a?2:1;return[this.xNumSteps[b-2],this.xVal[b-c],this.xNumSteps[b-c]]},s.prototype.convert=function(a){return this.getStep(this.toStepping(a))};var V={to:function(a){return a.toFixed(2)},from:Number};a.fn.val=function(b){function c(a){return a.hasClass(U[0])?P:R}if(!arguments.length){var d=a(this[0]);return c(d).call(d)}var e=a.isFunction(b);return this.each(function(d){var f=b,g=a(this);e&&(f=b.call(this,d,g.val())),c(g).call(g,f)})},a.fn.noUiSlider=function(a,b){switch(a){case"step":return this[0].getCurrentStep();case"options":return this[0].getOriginalOptions()}return(b?O:N).call(this,a)}}(window.jQuery||window.Zepto); -------------------------------------------------------------------------------- /app/jquery.nouislider.pips.min.css: -------------------------------------------------------------------------------- 1 | /*! noUiSlider - 7.0.10 - 2014-12-27 14:50:47 */ 2 | 3 | 4 | .noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;font:400 12px Arial;color:#999}.noUi-value{width:40px;position:absolute;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-large,.noUi-marker-sub{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:50px;top:100%;left:0;width:100%}.noUi-value-horizontal{margin-left:-20px;padding-top:20px}.noUi-value-horizontal.noUi-value-sub{padding-top:15px}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{width:15px;margin-left:20px;margin-top:-5px}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px} -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // hide $digest error 4 | console.error = function(){}; 5 | /** 6 | * @ngdoc overview 7 | * @name dauriaSearchApp 8 | * @description 9 | * # dauriaSearchApp 10 | * 11 | * Main module of the application. 12 | */ 13 | angular 14 | .module('dauriaSearchApp', [ 15 | 'ngAnimate', 16 | 'ngCookies', 17 | 'ngResource', 18 | 'ngRoute', 19 | 'ngSanitize', 20 | 'ngTouch', 21 | 'leaflet-directive', 22 | 'ui.bootstrap' 23 | ]); 24 | -------------------------------------------------------------------------------- /app/scripts/controllers/advanced.js: -------------------------------------------------------------------------------- 1 | /* global multiDownload */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * @ngdoc function 7 | * @name dauriaSearchApp.controller:MainCtrl 8 | * @description 9 | * # MainCtrl 10 | * Controller of the dauriaSearchApp 11 | */ 12 | angular.module('dauriaSearchApp') 13 | .controller('AdvancedCtrl', ['$scope', '$filter', 'selectedResult', function($scope, $filter, selectedResult) { 14 | 15 | $scope.bandPrefill = function (array) { 16 | jQuery('.band-selection input').each(function(){ 17 | jQuery(this).prop('checked',false); 18 | }); 19 | for (var i=0; i < array.length; i++) { 20 | jQuery('#band-' + array[i]).prop('checked',true); 21 | } 22 | }; 23 | 24 | $scope.downloadBands = function () { 25 | var bands = []; 26 | jQuery('.band-selection input').each(function(){ 27 | if(jQuery(this).prop('checked') === true){ 28 | bands.push(jQuery(this).attr('value').replace('band-','')); 29 | } 30 | }); 31 | var urls = bands.map(function(band){ 32 | return 'https://landsat-pds.s3.amazonaws.com/L8/' + zeroPad(selectedResult.path,3) + '/' + zeroPad(selectedResult.row,3) + '/' + selectedResult.sceneID + '/' + selectedResult.sceneID + '_B' + band + '.TIF'; 33 | }); 34 | multiDownload(urls); 35 | }; 36 | 37 | function zeroPad(n,c) { 38 | var s = String(n); 39 | if (s.length < c) { 40 | return zeroPad('0' + n,c); 41 | } 42 | else { 43 | return s; 44 | } 45 | } 46 | 47 | }]); 48 | -------------------------------------------------------------------------------- /app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | /* global ga moment multiDownload */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * @ngdoc function 7 | * @name dauriaSearchApp.controller:MainCtrl 8 | * @description 9 | * # MainCtrl 10 | * Controller of the dauriaSearchApp 11 | */ 12 | angular.module('dauriaSearchApp') 13 | .controller('MainCtrl', ['$scope', '$filter', 'leafletData', 'leafletBoundsHelpers', '$http', '$sce', '$q', '$modal', function($scope, $filter, leafletData, leafletBoundsHelpers, $http, $sce, $q, $modal) { 14 | 15 | // array of everything returned from api calls 16 | $scope.results = []; 17 | 18 | // api endpoint and canceller 19 | var endpoint = 'https://api.developmentseed.org'; 20 | var canceller = $q.defer(); 21 | 22 | // Cloud coverage. 23 | $scope.cloudCoverageMin = 0; 24 | $scope.cloudCoverageMax = 20; 25 | // Sun azimuth. 26 | $scope.sunAzimuthMin = 0; 27 | $scope.sunAzimuthMax = 180; 28 | // Date range. 29 | // Default date is from 'now' to 'now - 6 months' 30 | var dateEnd = moment(), 31 | dateStart = moment().subtract(6, 'months'), 32 | dateStartStr = dateStart.format('YYYY-MM-DD'), 33 | dateEndStr = dateEnd.format('YYYY-MM-DD'); 34 | 35 | $scope.dateRangeStart = dateStartStr; 36 | $scope.dateRangeEnd = dateEndStr; 37 | 38 | // Temporary date range variables for display reasons 39 | $scope.dateRangeStartTemp = $scope.dateRangeStart; 40 | $scope.dateRangeEndTemp = $scope.dateRangeEnd; 41 | 42 | // Sorting. 43 | $scope.sortField = 'acquisitionDate'; 44 | $scope.sortReverse = true; 45 | 46 | // Store selected result. 47 | $scope.selectedResult = null; 48 | 49 | // Store parameter for filtering based on map clicks 50 | $scope.rowPathSelect = null; 51 | 52 | // Store parameter for opened filter panes 53 | $scope.openFilter = null; 54 | 55 | // d3 parameters for multiple uses 56 | var brush = {}, 57 | graphParam = { 58 | margin: { 59 | top: 10, 60 | right: 40, 61 | bottom: 30, 62 | left: 40 63 | } 64 | }; 65 | 66 | // spinner parameters 67 | var opts = { 68 | lines: 13, // The number of lines to draw 69 | length: 10, // The length of each line 70 | width: 4, // The line thickness 71 | radius: 16, // The radius of the inner circle 72 | corners: 0, // Corner roundness (0..1) 73 | color: '#231850', // #rgb or #rrggbb or array of colors 74 | speed: 0.7, // Rounds per second 75 | trail: 60, // Afterglow percentage 76 | }; 77 | var target = document.getElementById('spinner'); 78 | $scope.spinner = new Spinner(opts); 79 | 80 | var bounds = leafletBoundsHelpers.createBoundsFromArray([ 81 | [ 90, -180 ], 82 | [ -90, 180 ] 83 | ]); 84 | 85 | $scope.defaults = { 86 | tileLayer: 'http://api.tiles.mapbox.com/v4/nate.kna67bkd/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiZGV2c2VlZCIsImEiOiJnUi1mbkVvIn0.018aLhX0Mb0tdtaT2QNe2Q', 87 | maxZoom: 14 88 | }; 89 | 90 | $scope.safeApply = function(fn) { 91 | var phase = this.$root.$$phase; 92 | if(phase === '$apply' || phase === '$digest') { 93 | if(fn && (typeof(fn) === 'function')) { 94 | fn(); 95 | } 96 | } else { 97 | this.$apply(fn); 98 | } 99 | }; 100 | 101 | angular.extend($scope, { 102 | bounds: bounds, 103 | center: { lat: 1, lng: 1, zoom: 2 }, // show global to start 104 | markers: {}, 105 | events: { 106 | markers: { 107 | enable: ['mouseover', 'mouseout'] 108 | } 109 | }, 110 | paths: {} 111 | }); 112 | 113 | // Get starting center to see if the user has moved map and 114 | // disable auto-centering if so #36 115 | var startingCenter = $scope.center; 116 | var userHasMoved = function () { 117 | return ($scope.center !== startingCenter); 118 | }; 119 | 120 | // For keeping some stuff clean until after the first zoom 121 | $scope.firstZoomDone = false; 122 | 123 | // Move to user's IP location, fall back to east of San Fran 124 | var demoCenter = { lat: 37.7833, lng: -115.4167, zoom: 6 }; 125 | $http.get('https://vast-coast-1838.herokuapp.com/location') 126 | .success(function(data) { 127 | // Do nothing if the user has already moved their location 128 | if (userHasMoved()) { return;} 129 | 130 | // We got a response, make sure we have lat/lon and set it 131 | if (data && data.location && data.location.lat && data.location.lon) { 132 | $scope.center = { 133 | lat: parseFloat(data.location.lat), 134 | lng: parseFloat(data.location.lon), 135 | zoom: 6 136 | }; // Centered on browser's IP 137 | setTimeout(function(){ $scope.firstZoomDone = true; },500); 138 | } else { 139 | // Do nothing if the user has already moved their location 140 | if (userHasMoved()) { return; } 141 | $scope.center = demoCenter; // centered on east of San Francisco 142 | setTimeout(function(){ $scope.firstZoomDone = true; },500); 143 | } 144 | }). 145 | error(function() { 146 | $scope.center = demoCenter; // centered on east of San Francisco 147 | setTimeout(function(){ $scope.firstZoomDone = true; },500); 148 | }); 149 | 150 | $scope.$watchGroup(['bounds','dateRangeStart','dateRangeEnd'], function() { 151 | $scope.cleanPaths(); 152 | $scope.execQuery(); 153 | }); 154 | 155 | $scope.$watchGroup(['cloudCoverageMin', 'cloudCoverageMax', 'sunAzimuthMin', 'sunAzimuthMax'], function() { 156 | $scope.markers = {}; 157 | updateMarkers(); 158 | }); 159 | 160 | $scope.$watch('openFilter', function() { 161 | // this will make all tests fail so we should fix in the future 162 | $scope.$apply(); 163 | $scope.switchOpenFilter(); 164 | }); 165 | 166 | /** 167 | * Show the info pane with a message or hide it if no message given 168 | * 169 | * @param {String} msg 170 | */ 171 | var setInfoPane = function (msg) { 172 | if (msg === undefined || msg === '' || $scope.firstZoomDone === false) { 173 | $scope.showInfoPane = false; 174 | $scope.infoPaneMessage = ''; 175 | } else { 176 | $scope.infoPaneMessage = msg; 177 | $scope.showInfoPane = true; 178 | } 179 | }; 180 | 181 | /** 182 | * Queries the Api for resources. 183 | */ 184 | $scope.execQuery = function() { 185 | // cancel any existing requests and renew canceller 186 | canceller.resolve(); 187 | canceller = $q.defer(); 188 | 189 | // spin! 190 | $scope.spinner.spin(target); 191 | 192 | // no satellite gif 193 | $scope.satellite = false; 194 | 195 | // check continuity of longitude range (bool) 196 | var continuous = Math.floor(($scope.bounds.northEast.lng + 180) / 360) === Math.floor(($scope.bounds.southWest.lng + 180) / 360); 197 | 198 | $scope.searchString = queryConstructor({ 199 | dateRange: [$scope.dateRangeStart, $scope.dateRangeEnd], 200 | limit: 3000, 201 | sceneCenterLatRange: [$scope.bounds.northEast.lat, $scope.bounds.southWest.lat], 202 | // mod function here supports negative modulo in the 'expected fashion' 203 | // we are treating the longitude this way to support multiple rotations around the earth 204 | sceneCenterLonRange: [mod($scope.bounds.northEast.lng + 180, 360) - 180, mod($scope.bounds.southWest.lng + 180, 360) - 180], 205 | continuous: continuous 206 | }); 207 | 208 | $http.post(endpoint + '/v1/landsat/', $scope.searchString, { timeout: canceller.promise }) 209 | .success(function(data) { 210 | console.log(data); 211 | setInfoPane(); // Hide info pane so it doesn't flash when results are redrawn 212 | var total = data.meta.found; 213 | $scope.results = []; 214 | $scope.markers = {}; 215 | // clear histograms 216 | d3.select('.cloudCoverSlider svg').selectAll('.bar').remove(); 217 | d3.select('.sunAzimuthSlider svg').selectAll('.bar').remove(); 218 | // only renew results if we have a 'resonable' number 219 | if (total < 3000){ 220 | for (var i=0; i < data.results.length; i++){ 221 | var scene = data.results[i]; 222 | scene.className = scene.sceneID + '-' + scene.row + '-' + scene.path; 223 | scene.lat = scene.sceneCenterLatitude; 224 | // correction for being on "left or right earths" 225 | // we force everything to be in our map range 226 | // rotate according to which part of the range the scene is in 227 | // for continuous ranges, it doesn't matter but this covers all cases 228 | if (scene.sceneCenterLongitude > 0) { 229 | scene.lng = scene.sceneCenterLongitude + Math.floor(($scope.bounds.southWest.lng + 180) / 360) * 360; 230 | } 231 | else { 232 | scene.lng = scene.sceneCenterLongitude + Math.floor(($scope.bounds.northEast.lng + 180) / 360) * 360; 233 | } 234 | scene.icon = {}; 235 | scene.icon.type = 'div'; 236 | scene.icon.className = 'map-icon text-center'; 237 | scene.downloadURL = 'https://storage.googleapis.com/earthengine-public/landsat/L8/' + zeroPad(scene.path,3) + '/' + zeroPad(scene.row,3) + '/' + scene.sceneID + '.tar.bz'; 238 | scene.downloadSize = false; 239 | $scope.results.push(scene); 240 | } 241 | updateMarkers(); 242 | if ($scope.openFilter === 'cloud' || $scope.openFilter === 'sun' ){ 243 | var helperNames = ($scope.openFilter === 'cloud') ? {main:'cloudCover', val: 'cloudCoverFull', bound: 100, bin: 20} : {main:'sunAzimuth', val: 'sunAzimuth', bound: 180, bin: 36}; 244 | var vals = $scope.results.map(function(result){ return Math.max(result[helperNames.val],0); }); 245 | if ($scope.openFilter === 'sun'){ 246 | vals = vals.filter(function(sun){ return sun > 0;}); 247 | } 248 | var size = getSize($scope.openFilter); 249 | $scope.updateHistogram('.' + helperNames.main + 'Slider', vals, [0, helperNames.bound], helperNames.bin, size); 250 | } 251 | } else { 252 | // Not showing anything because we have too many results, let user know 253 | var msg = 'Looks like we have too much data in our catalogs to show you!

' + 254 | 'Try zooming in or changing the date range to narrow it ' + 255 | 'down a bit.'; 256 | setInfoPane(msg); // Show helpful message 257 | // also clear paths and rowPath selection and selectedResult because why not 258 | $scope.paths = {}; 259 | $scope.rowPathSelect = null; 260 | $scope.selectedResult = null; // should either clear the selection or prevent the message 261 | } 262 | 263 | // stop the spinner 264 | $scope.spinner.stop(); 265 | }).error(function (data, status) { 266 | // need to check for an error because cancelling the request also sends us here 267 | if (status !== 0) { 268 | $scope.results = []; 269 | var msg = 'Oops, looks like we ran into an unknown error with the ' + 270 | 'data service, repositioning the satellite for you.'; 271 | $scope.satellite = true; 272 | $scope.satGif = $sce.trustAsHtml(''); 273 | if (data && data.error && data.error.code) { 274 | if (data.error.code === 'NOT_FOUND') { 275 | msg = 'Oops, looks like you zoomed in too much, try zooming out ' + 276 | 'to get more results.'; 277 | $scope.satellite = false; 278 | } 279 | } 280 | setInfoPane(msg); // Show nice error message 281 | $scope.paths = {}; 282 | $scope.rowPathSelect = null; 283 | $scope.selectedResult = null; // should either clear the selection or prevent the message 284 | // stop the spinner 285 | $scope.spinner.stop(); 286 | } 287 | }); 288 | }; 289 | 290 | /** 291 | * Filter function used by ng-repeat. 292 | * Filters api results locally based on cloudCoverage and sun azimuth. 293 | * 294 | * @param {Object} val 295 | */ 296 | $scope.resultsFilter = function(val) { 297 | return val.cloudCoverFull >= $scope.cloudCoverageMin && 298 | val.cloudCoverFull <= $scope.cloudCoverageMax && 299 | val.sunAzimuth >= $scope.sunAzimuthMin && 300 | val.sunAzimuth <= $scope.sunAzimuthMax; 301 | }; 302 | 303 | /** 304 | * Filter function used by ng-repeat. 305 | * Filters api results locally based on map selections. 306 | * 307 | * @param {Object} val 308 | */ 309 | $scope.mapSelectionFilter = function(val) { 310 | if ($scope.rowPathSelect === null) { 311 | return true; 312 | } 313 | else { 314 | return val.row === $scope.rowPathSelect.row && val.path === $scope.rowPathSelect.path; 315 | } 316 | }; 317 | 318 | 319 | /** 320 | * Sets the sort field and direction. 321 | * 322 | * @param {String} val 323 | */ 324 | $scope.setSortExpression = function(val) { 325 | 326 | if ($scope.sortField === val) { 327 | $scope.toggleSortReverse(); 328 | } 329 | else { 330 | $scope.sortField = val; 331 | switch (val) { 332 | case 'acquisitionDate': 333 | $scope.sortReverse = true; 334 | break; 335 | case 'cloudCoverFull': 336 | $scope.sortReverse = false; 337 | break; 338 | case 'sunAzimuth': 339 | $scope.sortReverse = false; 340 | break; 341 | } 342 | } 343 | }; 344 | 345 | /** 346 | * Sets a result as the selected one. 347 | * 348 | * @param {Object} result 349 | */ 350 | $scope.selectResult = function(result) { 351 | $scope.selectedResult = result; 352 | $scope.drawMarkerOutline($scope.getMarkerName(result)); 353 | $scope.getDownloadSize(result); 354 | }; 355 | 356 | /** 357 | * Resets the selected result to null. 358 | * 359 | */ 360 | $scope.resetSelectedResult = function() { 361 | $scope.selectedResult = null; 362 | $scope.conditionalResetPaths(); 363 | }; 364 | 365 | /** 366 | * Resets the paths to an empty object. 367 | * 368 | */ 369 | $scope.conditionalResetPaths = function() { 370 | if ($scope.rowPathSelect === null && $scope.selectedResult === null) { 371 | $scope.paths = {}; 372 | } 373 | }; 374 | 375 | /** 376 | * Draws an outline around the selected marker 377 | * 378 | * @param {String} markerName 379 | */ 380 | 381 | $scope.drawMarkerOutline = function(markerName) { 382 | // first checks to see if the marker is on the map, then outlines 383 | // how would it not be on the map? 384 | 385 | if ($scope.markers[markerName]) { 386 | // calculate a longitude adjustment object for being off the first earth 387 | // needs to be unique for each corner to cover edge cases 388 | var lonCorners = ['upperLeftCornerLongitude','upperRightCornerLongitude','lowerRightCornerLongitude','lowerLeftCornerLongitude']; 389 | var adjust = {}; 390 | var adjustBound; 391 | for (var i=0; i < lonCorners.length; i++){ 392 | adjustBound = ($scope.markers[markerName][lonCorners[i]] > 0) ? 'southWest' : 'northEast'; 393 | adjust[lonCorners[i]] = Math.floor(($scope.bounds[adjustBound].lng + 180) / 360) * 360; 394 | } 395 | 396 | $scope.paths[markerName] = { 397 | type: 'polygon', 398 | latlngs: [ 399 | { lat: $scope.markers[markerName].upperLeftCornerLatitude, lng: $scope.markers[markerName].upperLeftCornerLongitude + adjust.upperLeftCornerLongitude}, 400 | { lat: $scope.markers[markerName].upperRightCornerLatitude, lng: $scope.markers[markerName].upperRightCornerLongitude + adjust.upperRightCornerLongitude}, 401 | { lat: $scope.markers[markerName].lowerRightCornerLatitude, lng: $scope.markers[markerName].lowerRightCornerLongitude + adjust.lowerRightCornerLongitude}, 402 | { lat: $scope.markers[markerName].lowerLeftCornerLatitude, lng: $scope.markers[markerName].lowerLeftCornerLongitude + adjust.lowerLeftCornerLongitude} 403 | ], 404 | color: '#555', 405 | weight: 1 406 | }; 407 | } 408 | }; 409 | 410 | /** 411 | * Sets the visual filter to display 412 | * 413 | * @param {String} openFilter 414 | */ 415 | 416 | $scope.toggleOpenFilter = function(openFilter) { 417 | $scope.openFilter = ($scope.openFilter !== openFilter) ? openFilter : null; 418 | }; 419 | 420 | $scope.switchOpenFilter = function(){ 421 | var size; 422 | switch ($scope.openFilter){ 423 | case 'date': 424 | $scope.newDateFilter(); 425 | break; 426 | case 'cloud': 427 | var cloudVals = $scope.results.map(function(result){ return Math.max(result.cloudCoverFull,0); }); 428 | size = getSize($scope.openFilter); 429 | $scope.newFilterGraph(['cloudCoverageMin', 'cloudCoverageMax'], [0, 100], '.cloudCoverSlider', size, '%'); 430 | $scope.updateHistogram('.cloudCoverSlider', cloudVals, [0, 100], 20, size); 431 | break; 432 | case 'sun': 433 | var sunVals = $scope.results.map(function(result){ return Math.max(result.sunAzimuth,0); }) 434 | .filter(function(sun){ return sun > 0;}); 435 | size = getSize($scope.openFilter); 436 | $scope.newFilterGraph(['sunAzimuthMin','sunAzimuthMax'], [0, 180], '.sunAzimuthSlider', size, '\xB0'); 437 | $scope.updateHistogram('.sunAzimuthSlider',sunVals, [0, 180], 36, size); 438 | break; 439 | case null: 440 | break; 441 | } 442 | }; 443 | 444 | /** 445 | * If we have a selected row/path and the outline is outside the bounds, erase it and clear the rowPathSelect filter 446 | * 447 | */ 448 | 449 | $scope.cleanPaths = function(){ 450 | if ($scope.rowPathSelect) { 451 | if (!inBounds($scope.rowPathSelect)){ 452 | $scope.paths = {}; 453 | $scope.rowPathSelect = null; 454 | } 455 | } 456 | }; 457 | 458 | /** 459 | * Toggle sortReverse 460 | * 461 | */ 462 | $scope.toggleSortReverse = function() { 463 | $scope.sortReverse = ($scope.sortReverse) ? false : true; 464 | }; 465 | 466 | /// event listeners 467 | 468 | $scope.$on('leafletDirectiveMarker.mouseover', function(event, args){ 469 | if ($scope.rowPathSelect === null && $scope.selectedResult === null) { 470 | $scope.drawMarkerOutline(args.markerName); 471 | } 472 | }); 473 | 474 | $scope.$on('leafletDirectiveMarker.mouseout', function(){ 475 | $scope.conditionalResetPaths(); 476 | }); 477 | 478 | $scope.$on('leafletDirectiveMarker.click', function(event, args){ 479 | $scope.paths = {}; 480 | $scope.selectedResult = null; 481 | $scope.drawMarkerOutline(args.markerName); 482 | $scope.rowPathSelect = $scope.markers[args.markerName]; 483 | }); 484 | 485 | $scope.$on('leafletDirectiveMap.click', function(){ 486 | $scope.rowPathSelect = null; 487 | $scope.conditionalResetPaths(); 488 | }); 489 | 490 | /// d3 and slider logic 491 | $scope.newDateFilter = function() { 492 | var dateFirst = Date.parse('February 11, 2013'), // Landsat 8 launch 493 | dateRange = Date.now() - dateFirst, 494 | start = Date.parse($scope.dateRangeStart), 495 | end = Date.parse($scope.dateRangeEnd); 496 | 497 | jQuery('.date-slider').noUiSlider({ 498 | // Define a range. 499 | range: { 500 | min: dateFirst, 501 | max: dateRange + dateFirst 502 | }, 503 | connect: true, 504 | step: 1, 505 | 506 | // Indicate the handle starting positions. 507 | start: [start, end] 508 | 509 | }); 510 | 511 | jQuery('.date-slider').noUiSlider_pips({ 512 | mode: 'values', 513 | values: [dateFirst,dateFirst + dateRange/4,dateFirst + 2*dateRange/4,dateFirst + 3*dateRange/4,end], 514 | density: 4, 515 | stepped: true 516 | }); 517 | 518 | // link the slider to the display 519 | jQuery('.date-slider').Link('lower').to(jQuery('#lower'), dateFormatAdd); 520 | jQuery('.date-slider').Link('upper').to(jQuery('#upper'), dateFormatAdd); 521 | 522 | // format the pips post-hoc 523 | jQuery('.noUi-value').each(function(){ jQuery(this).html(dateFormat(jQuery(this).html())); }); 524 | 525 | // only update on mouseup (so we don't continuously trigger the API) 526 | jQuery('.noUi-handle').on('mousedown',function(){ 527 | jQuery(window).one('mouseup',function(){ 528 | $scope.dateRangeStart = jQuery('#lower').html(); 529 | $scope.dateRangeEnd = jQuery('#upper').html(); 530 | // irresponsible use of apply 531 | $scope.$apply(); 532 | }); 533 | }); 534 | }; 535 | 536 | $scope.newFilterGraph = function(controls, span, className, size, symbol) { 537 | 538 | d3.select(className + ' svg').remove(); 539 | 540 | var margin = graphParam.margin; 541 | var width = size.width - margin.left - margin.right; 542 | var height = size.height - margin.top - margin.bottom; 543 | 544 | var x = d3.scale.linear() 545 | .domain([span[0], span[1]]) 546 | .range([0, width]); 547 | 548 | brush[className.slice(1,100)] = d3.svg.brush() 549 | .x(x) 550 | .extent([$scope[controls[0]],$scope[controls[1]]]) 551 | .on('brush', brushed); 552 | 553 | var svg = d3.select(className).append('svg') 554 | .attr('width', width + margin.left + margin.right) 555 | .attr('height', height + margin.top + margin.bottom) 556 | .append('g') 557 | .attr('class','main') 558 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 559 | 560 | svg.append('rect') 561 | .attr('class', 'grid-background') 562 | .attr('width', width) 563 | .attr('height', height); 564 | 565 | svg.append('g') 566 | .attr('class', 'x axis') 567 | .attr('transform', 'translate(0,' + height + ')') 568 | .call(d3.svg.axis().scale(x).orient('bottom').tickFormat(function(d) { return d + symbol; })); 569 | 570 | var gBrush = svg.append('g') 571 | .attr('class', 'brush') 572 | .call(brush[className.slice(1,100)]); 573 | 574 | gBrush.selectAll('rect') 575 | .attr('height', height); 576 | 577 | gBrush.selectAll('.resize').append('path').attr('d', resizePath); 578 | 579 | function brushed() { 580 | var extent0 = brush[className.slice(1,100)].extent(), 581 | extent1; 582 | 583 | // if dragging, preserve the width of the extent 584 | if (d3.event.mode === 'move') { 585 | var d0 = round(extent0[0],1), 586 | d1 = Math.round((extent0[1] - extent0[0]) + d0); 587 | extent1 = [d0, d1]; 588 | } 589 | 590 | // otherwise, if resizing, round both numbers 591 | else { 592 | extent1 = extent0.map(function(num){ 593 | return round(num,1); 594 | }); 595 | } 596 | 597 | d3.select(this).call(brush[className.slice(1,100)].extent(extent1)); 598 | svg.selectAll('.bar').classed('brushed', function(d) { 599 | return extent1[0] <= (d.x + d.dx - 1) && (d.x + 1) <= extent1[1]; 600 | }); 601 | // if the updates happen too fast, the slider is less responsive 602 | // purposefully slowing down angular for better UX 603 | setTimeout(function(){ 604 | $scope[controls[0]] = extent1[0]; 605 | $scope[controls[1]] = extent1[1]; 606 | // irresponsible use of apply to force update 607 | $scope.$apply(); 608 | },50); 609 | 610 | } 611 | }; 612 | 613 | $scope.updateHistogram = function(graph, vals, span, bins, size) { 614 | 615 | d3.select(graph + ' svg').selectAll('.bar').remove(); 616 | 617 | var margin = graphParam.margin; 618 | var width = size.width - margin.left - margin.right; 619 | var height = size.height - margin.top - margin.bottom; 620 | 621 | var svg = d3.select(graph + ' .main'); 622 | 623 | var x = d3.scale.linear() 624 | .domain([span[0], span[1]]) 625 | .range([0, width]); 626 | 627 | var data = d3.layout.histogram() 628 | .bins(bins) 629 | .range([span[0], span[1]]) 630 | (vals); 631 | 632 | var y = d3.scale.linear() 633 | .domain([0, d3.max(data, function(d) { return d.y; })]) 634 | .range([height, 0]); 635 | 636 | var bar = svg.selectAll('.bar') 637 | .data(data) 638 | .enter().append('g') 639 | .attr('class', 'bar') 640 | .attr('transform', function(d) { return 'translate(' + x(d.x) + ',' + y(d.y) + ')'; }); 641 | 642 | bar.append('rect') 643 | .attr('x', 1) 644 | .attr('width', x(data[0].dx) - 1) 645 | .attr('height', function(d) { return height - y(d.y); }); 646 | 647 | // delete and redraw brush 648 | svg.select('.brush').remove(); 649 | 650 | var gBrush = svg.append('g') 651 | .attr('class', 'brush') 652 | .call(brush[graph.slice(1,100)]); 653 | 654 | gBrush.selectAll('rect') 655 | .attr('height', height); 656 | 657 | gBrush.selectAll('.resize').append('path').attr('d', resizePath); 658 | 659 | brushed(svg,brush[graph.slice(1,100)].extent()); 660 | 661 | }; 662 | 663 | $scope.openModal = function () { 664 | $scope.openFilter = null; 665 | $scope.modalInstance = $modal.open({ 666 | templateUrl: 'views/modal.html', 667 | controller: 'MainCtrl', 668 | }); 669 | }; 670 | 671 | $scope.openAdvancedDownload = function () { 672 | $scope.modalInstance = $modal.open({ 673 | templateUrl: 'views/advanced.html', 674 | controller: 'AdvancedCtrl', 675 | size: 'lg', 676 | resolve: { 677 | selectedResult: function () { 678 | return $scope.selectedResult; 679 | } 680 | } 681 | }); 682 | }; 683 | 684 | /** 685 | * Makes a Google Storage JSON API request to get the download file size. 686 | * 687 | * @param {Object} result 688 | */ 689 | $scope.getDownloadSize = function(result) { 690 | if (!result.downloadSize) { 691 | var requestURL = 'https://www.googleapis.com/storage/v1/b/earthengine-public/o/landsat%2FL8%2F' + zeroPad(result.path,3) + '%2F' + zeroPad(result.row,3) + '%2F' + result.sceneID + '.tar.bz'; 692 | $http.get(requestURL) 693 | .success( function(data){ 694 | // lazily formatted assuming it's always in the MB range 695 | result.downloadSize = ' (' + Math.round(data.size / 1048576) + ' MB)'; 696 | }) 697 | .error( function(){ 698 | // google doesn't have the scene yet 699 | result.noData = true; 700 | result.downloadSize = ''; 701 | }); 702 | } 703 | }; 704 | 705 | /** 706 | * sends a google analytics click event for download tracking 707 | * 708 | */ 709 | 710 | $scope.downloadBundle = function() { 711 | multiDownload([$scope.selectedResult.downloadURL]); 712 | $scope.downloadTrack(); 713 | }; 714 | 715 | $scope.downloadTrack = function() { 716 | ga('send', 'event', 'download', 'click', $scope.selectedResult.sceneID); 717 | }; 718 | 719 | 720 | //////////////////////////////////////////////////////////// 721 | /////////// Helper functions ///////////////////////// 722 | //////////////////////////////////////////////////////////// 723 | 724 | $scope.getMarkerName = function(result) { 725 | return 'r' + result.row + 'p' + result.path; 726 | }; 727 | 728 | function updateMarkers() { 729 | $scope.results.forEach(function(result){ 730 | if ($scope.resultsFilter(result)){ 731 | var rowPathObj = $scope.markers[$scope.getMarkerName(result)]; 732 | if (rowPathObj){ 733 | rowPathObj.icon.iconSize = [30, 30]; 734 | if (rowPathObj.icon.html === '') { 735 | rowPathObj.icon.html = 2; 736 | } 737 | else { 738 | rowPathObj.icon.html++; 739 | } 740 | } 741 | else { 742 | $scope.markers[$scope.getMarkerName(result)] = result; 743 | $scope.markers[$scope.getMarkerName(result)].icon.html = ''; 744 | $scope.markers[$scope.getMarkerName(result)].icon.iconSize = [20, 20]; 745 | } 746 | } 747 | }); 748 | } 749 | 750 | function queryConstructor (options) { 751 | var query = {}; 752 | 753 | // dateRange -- array of date strings. format: [YYYY-MM-DD,YYYY-MM-DD] 754 | var dateRange = options.dateRange || ['2014-01-01', '2015-01-05']; 755 | query.date_from = dateRange[0]; 756 | query.date_to = dateRange[1]; 757 | 758 | // sceneCenterLatRange -- array of floats specifying the scene centroid latitude. e.g. [4.3, 78.9] 759 | var sceneCenterLatRange = options.sceneCenterLatRange.sort(sortNumber) || ['-90', '90']; 760 | // query.push(arrayHelper(sceneCenterLatRange,'sceneCenterLatitude')); 761 | 762 | // sceneCenterLonRange -- array of floats specifying the scene centroid longitude. e.g. [4.3, 78.9] 763 | // also uses options.continuous to decide if we need two separate ranges to wrap around the 180th meridian 764 | if (options.continuous) { 765 | var sceneCenterLonRange = options.sceneCenterLonRange.sort(sortNumber) || ['-180', '180']; 766 | } else { 767 | var range1 = [-180,options.sceneCenterLonRange.sort(sortNumber)[0]]; 768 | var range2 = [options.sceneCenterLonRange.sort(sortNumber)[1],180]; 769 | } 770 | 771 | // TODO handle wraparound case 772 | var geojson = '{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[' + sceneCenterLonRange[0] + ',' + sceneCenterLatRange[1] + '],[' + sceneCenterLonRange[0] + ',' + sceneCenterLatRange[0] + '],[' + sceneCenterLonRange[1] + ',' + sceneCenterLatRange[0] + '],[' + sceneCenterLonRange[1] + ',' + sceneCenterLatRange[1] + '],[' + sceneCenterLonRange[0] + ',' + sceneCenterLatRange[1] + ']]]},"properties":{}}'; 773 | query.intersects = geojson; 774 | 775 | // limit -- integer specifying the maximum results return. 776 | if (options.limit) { 777 | query.limit = options.limit; 778 | } 779 | 780 | // skip: integer specifying the number of results to skip 781 | if (options.skip) { 782 | query.skip = options.skip; 783 | } 784 | 785 | query.summary = 'true'; 786 | 787 | return query; 788 | } 789 | 790 | function arrayHelper(range,field) { 791 | return field + ':[' + range[0] + '+TO+' + range[1] + ']'; 792 | } 793 | 794 | function sortNumber(a,b) { 795 | return a - b; 796 | } 797 | 798 | function zeroPad(n,c) { 799 | var s = String(n); 800 | if (s.length < c) { 801 | return zeroPad('0' + n,c); 802 | } 803 | else { 804 | return s; 805 | } 806 | } 807 | 808 | function round(num, rounder){ 809 | return rounder * Math.round(num / rounder); 810 | } 811 | 812 | function brushed(svg,extent){ 813 | svg.selectAll('.bar').classed('brushed', function(d) { 814 | return extent[0] <= (d.x + d.dx - 1) && (d.x + 1) <= extent[1]; 815 | }); 816 | } 817 | 818 | function dateFormat(date){ 819 | return $filter('date')(Number(date), 'yyyy-MM'); 820 | } 821 | 822 | function dateFormatAdd(date){ 823 | jQuery(this).html($filter('date')(Number(date), 'yyyy-MM-dd')); 824 | } 825 | 826 | function getSize(className) { 827 | var size = {}; 828 | size.width = Number(d3.select('.' + className + '-container').style('width').slice(0,-2)); 829 | size.height = Number(d3.select('.' + className + '-container').style('height').slice(0,-2)); 830 | return size; 831 | } 832 | 833 | function resizePath(d) { 834 | var e = +(d === 'e'), 835 | x = e ? 1 : -1; 836 | // hardcoding for now 837 | var height = 80; 838 | var y = height / 3; 839 | return 'M' + (0.5 * x) + ',' + y + 840 | 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) + 841 | 'V' + (2 * y - 6) + 842 | 'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y) + 843 | 'Z' + 844 | 'M' + (2.5 * x) + ',' + (y + 8) + 845 | 'V' + (2 * y - 8) + 846 | 'M' + (4.5 * x) + ',' + (y + 8) + 847 | 'V' + (2 * y - 8); 848 | } 849 | 850 | function inBounds(result) { 851 | return result.sceneCenterLatitude <= $scope.bounds.northEast.lat % 360 && 852 | result.sceneCenterLatitude >= $scope.bounds.southWest.lat % 360 && 853 | result.sceneCenterLongitude >= $scope.bounds.southWest.lng % 360 && 854 | result.sceneCenterLongitude <= $scope.bounds.northEast.lng % 360; 855 | } 856 | 857 | function mod(number, dividend) { 858 | return ((number % dividend) + dividend) % dividend; 859 | } 860 | 861 | }]); 862 | -------------------------------------------------------------------------------- /app/scripts/filters/suffix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('dauriaSearchApp').filter('suffix', [ function () { 4 | return function (input, choice) { 5 | var suffix; 6 | switch (choice) { 7 | case 'per': 8 | suffix = '%'; 9 | break; 10 | case 'deg': 11 | suffix = '\xB0'; 12 | break; 13 | } 14 | return input + suffix; 15 | }; 16 | }]); 17 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/"; 2 | // bower:scss 3 | @import "bootstrap-sass-official/assets/stylesheets/_bootstrap.scss"; 4 | // endbower 5 | 6 | .browsehappy { 7 | margin: 0.2em 0; 8 | background: #ccc; 9 | color: #000; 10 | padding: 0.2em 0; 11 | } 12 | 13 | html, body { 14 | height: 100%; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | 19 | body { 20 | font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; 21 | font-size: 15px; 22 | } 23 | 24 | /* Everything but the jumbotron gets side spacing for mobile first views */ 25 | .header, 26 | .marketing, 27 | .footer { 28 | padding-left: 15px; 29 | padding-right: 15px; 30 | } 31 | 32 | /* Custom page header */ 33 | .header { 34 | border-bottom: 1px solid #e5e5e5; 35 | background-color: rgba(35, 24, 80, 0.8); 36 | height: 41px; 37 | color: white; 38 | 39 | /* Make the masthead heading the same height as the navigation */ 40 | h3 { 41 | margin-top: 0; 42 | margin-bottom: 0; 43 | padding-left: 10px; 44 | line-height: 40px; 45 | text-transform: uppercase; 46 | font-weight: 700; 47 | letter-spacing: 0.2em; 48 | } 49 | } 50 | 51 | /* Custom page footer */ 52 | .footer { 53 | color: #777; 54 | border-top: 1px solid #e5e5e5; 55 | } 56 | 57 | .container-narrow > hr { 58 | margin: 30px 0; 59 | } 60 | 61 | .filters { 62 | position: absolute; 63 | top: 11px; 64 | height: auto; 65 | width: auto; 66 | background: white; 67 | color: white; 68 | margin: 0 20px 0 50px; 69 | z-index: 1000; 70 | user-select: none; 71 | } 72 | 73 | .sort { 74 | height: 50px; 75 | padding: 5px 5px 5px 5px; 76 | text-align: center; 77 | border-left: 1px solid #555; 78 | background: rgba(35,24,80,0.8); 79 | img { 80 | width: 24px; 81 | height: 24px; 82 | } 83 | 84 | .btn { 85 | margin: 0 5px 0 5px; 86 | padding-left: 27px; 87 | text-align: left; 88 | width: 80px; 89 | } 90 | } 91 | 92 | .sort-icon { 93 | margin-right: 15px; 94 | cursor: pointer; 95 | } 96 | 97 | .results-pane { 98 | position: absolute; 99 | top: 0px; 100 | bottom: 0px; 101 | height: auto ; 102 | width: 100%; 103 | font-size: 12px; 104 | } 105 | 106 | .results-pane .scroll { 107 | position: absolute; 108 | top: 50px; 109 | bottom: 0px; 110 | height: auto; 111 | width: 100%; 112 | overflow: auto; 113 | border-left: 1px solid #555; 114 | } 115 | 116 | .results-pane .result { 117 | height: auto; 118 | width: 100%; 119 | position: relative; 120 | } 121 | 122 | .results-pane .result:before{ 123 | content: ""; 124 | display: block; 125 | padding-top: 100%; 126 | } 127 | 128 | .result-content { 129 | position: absolute; 130 | top: 0; 131 | left: 0; 132 | bottom: 0; 133 | right: 0; 134 | background: #000; 135 | } 136 | 137 | .results-pane .info-pane { 138 | font-size: 16px; 139 | font-weight: 300; 140 | font-family: Lato, sans-serif; 141 | letter-spacing: 0.05em; 142 | text-align: center; 143 | padding: 20px; 144 | 145 | } 146 | 147 | .result-overlay { 148 | position: absolute; 149 | top: 5px; 150 | right: 5px; 151 | color: white; 152 | width: 110px; 153 | font-weight: 100; 154 | letter-spacing: 0.1em; 155 | 156 | span { 157 | padding-right: 0.3em; 158 | } 159 | 160 | div { 161 | float: right; 162 | margin-bottom: 1px; 163 | } 164 | } 165 | 166 | .table-icon { 167 | width: 18px; 168 | height: 18px; 169 | margin-top: -3px; 170 | } 171 | 172 | .thumb { 173 | width: 100%; 174 | height: 100%; 175 | } 176 | 177 | .thumb:hover { 178 | opacity: 0.7; 179 | cursor: pointer; 180 | } 181 | 182 | 183 | .no-padding { 184 | padding: 0 !important; 185 | margin: 0 !important; 186 | } 187 | 188 | .single-result-container { 189 | position: absolute; 190 | top: 0; 191 | bottom: 0; 192 | height: auto; 193 | width: 100%; 194 | overflow: auto; 195 | } 196 | 197 | .top-bar { 198 | height: 50px; 199 | font-size: 16px; 200 | font-weight: 100; 201 | letter-spacing: 0.1em; 202 | line-height: 50px; 203 | padding: 0 10px 0 10px; 204 | color: white; 205 | background: rgba(35,24,80,0.8); 206 | 207 | img { 208 | width: 30px; 209 | height: 30px; 210 | margin: 10px 15px 0 0; 211 | cursor: pointer; 212 | } 213 | } 214 | 215 | .single-result { 216 | 217 | font-weight: 300; 218 | letter-spacing: 0.1em; 219 | text-align: center; 220 | 221 | .back { 222 | display: inline-block; 223 | } 224 | 225 | .result-data { 226 | width: 100%; 227 | font-size: 16px; 228 | 229 | margin: 15px 0 15px 0; 230 | 231 | img { 232 | width: 22px; 233 | height: 22px; 234 | margin-top: -3px; 235 | margin-right: 2px; 236 | } 237 | } 238 | 239 | .btn { 240 | font-weight: 100; 241 | font-size: 15px; 242 | background: rgba(35,24,80,0.8); 243 | border-color: rgb(79,70,115); 244 | 245 | &:hover { 246 | border-color: #231850; 247 | } 248 | } 249 | 250 | .download-info { 251 | font-size: 14px; 252 | margin-top: 30px; 253 | padding: 10px; 254 | } 255 | 256 | } 257 | 258 | .right-side { 259 | padding: 0; 260 | position: absolute; 261 | top: 0px; 262 | bottom: 0px; 263 | right: 0px; 264 | height: auto; 265 | background: white; 266 | } 267 | 268 | .left-side { 269 | padding: 0; 270 | position: absolute; 271 | top: 0px; 272 | bottom: 0px; 273 | left: 0px; 274 | height: auto; 275 | } 276 | 277 | .map { 278 | position: absolute; 279 | top: 0px; 280 | bottom: 0; 281 | width: 100%; 282 | border-top: 1px solid #555; 283 | } 284 | 285 | .date-container { 286 | 287 | color: #aaa; 288 | 289 | .date-slider-container { 290 | width: 84%; 291 | padding: 30px 0 30px 0; 292 | margin: 0 8% 0 8%; 293 | } 294 | } 295 | 296 | .filter-container { 297 | position: absolute; 298 | top: 50px; 299 | width: 100%; 300 | background: white; 301 | height: 110px; 302 | box-shadow: 0px 1px 4px rgba(0,0,0,0.65) 303 | } 304 | 305 | .main-filter-display { 306 | font-size: 16px; 307 | letter-spacing: 0.15em; 308 | font-weight: lighter; 309 | text-align: left; 310 | height: 50px; 311 | width: auto; 312 | box-shadow: 0px 1px 4px rgba(0,0,0,0.65); 313 | 314 | img { 315 | width: 40px; 316 | height: 40px; 317 | } 318 | } 319 | 320 | .filter-display { 321 | display: inline-block; 322 | text-align: left; 323 | height: 100%; 324 | line-height: 50px; 325 | padding: 0 10px 0; 326 | cursor: pointer; 327 | background: rgba(35,24,80,0.8); 328 | border-left: 1px solid #fff; 329 | vertical-align: top; 330 | 331 | img { 332 | margin-top: -3px; 333 | } 334 | 335 | &:hover { 336 | background: #231850; 337 | } 338 | } 339 | 340 | .filter-display.active { 341 | background: #231850; 342 | } 343 | 344 | .date-filter-display { 345 | min-width: 308px; 346 | } 347 | 348 | .cloud-filter-display, .sun-filter-display { 349 | min-width: 190px; 350 | } 351 | 352 | .info { 353 | min-width: 52px; 354 | padding: 0 7px 0 5px; 355 | } 356 | 357 | .map-icon { 358 | color: white; 359 | border-radius: 50%; 360 | background: rgba(35, 24, 80, 0.8); 361 | line-height: 30px; 362 | font-size: 16px; 363 | font-weight: 100; 364 | text-align: center; 365 | } 366 | 367 | .noUi-connect { 368 | background: #231850; 369 | } 370 | 371 | .noUi-value { 372 | white-space: nowrap; 373 | font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 374 | font-weight: 100 !important; 375 | color: #555; 376 | } 377 | 378 | .btn { 379 | border-radius: 0; 380 | border-color: #fff; 381 | background-color: rgba(0,0,0,0); 382 | 383 | &:hover { 384 | border-color: #fff; 385 | background-color: #231850; 386 | } 387 | } 388 | 389 | .btn-default.active { 390 | border-color: #fff; 391 | background-color: #231850; 392 | color: #fff; 393 | } 394 | 395 | // custom leaflet zoom 396 | 397 | .leaflet-control-zoom { 398 | margin-left: 24px !important; 399 | background: white !important; 400 | border-radius: 0px !important; 401 | 402 | .leaflet-control-zoom-in { 403 | border-radius: 0px !important; 404 | border: 0 !important; 405 | border-bottom: 1px solid #fff !important; 406 | height: 25px !important; 407 | background: rgba(35,24,80,0.8) !important; 408 | color: white !important; 409 | 410 | &:hover { 411 | background: #231850 !important; 412 | } 413 | } 414 | 415 | .leaflet-control-zoom-out { 416 | border-radius: 0px !important; 417 | border: 0 !important; 418 | height: 25px !important; 419 | background: rgba(35,24,80,0.8) !important; 420 | color: white !important; 421 | 422 | &:hover { 423 | background: #231850 !important; 424 | } 425 | } 426 | } 427 | 428 | .leaflet-control-attribution { 429 | font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 430 | font-weight: 100 !important; 431 | letter-spacing: 0.1em; 432 | } 433 | 434 | @-webkit-keyframes oscillation 435 | { 436 | from 437 | { 438 | -webkit-transform:rotate(-45deg); 439 | } 440 | to 441 | { 442 | -webkit-transform:rotate(45deg); 443 | } 444 | } 445 | 446 | .sat-gif { 447 | text-align: center; 448 | 449 | img { 450 | width: 72px; 451 | height: 72px; 452 | -webkit-animation: oscillation 5s ease-in-out infinite alternate; 453 | background-color: rgba(0,0,0,0); 454 | } 455 | } 456 | 457 | .spin-box { 458 | position: absolute; 459 | top: 0; 460 | right: 0; 461 | height: 80px; 462 | width: 80px; 463 | } 464 | 465 | 466 | .modal-dialog { 467 | margin-top: 70px; 468 | font-weight: 300; 469 | font-size: 18px; 470 | font-family: Lato, sans-serif; 471 | } 472 | 473 | .modal-header { 474 | text-align: center; 475 | background-color: rgba(35, 24, 80, 0.8); 476 | color: white; 477 | 478 | h3 { 479 | letter-spacing: 0.4em; 480 | font-weight: 100; 481 | } 482 | } 483 | 484 | .modal-body { 485 | padding: 20px; 486 | 487 | p { 488 | margin-bottom: 15px; 489 | } 490 | 491 | a { 492 | text-decoration: none; 493 | } 494 | } 495 | 496 | .logos { 497 | text-align: center; 498 | 499 | img { 500 | height: 40px; 501 | width: auto; 502 | margin: 10px 0 10px; 503 | padding: 0px 20px 0; 504 | } 505 | } 506 | 507 | // svg styling 508 | 509 | .bar { 510 | fill: #777; 511 | } 512 | 513 | .bar.brushed { 514 | fill: #231850; 515 | } 516 | 517 | .axis text { 518 | font-size: 11px; 519 | font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; 520 | font-weight: 100; 521 | fill: #555; 522 | letter-spacing: 0.2em; 523 | } 524 | 525 | .axis path { 526 | display: none; 527 | } 528 | 529 | .axis line { 530 | fill: none; 531 | stroke: #000; 532 | shape-rendering: crispEdges; 533 | } 534 | 535 | .grid-background { 536 | fill: #ddd; 537 | } 538 | 539 | .grid line, 540 | .grid path { 541 | fill: none; 542 | stroke: #fff; 543 | shape-rendering: crispEdges; 544 | } 545 | 546 | .brush .extent { 547 | stroke: #000; 548 | fill-opacity: .125; 549 | shape-rendering: crispEdges; 550 | } 551 | 552 | .brush .resize path { 553 | fill: #eee; 554 | stroke: #666; 555 | } 556 | 557 | /* Responsive: Portrait tablets and up */ 558 | @media screen and (min-width: 768px) { 559 | .container { 560 | max-width: 730px; 561 | } 562 | 563 | /* Remove the padding we set earlier */ 564 | .header, 565 | .marketing, 566 | .footer { 567 | padding-left: 0; 568 | padding-right: 0; 569 | } 570 | 571 | } 572 | 573 | .download-prefills{ 574 | ul { 575 | list-style-type: none; 576 | margin: 0; 577 | padding: 0; 578 | text-align: center; 579 | font-size: 14px; 580 | } 581 | li { 582 | display: inline-block; 583 | height: 88px; 584 | margin: 5px 20px; 585 | cursor: pointer; 586 | border: 1px solid rgba(35,24,80,0.8); 587 | &:hover { 588 | img { 589 | opacity: 0.8; 590 | } 591 | } 592 | div { 593 | display: inline-block; 594 | background: black; 595 | width: 120px; 596 | vertical-align: middle; 597 | } 598 | div:first-of-type { 599 | padding: 5px; 600 | background: white; 601 | } 602 | img { 603 | width: 120px; 604 | height: 86px; 605 | } 606 | } 607 | } 608 | .band-selection { 609 | label { 610 | display: inline-block; 611 | font-weight: 100; 612 | margin: 5px 10px; 613 | color: white; 614 | letter-spacing: 0.1em; 615 | height: 26px; 616 | line-height: 26px; 617 | vertical-align: bottom; 618 | } 619 | label:before { 620 | border: 1px solid white; 621 | content: ''; 622 | display: inline-block; 623 | width: 20px; 624 | height: 20px; 625 | margin-right: 8px; 626 | vertical-align: middle; 627 | line-height: normal; 628 | } 629 | label:hover::before { 630 | background: rgba(255,255,255,0.2); 631 | } 632 | input { 633 | display: none; 634 | } 635 | input[type=checkbox]:checked + label:before { 636 | background: rgba(255,255,255,0.6); 637 | } 638 | } 639 | 640 | .download-individual { 641 | text-align: center; 642 | .btn { 643 | font-weight: 100; 644 | font-size: 18px; 645 | letter-spacing: 0.1em; 646 | background: rgba(35,24,80,0.8); 647 | border: 0; 648 | 649 | &:hover { 650 | background: #231850; 651 | } 652 | } 653 | } 654 | 655 | .band-options { 656 | div { 657 | background: rgba(35,24,80,0.8); 658 | margin-bottom: 5px; 659 | } 660 | } 661 | 662 | .advanced-modal { 663 | font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif; 664 | font-weight: 100; 665 | p { 666 | font-weight: 300; 667 | font-family: Lato, sans-serif; 668 | } 669 | hr { 670 | margin-top: 10px; 671 | margin-bottom: 10px; 672 | } 673 | } 674 | -------------------------------------------------------------------------------- /app/views/advanced.html: -------------------------------------------------------------------------------- 1 | 4 | 37 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/modal.html: -------------------------------------------------------------------------------- 1 | 4 | 14 | -------------------------------------------------------------------------------- /attributions.md: -------------------------------------------------------------------------------- 1 | ### tracking attributions for images 2 | 3 | #### noun project 4 | 5 | - Calendar (noun_2019_cc.png): Laurent Patain 6 | - Sun (noun_88350_cc.png) and Clouds (noun_88351_cc): Arthur Shlain 7 | - Info (noun_52713_cc.png): NAS 8 | - Satellite (noun_69444_cc.svg): iconsmind.com 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dauria-search", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "1.3.10", 6 | "json3": "^3.3.0", 7 | "es5-shim": "^4.0.0", 8 | "bootstrap-sass-official": "^3.2.0", 9 | "angular-animate": "^1.3.0", 10 | "angular-cookies": "^1.3.0", 11 | "angular-resource": "^1.3.0", 12 | "angular-route": "^1.3.0", 13 | "angular-sanitize": "^1.3.0", 14 | "angular-touch": "^1.3.0", 15 | "angular-leaflet-directive": "0.7.10", 16 | "d3": "^3.5.3", 17 | "leaflet": "^0.7.3", 18 | "nouislider": "^7.0.10", 19 | "jquery": "~2.1", 20 | "spin.js": "~2.0.2", 21 | "angular-bootstrap": "~0.12.0", 22 | "multi-download": "~1.0.0", 23 | "moment": "~2.10.6" 24 | }, 25 | "devDependencies": { 26 | "jasmine-jquery": "^2.0.5", 27 | "angular-mocks": "~1.3.0", 28 | "angular-scenario": "~1.3.0" 29 | }, 30 | "appPath": "app", 31 | "resolutions": { 32 | "angular": "1.3.10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dauriasearch", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "^0.4.1", 7 | "grunt-autoprefixer": "^0.7.3", 8 | "grunt-concurrent": "^0.5.0", 9 | "grunt-contrib-clean": "^0.5.0", 10 | "grunt-contrib-compass": "^0.7.2", 11 | "grunt-contrib-concat": "^0.4.0", 12 | "grunt-contrib-connect": "^0.7.1", 13 | "grunt-contrib-copy": "^0.5.0", 14 | "grunt-contrib-cssmin": "^0.9.0", 15 | "grunt-contrib-htmlmin": "^0.3.0", 16 | "grunt-contrib-imagemin": "^0.8.1", 17 | "grunt-contrib-jshint": "^0.10.0", 18 | "grunt-contrib-uglify": "^0.4.0", 19 | "grunt-contrib-watch": "^0.6.1", 20 | "grunt-filerev": "^0.2.1", 21 | "grunt-google-cdn": "^0.4.0", 22 | "grunt-karma": "^0.9.0", 23 | "grunt-newer": "^0.7.0", 24 | "grunt-ng-annotate": "^1.0", 25 | "grunt-svgmin": "^0.4.0", 26 | "grunt-usemin": "^2.1.1", 27 | "grunt-wiredep": "^1.7.0", 28 | "jasmine-core": "^2.1.3", 29 | "jshint-stylish": "^0.2.0", 30 | "karma": "^0.12.31", 31 | "karma-jasmine": "^0.3.4", 32 | "karma-nyan-reporter": "0.0.50", 33 | "karma-phantomjs-launcher": "^0.1.4", 34 | "load-grunt-tasks": "^0.4.0", 35 | "time-grunt": "^0.3.1" 36 | }, 37 | "engines": { 38 | "node": ">=0.10.0" 39 | }, 40 | "scripts": { 41 | "test": "grunt test", 42 | "serve": "grunt serve" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2015-01-05 using 4 | // generator-karma 0.8.3 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | 'bower_components/d3/d3.min.js', 22 | 'bower_components/angular/angular.js', 23 | 'bower_components/angular-mocks/angular-mocks.js', 24 | 'bower_components/angular-animate/angular-animate.js', 25 | 'bower_components/angular-cookies/angular-cookies.js', 26 | 'bower_components/angular-resource/angular-resource.js', 27 | 'bower_components/angular-route/angular-route.js', 28 | 'bower_components/angular-sanitize/angular-sanitize.js', 29 | 'bower_components/angular-touch/angular-touch.js', 30 | 'bower_components/jquery/dist/jquery.js', 31 | 'bower_components/jasmine-jquery/lib/jasmine-jquery.js', 32 | 'bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js', 33 | 'bower_components/spin.js/spin.js', 34 | 'app/scripts/**/*.js', 35 | 'test/spec/**/*.js', 36 | 37 | // fixtures 38 | {pattern: 'test/mock/*.json', watched: true, served: true, included: false} 39 | ], 40 | 41 | // list of files / patterns to exclude 42 | exclude: [], 43 | 44 | // web server port 45 | port: 8080, 46 | 47 | // Start these browsers, currently available: 48 | // - Chrome 49 | // - ChromeCanary 50 | // - Firefox 51 | // - Opera 52 | // - Safari (only Mac) 53 | // - PhantomJS 54 | // - IE (only Windows) 55 | browsers: [ 56 | 'PhantomJS' 57 | ], 58 | 59 | // Which plugins to enable 60 | plugins: [ 61 | 'karma-phantomjs-launcher', 62 | 'karma-jasmine', 63 | 'karma-nyan-reporter' 64 | ], 65 | 66 | // Continuous Integration mode 67 | // if true, it capture browsers, run tests and exit 68 | singleRun: false, 69 | 70 | colors: true, 71 | 72 | reporters: [ 73 | 'nyan' 74 | ], 75 | 76 | // level of logging 77 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 78 | logLevel: config.LOG_INFO, 79 | 80 | // Uncomment the following lines if you are using grunt's server to run the tests 81 | // proxies: { 82 | // '/': 'http://localhost:9000/' 83 | // }, 84 | // URL root prevent conflicts with the site root 85 | // urlRoot: '_karma_' 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | /* global getJSONFixture */ 2 | 'use strict'; 3 | 4 | describe('Controller: MainCtrl', function () { 5 | 6 | // load the controller's module 7 | beforeEach(module('dauriaSearchApp')); 8 | 9 | var MainCtrl; 10 | var scope; 11 | var httpBackend; 12 | var userLocationHandler; 13 | var landsatAPIHandler; 14 | 15 | // Check if it's a call to our Landsat API 16 | var isAPIURL = function (url) { 17 | return (url.slice(0,36) === 'http://api.developmentseed.com:8000/'); 18 | }; 19 | 20 | // Initialize the controller and a mock scope 21 | beforeEach(inject(function ($controller, $rootScope, _$httpBackend_) { 22 | scope = $rootScope.$new(); 23 | httpBackend = _$httpBackend_; 24 | MainCtrl = $controller('MainCtrl', { 25 | $scope: scope 26 | }); 27 | 28 | // Set location of base fixtures location 29 | jasmine.getJSONFixtures().fixturesPath='base/test/mock'; 30 | 31 | // Mock for user location call 32 | userLocationHandler = httpBackend.whenGET('https://vast-coast-1838.herokuapp.com/location'). 33 | respond([{ 34 | location: { 35 | lat: 40.23, 36 | lon: 87.23 37 | } 38 | }]); 39 | 40 | // Mock for Landsat API call 41 | landsatAPIHandler = httpBackend.whenGET(isAPIURL). 42 | respond(getJSONFixture('landsatAPI.json')); 43 | })); 44 | 45 | afterEach(function() { 46 | httpBackend.verifyNoOutstandingExpectation(); 47 | httpBackend.verifyNoOutstandingRequest(); 48 | }); 49 | 50 | it('should start with initial defaults', function () { 51 | expect(scope.results.length).toEqual(0); 52 | expect(scope.cloudCoverageMin).toEqual(0); 53 | expect(scope.cloudCoverageMax).toEqual(20); 54 | expect(scope.sunAzimuthMin).toEqual(0); 55 | expect(scope.sunAzimuthMax).toEqual(180); 56 | expect(scope.sortField).toEqual('acquisitionDate'); 57 | httpBackend.flush(); 58 | }); 59 | 60 | it('should make a call to get user location', function () { 61 | httpBackend.expectGET('https://vast-coast-1838.herokuapp.com/location'); 62 | httpBackend.flush(); 63 | }); 64 | 65 | it('should make a call to api server', function () { 66 | httpBackend.expectGET(isAPIURL); 67 | httpBackend.flush(); 68 | }); 69 | 70 | 71 | }); 72 | --------------------------------------------------------------------------------