├── .yo-rc.json ├── app ├── shared │ ├── math.js │ ├── main.html │ ├── factories │ │ ├── selectedMediaFactory.js │ │ ├── mediaFactory.js │ │ ├── labeledMediaFactory.js │ │ └── shipsFactory.js │ ├── navigation │ │ ├── navigationDirective.js │ │ └── navigationView.html │ ├── selectableMediaDisplay │ │ ├── selectableMediaDisplayView.html │ │ └── selectableMediaDisplayDirective.js │ ├── angular.timeSlider.js │ └── imageClustererFactory.js ├── .buildignore ├── components │ ├── ngalogo │ │ ├── ngaView.html │ │ └── ngalogoDirective.js │ ├── threeDimMediaClusterer │ │ ├── threeDimMediaClustererView.html │ │ ├── threeDimMediaClustererController.js │ │ ├── threeDimMediaClustererInnerView.html │ │ └── threeDimMediaClustererDirective.js │ ├── mediaGeo │ │ ├── mediaGeoView.html │ │ └── mediaGeoController.js │ ├── labeledMediaGeo │ │ ├── labeledMediaGeoView.html │ │ └── labeledMediaGeoController.js │ └── shiptracker │ │ ├── shiptrackerView.html │ │ └── shipTrackerController.js ├── robots.txt ├── favicon.ico ├── index.html ├── 404.html ├── app.js └── .htaccess ├── .gitattributes ├── .bowerrc ├── assets ├── data │ ├── media.csv │ └── ships.json ├── images │ ├── disc.png │ ├── seal.png │ ├── seal.psd │ ├── ship.png │ ├── shipred.png │ ├── shipgreen.png │ └── example_media │ │ └── media.csv ├── css │ ├── _variables.scss │ ├── main.scss │ ├── _geo.scss │ ├── _site.scss │ ├── _threeDimMediaClusterer.scss │ ├── _navigation.scss │ ├── _selectedMediaDisplay.scss │ ├── _timeSlider.scss │ └── _component.scss └── js │ ├── Detector.js │ └── ThreeOrbitControls.js ├── docs ├── geo-view.png └── 3d-cluster-view.png ├── .jshintrc ├── .gitignore ├── .editorconfig ├── test ├── .jshintrc └── spec │ └── controllers │ └── main.js ├── bower.json ├── package.json ├── NOTICE.txt ├── README.md ├── LICENSE.txt └── Gruntfile.js /.yo-rc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/shared/math.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/components/ngalogo/ngaView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/shared/main.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /assets/data/media.csv: -------------------------------------------------------------------------------- 1 | ex1.jpg,6.50433,-0.53777,5.04677 2 | ex2.jpg,7.50433,-1.53777,5.94677 -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /docs/geo-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/docs/geo-view.png -------------------------------------------------------------------------------- /assets/images/disc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/disc.png -------------------------------------------------------------------------------- /assets/images/seal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/seal.png -------------------------------------------------------------------------------- /assets/images/seal.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/seal.psd -------------------------------------------------------------------------------- /assets/images/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/ship.png -------------------------------------------------------------------------------- /assets/images/shipred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/shipred.png -------------------------------------------------------------------------------- /docs/3d-cluster-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/docs/3d-cluster-view.png -------------------------------------------------------------------------------- /assets/images/shipgreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngageoint/social-media-picture-explorer-ui/HEAD/assets/images/shipgreen.png -------------------------------------------------------------------------------- /assets/images/example_media/media.csv: -------------------------------------------------------------------------------- 1 | ex1.jpg,43.2106,-75.6459,"Lorem ipsum dolor sit amet, vitae legimus mnesarchum duo ad, eum aeterno elaboraret ea." -------------------------------------------------------------------------------- /app/components/threeDimMediaClusterer/threeDimMediaClustererView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/mediaGeo/mediaGeoView.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/data/ships.json: -------------------------------------------------------------------------------- 1 | [ 2 | [230625000, 1438191702152, "Under way using engine", 71.4367167, 72.5069383, "Example Ship", "Bulk Carrier", "Country", 9590802.0, 2011.0, 33958.0, 197.0, 33.0, null, 56348.0, "Cargo", "TestData", "TestData"] 3 | ] -------------------------------------------------------------------------------- /assets/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/"; 2 | $primary-color: #252235; 3 | $secondary-color: #635d6d; 4 | $tertiary-color: #46bdd2; 5 | $complement-color: #1d99af; 6 | $primary-font: "Roboto Condensed"; 7 | -------------------------------------------------------------------------------- /assets/css/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables.scss"; 2 | // bower:scss 3 | @import "components-font-awesome/scss/font-awesome.scss"; 4 | // endbower 5 | @import "site.scss"; 6 | @import "navigation.scss"; 7 | @import "threeDimMediaClusterer.scss"; 8 | @import "geo.scss"; 9 | @import "component.scss"; 10 | @import "selectedMediaDisplay.scss"; 11 | @import "timeSlider.scss"; 12 | -------------------------------------------------------------------------------- /app/components/threeDimMediaClusterer/threeDimMediaClustererController.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .controller('threeDimMediaClustererCtrl', ['media', threeDimMediaClustererCtrl]); 8 | 9 | function threeDimMediaClustererCtrl($media) { 10 | var vm = this; 11 | vm.media = $media; 12 | } 13 | })(); -------------------------------------------------------------------------------- /app/shared/factories/selectedMediaFactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | selectedImages provides a way to access the i 3 | */ 4 | (function() { 5 | 6 | 'use strict'; 7 | 8 | angular 9 | .module('socialMediaExplorerApp') 10 | .factory('selectedMediaFactory', [selectedMediaFactory]); 11 | 12 | function selectedMediaFactory() { 13 | return { 14 | media: [], 15 | selected: {} 16 | }; 17 | } 18 | })(); -------------------------------------------------------------------------------- /.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 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | *.png 7 | *.jpg 8 | assets/images/cargo_helicopter/ 9 | assets/images/drilling_platform/ 10 | assets/images/military_uniform/ 11 | assets/images/ship/ 12 | assets/images/tank/ 13 | !assets/images/*.png 14 | !docs/*.png 15 | assets/data/* 16 | !assets/data/media.csv 17 | !assets/data/ships.json 18 | !assets/images/example_media/* 19 | !assets/images/example_media/*.jpg 20 | !assets/images/example_media/*.csv 21 | 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /assets/css/_geo.scss: -------------------------------------------------------------------------------- 1 | .leaflet-marker-icon, 2 | .leaflet-marker-shadow { 3 | display: block; 4 | 5 | border: 3px solid #444; 6 | border-radius: 1px; 7 | } 8 | .labelled-media-file { 9 | width: auto; 10 | 11 | cursor: hand; 12 | 13 | border-radius: 3px; 14 | background-color: #999; 15 | } 16 | 17 | #ship-tracker .leaflet-marker-icon, 18 | .leaflet-marker-shadow { 19 | display: block; 20 | 21 | border-width: 0; 22 | border-radius: 1px; 23 | } 24 | 25 | .angular-leaflet-map { 26 | color: black; 27 | } 28 | -------------------------------------------------------------------------------- /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 | "jasmine": true, 22 | "globals": { 23 | "angular": false, 24 | "browser": false, 25 | "inject": false 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/shared/navigation/navigationDirective.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .directive('navigationBar', ['$rootScope', '$state', navigationDirective]); 8 | 9 | function navigationDirective($rootScope, $state) { 10 | return { 11 | restrict: 'E', 12 | templateUrl: 'views/navigationView.html', 13 | controller: function($rootScope, $stateParams, $scope, $state, $element) { 14 | $scope.state = $state; 15 | } 16 | } 17 | } 18 | })(); -------------------------------------------------------------------------------- /test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('socialMediaExplorerApp')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | MainCtrl = $controller('MainCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/shared/selectableMediaDisplay/selectableMediaDisplayView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/labeledMediaGeo/labeledMediaGeoView.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
-------------------------------------------------------------------------------- /assets/css/_site.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 62px; 3 | overflow: hidden; 4 | 5 | color: white; 6 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#991d40+0,d1455a+55 */ 7 | background-color: $primary-color; 8 | } 9 | 10 | select { 11 | color: black; 12 | } 13 | 14 | .row { 15 | top: 0; 16 | 17 | margin: 0; 18 | } 19 | 20 | #main-bg { 21 | padding-top: 0; 22 | margin: 10px; 23 | } 24 | #nga-logo { 25 | width: 500px; 26 | height: 500px; 27 | margin: 100px auto; 28 | } 29 | /* Specify styling for tooltip contents */ 30 | .tooltip.customClass .tooltip-inner { 31 | width: auto; 32 | 33 | background-color: #999; 34 | box-shadow: 0 6px 12px rgba(0,0,0,.175); 35 | } 36 | 37 | #sub-menu-elements { 38 | position: absolute; 39 | right: 10px; 40 | 41 | width: 200px; 42 | } 43 | -------------------------------------------------------------------------------- /assets/css/_threeDimMediaClusterer.scss: -------------------------------------------------------------------------------- 1 | #three-dim-media-clusterer #container { 2 | margin: 0; 3 | } 4 | .three-dim-media-clusterer-stats { 5 | position: relative; 6 | 7 | width: 100%; 8 | height: auto; 9 | padding: 7px; 10 | margin-bottom: 10px; 11 | 12 | font-size: 14px; 13 | font-style: $primary-font; 14 | 15 | border-radius: 5px; 16 | background-color: $secondary-color; 17 | } 18 | 19 | #three-dim-media-clusterer-nav { 20 | width: 200px; 21 | height: auto; 22 | padding: 7px; 23 | 24 | font-size: 14px; 25 | font-style: $primary-font; 26 | 27 | border-radius: 5px; 28 | background-color: $secondary-color; 29 | } 30 | .three-dim-media-clusterer-stats-select { 31 | padding: 1px 10px 1px 10px; 32 | 33 | font-weight: bold; 34 | 35 | cursor: pointer; 36 | 37 | border-radius: 3px; 38 | background-color: #433d4d; 39 | } 40 | -------------------------------------------------------------------------------- /app/shared/selectableMediaDisplay/selectableMediaDisplayDirective.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .directive("selectableMediaDisplay", [selectableMediaDisplayDirective]); 8 | 9 | function selectableMediaDisplayDirective() { 10 | 11 | var directive = { 12 | restrict: "E", 13 | scope: { 14 | media: "=media" 15 | }, 16 | templateUrl: 'views/selectableMediaDisplayView.html', 17 | controller: controller, 18 | controllerAs: 'vm', 19 | bindToController: true, 20 | link: link 21 | }; 22 | 23 | return directive; 24 | 25 | function controller($scope) { 26 | var vm = this; 27 | 28 | vm.mediaClick = function(index) { 29 | vm.media.selected = index; 30 | } 31 | }; 32 | 33 | function link($scope, elem, attr) {} 34 | } 35 | })(); -------------------------------------------------------------------------------- /app/components/threeDimMediaClusterer/threeDimMediaClustererInnerView.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 | -------------------------------------------------------------------------------- /assets/css/_navigation.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | margin: 10px; 3 | 4 | border: 0; 5 | } 6 | .navbar-fixed-top { 7 | top: 0; 8 | } 9 | .navbar-inverse { 10 | background-color: $secondary-color; 11 | } 12 | .navbar-inverse .navbar-nav > .active > a, 13 | .navbar-inverse .navbar-nav > .active > a:hover, 14 | .navbar-inverse .navbar-nav > .active > a:focus { 15 | color: #fff; 16 | background-color: #1d99af; 17 | } 18 | /* entire container, keeps perspective */ 19 | .navbar-inverse .navbar-brand { 20 | font-family: "Roboto Condensed", sans-serif; 21 | font-size: 24px; 22 | font-weight: bold; 23 | 24 | color: $tertiary-color; 25 | } 26 | .navbar-inverse .navbar-nav > li > a { 27 | font-family: $primary-font, sans-serif; 28 | font-size: 16px; 29 | font-weight: lighter; 30 | 31 | color: $tertiary-color; 32 | } 33 | .side-nav { 34 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#5e9291+75,4c6f75+100 */ 35 | background: #f1e061; /* Old browsers */ 36 | } 37 | -------------------------------------------------------------------------------- /assets/css/_selectedMediaDisplay.scss: -------------------------------------------------------------------------------- 1 | #selected-media { 2 | position: absolute; 3 | bottom: 0; 4 | left: 10px; 5 | 6 | max-width: 90%; 7 | overflow-x: auto; 8 | overflow-y: hidden; 9 | 10 | white-space: nowrap; 11 | 12 | background: none; 13 | } 14 | 15 | .selected-media { 16 | bottom: 0; 17 | 18 | width: 85px; 19 | height: 85px; 20 | padding: 0; 21 | margin: 0; 22 | 23 | cursor: pointer; 24 | transition: all .2s; 25 | vertical-align: bottom; 26 | 27 | border-width: 3px 2px 3px 1px; 28 | border-style: solid; 29 | border-color: #000; 30 | border-radius: 5px; 31 | } 32 | 33 | .selected-media:hover { 34 | width: 190px; 35 | height: 190px; 36 | 37 | border-color: #635d6d; 38 | border-radius: 2px; 39 | } 40 | 41 | #no-selected-media { 42 | font-family: "Roboto Condensed"; 43 | 44 | background: none; 45 | } 46 | 47 | .selected-media-container { 48 | bottom: 0; 49 | left: 0; 50 | 51 | width: auto; 52 | min-height: 20px; 53 | padding: 0; 54 | 55 | color: white; 56 | } 57 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social-media-explorer", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "^1.3.0", 6 | "angular-animate": "^1.3.0", 7 | "angular-cookies": "^1.3.0", 8 | "angular-resource": "^1.3.0", 9 | "angular-route": "^1.3.0", 10 | "angular-sanitize": "^1.3.0", 11 | "angular-touch": "^1.3.0", 12 | "angular-leaflet-directive": "~0.9.3", 13 | "three.js": "threejs#*", 14 | "angularjs-dropdown-multiselect": "~1.5.2", 15 | "angular-ui-router": "~0.2.15", 16 | "angular-bootstrap": "~0.14.3", 17 | "THREEOrbitControls": "~71.1.0", 18 | "bootstrap": "~3.3.4", 19 | "bootstrap-css": "~3.3.4", 20 | "components-font-awesome": "~4.4.0", 21 | "angular-underscore": "~0.5.0" 22 | }, 23 | "devDependencies": { 24 | "angular-mocks": "^1.3.0", 25 | "angular-bootstrap": "~0.14.3", 26 | "bootstrap": "~3.3.4" 27 | }, 28 | "appPath": "app", 29 | "moduleName": "socialMediaExplorerApp", 30 | "resolutions": { 31 | "angular": "1.4.7" 32 | } 33 | } -------------------------------------------------------------------------------- /assets/css/_timeSlider.scss: -------------------------------------------------------------------------------- 1 | .startTime, 2 | .curTime { 3 | font: 15px $primary-font, sans-serif; 4 | } 5 | 6 | .curTime { 7 | padding: 6px; 8 | 9 | background-color: #1d99af; 10 | } 11 | 12 | .endTime { 13 | float: right; 14 | 15 | font: 15px $primary-font, sans-serif; 16 | } 17 | 18 | .startTime, 19 | .endTime { 20 | padding: 4px; 21 | 22 | background-color: $secondary-color; 23 | } 24 | input[type=range] { 25 | margin: 20px 0 20px 0; 26 | 27 | -webkit-appearance: none; 28 | } 29 | 30 | input[type=range]::-webkit-slider-runnable-track { 31 | width: 300px; 32 | height: 5px; 33 | 34 | border: none; 35 | border-radius: 3px; 36 | background: #ddd; 37 | } 38 | 39 | input[type=range]::-webkit-slider-thumb { 40 | width: 20px; 41 | height: 20px; 42 | margin-top: -4px; 43 | 44 | border: none; 45 | border-radius: 50%; 46 | background: $tertiary-color; 47 | 48 | -webkit-appearance: none; 49 | } 50 | 51 | input[type=range]:focus { 52 | outline: none; 53 | } 54 | 55 | input[type=range]:focus::-webkit-slider-runnable-track { 56 | background: #ccc; 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialmediaexplorer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": {}, 6 | "repository": {}, 7 | "devDependencies": { 8 | "grunt": "^0.4.5", 9 | "grunt-autoprefixer": "^2.0.0", 10 | "grunt-concurrent": "^1.0.0", 11 | "grunt-contrib-clean": "^0.6.0", 12 | "grunt-contrib-compass": "^1.0.0", 13 | "grunt-contrib-concat": "^0.5.0", 14 | "grunt-contrib-connect": "^0.9.0", 15 | "grunt-contrib-copy": "^0.7.0", 16 | "grunt-contrib-cssmin": "^0.12.0", 17 | "grunt-contrib-htmlmin": "^0.4.0", 18 | "grunt-contrib-imagemin": "^0.9.2", 19 | "grunt-contrib-jshint": "^0.11.0", 20 | "grunt-contrib-uglify": "^0.7.0", 21 | "grunt-contrib-watch": "^0.6.1", 22 | "grunt-file-blocks": "^0.4.0", 23 | "grunt-filerev": "^2.1.2", 24 | "grunt-google-cdn": "^0.4.3", 25 | "grunt-include-source": "^0.7.0", 26 | "grunt-newer": "^1.1.0", 27 | "grunt-ng-annotate": "^0.9.2", 28 | "grunt-svgmin": "^2.0.0", 29 | "grunt-usemin": "^3.0.0", 30 | "grunt-wiredep": "^2.0.0", 31 | "jshint-stylish": "^1.0.0", 32 | "load-grunt-tasks": "^3.1.0", 33 | "time-grunt": "^1.0.0" 34 | }, 35 | "engines": { 36 | "node": ">=0.10.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/components/shiptracker/shiptrackerView.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /app/shared/navigation/navigationView.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/js/Detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | var Detector = { 7 | 8 | canvas: !! window.CanvasRenderingContext2D, 9 | webgl: ( function () { 10 | 11 | try { 12 | 13 | var canvas = document.createElement( 'canvas' ); return !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) ); 14 | 15 | } catch ( e ) { 16 | 17 | return false; 18 | 19 | } 20 | 21 | } )(), 22 | workers: !! window.Worker, 23 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 24 | 25 | getWebGLErrorMessage: function () { 26 | 27 | var element = document.createElement( 'div' ); 28 | element.id = 'webgl-error-message'; 29 | element.style.fontFamily = 'monospace'; 30 | element.style.fontSize = '13px'; 31 | element.style.fontWeight = 'normal'; 32 | element.style.textAlign = 'center'; 33 | element.style.background = '#fff'; 34 | element.style.color = '#000'; 35 | element.style.padding = '1.5em'; 36 | element.style.width = '400px'; 37 | element.style.margin = '5em auto 0'; 38 | 39 | if ( ! this.webgl ) { 40 | 41 | element.innerHTML = window.WebGLRenderingContext ? [ 42 | 'Your graphics card does not seem to support WebGL.
', 43 | 'Find out how to get it here.' 44 | ].join( '\n' ) : [ 45 | 'Your browser does not seem to support WebGL.
', 46 | 'Find out how to get it here.' 47 | ].join( '\n' ); 48 | 49 | } 50 | 51 | return element; 52 | 53 | }, 54 | 55 | addGetWebGLMessage: function ( parameters ) { 56 | 57 | var parent, id, element; 58 | 59 | parameters = parameters || {}; 60 | 61 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 62 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 63 | 64 | element = Detector.getWebGLErrorMessage(); 65 | element.id = id; 66 | 67 | parent.appendChild( element ); 68 | 69 | } 70 | 71 | }; 72 | 73 | // browserify support 74 | if ( typeof module === 'object' ) { 75 | 76 | module.exports = Detector; 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/shared/factories/mediaFactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | mediaFactory loads data about the media including three dimensional representation 3 | and provides an object to access the data. Upcoming versions should include 4 | informatino such as latitude, longitude, media details, etc. 5 | */ 6 | 7 | (function() { 8 | 9 | 'use strict'; 10 | 11 | angular 12 | .module('socialMediaExplorerApp') 13 | .factory('mediaFactory', ['$http', 'APP_CONFIG', mediaFactory]); 14 | 15 | function mediaFactory($http, APP_CONFIG) { 16 | function media(mediaData) { 17 | if (mediaData) { 18 | this.setData(mediaData); 19 | } 20 | // Some other initializations related to book 21 | }; 22 | media.prototype = { 23 | setData: function(mediaData) { 24 | this.mediaFilenameIdx = 0; 25 | this.xCoordinateIdx = 1; 26 | this.yCoordinateIdx = 2; 27 | this.zCoordinateIdx = 3; 28 | this.splitChar = ","; 29 | this.pathToMedia = APP_CONFIG.mediaThumbnailUrl; 30 | this.data = mediaData.split("\n"); 31 | }, 32 | getRowAsArray: function(idx) { 33 | return this.data[idx].split(this.splitChar); 34 | }, 35 | getMediaUrl: function(idx) { 36 | return this.pathToMedia + this.getRowAsArray(idx)[this.mediaFilenameIdx]; 37 | }, 38 | //the x, y, z coordinates correspond to represenation of the media 39 | //in 3d space in relation to the similarity with the other media 40 | getXCoordinate: function(idx) { 41 | return this.getRowAsArray(idx)[this.xCoordinateIdx]; 42 | }, 43 | getYCoordinate: function(idx) { 44 | return this.getRowAsArray(idx)[this.yCoordinateIdx]; 45 | }, 46 | getZCoordinate: function(idx) { 47 | return this.getRowAsArray(idx)[this.zCoordinateIdx]; 48 | }, 49 | getCount: function() { 50 | return this.data.length; 51 | } 52 | }; 53 | return { 54 | getMedia: function() { 55 | 56 | return $http({ 57 | url: APP_CONFIG.baseDataUrl + APP_CONFIG.mediaFactoryFilename, 58 | method: "GET", 59 | cache: true 60 | }) 61 | .then(function(response) { 62 | return new media(response.data); 63 | }); 64 | } 65 | }; 66 | } 67 | })(); -------------------------------------------------------------------------------- /app/shared/factories/labeledMediaFactory.js: -------------------------------------------------------------------------------- 1 | /* 2 | labeledMediaFactory loads data from the labeled media files 3 | and provides an object to access the data. 4 | */ 5 | 6 | (function() { 7 | 8 | 'use strict'; 9 | 10 | angular 11 | .module('socialMediaExplorerApp') 12 | .factory('labeledMediaFactory', ['$http', 'APP_CONFIG', labeledMediaFactory]); 13 | 14 | function labeledMediaFactory($http, APP_CONFIG) { 15 | function labeledMedia(labeledMediaData, path) { 16 | if (labeledMediaData) { 17 | this.setData(labeledMediaData); 18 | this.path = path; 19 | } 20 | } 21 | labeledMedia.prototype = { 22 | setData: function(labeledMediaData) { 23 | this.data = labeledMediaData.split("\n"); 24 | this.mediaFilenameIdx = 0; 25 | this.latitudeIdx = 1; 26 | this.longitudeIdx = 2; 27 | this.messageIdx = 3; 28 | this.pathToMedia = APP_CONFIG.baseImageUrl; 29 | }, 30 | getRowAsArray: function(idx) { 31 | return this.data[idx].trim().match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); 32 | }, 33 | getId: function(idx) { 34 | return this.getRowAsArray(idx)[this.mediaFilenameIdx].toString(); 35 | }, 36 | getMediaUrl: function(idx) { 37 | return this.pathToMedia + this.path + "/" + this.getRowAsArray(idx)[this.mediaFilenameIdx] + ".jpg"; 38 | }, 39 | getLatitude: function(idx) { 40 | return parseFloat(this.getRowAsArray(idx)[this.latitudeIdx]); 41 | }, 42 | getLongitude: function(idx) { 43 | return parseFloat(this.getRowAsArray(idx)[this.longitudeIdx]); 44 | }, 45 | getMessage: function(idx) { 46 | return this.getRowAsArray(idx)[this.messageIdx]; 47 | }, 48 | getCount: function() { 49 | return this.data.length; 50 | } 51 | } 52 | return { 53 | //getLabeledMedia takes a path and returns 54 | //a labeled media object from the data file 55 | //stored at that path 56 | getLabeledMedia: function(path) { 57 | return (function(path) { 58 | var p = path; 59 | return $http({ 60 | url: APP_CONFIG.baseImageUrl + path + "/" + APP_CONFIG.labeledMediaFactoryFilename, 61 | method: "GET", 62 | cache: true 63 | }).then(function(response) { 64 | return new labeledMedia(response.data, p); 65 | }); 66 | })(path); 67 | } 68 | } 69 | } 70 | })(); -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NGA Pathfinder Social Media Explorer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/components/mediaGeo/mediaGeoController.js: -------------------------------------------------------------------------------- 1 | /* Module to show a set of images on a map */ 2 | /* Currently uses artificial coordinates */ 3 | 4 | (function() { 5 | 6 | 'use strict'; 7 | 8 | angular 9 | .module('socialMediaExplorerApp') 10 | .controller("mediaGeoCtrl", ['$scope', 'selectedMediaFactory', mediaGeoCtrl]); 11 | 12 | //selectedMedia is a service that provides access to the selected images in the dashboard 13 | function mediaGeoCtrl($scope, selectedMedia) { 14 | 15 | var vm = this; //set vm to current scope 16 | 17 | vm.selectedMedia = selectedMedia; 18 | 19 | var leaflet = { 20 | markerPrefix: "img", //prefix for the names of the markers 21 | defaultZoom: 4, //default zoom for leaflet 22 | focusZoom: 8, 23 | iconSize: [75, 75] 24 | }; 25 | 26 | 27 | //watch for a change in the current selected image from all of the selectedMedia 28 | $scope.$watch('vm.selectedMedia.selected', selectedMediaChange); 29 | 30 | function selectedMediaChange(newVal, oldVal) { 31 | if (newVal != oldVal && !angular.isUndefined(newVal)) { 32 | focusOnImage(newVal); 33 | } 34 | }; 35 | 36 | //create markers from the images for the map 37 | function createMarkers(arr, leaflet) { 38 | var ctr = 0; 39 | var markers = {}; 40 | 41 | arr.media.forEach(function(image) { 42 | markers[leaflet.markerPrefix + ctr] = {}; 43 | markers[leaflet.markerPrefix + ctr]["lat"] = Math.floor(Math.random() * 180) + 1; //ranom 44 | markers[leaflet.markerPrefix + ctr]["lng"] = Math.floor(Math.random() * 90) + 1; //random 45 | 46 | //create an object to represent the icon for the marker 47 | var icon = { 48 | iconUrl: image, 49 | iconSize: leaflet.iconSize 50 | } 51 | 52 | markers[leaflet.markerPrefix + ctr]["icon"] = icon 53 | ctr++; 54 | }); 55 | 56 | return markers; 57 | } 58 | 59 | //focus the map on the specified index coords in the markers 60 | function focusOnImage(index) { 61 | $scope.center.lat = $scope.markers[leaflet.markerPrefix + index].lat; 62 | $scope.center.lng = $scope.markers[leaflet.markerPrefix + index].lng; 63 | $scope.center.zoom = leaflet.focusZoom; 64 | } 65 | 66 | angular.extend($scope, { 67 | center: { 68 | lat: 0, 69 | lng: 0, 70 | zoom: leaflet.defaultZoom 71 | }, 72 | markers: createMarkers(vm.selectedMedia, leaflet), 73 | layers: { 74 | baselayers: { 75 | mapbox_light: { 76 | name: 'Mapbox Light', 77 | url: 'http://api.tiles.mapbox.com/v4/{mapid}/{z}/{x}/{y}.png?access_token={apikey}', 78 | type: 'xyz', 79 | layerOptions: { 80 | apikey: 'pk.eyJ1IjoiYnVmYW51dm9scyIsImEiOiJLSURpX0pnIn0.2_9NrLz1U9bpwMQBhVk97Q', 81 | mapid: 'bufanuvols.lia22g09' 82 | } 83 | }, 84 | osm: { 85 | name: 'OpenStreetMap', 86 | url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 87 | type: 'xyz' 88 | } 89 | } 90 | } 91 | }); 92 | } 93 | })(); -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | This notice lists all license stipulations from dependencies outside of major OSI and FSF reviewed licenses. 2 | 3 | AngularJS Dropdown Multiselect 4 | Unspecified license at https://github.com/dotansimha/angularjs-dropdown-multiselect 5 | 6 | Python 2.7 https://www.python.org/download/releases/2.7/license/ 7 | 8 | NVIDIA DIGITS https://github.com/nvidia/digits/blob/master/LICENSE 9 | 10 | Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions 14 | are met: 15 | * Redistributions of source code must retain the above copyright 16 | notice, this list of conditions and the following disclaimer. 17 | * Redistributions in binary form must reproduce the above copyright 18 | notice, this list of conditions and the following disclaimer in the 19 | documentation and/or other materials provided with the distribution. 20 | * Neither the name of NVIDIA CORPORATION nor the names of its 21 | contributors may be used to endorse or promote products derived 22 | from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY 25 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 27 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 28 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 29 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 30 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 31 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 32 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | scipy https://www.scipy.org/scipylib/license.html 37 | 38 | Copyright © 2001, 2002 Enthought, Inc. 39 | All rights reserved. 40 | 41 | Copyright © 2003-2013 SciPy Developers. 42 | All rights reserved. 43 | 44 | Redistribution and use in source and binary forms, with or without 45 | modification, are permitted provided that the following conditions 46 | are met: 47 | * Redistributions of source code must retain the above copyright 48 | notice, this list of conditions and the following disclaimer. 49 | * Redistributions in binary form must reproduce the above copyright 50 | notice, this list of conditions and the following disclaimer in the 51 | documentation and/or other materials provided with the distribution. 52 | * Neither the name of Enthought nor the names of the SciPy Developers 53 | nor the names of its contributors may be used to endorse or promote 54 | products derived from this software without specific prior written 55 | permission. 56 | 57 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY 58 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 59 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 60 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 61 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 62 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 63 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 64 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 65 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 66 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 67 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | -------------------------------------------------------------------------------- /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 |
    148 |
  • a mistyped address
  • 149 |
  • an out-of-date link
  • 150 |
151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /assets/css/_component.scss: -------------------------------------------------------------------------------- 1 | svg { 2 | pointer-events: none; 3 | } 4 | 5 | /* Demo 1 */ 6 | 7 | .demo-drawings figcaption { 8 | width: 95%; 9 | max-width: 40em; 10 | margin: 0 auto 0 auto; 11 | 12 | text-align: center; 13 | } 14 | 15 | .demo-drawings figure:first-child figcaption { 16 | margin-top: 8em; 17 | } 18 | 19 | .demo-drawings figcaption h2 { 20 | margin-bottom: .2em; 21 | 22 | font-size: 3em; 23 | font-weight: 300; 24 | } 25 | 26 | .demo-drawings figcaption p { 27 | font-size: 1.3em; 28 | } 29 | 30 | .drawings { 31 | position: relative; 32 | 33 | max-height: 600px; 34 | } 35 | 36 | .illustration { 37 | position: absolute; 38 | top: 50%; 39 | left: 50%; 40 | 41 | max-width: 100%; 42 | max-height: 100%; 43 | 44 | -webkit-transform: translateX(-50%) translateY(-50%); 45 | transform: translateX(-50%) translateY(-50%); 46 | 47 | opacity: 0; 48 | } 49 | 50 | .show { 51 | opacity: 1; 52 | } 53 | 54 | .hide { 55 | opacity: 0; 56 | } 57 | 58 | .line-drawing, 59 | .illustration { 60 | -webkit-transition: opacity 1.0s; 61 | transition: opacity 1.0s; 62 | } 63 | 64 | .line-drawing path { 65 | fill: none; 66 | stroke: #1d99af; 67 | stroke-width: 15; 68 | } 69 | 70 | path.line-round { 71 | stroke-linecap: round; 72 | } 73 | 74 | path.stroke-medium { 75 | stroke-width: 15; 76 | } 77 | 78 | path.stroke-thin { 79 | stroke-width: 15; 80 | } 81 | 82 | /* Demo 2 and demo 3 */ 83 | .demo-loading .show, 84 | .demo-loading .hide { 85 | -webkit-transition: opacity .5s; 86 | transition: opacity .5s; 87 | } 88 | 89 | .codrops-logo { 90 | position: relative; 91 | 92 | width: 80px; 93 | height: 80px; 94 | margin: 0 auto 30px; 95 | 96 | background: url(../img/codrops_logo.png) no-repeat 50% 50%; 97 | } 98 | 99 | .demo-loading .headline { 100 | position: relative; 101 | 102 | text-align: center; 103 | 104 | color: #2173a3; 105 | } 106 | 107 | .demo-loading .headline div { 108 | position: absolute; 109 | top: 0; 110 | left: 0; 111 | 112 | width: 100%; 113 | height: 100%; 114 | 115 | background-color: #5ca9d6; 116 | } 117 | 118 | .demo-loading .headline h1 { 119 | position: relative; 120 | z-index: 10; 121 | 122 | padding: 3em 0; 123 | margin: 0; 124 | 125 | font-size: 3em; 126 | } 127 | 128 | .demo-loading .headline h1 span:last-child { 129 | display: block; 130 | 131 | font-size: 50%; 132 | font-weight: 300; 133 | } 134 | 135 | .demo-loading section { 136 | max-width: 700px; 137 | margin: 2em auto 8em; 138 | 139 | text-align: center; 140 | } 141 | 142 | .demo-loading section figure { 143 | position: relative; 144 | 145 | display: inline-block; 146 | width: 200px; 147 | margin: 0 15px 20px; 148 | 149 | text-align: left; 150 | } 151 | 152 | .demo-loading section figure img { 153 | display: block; 154 | margin: 0 auto 10px; 155 | } 156 | 157 | .demo-loading svg.line-drawing { 158 | position: absolute; 159 | top: 0; 160 | left: 50%; 161 | z-index: 10; 162 | 163 | max-width: 100%; 164 | 165 | -webkit-transform: translateX(-50%); 166 | transform: translateX(-50%); 167 | } 168 | 169 | .demo-loading svg.line-drawing path { 170 | fill: none; 171 | stroke: #fff; 172 | stroke-width: 4; 173 | } 174 | 175 | .demo-loading svg.line-drawing path.darker { 176 | stroke: #aaa; 177 | } 178 | 179 | .demo-loading svg#rectangle { 180 | width: 100%; 181 | height: 100%; 182 | } 183 | 184 | .demo-loading svg#headline { 185 | top: 50%; 186 | 187 | margin-top: 2px; 188 | 189 | -webkit-transform: translateX(-50%) translateY(-50%); 190 | transform: translateX(-50%) translateY(-50%); 191 | } 192 | 193 | .demo-loading svg#related { 194 | width: 100%; 195 | height: 100%; 196 | max-width: 490px; 197 | } 198 | 199 | /* Positions */ 200 | #logo { 201 | top: 39px; 202 | } 203 | 204 | #demo-link-1, 205 | #demo-link-2, 206 | #demo-link-3 { 207 | top: 148px; 208 | } 209 | 210 | #demo-link-1 { 211 | -webkit-transform: translateX(-130px); 212 | transform: translateX(-130px); 213 | } 214 | 215 | #demo-link-3 { 216 | -webkit-transform: translateX(48px); 217 | transform: translateX(48px);;; 218 | } 219 | 220 | @media screen and (max-width: 35.375em) { 221 | .demo-loading .headline h1 { 222 | font-size: 1.4em; 223 | } 224 | } 225 | 226 | @media screen and (max-width: 30em) { 227 | .related { 228 | font-size: .8em; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/shared/angular.timeSlider.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('ui-timeSlider', []).directive('timeSlider', function() { 5 | var updateCurrentTime, updateEndTime, updateStartTime; 6 | updateStartTime = function(span, value) { 7 | var sd, startDate, startTime; 8 | sd = new Date(value); 9 | startDate = sd.toLocaleDateString(); 10 | startTime = sd.toLocaleTimeString(); 11 | return span.innerHTML = startDate.trim(); 12 | }; 13 | updateEndTime = function(span, value) { 14 | var ed, endDate, endTime; 15 | ed = new Date(value); 16 | endDate = ed.toLocaleDateString(); 17 | endTime = ed.toLocaleTimeString(); 18 | return span.innerHTML = endDate; 19 | }; 20 | updateCurrentTime = function(span, value) { 21 | var cd, curDate, curTime; 22 | cd = new Date(value); 23 | curDate = cd.toLocaleDateString(); 24 | curTime = cd.toLocaleTimeString(); 25 | return span.innerHTML = curDate; 26 | }; 27 | return { 28 | template: "
" + 29 | "" + 30 | "" + 31 | "" + 32 | "
", 33 | restrict: 'E', 34 | scope: { 35 | min: '=', 36 | max: '=', 37 | curtime: '=', 38 | callback: '=' 39 | }, 40 | link: function(scope, element, attrs) { 41 | var adjusting, curTimeDiv, moveCurTimeDiv, rangeInput, rangeInputElement, rangeInputOffset, spans; 42 | rangeInput = element.find('input')[0]; 43 | rangeInputElement = angular.element(rangeInput); 44 | spans = element.find('span'); 45 | curTimeDiv = element.find('div')[1]; 46 | rangeInputOffset = $(rangeInput).offset(); 47 | adjusting = false; 48 | rangeInput.min = scope.min; 49 | rangeInput.max = scope.max; 50 | rangeInput.step = attrs.step; 51 | rangeInput.value = scope.min; 52 | updateStartTime(spans[0], rangeInput.min); 53 | updateEndTime(spans[1], rangeInput.max); 54 | moveCurTimeDiv = function(curValue) { 55 | console.log(curValue); 56 | var ctdElement, curPercentage, curValueLocation; 57 | curPercentage = (curValue - scope.min) / (scope.max - scope.min); 58 | curValueLocation = (rangeInput.clientWidth * curPercentage) - 25; // + rangeInputOffset.left; 59 | ctdElement = angular.element(curTimeDiv); 60 | ctdElement.css('left', curValueLocation + 'px'); 61 | return ctdElement.css('top', rangeInput.clientHeight - curTimeDiv.clientHeight + 'px'); 62 | }; 63 | rangeInputElement.bind('change', function(event) { 64 | var curValue; 65 | adjusting = true; 66 | curValue = parseInt(event.target.value); 67 | updateCurrentTime(spans[2], curValue); 68 | return moveCurTimeDiv(curValue); 69 | }); 70 | rangeInputElement.bind('mouseup', function(event) { 71 | adjusting = false; 72 | return scope.callback.call(this, parseInt(event.target.value)); 73 | }); 74 | scope.$watch('curtime', function(newValue, oldValue) { 75 | var curValue; 76 | adjusting = true; 77 | curValue = parseInt(newValue); 78 | rangeInput.value = curValue; 79 | updateCurrentTime(spans[2], curValue); 80 | return moveCurTimeDiv(curValue); 81 | }); 82 | scope.$watch('min', function(newValue, oldValue) { 83 | updateStartTime(spans[0], parseInt(newValue)); 84 | rangeInput.min = scope.min; 85 | rangeInput.value = scope.min; 86 | return spans[2].innerHTML = ''; 87 | }); 88 | return scope.$watch('max', function(newValue, oldValue) { 89 | updateEndTime(spans[1], parseInt(newValue)); 90 | return rangeInput.max = scope.max; 91 | }); 92 | } 93 | }; 94 | }); 95 | 96 | }).call(this); -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 'use strict'; 3 | 4 | angular 5 | .module('socialMediaExplorerApp', [ 6 | 'ui.bootstrap', 7 | 'ui.router', 8 | 'ui-timeSlider', 9 | 'angularjs-dropdown-multiselect', 10 | 'angular-underscore', 11 | 'leaflet-directive' 12 | ]); 13 | 14 | 15 | angular 16 | .module('socialMediaExplorerApp') 17 | .run(['$rootScope', '$window', 18 | function($rootScope, $window) {} 19 | ]) 20 | 21 | .config( 22 | ['$stateProvider', '$urlRouterProvider', 23 | function($stateProvider, $urlRouterProvider) { 24 | 25 | var componentsPath = 'views'; 26 | var sharedPath = 'views/'; 27 | // Use $urlRouterProvider to configure any redirects (when) and invalid urls (otherwise). 28 | $urlRouterProvider.otherwise('/'); 29 | // Use $stateProvider to configure your states. 30 | $stateProvider 31 | .state('home', { 32 | url: '/', 33 | views: { 34 | '': { 35 | templateUrl: sharedPath + 'main.html' 36 | }, 37 | 'main@home': { 38 | templateUrl: componentsPath + '/ngaView.html', 39 | } 40 | } 41 | }) 42 | .state('home.nga', { 43 | url: 'nga', 44 | views: { 45 | 'main@home': { 46 | templateUrl: componentsPath + '/ngaView.html', 47 | } 48 | } 49 | }) 50 | .state('home.3dClusterer', { 51 | url: '3dClusterer', 52 | views: { 53 | 'main@home': { 54 | templateUrl: componentsPath + '/threeDimMediaClustererView.html', 55 | controller: 'threeDimMediaClustererCtrl', 56 | controllerAs: 'vm', 57 | resolve: { 58 | webServiceRef: "mediaFactory", 59 | media: function(webServiceRef) { 60 | return webServiceRef.getMedia(); 61 | } 62 | } 63 | } 64 | } 65 | }) 66 | .state('home.geo', { 67 | url: 'geo', 68 | views: { 69 | 'main@home': { 70 | templateUrl: componentsPath + '/mediaGeoView.html', 71 | controller: 'mediaGeoCtrl', 72 | controllerAs: 'vm' 73 | } 74 | } 75 | }) 76 | .state('home.labeledgeo', { 77 | url: 'labeledgeo', 78 | views: { 79 | 'main@home': { 80 | templateUrl: componentsPath + '/labeledMediaGeoView.html', 81 | controller: 'labeledMediaGeoCtrl', 82 | resolve: { 83 | webServiceRef: "labeledMediaFactory", 84 | labeledMedia: function(webServiceRef) { 85 | return webServiceRef.getLabeledMedia("example_media"); 86 | } 87 | } 88 | } 89 | } 90 | }) 91 | .state('home.shiptracker', { 92 | url: 'shiptracker', 93 | views: { 94 | 'main@home': { 95 | templateUrl: componentsPath + '/shipTrackerView.html', 96 | controller: 'shipTrackerCtrl', 97 | resolve: { 98 | webServiceRef: "shipsFactory", 99 | ships: function(webServiceRef) { 100 | return webServiceRef.getShips(); 101 | } 102 | } 103 | } 104 | } 105 | }); 106 | } 107 | ]); 108 | 109 | angular.module('socialMediaExplorerApp').constant('APP_CONFIG', { 110 | baseImageUrl: 'assets/images/', 111 | baseDataUrl: 'assets/data/', 112 | mediaThumbnailUrl: 'assets/images/thumbnails/', 113 | shipsFactoryFilename: 'ships.json', 114 | mediaFactoryFilename: 'media.csv', 115 | labeledMediaFactoryFilename: 'media.csv' 116 | }); -------------------------------------------------------------------------------- /app/shared/factories/shipsFactory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .factory('shipsFactory', ['$http', 'APP_CONFIG', shipsFactory]); 8 | 9 | function shipsFactory($http, APP_CONFIG) { 10 | function ships(shipData) { 11 | if (shipData) { 12 | this.setData(shipData); 13 | } 14 | } 15 | ships.prototype = { 16 | setData: function(shipData) { 17 | this.data = shipData; 18 | this.mmsiIdx = 0; 19 | this.dateIdx = 1; 20 | this.statusIdx = 2; 21 | this.latitudeIdx = 3; 22 | this.longitudeIdx = 4; 23 | this.shipnameIdx = 5; 24 | this.typeIdx = 6; 25 | this.nameIdx = 7; 26 | this.countryIdx = 8; 27 | this.imoIdx = 9; 28 | this.yobIdx = 10; 29 | this.gtIdx = 11; 30 | this.loaIdx = 12 31 | this.beamIdx = 13; 32 | this.shiptypeIdx = 14; 33 | this.dwtIdx = 15; 34 | this.type2Idx = 16; 35 | this.summaryIdx = 17; 36 | this.prevIdx = 18; 37 | this.portIdx = 19; 38 | this.nextPortIdx = 20; 39 | this.pathToMedia = APP_CONFIG.baseImageUrl; 40 | }, 41 | getRowAsArray: function(idx) { 42 | return this.data[idx]; 43 | }, 44 | get: function(idx, varIdx) { 45 | return this.getRowAsArray(idx)[varIdx]; 46 | }, 47 | getId: function(idx) { 48 | return this.get(idx, this.mmsiIdx).toString(); 49 | }, 50 | getDate: function(idx) { 51 | return this.get(idx, this.dateIdx); 52 | }, 53 | getLatitude: function(idx) { 54 | return parseFloat(this.get(idx, this.latitudeIdx)); 55 | }, 56 | getLongitude: function(idx) { 57 | return parseFloat(this.get(idx, this.longitudeIdx)); 58 | }, 59 | getStatus: function(idx) { 60 | return this.get(idx, this.statusIdx); 61 | }, 62 | getShipname: function(idx) { 63 | return this.get(idx, this.shipnameIdx); 64 | }, 65 | getType: function(idx) { 66 | return this.get(idx, this.typeIdx); 67 | }, 68 | getCount: function() { 69 | return this.data.length; 70 | }, 71 | //get the min and max dates from the data 72 | getMinMaxDates: function(data) { 73 | var min = null, 74 | max = 0; 75 | 76 | //search through all of the data for the min and max dates 77 | _.each(this.data, function(elem) { 78 | min = (min == null || elem[1] < min) ? elem[1] : min; 79 | max = elem[1] > max ? elem[1] : max; 80 | }); 81 | 82 | return { 83 | min: min, 84 | max: max 85 | }; 86 | }, 87 | getShipIconUrl: function(idx) { 88 | var iconUrl = APP_CONFIG.baseImageUrl + "shipgreen.png"; 89 | var status = this.getStatus(idx).toLowerCase(); 90 | if (status == "at anchor" || status == "moored" || status == "not under command") { 91 | iconUrl = APP_CONFIG.baseImageUrl + "shipred.png"; 92 | } 93 | 94 | return iconUrl; 95 | }, 96 | //get the unique values for a column 97 | getUniqueColValues: function(idx) { 98 | var vals = []; 99 | //loop through all rows of data 100 | for (var i = 0; i < this.data.length; i++) { 101 | var val = this.data[i][idx]; //current row col val 102 | if (_.findWhere(vals, { 103 | 'label': val 104 | }) == undefined) { 105 | vals.push({ 106 | 'id': vals.length, 107 | 'label': val 108 | }); 109 | } 110 | } 111 | return vals; 112 | } 113 | } 114 | return { 115 | getShips: function() { 116 | return $http({ 117 | url: APP_CONFIG.baseDataUrl + APP_CONFIG.shipsFactoryFilename, 118 | method: "GET", 119 | cache: true 120 | }).then(function(response) { 121 | return new ships(response.data); 122 | }); 123 | }, 124 | setShips: function(data) { 125 | return new ships(data); 126 | } 127 | } 128 | } 129 | })(); -------------------------------------------------------------------------------- /app/components/labeledMediaGeo/labeledMediaGeoController.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .controller("labeledMediaGeoCtrl", ['$scope', 'labeledMedia', 'labeledMediaFactory', labeledMediaCtrl]); 8 | 9 | function labeledMediaCtrl($scope, initLabeledMedia, labeledMediaFactory) { 10 | 11 | var initPath = 'cargo_helicopter'; 12 | 13 | $scope.markerWidth = 40; 14 | $scope.markerHeight = 40; 15 | 16 | $scope.markers = createMarkers(initPath, initLabeledMedia); 17 | 18 | //watch for zoom action and change the image sizes based on the zoom level 19 | $scope.$watch("center.zoom", function(event) { 20 | 21 | var currentZoom = $scope.center.zoom; //the current zoom level 22 | 23 | //look through the keys of all the markers 24 | Object.keys($scope.markers).forEach(function(key) { 25 | 26 | //get the iterated marker 27 | var marker = $scope.markers[key]; 28 | 29 | //resize based on the log of the current zoom 30 | var resize = Math.pow(Math.log(currentZoom), 2); 31 | //set the new marker width and height 32 | var markerWidth = $scope.markerWidth * resize; 33 | var markerHeight = $scope.markerHeight * resize; 34 | //change the current marker icon size and reanchor the icon so it is properly shown 35 | var icon = { 36 | iconSize: [markerWidth, markerHeight], 37 | iconAnchor: [markerWidth / 2, 0] 38 | }; 39 | 40 | angular.extend(marker["icon"], icon); 41 | }); 42 | }); 43 | 44 | //events to occur on a double click on the leaflet map 45 | $scope.$on('leafletDirectiveMarker.dblclick', function(e, args) { 46 | // Args will contain the marker name and other relevant information 47 | var markerKey = args.modelName; //the key of the marker 48 | //get the lat and lng of the double clicked marker 49 | var lat = $scope.markers[markerKey]["lat"]; 50 | var lng = $scope.markers[markerKey]["lng"]; 51 | 52 | //resize the clicked marker 53 | var curMarker = $scope.markers[markerKey]; 54 | var icon = { 55 | iconSize: [300, 300], 56 | iconAnchor: [300 / 2, 0] 57 | }; 58 | 59 | angular.extend(curMarker["icon"], icon); 60 | //recenter the map to the double clicked image 61 | angular.extend($scope.center, { 62 | lat: lat, 63 | lng: lng, 64 | zoom: 10 65 | }); 66 | }); 67 | 68 | 69 | $scope.changeData = function(path) { 70 | //create a promise and get the image data based on the passed in path 71 | var promise = labeledMediaFactory.getLabeledMedia(path); 72 | promise.then( 73 | function(labeledMedia) { 74 | $scope.markers = createMarkers(path, labeledMedia); 75 | }, 76 | function(errorPayload) { 77 | $log.error('failure loading labeled media', errorPayload); 78 | }); 79 | } 80 | 81 | //create the markers with the data set 82 | function createMarkers(path, labeledMedia) { 83 | 84 | var markers = {}; 85 | var totalLat = 0.0; 86 | var totalLng = 0.0; 87 | var ctr = 0; 88 | 89 | //each data record pertains to a labelled image 90 | for (var idx = 0; idx < labeledMedia.getCount(); idx++) { 91 | 92 | var key = "marker" + labeledMedia.getId(idx); //the key used for the current marker 93 | 94 | markers[key] = { 95 | lat: labeledMedia.getLatitude(idx), 96 | lng: labeledMedia.getLongitude(idx), 97 | message: labeledMedia.getMessage(idx) 98 | }; //create a new marker 99 | 100 | //increment the total lat and lng for the entirety of the markers 101 | totalLat += markers[key]["lat"]; 102 | totalLng += markers[key]["lng"]; 103 | 104 | //create the icon for the current marker (an image) 105 | markers[key]["icon"] = { 106 | iconUrl: labeledMedia.getMediaUrl(idx), 107 | iconSize: [$scope.markerWidth, $scope.markerHeight], 108 | iconAnchor: [$scope.markerWidth, 0], // point of the icon which will correspond to marker's location 109 | popupAnchor: [0, 0] // point from which the popup should open relative to the iconAnchor 110 | }; 111 | ctr++; //increment counter 112 | } 113 | 114 | //the map is centered around the average lat and lng of all markers on the map 115 | $scope.center = { 116 | lat: totalLat / labeledMedia.getCount(), 117 | lng: totalLng / labeledMedia.getCount(), 118 | zoom: 4 119 | }; 120 | 121 | return markers; 122 | } 123 | 124 | angular.extend($scope, { 125 | events: { 126 | map: { 127 | enable: ['zoomstart', 'zoomend', 'zoomlevelschange'], 128 | logic: 'emit' 129 | } 130 | }, 131 | layers: { 132 | baselayers: { 133 | mapbox_light: { 134 | name: 'Mapbox Light', 135 | url: 'http://api.tiles.mapbox.com/v4/{mapid}/{z}/{x}/{y}.png?access_token={apikey}', 136 | type: 'xyz', 137 | layerOptions: { 138 | apikey: 'pk.eyJ1IjoiYnVmYW51dm9scyIsImEiOiJLSURpX0pnIn0.2_9NrLz1U9bpwMQBhVk97Q', 139 | mapid: 'bufanuvols.lia22g09' 140 | } 141 | }, 142 | osm: { 143 | name: 'OpenStreetMap', 144 | url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 145 | type: 'xyz' 146 | } 147 | } 148 | } 149 | }); 150 | } 151 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What it Does 2 | 3 | The Social Media Picture Explorer allows users to explore social media by using state of the art machine learning techniques to cluster similar images and provide automatic object recognition. This allows users to navigate social media and attached pictures via visual similarity to ease sifting through large amounts of graphical data without the need for specialized language skills for meta-text descriptions. 4 | 5 | Backend of UI https://github.com/ngageoint/social-media-picture-explorer 6 | 7 | ####It currently consists of 4 main areas: 8 | 9 | 1. 3D Clusterer 10 | 2. Geo 11 | 3. Labelled Geo 12 | 4. Ship Tracker 13 | 14 | These views are discussed in further depth below. 15 | 16 | ## Build & development 17 | * This project was scaffolded with yeoman angular generator. 18 | * Bower is used for dependency injection and Grunt is used as the task runner. 19 | * Run `grunt build` for building distribution and `grunt serve` for building for development. 'grunt serve:dist' will build and view for distribution. 20 | 21 | ##Steps to get you up and running :) 22 | * $ represent cmd prompt. Do not type it in the prompt. 23 | * Clone repo! 24 | * Download Node.js at https://nodejs.org/ 25 | * Verify Node Package Manager is installed $ npm -v 26 | * If previous command does not echo npm version num search google for help :) 27 | * Install Bower (http://bower.io/), Grunt (http://gruntjs.com/) or Node Package Manager (https://www.npmjs.com/) for details. 28 | * To install use $ npm install 29 | * See bower.io, gruntjs or for more details. 30 | * Install Bower dependencies (this downloads all external needed js/css/etc for the app to run) 31 | * In command prompt navigate to the repo directory and use $ bower install 32 | * You may be asked to resolve dependencies. Don't panic. 33 | * Run app locally using grunt serve. Files are output to .tmp folder. 34 | * You may need to type in the localhost:port into your browser to view the running app. 35 | * Deploy app using grunt build and copy contents of .dist to serve 36 | 37 | ##Leveraged frameworks and libraries 38 | The application is built using bootstrap, sass, and AngularJS 1.4.7 with some angular plugins. It heavily relies on the Threejs and the angular leaflet directive. 39 | 40 | * threejs : http://threejs.org/ 41 | * angular leaflet : https://github.com/tombatossals/angular-leaflet-directive 42 | * bootstrap : http://getbootstrap.com/ 43 | * sass : http://sass-lang.com/ 44 | 45 | also used : 46 | * angular bootstrap : https://angular-ui.github.io/bootstrap/ 47 | * angular multiselect : http://dotansimha.github.io/angularjs-dropdown-multiselect/ 48 | * svg draw animation : http://tympanus.net/Development/SVGDrawingAnimation/ 49 | * angular timeslider : http://jsfiddle.net/lsiv568/WJqx7/1/ 50 | 51 | The app is currently structured into 4 main areas. All file paths are defined in APP_CONFIG in app.js. Feel free to change these paths to meet the needs of your app. 52 | 53 | ##1. 3D Clusterer 54 | The 3D clusterer takes a file of media data with 3d coordinates and displayed via threejs. Media is highlighted according to a distance algorithm that takes in account the distance of the camera from the origin and the distance of the media from the mouse and each other. Media can be selected by double clicking when it ise highlighted. When clicking Geo, your selected items are carried over to the geographic view and a represented geographically. 55 | 56 | Images are read from a file called imdata3d.txt stored in the images directory 57 | The format of the file is below: 58 | 1. filename 59 | 2. xcoord 60 | 3. ycoord 61 | 4. zcoord 62 | 63 | The records are delimited by tabs and each line is delimted by a new line. The media is read in from the mediaFactory.js 64 | 65 | ![alt tag](https://github.com/ngageoint/social-media-explorer-ui/blob/master/docs/3d-cluster-view.png) 66 | 67 | ##2. Geo 68 | The geographic view displays the media that are selected from the 3D clusterer on a leaflet provided map. The directive provides multiple basemaps and can be extended to provide more. If you click on one of the items from the row of selected media on the bottom of the screen, the map will focus on the image. 69 | 70 | The selectedMediaDisplay directive and service manages the selected items between the 3D Clusterer and Geo views. 71 | 72 | ![alt tag](https://github.com/ngageoint/social-media-explorer-ui/blob/master/docs/geo-view.png) 73 | 74 | ##3. Labeled Geo 75 | The labeled geo view presents media that was classified (labeled) by deep learning algorithms. Currently, a folder in the assets/images directory represents a class of labeled images. The buttons on the page are currently static, and simply send the path to a function that will refresh the screen with the desired data. images.csv with the specified path contains the data and is as follows: 76 | 77 | 1. mediafilename 78 | 2. lat 79 | 3. lng 80 | 4. message 81 | 82 | The records are delimited by commas and each line is delimited by a new line. This file is read in by the labeledMediaFactory and turned into a javascript object for easy access throughout the application. Modify this file to alter the object and/or data format. 83 | 84 | ##4. Ship Tracker 85 | The ship tracker loads data that is an array of arrays and is formatted as follows: 86 | In the current data format, one row of the array corresponds to one recording from a ship. Only a max of one record is recorded for a ship per day. 87 | 88 | Array values: 89 | 0. MMSI 90 | 1. Date 91 | 2. Status 92 | 3. Latitude 93 | 4. Longitude 94 | 5. shipname 95 | 6. type 96 | 7. name 97 | 8. country 98 | 9. IMO 99 | 10. YOB 100 | 11. GT 101 | 12. LOA 102 | 13. Beam 103 | 14. shiptype 104 | 15. DWT 105 | 16. Type 106 | 17. summary 107 | 18. Prev Port 108 | 19. Next Port 109 | 110 | ### only fields 1 through 8 are currently used in the application. The shipsFactory class reads in the data and provides an object to use to access throughout the application. Modify this file to alter the object and/or data format. 111 | 112 | The ship tracker colors ships green that are not anchored or moored. The tracker can be "played" to see the ships move over time. 113 | 114 | ##Origin 115 | Social Media Picture Explorer UI was developed at the National Geospatial-Intelligence Agency (NGA) in collaboration with Booz Allen. The government has "unlimited rights" and is releasing this software to increase the impact of government instruments by providing developers with the opportunity to take things in new directions. The software use, modification, and distribution rights are stipulated within the Apache 2.0 License. 116 | 117 | ###Pull Requests 118 | If you'd like to contribute to this project, please make a pull request. We'll review the pull request and discuss the changes. All pull request contributions to this project will be released under the Apache 2.0 license. 119 | 120 | Software source code previously released under an open source license and then modified by NGA staff is considered a "joint work" (see 17 USC 101); it is partially copyrighted, partially public domain, and as a whole is protected by the copyrights of the non-government authors and must be released according to the terms of the original open source license. 121 | -------------------------------------------------------------------------------- /app/components/shiptracker/shipTrackerController.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .controller("shipTrackerCtrl", ['$scope', 'ships', 'shipsFactory', '$interval', shipTrackerController]); 8 | 9 | function shipTrackerController($scope, ships, shipsFactory, $interval) { 10 | //model for ship types dropdown 11 | $scope.shipTypesModel = []; 12 | //settings for the ship type dropdown 13 | $scope.shipTypesDropDownSettings = { 14 | displayProp: 'label', 15 | idProp: 'label' 16 | }; 17 | //text for the ship type drop down 18 | $scope.shipTypesDropDownTexts = { 19 | buttonDefaultText: 'Select Ship Types' 20 | }; 21 | 22 | $scope.markerWidth = 40; 23 | $scope.markerHeight = 40; 24 | $scope.items = ships.getUniqueColValues(ships.typeIdx); 25 | 26 | $scope.$watch("shipTypesModel", function(event) { 27 | $scope.getData(ships, $scope.currentDate); 28 | }, true); 29 | 30 | $scope.layoutSliderChanged = function(value) { 31 | $scope.getData(ships, value); 32 | }; 33 | 34 | //events to occur on a double click on the leaflet map 35 | $scope.$on('leafletDirectiveMarker.dblclick', function(e, args) { 36 | // Args will contain the marker name and other relevant information 37 | var markerKey = args.modelName; //the key of the marker 38 | //get the lat and lng of the double clicked marker 39 | var lat = $scope.markers[markerKey]["lat"]; 40 | var lng = $scope.markers[markerKey]["lng"]; 41 | 42 | //resize the clicked marker 43 | var curMarker = $scope.markers[markerKey]; 44 | var icon = { 45 | iconSize: [300, 300], 46 | iconAnchor: [300 / 2, 0] 47 | }; 48 | 49 | angular.extend(curMarker["icon"], icon); 50 | 51 | //recenter the map to the double clicked image 52 | angular.extend($scope.center, { 53 | lat: lat, 54 | lng: lng, 55 | zoom: 10 56 | }); 57 | }); 58 | 59 | setSliderMinMaxDates(ships.getMinMaxDates()); 60 | 61 | //set the min and max dates for the slider from the range object 62 | function setSliderMinMaxDates(range) { 63 | $scope.layoutEndTimeMS = range.max; 64 | $scope.layoutStartTimeMS = range.min; 65 | $scope.currentDate = range.min; 66 | $scope.layoutCurTimeMS = range.min; 67 | } 68 | 69 | var stop; 70 | //increment the date every second automatically on play 71 | $scope.play = function() { 72 | // Don't start if already going 73 | if (angular.isDefined(stop)) return; 74 | 75 | stop = $interval(function() { 76 | 77 | var dayMS = 86400000; //ms in days 78 | 79 | //if the current date is less then the last date of the date range 80 | if ($scope.currentDate < $scope.layoutEndTimeMS) { 81 | //the new current date equals the current date + one day 82 | $scope.currentDate = $scope.currentDate + dayMS; 83 | //set the current time to the current date 84 | $scope.layoutCurTimeMS = $scope.currentDate; 85 | $scope.getData(ships, $scope.currentDate); 86 | } else { 87 | //if the date has incremented to the end stop play 88 | $scope.stopPlay(); 89 | } 90 | }, 1000); 91 | }; 92 | 93 | //stop the date from incrementing automatically 94 | $scope.stopPlay = function() { 95 | if (angular.isDefined(stop)) { 96 | $interval.cancel(stop); 97 | stop = undefined; 98 | } 99 | }; 100 | 101 | //update the data 102 | $scope.getData = function(data, date) { 103 | 104 | var tempData = _.filter(ships.data, function(elem) { 105 | return (new Date(elem[ships.dateIdx]).toLocaleDateString() == new Date(date).toLocaleDateString() && ($scope.shipTypesModel.length == 0 || _.findWhere($scope.shipTypesModel, { 106 | 'id': elem[ships.typeIdx] 107 | }) != undefined)); 108 | }); 109 | 110 | $scope.markers = updateMarkers(shipsFactory.setShips(tempData)); 111 | }; 112 | 113 | function updateMarkers(data) { 114 | 115 | var ctr = 0; 116 | var markers = {}; 117 | var totalLat = 0.0; 118 | var totalLng = 0.0; 119 | 120 | for (var idx = 0; idx < data.getCount(); idx++) { 121 | var iconUrl = data.getShipIconUrl(idx); 122 | 123 | var markerId = data.getDate(idx).toString() + data.getId(idx).toString(); 124 | var key = "marker" + markerId; 125 | 126 | markers[key] = { 127 | lat: data.getLatitude(idx), 128 | lng: data.getLongitude(idx), 129 | icon: { 130 | iconSize: [$scope.markerWidth, $scope.markerHeight], 131 | iconUrl: iconUrl 132 | }, 133 | message: "
" + data.getShipname(idx) + "
" + data.getStatus(idx) + "
" + data.getType(idx) 134 | }; 135 | 136 | totalLat += data.getLatitude(idx); 137 | totalLng += data.getLongitude(idx); 138 | } 139 | return markers; 140 | } 141 | 142 | 143 | angular.extend($scope, { 144 | center: { 145 | lat: 70, 146 | lng: 70, 147 | zoom: 3 148 | }, 149 | events: { 150 | map: { 151 | enable: ['zoomstart', 'zoomend', 'zoomlevelschange'], 152 | logic: 'emit' 153 | } 154 | }, 155 | markers: {}, 156 | layers: { 157 | baselayers: { 158 | mapbox_light: { 159 | name: 'Mapbox Light', 160 | url: 'http://api.tiles.mapbox.com/v4/{mapid}/{z}/{x}/{y}.png?access_token={apikey}', 161 | type: 'xyz', 162 | layerOptions: { 163 | apikey: 'pk.eyJ1IjoiYnVmYW51dm9scyIsImEiOiJLSURpX0pnIn0.2_9NrLz1U9bpwMQBhVk97Q', 164 | mapid: 'bufanuvols.lia22g09' 165 | } 166 | }, 167 | osm: { 168 | name: 'OpenStreetMap', 169 | url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 170 | type: 'xyz' 171 | } 172 | } 173 | } 174 | }); 175 | 176 | $scope.getData(ships); 177 | 178 | $scope.$on('$destroy', function() { 179 | // Make sure that the interval is destroyed too 180 | $scope.stopPlay(); 181 | }); 182 | 183 | 184 | } 185 | })(); -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright {yyyy} {name of copyright owner} 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | -------------------------------------------------------------------------------- /app/components/ngalogo/ngalogoDirective.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | angular 5 | .module('socialMediaExplorerApp') 6 | .directive('nga', ['$rootScope', '$state', ngaLogoDirective]); 7 | 8 | 9 | function ngaLogoDirective($rootScope, $state) { 10 | return { 11 | restrict: 'E', 12 | templateUrl: 'views/ngalogoView.html', 13 | link: function($rootScope, $stateParams, $scope, $state, $element) { 14 | (function(window) { 15 | 16 | 'use strict'; 17 | 18 | // class helper functions from bonzo https://github.com/ded/bonzo 19 | 20 | function classReg(className) { 21 | return new RegExp("(^|\\s+)" + className + "(\\s+|$)"); 22 | } 23 | 24 | // classList support for class management 25 | // altho to be fair, the api sucks because it won't accept multiple classes at once 26 | var hasClass, addClass, removeClass; 27 | 28 | if ('classList' in document.documentElement) { 29 | hasClass = function(elem, c) { 30 | return elem.classList.contains(c); 31 | }; 32 | addClass = function(elem, c) { 33 | elem.classList.add(c); 34 | }; 35 | removeClass = function(elem, c) { 36 | elem.classList.remove(c); 37 | }; 38 | } else { 39 | hasClass = function(elem, c) { 40 | return classReg(c).test(elem.className); 41 | }; 42 | addClass = function(elem, c) { 43 | if (!hasClass(elem, c)) { 44 | elem.className = elem.className + ' ' + c; 45 | } 46 | }; 47 | removeClass = function(elem, c) { 48 | elem.className = elem.className.replace(classReg(c), ' '); 49 | }; 50 | } 51 | 52 | function toggleClass(elem, c) { 53 | var fn = hasClass(elem, c) ? removeClass : addClass; 54 | fn(elem, c); 55 | } 56 | 57 | var classie = { 58 | // full names 59 | hasClass: hasClass, 60 | addClass: addClass, 61 | removeClass: removeClass, 62 | toggleClass: toggleClass, 63 | // short names 64 | has: hasClass, 65 | add: addClass, 66 | remove: removeClass, 67 | toggle: toggleClass 68 | }; 69 | 70 | // transport 71 | if (typeof define === 'function' && define.amd) { 72 | // AMD 73 | define(classie); 74 | } else { 75 | // browser global 76 | window.classie = classie; 77 | } 78 | 79 | })(window); 80 | (function() { 81 | 82 | 'use strict'; 83 | 84 | var docElem = window.document.documentElement; 85 | 86 | window.requestAnimFrame = function() { 87 | return ( 88 | window.requestAnimationFrame || 89 | window.webkitRequestAnimationFrame || 90 | window.mozRequestAnimationFrame || 91 | window.oRequestAnimationFrame || 92 | window.msRequestAnimationFrame || 93 | function( /* function */ callback) { 94 | window.setTimeout(callback, 1200 / 60); 95 | } 96 | ); 97 | }(); 98 | 99 | window.cancelAnimFrame = function() { 100 | return ( 101 | window.cancelAnimationFrame || 102 | window.webkitCancelAnimationFrame || 103 | window.mozCancelAnimationFrame || 104 | window.oCancelAnimationFrame || 105 | window.msCancelAnimationFrame || 106 | function(id) { 107 | window.clearTimeout(id); 108 | } 109 | ); 110 | }(); 111 | 112 | function SVGEl(el) { 113 | this.el = el; 114 | this.image = this.el.previousElementSibling; 115 | this.current_frame = 0; 116 | this.total_frames = 90; 117 | this.path = new Array(); 118 | this.length = new Array(); 119 | this.handle = 0; 120 | this.init(); 121 | } 122 | 123 | SVGEl.prototype.init = function() { 124 | var self = this; 125 | [].slice.call(this.el.querySelectorAll('path')).forEach(function(path, i) { 126 | self.path[i] = path; 127 | var l = self.path[i].getTotalLength(); 128 | self.length[i] = l; 129 | self.path[i].style.strokeDasharray = l + ' ' + l; 130 | self.path[i].style.strokeDashoffset = l; 131 | }); 132 | }; 133 | 134 | SVGEl.prototype.render = function() { 135 | if (this.rendered) return; 136 | this.rendered = true; 137 | this.draw(); 138 | }; 139 | 140 | SVGEl.prototype.draw = function() { 141 | var self = this, 142 | progress = this.current_frame / this.total_frames; 143 | if (progress > 1) { 144 | window.cancelAnimFrame(this.handle); 145 | this.showImage(); 146 | } else { 147 | this.current_frame++; 148 | for (var j = 0, len = this.path.length; j < len; j++) { 149 | this.path[j].style.strokeDashoffset = Math.floor(this.length[j] * (1 - progress)); 150 | } 151 | this.handle = window.requestAnimFrame(function() { 152 | self.draw(); 153 | }); 154 | } 155 | }; 156 | 157 | SVGEl.prototype.showImage = function() { 158 | classie.add(this.image, 'show'); 159 | classie.add(this.el, 'hide'); 160 | }; 161 | 162 | function getViewportH() { 163 | var client = docElem['clientHeight'], 164 | inner = window['innerHeight']; 165 | 166 | if (client < inner) 167 | return inner; 168 | else 169 | return client; 170 | } 171 | 172 | function scrollY() { 173 | return window.pageYOffset || docElem.scrollTop; 174 | } 175 | 176 | // http://stackoverflow.com/a/5598797/989439 177 | function getOffset(el) { 178 | var offsetTop = 0, 179 | offsetLeft = 0; 180 | do { 181 | if (!isNaN(el.offsetTop)) { 182 | offsetTop += el.offsetTop; 183 | } 184 | if (!isNaN(el.offsetLeft)) { 185 | offsetLeft += el.offsetLeft; 186 | } 187 | } while (el = el.offsetParent) 188 | 189 | return { 190 | top: offsetTop, 191 | left: offsetLeft 192 | }; 193 | } 194 | 195 | function inViewport(el, h) { 196 | var elH = el.offsetHeight, 197 | scrolled = scrollY(), 198 | viewed = scrolled + getViewportH(), 199 | elTop = getOffset(el).top, 200 | elBottom = elTop + elH, 201 | // if 0, the element is considered in the viewport as soon as it enters. 202 | // if 1, the element is considered in the viewport only when it's fully inside 203 | // value in percentage (1 >= h >= 0) 204 | h = h || 0; 205 | 206 | return (elTop + elH * h) <= viewed && (elBottom) >= scrolled; 207 | } 208 | 209 | function init() { 210 | var svgs = Array.prototype.slice.call(document.querySelectorAll('.drawings svg')), 211 | svgArr = new Array(), 212 | didScroll = false, 213 | resizeTimeout; 214 | 215 | // the svgs already shown... 216 | svgs.forEach(function(el, i) { 217 | var svg = new SVGEl(el); 218 | svgArr[i] = svg; 219 | setTimeout(function(el) { 220 | return function() { 221 | if (inViewport(el.parentNode)) { 222 | svg.render(); 223 | } 224 | }; 225 | }(el), 250); 226 | }); 227 | 228 | var scrollHandler = function() { 229 | if (!didScroll) { 230 | didScroll = true; 231 | setTimeout(function() { 232 | scrollPage(); 233 | }, 60); 234 | } 235 | }, 236 | scrollPage = function() { 237 | svgs.forEach(function(el, i) { 238 | if (inViewport(el.parentNode, 0.5)) { 239 | svgArr[i].render(); 240 | } 241 | }); 242 | didScroll = false; 243 | }, 244 | resizeHandler = function() { 245 | function delayed() { 246 | scrollPage(); 247 | resizeTimeout = null; 248 | } 249 | if (resizeTimeout) { 250 | clearTimeout(resizeTimeout); 251 | } 252 | resizeTimeout = setTimeout(delayed, 200); 253 | }; 254 | 255 | window.addEventListener('scroll', scrollHandler, false); 256 | window.addEventListener('resize', resizeHandler, false); 257 | } 258 | 259 | init(); 260 | })(); 261 | 262 | } 263 | } 264 | } 265 | })(); -------------------------------------------------------------------------------- /app/shared/imageClustererFactory.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .factory('imageClustererFactory', [returnViewerImageClusterer]); 8 | 9 | function returnViewerImageClusterer() { 10 | return new ViewerImageClusterer(); 11 | } 12 | 13 | function ViewerImageClusterer() { 14 | 15 | this.camera = null; 16 | this.selectedImages = null; 17 | this.highlightedImageCount = null; 18 | 19 | 20 | var scene, renderer, particles, geometry, camera, material, i, h, color, sprite, size, imagePlanes, raycaster, mouse, controls; 21 | var mouseX = 0, 22 | mouseY = 0, 23 | initRayCast = true; 24 | 25 | this.init = function() {} 26 | 27 | /* Public Methods */ 28 | this.CreateWorld = function(imageCoords, container) { 29 | THREE.ImageUtils.crossOrigin = ''; //allow loading of images from cross origin 30 | //set camera 31 | Cameras(); 32 | 33 | raycaster = new THREE.Raycaster(); 34 | mouse = new THREE.Vector2(); 35 | 36 | controls = new THREE.OrbitControls(camera, container); 37 | 38 | controls.rotateSpeed = 1.0; 39 | controls.zoomSpeed = 1.2; 40 | controls.panSpeed = 1.1; 41 | 42 | controls.noZoom = false; 43 | controls.noPan = false; 44 | 45 | controls.staticMoving = true; 46 | controls.dynamicDampingFactor = 0.3; 47 | 48 | controls.keys = [65, 83, 68]; 49 | 50 | //add fog to the scene to make images further back less vibrant 51 | scene = new THREE.Scene(); 52 | scene.fog = new THREE.FogExp2(0x353245, 0.02); 53 | Lights(); 54 | 55 | //add lighting to the scene 56 | //setup the images and then add all images to the scene 57 | geometry = new THREE.Geometry(); 58 | 59 | imagePlanes = Objects(imageCoords); 60 | scene.add(imagePlanes); 61 | 62 | //setup the webgl rendering 63 | renderer = new THREE.WebGLRenderer({ 64 | devicePixelRatio: 1, 65 | antialias: true 66 | }); 67 | 68 | renderer.setClearColor(0x252235); 69 | 70 | // set the size of the drawingBuffer 71 | renderer.setSize(window.innerWidth - 20, getWindowHeightAdjusted() - 83); 72 | container.appendChild(renderer.domElement); 73 | 74 | // User interaction 75 | Listeners(); 76 | } 77 | 78 | this.AnimateWorld = function(selectSize) { 79 | //pass initial raycast because screen will be set with incorrect images selected 80 | if (!initRayCast) { 81 | styleObjects(imagePlanes, camera, selectSize); 82 | } 83 | 84 | initRayCast = false; 85 | controls.update(); 86 | renderer.render(scene, camera); 87 | } 88 | 89 | this.Destroy = function(requestId, container) { 90 | window.cancelAnimationFrame(requestId); 91 | renderer.domElement.addEventListener('dblclick', null, false); //remove listener to render 92 | scene, camera, controls, renderer, geometry, imagePlanes = null; 93 | empty(container); 94 | 95 | function empty(elem) { 96 | while (elem.lastChild) elem.removeChild(elem.lastChild); 97 | } 98 | } 99 | 100 | var Cameras = function() { 101 | camera = new THREE.PerspectiveCamera(45, window.innerWidth / getWindowHeightAdjusted(), 1, 10000); 102 | camera.position.set(0, 0, 50); 103 | camera.lookAt(new THREE.Vector3(0, 0, 0)); 104 | } 105 | 106 | var Listeners = function() { 107 | document.addEventListener('mousemove', onMouseMove, false); 108 | window.addEventListener('resize', onWindowResize, false); 109 | //EVENTS 110 | //resize the window 111 | function onWindowResize() { 112 | camera.aspect = window.innerWidth / getWindowHeightAdjusted(); 113 | camera.updateProjectionMatrix(); 114 | renderer.setSize(window.innerWidth, getWindowHeightAdjusted()); 115 | } 116 | 117 | function onMouseMove(event) { 118 | // calculate mouse position in normalized device coordinates 119 | // (-1 to +1) for both components 120 | mouse.realX = event.clientX + 10; 121 | mouse.realY = event.clientY - 50; 122 | 123 | mouse.x = (mouse.realX / window.innerWidth) * 2 - 1; 124 | mouse.y = -(mouse.realY / getWindowHeightAdjusted()) * 2 + 1; 125 | } 126 | 127 | /* container.dblclick(function(event) { 128 | event.preventDefault(); 129 | selectedImages.images = _.pluck(highlightedImages, "imgSrc"); 130 | $scope.$apply(); 131 | camera.lookAt(new THREE.Vector3(0, 0, 0)); 132 | });*/ 133 | } 134 | 135 | var Lights = function() { 136 | var light; 137 | 138 | light = new THREE.DirectionalLight(0xffffff); 139 | light.position.set(1, 1, 1); 140 | scene.add(light); 141 | 142 | light = new THREE.DirectionalLight(0xffffff); 143 | light.position.set(-1, -1, -1); 144 | scene.add(light); 145 | 146 | var ambientLight = new THREE.AmbientLight(0xCCCCCC); 147 | scene.add(ambientLight); 148 | } 149 | 150 | var Objects = function(data) { 151 | var planes = new THREE.Object3D(); 152 | for (i = 0; i < 300; i++) { 153 | var bitmap = new Image(); 154 | bitmap.src = '/assets/images/thumbnails/' + data[i].split("\t")[0]; // Pre-load the bitmap, in conjunction with the Start button, to avoid any potential THREE.ImageUtils.loadTexture async issues. 155 | bitmap.onerror = function() { 156 | console.error("Error loading: " + bitmap.src); 157 | } 158 | 159 | var texture = THREE.ImageUtils.loadTexture(bitmap.src); // Create texture object based on the given bitmap path. 160 | texture.minFilter = THREE.LinearFilter 161 | var material = new THREE.MeshPhongMaterial({ 162 | map: texture 163 | }); // Create a material (for the spherical mesh) that reflects light, potentially causing sphere surface shadows. 164 | var plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); 165 | 166 | plane.position.x = data[i].split("\t")[1]; 167 | plane.position.y = data[i].split("\t")[2]; 168 | plane.position.z = data[i].split("\t")[3]; 169 | plane.imgSrc = bitmap.src; 170 | 171 | planes.add(plane); 172 | } 173 | return planes; 174 | } 175 | 176 | var styleObjects = function(objects, camera, selectSize) { 177 | var selectedObjects = []; //array to hold the euclidean distances of all images to the mouse location 178 | 179 | //for each image, style the image and calculate the distance to the mouse 180 | objects.children.forEach(function(obj) { 181 | //style the image 182 | styleUnselectedObject(obj); 183 | 184 | var toSelect = toSelectObject(obj, camera, selectSize); 185 | 186 | if (toSelect) { 187 | selectedObjects.push({ 188 | "obj": obj 189 | }); 190 | } 191 | }); 192 | 193 | //closestCoords is equal to the closest coordinates intersecting the mouse. 194 | //if no object is intersecting with the mouse, the camera coordinates are returned 195 | var closestCoords = getClosestIntersectingObjCoords(mouse, camera, objects.children) != null ? 196 | getClosestIntersectingObjCoords(mouse, camera, objects.children) : { 197 | x: camera.position.x, 198 | y: camera.position.y, 199 | z: camera.position.z 200 | }; 201 | 202 | //calculate the distance between the selected objects and the closest coordinates 203 | var distances = getObjectAndCoordsDist(selectedObjects, closestCoords); 204 | 205 | //sort the images by the distance 206 | distances = _.sortBy(distances, "dist3D"); 207 | 208 | //get the coordinates of the closest object 209 | var closestObjCoords = null; 210 | if (distances.length > 0) { 211 | closestObjCoords = { 212 | x: distances[0].obj.position.x, 213 | y: distances[0].obj.position.y, 214 | z: distances[0].obj.position.z 215 | } 216 | } 217 | 218 | //select the object that are within a certain distance from the closest selectable coordinates 219 | //and style them appropriately 220 | var highlightedImages = []; 221 | 222 | //if an object existed in distance that is within a selectable range 223 | if (closestObjCoords != null) { 224 | distances.forEach(function(obj) { 225 | var objPosition = { 226 | x: obj.obj.position.x, 227 | y: obj.obj.position.y, 228 | z: obj.obj.position.z 229 | } 230 | if (euclideanDistance3D(objPosition, closestObjCoords) < 3 * selectSize) { 231 | highlightedImages.push(obj.obj); 232 | styleSelectedObject(obj); 233 | } 234 | }); 235 | } 236 | var highlightedImageCount = highlightedImages.length; 237 | 238 | } 239 | 240 | var styleUnselectedObject = function(plane) { 241 | plane.lookAt(camera.position); 242 | plane.material.color.setRGB(.75, .75, .75); 243 | plane.scale.set(1, 1, 1); 244 | } 245 | 246 | var styleSelectedObject = function(obj) { 247 | obj.obj.material.color.setRGB(1.0, .5, .5); 248 | obj.obj.scale.set(1.5, 1.5, 1.5); 249 | } 250 | 251 | var toSelectObject = function(obj, camera, selectSize) { 252 | //calculate the distance from the image to the mouse 253 | var distMouseToPlane = euclideanDistance(toScreenPosition(obj, camera), { 254 | 'x': mouse.x, 255 | 'y': mouse.y 256 | }); 257 | 258 | //the current position of the camera 259 | var cameraPosition = { 260 | x: camera.position.x, 261 | y: camera.position.y, 262 | z: camera.position.z 263 | }; 264 | 265 | //the position of the origin 266 | var originPosition = { 267 | x: 0, 268 | y: 0, 269 | z: 0 270 | }; 271 | 272 | //calculate the distance of the camera from the origin 273 | var cameraDistFromOrigin = euclideanDistance3D(cameraPosition, originPosition); 274 | 275 | //toSelectPlane determines if the plane should be selected based on the distance of the mouse to this plane 276 | //if its distance is less than the selectSize as chosen by the user or set by default scaled by the distance 277 | //the camera from the origin (the distance between planes and the mouse becomes smaller the further the camera 278 | //is from the origin) 279 | var toSelect = (distMouseToPlane / 3) < (selectSize / (cameraDistFromOrigin + 2)); 280 | 281 | return toSelect; 282 | } 283 | 284 | var getClosestIntersectingObjCoords = function(mouse, camera, objects) { 285 | // calculate planes intersecting the picking ray 286 | raycaster.setFromCamera(mouse, camera); 287 | var intersects = raycaster.intersectObjects(objects, true); 288 | 289 | //set closest coords equal to an intertersecting plane or the camera if no intersection is found 290 | if (typeof(intersects) !== "undefined" && intersects.length > 0) { 291 | return { 292 | x: intersects[0].object.position.x, 293 | y: intersects[0].object.position.y, 294 | z: intersects[0].object.position.z 295 | } 296 | } else { 297 | return null; 298 | } 299 | } 300 | 301 | var getObjectAndCoordsDist = function(objects, coords) { 302 | return _.map(objects, function(obj) { 303 | var objectPosition = { 304 | x: obj.obj.position.x, 305 | y: obj.obj.position.y, 306 | z: obj.obj.position.z 307 | }; 308 | 309 | return { 310 | "obj": obj.obj, 311 | "dist3D": euclideanDistance3D(coords, objectPosition) 312 | }; 313 | }); 314 | } 315 | 316 | //calculate the euclidean distance between two spaces 317 | var euclideanDistance = function(p, q) { 318 | p.x = (p.x / window.innerWidth) * 2 - 1; 319 | p.y = -(p.y / getWindowHeightAdjusted()) * 2 + 1; 320 | return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2)); 321 | } 322 | 323 | var euclideanDistance3D = function(loc1, loc2) { 324 | return Math.sqrt(Math.pow(loc1.x - loc2.x, 2) + Math.pow(loc1.y - loc2.y, 2) + Math.pow(loc1.z - loc2.z, 2)); 325 | } 326 | //project the image from 3d space to 2d coordinates 327 | var toScreenPosition = function(obj, camera) { 328 | var vector = new THREE.Vector3(); 329 | 330 | var widthHalf = 0.5 * renderer.domElement.width; 331 | var heightHalf = 0.5 * renderer.domElement.height; 332 | 333 | obj.updateMatrixWorld(true); 334 | vector.setFromMatrixPosition(obj.matrixWorld); 335 | vector.project(camera); 336 | 337 | vector.x = (vector.x * widthHalf) + widthHalf; 338 | vector.y = -(vector.y * heightHalf) + heightHalf; 339 | 340 | return { 341 | x: vector.x, 342 | y: vector.y 343 | }; 344 | } 345 | var getWindowHeightAdjusted = function() { 346 | return window.innerHeight - 45; 347 | } 348 | this.AnimateWorld.bind(this); 349 | } 350 | 351 | })(); -------------------------------------------------------------------------------- /app/components/threeDimMediaClusterer/threeDimMediaClustererDirective.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('socialMediaExplorerApp') 7 | .directive("threeDimMediaClusterer", [threedDimMediaClustererDirective]); 8 | 9 | function threedDimMediaClustererDirective(selectedMediaFactory) { 10 | return { 11 | restrict: "E", 12 | scope: { 13 | media: "=media" 14 | }, 15 | templateUrl: 'views/threeDimMediaClustererInnerView.html', 16 | link: link, 17 | controller: controller, 18 | controllerAs: 'vm', 19 | bindToController: true 20 | }; 21 | 22 | function controller($scope, selectedMediaFactory) { 23 | var vm = this; 24 | 25 | //scope variables to display for general information 26 | //the currently selected images 27 | vm.selectedMedia = selectedMediaFactory; 28 | //the amount of media currently highlighted (media are highlighted via proximity to mouse) 29 | vm.highlightedMediaCount = 0; 30 | //the size of the select range, currently can range from 1 to 3. 31 | //Alters the proximity of media highlighted and selected in relation to the mouse 32 | vm.selectSize = 2; 33 | 34 | //changes the select size based on the object passed to the function 35 | //1 is the min select size 36 | //3 is the max select size 37 | vm.changeSelectSize = function(obj) { 38 | var sizeChange = obj.target.attributes.size.value; //get the selected object sizeChange value (increase or decrease) 39 | if (sizeChange.toLowerCase() == "increase") { 40 | vm.selectSize = vm.selectSize < 3 ? vm.selectSize + 1 : vm.selectSize; 41 | } else if (sizeChange.toLowerCase() == "decrease") { 42 | vm.selectSize = vm.selectSize > 1 ? vm.selectSize - 1 : vm.selectSize; 43 | } 44 | } 45 | } 46 | 47 | function link($scope, elem, attr) { 48 | //init the 3d media clusterer with the media coordinates 49 | if (!Detector.webgl) Detector.addGetWebGLMessage(); 50 | 51 | var vm = $scope.vm; 52 | var container, stats; 53 | var scene, renderer, particles, geometry, material, i, h, color, sprite, size, mediaPlanes, raycaster, mouse, controls; 54 | var mouseX = 0, 55 | mouseY = 0, 56 | initRayCast = true; 57 | 58 | createWorld(vm.media); 59 | animateWorld(); 60 | 61 | // When the destroy event is triggered, check to see if the above 62 | // data is still available. 63 | $scope.$on( 64 | "$destroy", 65 | function handleDestroyEvent() { 66 | cancelAnimationFrame(vm.requestId); 67 | renderer.domElement.addEventListener('dblclick', null, false); //remove listener to render 68 | scene, camera, controls, renderer, geometry, mediaPlanes, vm.camera = null; 69 | empty(elem.find("#container")[0]); 70 | 71 | function empty(elem) { 72 | while (elem.lastChild) elem.removeChild(elem.lastChild); 73 | } 74 | } 75 | ); 76 | 77 | function createWorld(media) { 78 | 79 | __createScene__(); 80 | 81 | function __createScene__() { 82 | THREE.ImageUtils.crossOrigin = ''; //allow loading of media from cross origin 83 | container = elem.find("#container")[0]; //container for 3js 84 | 85 | vm.camera = new THREE.PerspectiveCamera(45, window.innerWidth / getWindowHeightAdjusted(), 1, 10000); 86 | vm.camera.position.set(0, 0, 50); 87 | vm.camera.lookAt(new THREE.Vector3(0, 0, 0)); 88 | 89 | raycaster = new THREE.Raycaster(); 90 | mouse = new THREE.Vector2(); 91 | controls = new THREE.OrbitControls(vm.camera, container); 92 | 93 | controls.rotateSpeed = 1.0; 94 | controls.zoomSpeed = 1.2; 95 | controls.panSpeed = 1.1; 96 | 97 | controls.noZoom = false; 98 | controls.noPan = false; 99 | 100 | controls.staticMoving = true; 101 | controls.dynamicDampingFactor = 0.3; 102 | 103 | controls.keys = [65, 83, 68]; 104 | 105 | //add fog to the scene to make images further back less vibrant 106 | scene = new THREE.Scene(); 107 | scene.fog = new THREE.FogExp2(0x353245, 0.02); 108 | 109 | //add lighting to the scene 110 | var light; 111 | 112 | light = new THREE.DirectionalLight(0xffffff); 113 | light.position.set(1, 1, 1); 114 | scene.add(light); 115 | 116 | light = new THREE.DirectionalLight(0xffffff); 117 | light.position.set(-1, -1, -1); 118 | scene.add(light); 119 | 120 | var ambientLight = new THREE.AmbientLight(0xCCCCCC); 121 | scene.add(ambientLight); 122 | 123 | //setup the images and then add all images to the scene 124 | geometry = new THREE.Geometry(); 125 | 126 | mediaPlanes = __createMediaPlanes__(media); 127 | scene.add(mediaPlanes); 128 | 129 | //setup the webgl rendering 130 | renderer = new THREE.WebGLRenderer({ 131 | devicePixelRatio: 1, 132 | antialias: true 133 | }); 134 | 135 | renderer.setClearColor(0x252235); 136 | 137 | // set the size of the drawingBuffer 138 | renderer.setSize(window.innerWidth - 20, getWindowHeightAdjusted() - 83); 139 | container.appendChild(renderer.domElement); 140 | 141 | // User interaction 142 | document.addEventListener('mousemove', onMouseMove, false); 143 | window.addEventListener('resize', onWindowResize, false); 144 | 145 | function __createMediaPlanes__(media) { 146 | var planes = new THREE.Object3D(); 147 | var maxMediaToShow = 300; //max size set to an amount of media that won't crush the user's browser 148 | var amtMediaToShow = media.getCount() > maxMediaToShow ? maxMediaToShow : media.getCount(); 149 | 150 | for (i = 0; i < amtMediaToShow; i++) { 151 | var bitmap = new Image(); 152 | bitmap.src = media.getMediaUrl(i); // Pre-load the bitmap, in conjunction with the Start button, to avoid any potential THREE.ImageUtils.loadTexture async issues. 153 | bitmap.onerror = function() { 154 | console.error("Error loading: " + bitmap.src); 155 | } 156 | 157 | var texture = THREE.ImageUtils.loadTexture(bitmap.src); // Create texture object based on the given bitmap path. 158 | texture.minFilter = THREE.LinearFilter 159 | var material = new THREE.MeshPhongMaterial({ 160 | map: texture 161 | }); // Create a material (for the spherical mesh) that reflects light, potentially causing sphere surface shadows. 162 | var plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); 163 | 164 | plane.position.x = media.getXCoordinate(i); 165 | plane.position.y = media.getYCoordinate(i); 166 | plane.position.z = media.getZCoordinate(i); 167 | plane.imgSrc = bitmap.src; 168 | 169 | planes.add(plane); 170 | } 171 | return planes; 172 | } 173 | 174 | //EVENTS 175 | //resize the window 176 | function onWindowResize() { 177 | vm.camera.aspect = window.innerWidth / getWindowHeightAdjusted(); 178 | vm.camera.updateProjectionMatrix(); 179 | renderer.setSize(window.innerWidth, getWindowHeightAdjusted()); 180 | } 181 | 182 | function onMouseMove(event) { 183 | // calculate mouse position in normalized device coordinates 184 | // (-1 to +1) for both components 185 | mouse.realX = event.clientX + 10; 186 | mouse.realY = event.clientY - 50; 187 | 188 | mouse.x = (mouse.realX / window.innerWidth) * 2 - 1; 189 | mouse.y = -(mouse.realY / getWindowHeightAdjusted()) * 2 + 1; 190 | } 191 | 192 | elem.dblclick(function(event) { 193 | event.preventDefault(); 194 | vm.selectedMedia.media = _.pluck(vm.highlightedMedia, "imgSrc"); 195 | $scope.$apply(); 196 | vm.camera.lookAt(new THREE.Vector3(0, 0, 0)); 197 | }); 198 | } 199 | 200 | 201 | } //end init function 202 | 203 | function animateWorld() { 204 | 205 | vm.requestId = requestAnimationFrame(animateWorld); //three js speak to call the animate method every so often 206 | 207 | //pass initial raycast because screen will be set with incorrect media selected 208 | if (!initRayCast) { 209 | __styleObjects__(mediaPlanes, vm.camera, vm.selectSize); 210 | } 211 | 212 | initRayCast = false; 213 | controls.update(); 214 | renderer.render(scene, vm.camera); 215 | 216 | function __styleObjects__(objects, camera, selectSize) { 217 | var selectedObjects = []; //array to hold the euclidean distances of all media to the mouse location 218 | 219 | //for each media, style the media and calculate the distance to the mouse 220 | objects.children.forEach(function(obj) { 221 | //style the image 222 | styleUnselectedObject(obj); 223 | 224 | var toSelect = toSelectObject(obj, camera, selectSize); 225 | 226 | if (toSelect) { 227 | selectedObjects.push({ 228 | "obj": obj 229 | }); 230 | } 231 | }); 232 | 233 | //closestCoords is equal to the closest coordinates intersecting the mouse. 234 | //if no object is intersecting with the mouse, the camera coordinates are returned 235 | var closestCoords = getClosestIntersectingObjCoords(mouse, camera, objects.children) != null ? 236 | getClosestIntersectingObjCoords(mouse, camera, objects.children) : { 237 | x: vm.camera.position.x, 238 | y: vm.camera.position.y, 239 | z: vm.camera.position.z 240 | }; 241 | 242 | //calculate the distance between the selected objects and the closest coordinates 243 | var distances = getObjectAndCoordsDist(selectedObjects, closestCoords); 244 | 245 | //sort the media by the distance 246 | distances = _.sortBy(distances, "dist3D"); 247 | 248 | //get the coordinates of the closest object 249 | var closestObjCoords = null; 250 | if (distances.length > 0) { 251 | closestObjCoords = { 252 | x: distances[0].obj.position.x, 253 | y: distances[0].obj.position.y, 254 | z: distances[0].obj.position.z 255 | } 256 | } 257 | 258 | //select the object that are within a certain distance from the closest selectable coordinates 259 | //and style them appropriately 260 | vm.highlightedMedia = []; 261 | 262 | //if an object existed in distance that is within a selectable range 263 | if (closestObjCoords != null) { 264 | distances.forEach(function(obj) { 265 | var objPosition = { 266 | x: obj.obj.position.x, 267 | y: obj.obj.position.y, 268 | z: obj.obj.position.z 269 | } 270 | if (euclideanDistance3D(objPosition, closestObjCoords) < 3 * vm.selectSize) { 271 | vm.highlightedMedia.push(obj.obj); 272 | styleSelectedObject(obj); 273 | } 274 | }); 275 | } 276 | vm.highlightedMediaCount = vm.highlightedMedia.length; 277 | $scope.$apply(); 278 | } 279 | 280 | function styleUnselectedObject(plane) { 281 | plane.lookAt(vm.camera.position); 282 | plane.material.color.setRGB(.75, .75, .75); 283 | plane.scale.set(1, 1, 1); 284 | } 285 | 286 | function styleSelectedObject(obj) { 287 | obj.obj.material.color.setRGB(1.0, .5, .5); 288 | obj.obj.scale.set(1.5, 1.5, 1.5); 289 | } 290 | 291 | function toSelectObject(obj, camera, selectSize) { 292 | //calculate the distance from the media to the mouse 293 | var distMouseToPlane = euclideanDistance(toScreenPosition(obj, camera), { 294 | 'x': mouse.x, 295 | 'y': mouse.y 296 | }); 297 | 298 | //the current position of the camera 299 | var cameraPosition = { 300 | x: camera.position.x, 301 | y: camera.position.y, 302 | z: camera.position.z 303 | }; 304 | 305 | //the position of the origin 306 | var originPosition = { 307 | x: 0, 308 | y: 0, 309 | z: 0 310 | }; 311 | 312 | //calculate the distance of the camera from the origin 313 | var cameraDistFromOrigin = euclideanDistance3D(cameraPosition, originPosition); 314 | 315 | //toSelectPlane determines if the plane should be selected based on the distance of the mouse to this plane 316 | //if its distance is less than the selectSize as chosen by the user or set by default scaled by the distance 317 | //the camera from the origin (the distance between planes and the mouse becomes smaller the further the camera 318 | //is from the origin) 319 | var toSelect = (distMouseToPlane / 3) < (selectSize / (cameraDistFromOrigin + 2)); 320 | 321 | return toSelect; 322 | } 323 | 324 | function getClosestIntersectingObjCoords(mouse, camera, objects) { 325 | // calculate planes intersecting the picking ray 326 | raycaster.setFromCamera(mouse, camera); 327 | var intersects = raycaster.intersectObjects(objects, true); 328 | 329 | //set closest coords equal to an intertersecting plane or the camera if no intersection is found 330 | if (typeof(intersects) !== "undefined" && intersects.length > 0) { 331 | return { 332 | x: intersects[0].object.position.x, 333 | y: intersects[0].object.position.y, 334 | z: intersects[0].object.position.z 335 | } 336 | } else { 337 | return null; 338 | } 339 | } 340 | 341 | function getObjectAndCoordsDist(objects, coords) { 342 | return _.map(objects, function(obj) { 343 | var objectPosition = { 344 | x: obj.obj.position.x, 345 | y: obj.obj.position.y, 346 | z: obj.obj.position.z 347 | }; 348 | 349 | return { 350 | "obj": obj.obj, 351 | "dist3D": euclideanDistance3D(coords, objectPosition) 352 | }; 353 | }); 354 | } 355 | 356 | //calculate the euclidean distance between two spaces 357 | function euclideanDistance(p, q) { 358 | p.x = (p.x / window.innerWidth) * 2 - 1; 359 | p.y = -(p.y / getWindowHeightAdjusted()) * 2 + 1; 360 | return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2)); 361 | } 362 | 363 | function euclideanDistance3D(loc1, loc2) { 364 | return Math.sqrt(Math.pow(loc1.x - loc2.x, 2) + Math.pow(loc1.y - loc2.y, 2) + Math.pow(loc1.z - loc2.z, 2)); 365 | } 366 | //project the media from 3d space to 2d coordinates 367 | function toScreenPosition(obj, camera) { 368 | var vector = new THREE.Vector3(); 369 | 370 | var widthHalf = 0.5 * renderer.domElement.width; 371 | var heightHalf = 0.5 * renderer.domElement.height; 372 | 373 | obj.updateMatrixWorld(true); 374 | vector.setFromMatrixPosition(obj.matrixWorld); 375 | vector.project(camera); 376 | 377 | vector.x = (vector.x * widthHalf) + widthHalf; 378 | vector.y = -(vector.y * heightHalf) + heightHalf; 379 | 380 | return { 381 | x: vector.x, 382 | y: vector.y 383 | }; 384 | } 385 | } 386 | 387 | function getWindowHeightAdjusted() { 388 | return window.innerHeight - 45; 389 | } 390 | } 391 | } 392 | })(); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2015-11-04 using generator-angular 0.11.1 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 %>/components/**/{,*/}*.js', '<%= yeoman.app %>/shared/**/{,*/}*.js', '{,*/}*.js'], 38 | tasks: ['copy:scripts'], 39 | options: { 40 | livereload: '<%= connect.options.livereload %>' 41 | } 42 | }, 43 | html: { 44 | files: ['<%= yeoman.app %>/components/**/{,*/}*.html', '<%= yeoman.app %>/shared/**/{,*/}*.html', '{,*/}*.html'], 45 | tasks: ['copy:views'], 46 | options: { 47 | livereload: '<%= connect.options.livereload %>' 48 | } 49 | }, 50 | jsTest: { 51 | files: ['test/spec/{,*/}*.js'], 52 | tasks: ['newer:jshint:test', 'karma '] 53 | }, 54 | compass: { 55 | files: ['assets/css/{,*/}*.{scss,sass}'], 56 | tasks: ['compass:server', 'autoprefixer'] 57 | }, 58 | gruntfile: { 59 | files: ['Gruntfile.js'] 60 | }, 61 | livereload: { 62 | options: { 63 | livereload: '<%= connect.options.livereload %>' 64 | }, 65 | files: [ 66 | '<%= yeoman.app %>/{,*/}*.html', 67 | '.tmp/styles/{,*/}*.css' 68 | ] 69 | } 70 | }, 71 | 72 | // The actual grunt server settings 73 | connect: { 74 | options: { 75 | port: 9002, 76 | // Change this to '0.0.0.0' to access the server from outside. 77 | hostname: 'localhost', 78 | livereload: 35729 79 | }, 80 | livereload: { 81 | options: { 82 | open: false, 83 | middleware: function(connect) { 84 | return [ 85 | connect.static('.tmp'), 86 | connect().use( 87 | '/bower_components', 88 | connect.static('./bower_components') 89 | ), //, 90 | /*connect().use( 91 | '/assets/css', 92 | connect.static('./assets/css') 93 | ),*/ 94 | connect.static(appConfig.app) 95 | ]; 96 | } 97 | } 98 | }, 99 | test: { 100 | options: { 101 | port: 9001, 102 | middleware: function(connect) { 103 | return [ 104 | connect.static('.tmp'), 105 | connect.static('test'), 106 | connect().use( 107 | '/bower_components', 108 | connect.static('./bower_components') 109 | ), 110 | connect.static(appConfig.app) 111 | ]; 112 | } 113 | } 114 | }, 115 | dist: { 116 | options: { 117 | open: true, 118 | base: '<%= yeoman.dist %>' 119 | } 120 | } 121 | }, 122 | 123 | includeSource: { 124 | options: { 125 | basePath: '', 126 | baseUrl: 'scripts/', 127 | flatten: true 128 | }, 129 | server: { 130 | files: { 131 | '.tmp/index.html': '<%= yeoman.app %>/index.html' 132 | } 133 | }, 134 | dist: { 135 | files: { 136 | '<%= yeoman.dist %>/index.html': '<%= yeoman.app %>/index.html' 137 | } 138 | } 139 | }, 140 | 141 | // Make sure code styles are up to par and there are no obvious mistakes 142 | jshint: { 143 | options: { 144 | jshintrc: '.jshintrc', 145 | reporter: require('jshint-stylish') 146 | }, 147 | all: { 148 | src: [ 149 | 'Gruntfile.js', 150 | '<%= yeoman.app %>/**/{,*/}*.js' 151 | ] 152 | }, 153 | test: { 154 | options: { 155 | jshintrc: 'test/.jshintrc' 156 | }, 157 | src: ['test/spec/{,*/}*.js'] 158 | } 159 | }, 160 | 161 | // Empties folders to start fresh 162 | clean: { 163 | dist: { 164 | files: [{ 165 | dot: true, 166 | src: [ 167 | '.tmp', 168 | '<%= yeoman.dist %>/{,*/}*', 169 | '!<%= yeoman.dist %>/.git{,*/}*' 170 | ] 171 | }] 172 | }, 173 | server: '.tmp' 174 | }, 175 | 176 | // Add vendor prefixed styles 177 | autoprefixer: { 178 | options: { 179 | browsers: ['last 1 version'] 180 | }, 181 | server: { 182 | options: { 183 | map: true, 184 | }, 185 | files: [{ 186 | expand: true, 187 | cwd: '.tmp/styles/', 188 | src: '{,*/}*.css', 189 | dest: '.tmp/styles/' 190 | }] 191 | }, 192 | dist: { 193 | files: [{ 194 | expand: true, 195 | cwd: '<%= yeoman.dist %>/styles/', 196 | src: '{,*/}*.css', 197 | dest: '<%= yeoman.dist %>/styles/' 198 | }] 199 | } 200 | }, 201 | 202 | // Automatically inject Bower components into the app 203 | wiredep: { 204 | app: { 205 | src: ['<%= yeoman.app %>/index.html'], 206 | ignorePath: /\.\.\// 207 | }, 208 | test: { 209 | devDependencies: true, 210 | src: '<%= karma.unit.configFile %>', 211 | ignorePath: /\.\.\//, 212 | fileTypes: { 213 | js: { 214 | block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, 215 | detect: { 216 | js: /'(.*\.js)'/gi 217 | }, 218 | replace: { 219 | js: '\'{{filePath}}\',' 220 | } 221 | } 222 | } 223 | }, 224 | sass: { 225 | src: ['assets/css/{,*/}*.{scss,sass}'], 226 | ignorePath: /(\.\.\/){1,2}bower_components\// 227 | } 228 | }, 229 | 230 | // Compiles Sass to CSS and generates necessary files if requested 231 | compass: { 232 | options: { 233 | sassDir: 'assets/css', 234 | cssDir: '.tmp/styles', 235 | generatedImagesDir: '.tmp/images/generated', 236 | imagesDir: 'assets/images', 237 | javascriptsDir: '/assets/js', 238 | fontsDir: 'assets/styles/fonts', 239 | importPath: './bower_components', 240 | httpImagesPath: '/images', 241 | httpGeneratedImagesPath: '/images/generated', 242 | httpFontsPath: '/styles/fonts', 243 | relativeAssets: false, 244 | assetCacheBuster: false, 245 | raw: 'Sass::Script::Number.precision = 10\n' 246 | }, 247 | dist: { 248 | options: { 249 | generatedImagesDir: '<%= yeoman.dist %>/images/generated' 250 | } 251 | }, 252 | server: { 253 | options: { 254 | sourcemap: true 255 | } 256 | } 257 | }, 258 | 259 | // Renames files for browser caching purposes 260 | filerev: { 261 | dist: { 262 | src: [ 263 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 264 | '<%= yeoman.dist %>/styles/{,*/}*.css', 265 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 266 | '<%= yeoman.dist %>/styles/fonts/*' 267 | ] 268 | } 269 | }, 270 | 271 | // Reads HTML for usemin blocks to enable smart builds that automatically 272 | // concat, minify and revision files. Creates configurations in memory so 273 | // additional tasks can operate on them 274 | 275 | useminPrepare: { 276 | html: '<%= yeoman.dist %>/index.html', 277 | options: { 278 | dest: '<%= yeoman.dist %>', 279 | flow: { 280 | html: { 281 | steps: { 282 | js: ['concat'], 283 | css: ['cssmin'] 284 | }, 285 | post: {} 286 | } 287 | } 288 | } 289 | }, 290 | 291 | // Performs rewrites based on filerev and the useminPrepare configuration 292 | usemin: { 293 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 294 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 295 | options: { 296 | assetsDirs: [ 297 | '<%= yeoman.dist %>', 298 | '<%= yeoman.dist %>/images', 299 | '<%= yeoman.dist %>/styles' 300 | ] 301 | } 302 | }, 303 | 304 | // The following *-min tasks will produce minified files in the dist folder 305 | // By default, your `index.html`'s will take care of 306 | // minification. These next options are pre-configured if you do not wish 307 | // to use the Usemin blocks. 308 | 309 | // cssmin: { 310 | // dist: { 311 | // files: { 312 | // '<%= yeoman.dist %>/styles/main.css': [ 313 | // '.tmp/styles/{,*/}*.css' 314 | // ] 315 | // } 316 | // } 317 | // }, 318 | // uglify: { 319 | // dist: { 320 | // files: { 321 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 322 | // '<%= yeoman.dist %>/scripts/scripts.js' 323 | // ] 324 | // }, 325 | // options: { 326 | // banner: '/*\n <%= grunt.template.today("yyyy-mm-dd") %> \n*/\n' 327 | // } 328 | // } 329 | // }, 330 | // concat: { 331 | // dist: {} 332 | // }, 333 | 334 | imagemin: { 335 | dist: { 336 | files: [{ 337 | expand: true, 338 | cwd: '<%= yeoman.app %>/assets/images', 339 | src: '{,*/}*.{png,jpg,jpeg,gif}', 340 | dest: '<%= yeoman.dist %>/images' 341 | }] 342 | } 343 | }, 344 | 345 | svgmin: { 346 | dist: { 347 | files: [{ 348 | expand: true, 349 | cwd: '<%= yeoman.app %>/assets/images', 350 | src: '{,*/}*.svg', 351 | dest: '<%= yeoman.dist %>/images' 352 | }] 353 | } 354 | }, 355 | 356 | htmlmin: { 357 | dist: { 358 | options: { 359 | collapseWhitespace: true, 360 | conservativeCollapse: true, 361 | collapseBooleanAttributes: true, 362 | removeCommentsFromCDATA: true, 363 | removeOptionalTags: true 364 | }, 365 | files: [{ 366 | expand: true, 367 | cwd: '<%= yeoman.dist %>', 368 | src: ['*.html', '**/{,*/}*.html'], 369 | dest: '<%= yeoman.dist %>' 370 | }] 371 | } 372 | }, 373 | 374 | // ng-annotate tries to make the code safe for minification automatically 375 | // by using the Angular long form for dependency injection. 376 | ngAnnotate: { 377 | dist: { 378 | files: [{ 379 | expand: true, 380 | cwd: '.tmp/concat/scripts', 381 | src: '*.js', 382 | dest: '.tmp/concat/scripts' 383 | }] 384 | } 385 | }, 386 | 387 | // Replace Google CDN references 388 | cdnify: { 389 | dist: { 390 | html: ['<%= yeoman.dist %>/*.html'] 391 | } 392 | }, 393 | 394 | // Copies remaining files to places other tasks can use 395 | copy: { 396 | dist: { 397 | files: [{ 398 | expand: true, 399 | cwd: '<%= yeoman.app %>/assets/css', 400 | dest: '<%= yeoman.dist %>/styles', 401 | src: '{,*/}*.css' 402 | }, { 403 | expand: true, 404 | flatten: true, 405 | cwd: '', 406 | dest: '.tmp/scripts/', 407 | src: ['<%= yeoman.app %>/{,*/}*.js', '<%= yeoman.app %>/shared/**/{,*/}*.js', '<%= yeoman.app %>/components/**/{,*/}*.js', 'assets/js/**/{,*/}*.js'] 408 | }, { 409 | expand: true, 410 | flatten: true, 411 | cwd: '<%= yeoman.app %>', 412 | dest: '<%= yeoman.dist %>/views/', 413 | src: ['shared/**/{,*/}*.html', 'components/**/{,*/}*.html', '{,*/}*.html', '!index.html'] 414 | }, { 415 | expand: true, 416 | flatten: false, 417 | cwd: '', 418 | dest: '<%= yeoman.dist %>/', 419 | src: ['assets/data/**/*.*'] 420 | }, { 421 | expand: true, 422 | flatten: false, 423 | cwd: '', 424 | dest: '<%= yeoman.dist %>/', 425 | src: ['assets/images/**/*.*'] 426 | }] 427 | }, 428 | styles: { 429 | expand: true, 430 | cwd: '<%= yeoman.app %>/assets/css', 431 | dest: '.tmp/styles/', 432 | src: '{,*/}*.css' 433 | }, 434 | scripts: { 435 | expand: true, 436 | flatten: true, 437 | cwd: '', 438 | dest: '.tmp/scripts/', 439 | src: ['<%= yeoman.app %>/{,*/}*.js', '<%= yeoman.app %>/shared/**/{,*/}*.js', '<%= yeoman.app %>/components/**/{,*/}*.js', 'assets/js/**/{,*/}*.js'] 440 | }, 441 | views: { 442 | expand: true, 443 | flatten: true, 444 | cwd: '<%= yeoman.app %>', 445 | dest: '.tmp/views/', 446 | src: ['shared/**/{,*/}*.html', 'components/**/{,*/}*.html', '{,*/}*.html'] 447 | }, 448 | data: { 449 | expand: true, 450 | flatten: false, 451 | cwd: '', 452 | dest: '.tmp/', 453 | src: ['assets/data/**/*.*'] 454 | }, 455 | images: { 456 | expand: true, 457 | flatten: false, 458 | cwd: '', 459 | dest: '.tmp/', 460 | src: ['assets/images/**/*.*'] 461 | } 462 | }, 463 | 464 | // Run some tasks in parallel to speed up the build process 465 | concurrent: { 466 | server: [ 467 | 'compass:server' 468 | ], 469 | test: [ 470 | 'compass' 471 | ], 472 | dist: [ 473 | 'compass:dist', 474 | 'imagemin', 475 | 'svgmin' 476 | ] 477 | }, 478 | 479 | // Test settings 480 | karma: { 481 | unit: { 482 | configFile: 'test/karma.conf.js', 483 | singleRun: true 484 | } 485 | } 486 | }); 487 | 488 | grunt.registerTask('serve', 'Compile then start a connect web server', function(target) { 489 | if (target === 'dist') { 490 | return grunt.task.run(['build', 'connect:dist:keepalive']); 491 | } 492 | 493 | grunt.task.run([ 494 | 'clean:server', 495 | 'wiredep', 496 | 'includeSource:server', 497 | 'copy:scripts', 498 | 'copy:views', 499 | 'copy:images', 500 | 'copy:data', 501 | 'concurrent:server', 502 | 'autoprefixer:server', 503 | 'connect:livereload', 504 | 'watch' 505 | ]); 506 | }); 507 | 508 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function(target) { 509 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 510 | grunt.task.run(['serve:' + target]); 511 | }); 512 | 513 | grunt.registerTask('test', [ 514 | 'clean:server', 515 | 'wiredep', 516 | 'includeSource:server', 517 | 'concurrent:test', 518 | 'autoprefixer', 519 | 'connect:test', 520 | 'karma' 521 | ]); 522 | 523 | grunt.registerTask('build', [ 524 | 'clean:dist', 525 | 'wiredep', 526 | 'includeSource:dist', 527 | 'copy:dist', 528 | 'useminPrepare', 529 | 'concurrent:dist', 530 | 'autoprefixer', 531 | 'concat', 532 | 'ngAnnotate', 533 | 'cssmin', 534 | // 'uglify', 535 | 'filerev', 536 | 'usemin', 537 | 'htmlmin' 538 | ]); 539 | 540 | grunt.registerTask('default', [ 541 | 'newer:jshint', 542 | 'test', 543 | 'build' 544 | ]); 545 | }; -------------------------------------------------------------------------------- /assets/js/ThreeOrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | 18 | THREE.OrbitControls = function(object, domElement) { 19 | 20 | this.object = object; 21 | this.domElement = (domElement !== undefined) ? domElement : document; 22 | 23 | // API 24 | 25 | // Set to false to disable this control 26 | this.enabled = true; 27 | 28 | // "target" sets the location of focus, where the control orbits around 29 | // and where it pans with respect to. 30 | this.target = new THREE.Vector3(); 31 | 32 | // center is old, deprecated; use "target" instead 33 | this.center = this.target; 34 | 35 | // This option actually enables dollying in and out; left as "zoom" for 36 | // backwards compatibility 37 | this.noZoom = false; 38 | this.zoomSpeed = 1.0; 39 | 40 | // Limits to how far you can dolly in and out ( PerspectiveCamera only ) 41 | this.minDistance = 0; 42 | this.maxDistance = Infinity; 43 | 44 | // Limits to how far you can zoom in and out ( OrthographicCamera only ) 45 | this.minZoom = 0; 46 | this.maxZoom = Infinity; 47 | 48 | // Set to true to disable this control 49 | this.noRotate = false; 50 | this.rotateSpeed = 1.0; 51 | 52 | // Set to true to disable this control 53 | this.noPan = false; 54 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 55 | 56 | // Set to true to automatically rotate around the target 57 | this.autoRotate = false; 58 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 59 | 60 | // How far you can orbit vertically, upper and lower limits. 61 | // Range is 0 to Math.PI radians. 62 | this.minPolarAngle = 0; // radians 63 | this.maxPolarAngle = Math.PI; // radians 64 | 65 | // How far you can orbit horizontally, upper and lower limits. 66 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 67 | this.minAzimuthAngle = -Infinity; // radians 68 | this.maxAzimuthAngle = Infinity; // radians 69 | 70 | // Set to true to disable use of the keys 71 | this.noKeys = false; 72 | 73 | // The four arrow keys 74 | this.keys = { 75 | LEFT: 37, 76 | UP: 38, 77 | RIGHT: 39, 78 | BOTTOM: 40 79 | }; 80 | 81 | // Mouse buttons 82 | this.mouseButtons = { 83 | ORBIT: THREE.MOUSE.LEFT, 84 | ZOOM: THREE.MOUSE.MIDDLE, 85 | PAN: THREE.MOUSE.RIGHT 86 | }; 87 | 88 | //////////// 89 | // internals 90 | 91 | var scope = this; 92 | 93 | var EPS = 0.000001; 94 | 95 | var rotateStart = new THREE.Vector2(); 96 | var rotateEnd = new THREE.Vector2(); 97 | var rotateDelta = new THREE.Vector2(); 98 | 99 | var panStart = new THREE.Vector2(); 100 | var panEnd = new THREE.Vector2(); 101 | var panDelta = new THREE.Vector2(); 102 | var panOffset = new THREE.Vector3(); 103 | 104 | var offset = new THREE.Vector3(); 105 | 106 | var dollyStart = new THREE.Vector2(); 107 | var dollyEnd = new THREE.Vector2(); 108 | var dollyDelta = new THREE.Vector2(); 109 | 110 | var theta; 111 | var phi; 112 | var phiDelta = 0; 113 | var thetaDelta = 0; 114 | var scale = 1; 115 | var pan = new THREE.Vector3(); 116 | 117 | var lastPosition = new THREE.Vector3(); 118 | var lastQuaternion = new THREE.Quaternion(); 119 | 120 | var STATE = { 121 | NONE: -1, 122 | ROTATE: 0, 123 | DOLLY: 1, 124 | PAN: 2, 125 | TOUCH_ROTATE: 3, 126 | TOUCH_DOLLY: 4, 127 | TOUCH_PAN: 5 128 | }; 129 | 130 | var state = STATE.NONE; 131 | 132 | // for reset 133 | 134 | this.target0 = this.target.clone(); 135 | this.position0 = this.object.position.clone(); 136 | this.zoom0 = this.object.zoom; 137 | 138 | // so camera.up is the orbit axis 139 | 140 | var quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)); 141 | var quatInverse = quat.clone().inverse(); 142 | 143 | // events 144 | 145 | var changeEvent = { 146 | type: 'change' 147 | }; 148 | var startEvent = { 149 | type: 'start' 150 | }; 151 | var endEvent = { 152 | type: 'end' 153 | }; 154 | 155 | this.rotateLeft = function(angle) { 156 | 157 | if (angle === undefined) { 158 | 159 | angle = getAutoRotationAngle(); 160 | 161 | } 162 | 163 | thetaDelta -= angle; 164 | 165 | }; 166 | 167 | this.rotateUp = function(angle) { 168 | 169 | if (angle === undefined) { 170 | 171 | angle = getAutoRotationAngle(); 172 | 173 | } 174 | 175 | phiDelta -= angle; 176 | 177 | }; 178 | 179 | // pass in distance in world space to move left 180 | this.panLeft = function(distance) { 181 | 182 | var te = this.object.matrix.elements; 183 | 184 | // get X column of matrix 185 | panOffset.set(te[0], te[1], te[2]); 186 | panOffset.multiplyScalar(-distance); 187 | 188 | pan.add(panOffset); 189 | 190 | }; 191 | 192 | // pass in distance in world space to move up 193 | this.panUp = function(distance) { 194 | 195 | var te = this.object.matrix.elements; 196 | 197 | // get Y column of matrix 198 | panOffset.set(te[4], te[5], te[6]); 199 | panOffset.multiplyScalar(distance); 200 | 201 | pan.add(panOffset); 202 | 203 | }; 204 | 205 | // pass in x,y of change desired in pixel space, 206 | // right and down are positive 207 | this.pan = function(deltaX, deltaY) { 208 | 209 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 210 | 211 | if (scope.object instanceof THREE.PerspectiveCamera) { 212 | 213 | // perspective 214 | var position = scope.object.position; 215 | var offset = position.clone().sub(scope.target); 216 | var targetDistance = offset.length(); 217 | 218 | // half of the fov is center to top of screen 219 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); 220 | 221 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 222 | scope.panLeft(2 * deltaX * targetDistance / element.clientHeight); 223 | scope.panUp(2 * deltaY * targetDistance / element.clientHeight); 224 | 225 | } else if (scope.object instanceof THREE.OrthographicCamera) { 226 | 227 | // orthographic 228 | scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth); 229 | scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight); 230 | 231 | } else { 232 | 233 | // camera neither orthographic or perspective 234 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); 235 | 236 | } 237 | 238 | }; 239 | 240 | this.dollyIn = function(dollyScale) { 241 | 242 | if (dollyScale === undefined) { 243 | 244 | dollyScale = getZoomScale(); 245 | 246 | } 247 | 248 | if (scope.object instanceof THREE.PerspectiveCamera) { 249 | 250 | scale /= dollyScale; 251 | 252 | } else if (scope.object instanceof THREE.OrthographicCamera) { 253 | 254 | scope.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom * dollyScale)); 255 | scope.object.updateProjectionMatrix(); 256 | scope.dispatchEvent(changeEvent); 257 | 258 | } else { 259 | 260 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 261 | 262 | } 263 | 264 | }; 265 | 266 | this.dollyOut = function(dollyScale) { 267 | 268 | if (dollyScale === undefined) { 269 | 270 | dollyScale = getZoomScale(); 271 | 272 | } 273 | 274 | if (scope.object instanceof THREE.PerspectiveCamera) { 275 | 276 | scale *= dollyScale; 277 | 278 | } else if (scope.object instanceof THREE.OrthographicCamera) { 279 | 280 | scope.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom / dollyScale)); 281 | scope.object.updateProjectionMatrix(); 282 | scope.dispatchEvent(changeEvent); 283 | 284 | } else { 285 | 286 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 287 | 288 | } 289 | 290 | }; 291 | 292 | this.update = function() { 293 | 294 | var position = this.object.position; 295 | 296 | offset.copy(position).sub(this.target); 297 | 298 | // rotate offset to "y-axis-is-up" space 299 | offset.applyQuaternion(quat); 300 | 301 | // angle from z-axis around y-axis 302 | 303 | theta = Math.atan2(offset.x, offset.z); 304 | 305 | // angle from y-axis 306 | 307 | phi = Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y); 308 | 309 | if (this.autoRotate && state === STATE.NONE) { 310 | 311 | this.rotateLeft(getAutoRotationAngle()); 312 | 313 | } 314 | 315 | theta += thetaDelta; 316 | phi += phiDelta; 317 | 318 | // restrict theta to be between desired limits 319 | theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta)); 320 | 321 | // restrict phi to be between desired limits 322 | phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi)); 323 | 324 | // restrict phi to be betwee EPS and PI-EPS 325 | phi = Math.max(EPS, Math.min(Math.PI - EPS, phi)); 326 | 327 | var radius = offset.length() * scale; 328 | 329 | // restrict radius to be between desired limits 330 | radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius)); 331 | 332 | // move target to panned location 333 | this.target.add(pan); 334 | 335 | offset.x = radius * Math.sin(phi) * Math.sin(theta); 336 | offset.y = radius * Math.cos(phi); 337 | offset.z = radius * Math.sin(phi) * Math.cos(theta); 338 | 339 | // rotate offset back to "camera-up-vector-is-up" space 340 | offset.applyQuaternion(quatInverse); 341 | 342 | position.copy(this.target).add(offset); 343 | 344 | this.object.lookAt(this.target); 345 | 346 | thetaDelta = 0; 347 | phiDelta = 0; 348 | scale = 1; 349 | pan.set(0, 0, 0); 350 | 351 | // update condition is: 352 | // min(camera displacement, camera rotation in radians)^2 > EPS 353 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 354 | 355 | if (lastPosition.distanceToSquared(this.object.position) > EPS || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { 356 | 357 | this.dispatchEvent(changeEvent); 358 | 359 | lastPosition.copy(this.object.position); 360 | lastQuaternion.copy(this.object.quaternion); 361 | 362 | } 363 | 364 | }; 365 | 366 | 367 | this.reset = function() { 368 | 369 | state = STATE.NONE; 370 | 371 | this.target.copy(this.target0); 372 | this.object.position.copy(this.position0); 373 | this.object.zoom = this.zoom0; 374 | 375 | this.object.updateProjectionMatrix(); 376 | this.dispatchEvent(changeEvent); 377 | 378 | this.update(); 379 | 380 | }; 381 | 382 | this.getPolarAngle = function() { 383 | 384 | return phi; 385 | 386 | }; 387 | 388 | this.getAzimuthalAngle = function() { 389 | 390 | return theta 391 | 392 | }; 393 | 394 | function getAutoRotationAngle() { 395 | 396 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 397 | 398 | } 399 | 400 | function getZoomScale() { 401 | 402 | return Math.pow(0.95, scope.zoomSpeed); 403 | 404 | } 405 | 406 | function onMouseDown(event) { 407 | 408 | if (scope.enabled === false) return; 409 | event.preventDefault(); 410 | 411 | if (event.button === scope.mouseButtons.ORBIT) { 412 | if (scope.noRotate === true) return; 413 | 414 | state = STATE.ROTATE; 415 | 416 | rotateStart.set(event.clientX, event.clientY); 417 | 418 | } else if (event.button === scope.mouseButtons.ZOOM) { 419 | if (scope.noZoom === true) return; 420 | 421 | state = STATE.DOLLY; 422 | 423 | dollyStart.set(event.clientX, event.clientY); 424 | 425 | } else if (event.button === scope.mouseButtons.PAN) { 426 | if (scope.noPan === true) return; 427 | 428 | state = STATE.PAN; 429 | 430 | panStart.set(event.clientX, event.clientY); 431 | 432 | } 433 | 434 | if (state !== STATE.NONE) { 435 | document.addEventListener('mousemove', onMouseMove, false); 436 | document.addEventListener('mouseup', onMouseUp, false); 437 | scope.dispatchEvent(startEvent); 438 | } 439 | 440 | } 441 | 442 | function onMouseMove(event) { 443 | 444 | if (scope.enabled === false) return; 445 | 446 | event.preventDefault(); 447 | 448 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 449 | 450 | if (state === STATE.ROTATE) { 451 | 452 | if (scope.noRotate === true) return; 453 | 454 | rotateEnd.set(event.clientX, event.clientY); 455 | rotateDelta.subVectors(rotateEnd, rotateStart); 456 | 457 | // rotating across whole screen goes 360 degrees around 458 | scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); 459 | 460 | // rotating up and down along whole screen attempts to go 360, but limited to 180 461 | scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); 462 | 463 | rotateStart.copy(rotateEnd); 464 | 465 | } else if (state === STATE.DOLLY) { 466 | 467 | if (scope.noZoom === true) return; 468 | 469 | dollyEnd.set(event.clientX, event.clientY); 470 | dollyDelta.subVectors(dollyEnd, dollyStart); 471 | 472 | if (dollyDelta.y > 0) { 473 | 474 | scope.dollyIn(); 475 | 476 | } else if (dollyDelta.y < 0) { 477 | 478 | scope.dollyOut(); 479 | 480 | } 481 | 482 | dollyStart.copy(dollyEnd); 483 | 484 | } else if (state === STATE.PAN) { 485 | 486 | if (scope.noPan === true) return; 487 | 488 | panEnd.set(event.clientX, event.clientY); 489 | panDelta.subVectors(panEnd, panStart); 490 | 491 | scope.pan(panDelta.x, panDelta.y); 492 | 493 | panStart.copy(panEnd); 494 | 495 | } 496 | 497 | if (state !== STATE.NONE) scope.update(); 498 | 499 | } 500 | 501 | function onMouseUp( /* event */ ) { 502 | 503 | if (scope.enabled === false) return; 504 | 505 | document.removeEventListener('mousemove', onMouseMove, false); 506 | document.removeEventListener('mouseup', onMouseUp, false); 507 | scope.dispatchEvent(endEvent); 508 | state = STATE.NONE; 509 | 510 | } 511 | 512 | function onMouseWheel(event) { 513 | 514 | if (scope.enabled === false || scope.noZoom === true || state !== STATE.NONE) return; 515 | 516 | event.preventDefault(); 517 | event.stopPropagation(); 518 | 519 | var delta = 0; 520 | 521 | if (event.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9 522 | 523 | delta = event.wheelDelta; 524 | 525 | } else if (event.detail !== undefined) { // Firefox 526 | 527 | delta = -event.detail; 528 | 529 | } 530 | 531 | if (delta > 0) { 532 | 533 | scope.dollyOut(); 534 | 535 | } else if (delta < 0) { 536 | 537 | scope.dollyIn(); 538 | 539 | } 540 | 541 | scope.update(); 542 | scope.dispatchEvent(startEvent); 543 | scope.dispatchEvent(endEvent); 544 | 545 | } 546 | 547 | function onKeyDown(event) { 548 | 549 | if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; 550 | 551 | switch (event.keyCode) { 552 | 553 | case scope.keys.UP: 554 | scope.pan(0, scope.keyPanSpeed); 555 | scope.update(); 556 | break; 557 | 558 | case scope.keys.BOTTOM: 559 | scope.pan(0, -scope.keyPanSpeed); 560 | scope.update(); 561 | break; 562 | 563 | case scope.keys.LEFT: 564 | scope.pan(scope.keyPanSpeed, 0); 565 | scope.update(); 566 | break; 567 | 568 | case scope.keys.RIGHT: 569 | scope.pan(-scope.keyPanSpeed, 0); 570 | scope.update(); 571 | break; 572 | 573 | } 574 | 575 | } 576 | 577 | function touchstart(event) { 578 | 579 | if (scope.enabled === false) return; 580 | 581 | switch (event.touches.length) { 582 | 583 | case 1: // one-fingered touch: rotate 584 | 585 | if (scope.noRotate === true) return; 586 | 587 | state = STATE.TOUCH_ROTATE; 588 | 589 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 590 | break; 591 | 592 | case 2: // two-fingered touch: dolly 593 | 594 | if (scope.noZoom === true) return; 595 | 596 | state = STATE.TOUCH_DOLLY; 597 | 598 | var dx = event.touches[0].pageX - event.touches[1].pageX; 599 | var dy = event.touches[0].pageY - event.touches[1].pageY; 600 | var distance = Math.sqrt(dx * dx + dy * dy); 601 | dollyStart.set(0, distance); 602 | break; 603 | 604 | case 3: // three-fingered touch: pan 605 | 606 | if (scope.noPan === true) return; 607 | 608 | state = STATE.TOUCH_PAN; 609 | 610 | panStart.set(event.touches[0].pageX, event.touches[0].pageY); 611 | break; 612 | 613 | default: 614 | 615 | state = STATE.NONE; 616 | 617 | } 618 | 619 | if (state !== STATE.NONE) scope.dispatchEvent(startEvent); 620 | 621 | } 622 | 623 | function touchmove(event) { 624 | 625 | if (scope.enabled === false) return; 626 | 627 | event.preventDefault(); 628 | event.stopPropagation(); 629 | 630 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 631 | 632 | switch (event.touches.length) { 633 | 634 | case 1: // one-fingered touch: rotate 635 | 636 | if (scope.noRotate === true) return; 637 | if (state !== STATE.TOUCH_ROTATE) return; 638 | 639 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 640 | rotateDelta.subVectors(rotateEnd, rotateStart); 641 | 642 | // rotating across whole screen goes 360 degrees around 643 | scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); 644 | // rotating up and down along whole screen attempts to go 360, but limited to 180 645 | scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); 646 | 647 | rotateStart.copy(rotateEnd); 648 | 649 | scope.update(); 650 | break; 651 | 652 | case 2: // two-fingered touch: dolly 653 | 654 | if (scope.noZoom === true) return; 655 | if (state !== STATE.TOUCH_DOLLY) return; 656 | 657 | var dx = event.touches[0].pageX - event.touches[1].pageX; 658 | var dy = event.touches[0].pageY - event.touches[1].pageY; 659 | var distance = Math.sqrt(dx * dx + dy * dy); 660 | 661 | dollyEnd.set(0, distance); 662 | dollyDelta.subVectors(dollyEnd, dollyStart); 663 | 664 | if (dollyDelta.y > 0) { 665 | 666 | scope.dollyOut(); 667 | 668 | } else if (dollyDelta.y < 0) { 669 | 670 | scope.dollyIn(); 671 | 672 | } 673 | 674 | dollyStart.copy(dollyEnd); 675 | 676 | scope.update(); 677 | break; 678 | 679 | case 3: // three-fingered touch: pan 680 | 681 | if (scope.noPan === true) return; 682 | if (state !== STATE.TOUCH_PAN) return; 683 | 684 | panEnd.set(event.touches[0].pageX, event.touches[0].pageY); 685 | panDelta.subVectors(panEnd, panStart); 686 | 687 | scope.pan(panDelta.x, panDelta.y); 688 | 689 | panStart.copy(panEnd); 690 | 691 | scope.update(); 692 | break; 693 | 694 | default: 695 | 696 | state = STATE.NONE; 697 | 698 | } 699 | 700 | } 701 | 702 | function touchend( /* event */ ) { 703 | 704 | if (scope.enabled === false) return; 705 | 706 | scope.dispatchEvent(endEvent); 707 | state = STATE.NONE; 708 | 709 | } 710 | 711 | this.domElement.addEventListener('contextmenu', function(event) { 712 | event.preventDefault(); 713 | }, false); 714 | this.domElement.addEventListener('mousedown', onMouseDown, false); 715 | this.domElement.addEventListener('mousewheel', onMouseWheel, false); 716 | this.domElement.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox 717 | 718 | this.domElement.addEventListener('touchstart', touchstart, false); 719 | this.domElement.addEventListener('touchend', touchend, false); 720 | this.domElement.addEventListener('touchmove', touchmove, false); 721 | 722 | window.addEventListener('keydown', onKeyDown, false); 723 | 724 | // force an update at start 725 | this.update(); 726 | 727 | }; 728 | 729 | THREE.OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype); 730 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------