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 |
--------------------------------------------------------------------------------
/viewer/app/images/mars.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-dynamodb-mars-json-demo/81c2536951bdfea5fbf3852b943d87c0e4a4cb32/viewer/app/images/mars.jpg
--------------------------------------------------------------------------------
/viewer/app/images/rover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-dynamodb-mars-json-demo/81c2536951bdfea5fbf3852b943d87c0e4a4cb32/viewer/app/images/rover.jpg
--------------------------------------------------------------------------------
/viewer/app/images/rover_jumbotron.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amazon-archives/aws-dynamodb-mars-json-demo/81c2536951bdfea5fbf3852b943d87c0e4a4cb32/viewer/app/images/rover_jumbotron.jpg
--------------------------------------------------------------------------------
/viewer/app/index.jade:
--------------------------------------------------------------------------------
1 | extends views/layout
2 |
3 | block content
4 |
5 |
6 | .jumbotron
7 | h1 MSL Image Explorer
8 | p Let's explore Mars with the rover!
9 |
10 |
11 | div(ng-view="")
12 |
13 | .copyright
14 | p Images Courtesy NASA/JPL-Caltech.
15 |
16 |
--------------------------------------------------------------------------------
/viewer/app/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/viewer/app/scripts/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | angular.module('MSLImageExplorerApp',
3 | [
4 | 'ngRoute',
5 | 'ngSanitize',
6 | 'ngTouch',
7 | 'ui.bootstrap.modal',
8 | 'config'
9 | ]).
10 | config(['$routeProvider', function($routeProvider) {
11 | $routeProvider.
12 | when('/', {
13 | templateUrl: 'views/partials/timeline.html',
14 | controller: 'TimelineCtrl'
15 | }).
16 | when('/timeline', {
17 | templateUrl: 'views/partials/timeline.html',
18 | controller: 'TimelineCtrl'
19 | }).
20 | when('/timeline/:instrument', {
21 | templateUrl: 'views/partials/timeline.html',
22 | controller: 'TimelineCtrl'
23 | }).
24 | when('/timeline/:instrument/:time', {
25 | templateUrl: 'views/partials/timeline.html',
26 | controller: 'TimelineCtrl'
27 | }).
28 | when('/topVoted', {
29 | templateUrl: 'views/partials/image-gallery.html',
30 | controller: 'TopVotedCtrl'
31 | }).
32 | when('/topVoted/:instrument', {
33 | templateUrl: 'views/partials/image-gallery.html',
34 | controller: 'TopVotedCtrl'
35 | }).
36 | when('/favorites', {
37 | templateUrl: 'views/partials/image-gallery.html',
38 | controller: 'FavoritesCtrl'
39 | }).
40 | otherwise({
41 | redirectTo: '/'
42 | });
43 | }]);
44 |
45 |
46 |
--------------------------------------------------------------------------------
/viewer/app/scripts/controllers/dialog.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /**
3 | * @ngdoc function
4 | * @name MSLImageExplorerApp.controller:DialogCtrl
5 | * @description
6 | * # DialogCtrl
7 | * Controller of the MSLImageExplorerApp that handles the error dialog view.
8 | */
9 | angular.module('MSLImageExplorerApp').
10 | controller('DialogCtrl', function ($scope, $modalInstance, title, message) {
11 | /**
12 | * Sets the title.
13 | */
14 | $scope.title = title;
15 |
16 | /**
17 | * Sets the message.
18 | */
19 | $scope.message = message;
20 |
21 | /**
22 | * Closes the dialog.
23 | */
24 | $scope.close = function(){
25 | $modalInstance.close();
26 | };
27 | });
28 |
29 |
--------------------------------------------------------------------------------
/viewer/app/scripts/controllers/favorites.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | global $
4 | */
5 |
6 | /**
7 | * @ngdoc function
8 | * @name MSLImageExplorerApp.controller:FavoritesCtrl
9 | * @description
10 | * # FavoritesCtrl
11 | * Controller of the MSLImageExplorerApp that takes care of favorite photos view.
12 | */
13 | angular.module('MSLImageExplorerApp').
14 | controller('FavoritesCtrl', function ($scope, $log, MarsPhotosDBAccess, Blueimp) {
15 |
16 | $scope.title = 'Mars Images You Liked';
17 | $scope.description = 'Photos that you liked.';
18 | $scope.photos = [];
19 |
20 | /**
21 | * Fetches photos that the user has voted
22 | * via MarsPhotos service and replaces $scope.photos with
23 | * the results. It also triggers Blueimp
24 | * image gallery to prepare for the slideshow.
25 | */
26 | $scope.updatePhotos = function(){
27 | MarsPhotosDBAccess.queryUserVotedPhotos({
28 | hashKey: localStorage.getItem('userid'),
29 | lastEvaluatedKey: $scope.lastEvaluatedKey
30 | }, function(error, data){
31 | if (!error) {
32 | var photos = data.Items;
33 | $scope.lastEvaluatedKey = data.LastEvaluatedKey;
34 | $scope.$apply(function(){
35 | $scope.photos = photos;
36 | });
37 | } else {
38 | $log.error(error);
39 | }
40 | });
41 | };
42 |
43 | $scope.updatePhotos();
44 | Blueimp.Gallery($('#links a'), $('#blueimp-gallery').data());
45 | });
46 |
47 |
--------------------------------------------------------------------------------
/viewer/app/scripts/controllers/sidemenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | global $
4 | */
5 |
6 | /**
7 | * @ngdoc function
8 | * @name MSLImageExplorerApp.controller:SideMenuCtrl
9 | * @description
10 | * # SideMenuCtrl
11 | * Controller of the MSLImageExplorerApp that takes care of sidemenu a.k.a Mission Control.
12 | */
13 | angular.module('MSLImageExplorerApp').
14 | controller('SideMenuCtrl', function ($scope, $modal, $log, MarsPhotosDBAccess) {
15 | /**
16 | * List of instruments to select from.
17 | */
18 | $scope.instrumentList = MarsPhotosDBAccess.instrumentList;
19 |
20 | $scope.active = true;
21 |
22 | /**
23 | * Flag that indicates if the sidebar is initialized
24 | */
25 | $scope.isSidrInitialized = false;
26 |
27 | /**
28 | * Flag that indicates if the instrument list should be shown or not.
29 | */
30 | $scope.showInstrumentList = false;
31 |
32 | /**
33 | * Opens the side menu. It initializes the component
34 | * on the first time it is called.
35 | */
36 | $scope.openSidr = function(){
37 | if (!$scope.isSidrInitialized) {
38 | initSidr();
39 | $scope.isSidrInitialized = true;
40 | }
41 | $.sidr('open');
42 | };
43 |
44 | /**
45 | * Closes the side menu.
46 | */
47 | $scope.closeSidr = function(){
48 | $.sidr('close');
49 | if ($scope.modalInstance) {
50 | $scope.modalInstance.dismiss('cancel');
51 | }
52 | };
53 |
54 | /**
55 | * Shows a modal window that shows the rover detail figure and
56 | * the list of instruments on the side menu.
57 | */
58 | $scope.showRoverDetails = function(){
59 | $scope.modalInstance = $modal.open({
60 | templateUrl: 'views/partials/rover-detail.html',
61 | controller: 'DialogCtrl',
62 | resolve: {
63 | title: 'Instrument Selection',
64 | message: ''
65 | }
66 | });
67 | $scope.showInstrumentList = true;
68 | };
69 |
70 | /**
71 | * Sets the instrument to the one the user selected and reloads
72 | * the parent view, e.g. the timeline view and top-voted photos view.
73 | * It also closes the modal window that shows the rover details.
74 | */
75 | $scope.setInstrument = function(instrument, $event) {
76 | $log.debug('Instrument is set to ' + instrument);
77 | $($event.currentTarget).addClass('active').siblings().removeClass('active');
78 | $scope.$parent.instrument = instrument;
79 | $scope.showInstrumentList = false;
80 | $scope.modalInstance.dismiss('cancel');
81 |
82 | reloadParentView();
83 | };
84 |
85 | /**
86 | * Called when the view is destroyed and closes the side menu.
87 | */
88 | $scope.$on('$destroy', function(){
89 | $scope.closeSidr();
90 | });
91 |
92 | /**
93 | * Calls reload function in the parent controller, e.g. timeline or
94 | * top-voted photos view, if exists. It triggers reloading of the
95 | * parent view with the specified time and instrument.
96 | */
97 | var reloadParentView = function(){
98 | if (typeof($scope.$parent.reload) === 'function') {
99 | $log.debug('Reloading parent view');
100 | $scope.$parent.reload();
101 | } else {
102 | $log.warn('Parent view does not have reload()');
103 | }
104 | };
105 |
106 | /**
107 | * Initializes the side menu and the date picker in it.
108 | */
109 | var initSidr = function(){
110 | $('#sidemenu').sidr({
111 | side: 'right'
112 | });
113 | $('.datepicker').datepicker('update', new Date());
114 | if ($scope.time) {
115 | try {
116 | $('.datepicker').datepicker('update', new Date(parseInt($scope.time)));
117 | } catch (e) {
118 | $log.error('Invalid time specified: ' + $scope.time);
119 | }
120 | }
121 |
122 | $('.datepicker').datepicker().on('changeDate', function(ev){
123 | $(ev.target).datepicker('hide');
124 | $scope.$parent.time = ev.date.valueOf();
125 | reloadParentView();
126 | });
127 | $('.datepicker').datepicker().on('show', function(){
128 | // Workaround to remove a triangle at left top
129 | $('.datepicker-dropdown').removeClass('datepicker-orient-top');
130 | });
131 | };
132 | });
133 |
134 |
--------------------------------------------------------------------------------
/viewer/app/scripts/controllers/timeline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | global $, moment
4 | */
5 |
6 | /**
7 | * @ngdoc function
8 | * @name MSLImageExplorerApp.controller:TimelineCtrl
9 | * @description
10 | * # TimelineCtrl
11 | * Controller of the MSLImageExplorerApp that takes care of the main timeline view.
12 | */
13 | angular.module('MSLImageExplorerApp').
14 | controller('TimelineCtrl', function ($scope, $log, $location, $routeParams, $timeout, $modal, MarsPhotosDBAccess, Blueimp) {
15 | var timelinePosition = $('.timeline').position();
16 | $scope.isUpdatingPhotos = false;
17 | $scope.showDatePicker = true;
18 | $scope.photos = [];
19 |
20 | /*
21 | * Initializes instrument in the scope according to $routeParams. It
22 | * uses the default instrument if not specified.
23 | */
24 | if ($routeParams.instrument) {
25 | $scope.instrument = $routeParams.instrument;
26 | } else {
27 | $scope.instrument = MarsPhotosDBAccess.defaultInstrument;
28 | }
29 | $scope.mission = MarsPhotosDBAccess.defaultMission;
30 | $scope.missionInstrument = $scope.mission + '+' + $scope.instrument;
31 | $scope.instrumentList = MarsPhotosDBAccess.instrumentList;
32 |
33 | /*
34 | * Parses time specified in $routeParams and sets it in $scope.
35 | */
36 | if ($routeParams.time) {
37 | var time = parseInt($routeParams.time);
38 | if (typeof(time) === 'number' && time > 0) {
39 | $scope.time = time;
40 | } else {
41 | $log.error('Failed to parse time parameter:' + $routeParams.time);
42 | }
43 | }
44 |
45 | /**
46 | * Fetches more photos through MarsPhotos service and concatinates results
47 | * to $scope.photos. Called when the page is loaded and when the user
48 | * scrolls to the bottom of window.
49 | */
50 | $scope.updatePhotos = function(){
51 | $scope.isUpdatingPhotos = true;
52 | MarsPhotosDBAccess.queryWithDateIndex({
53 | hashKey: $scope.missionInstrument,
54 | rangeKey: $scope.time,
55 | lastEvaluatedKey: $scope.lastEvaluatedKey
56 | }, function(error, data) {
57 | if (!error) {
58 | $scope.lastEvaluatedKey = data.LastEvaluatedKey;
59 | $scope.$apply(function(){
60 | for (var index in data.Items) {
61 | var photo = data.Items[index];
62 | /*jshint camelcase: false */
63 | photo.time.creation_time = moment(photo.time.creation_timestamp_utc).format('MMMM Do YYYY, h:mm:ss a zz');
64 | photo.time.received_in = moment(photo.time.received_timestamp_utc).from(photo.time.creation_timestamp_utc, true);
65 | $scope.photos.push(photo);
66 | }
67 | });
68 | $scope.isUpdatingPhotos = false;
69 | } else {
70 | $log.error(error);
71 | }
72 | });
73 | };
74 |
75 | /**
76 | * Updates the location path and reloads the page. Called from the child
77 | * controller, SideMenuCtrl.
78 | */
79 | $scope.reload = function(){
80 | var path = '/timeline/' + $scope.instrument;
81 | if ($scope.time) {
82 | path += '/' + $scope.time;
83 | }
84 | $log.debug('Updating path to ' + path);
85 | $timeout(function(){ // We use $timeout() to avoid calling $scope.$apply() during another $scope.$apply() call
86 | $scope.$apply(function(){
87 | $location.path(path);
88 | });
89 | });
90 | };
91 |
92 | /**
93 | * Votes on the specified photo via MarsPhotos service. It updates the voting
94 | * cound of the photo on the successful voting. It shows an error dialog if
95 | * the user has already voted on the photo.
96 | */
97 | $scope.vote = function(photo) {
98 | $log.debug('Voting on ' + photo.imageid);
99 | MarsPhotosDBAccess.voteOnPhoto(
100 | localStorage.getItem('userid'),
101 | photo,
102 | function(error, data) {
103 | if (!error) {
104 | $scope.$apply(function(){
105 | photo.votes = data.Attributes.votes;
106 | });
107 | } else {
108 | $modal.open({
109 | templateUrl: 'views/partials/dialog.html',
110 | controller: 'DialogCtrl',
111 | resolve: {
112 | title: function() {
113 | return 'Error';
114 | },
115 | message: function() {
116 | return error;
117 | }
118 | }
119 | });
120 | }
121 | });
122 | };
123 |
124 | /*
125 | * Handler called when the view content is loaded. The timelineAnimate function
126 | * and scroll handler need to be called after the view content is ready.
127 | */
128 | $scope.$on('$viewContentLoaded', function() {
129 | $log.debug('Content loaded');
130 | $scope.updatePhotos();
131 |
132 | // Activates image gallery feature
133 | Blueimp.Gallery($('.timeline a.mars-photos'), $('#blueimp-gallery').data());
134 |
135 | $(window).scroll(scrollHandler);
136 | });
137 |
138 | /*
139 | * Handler called when the view is destroyed. The scroll handler needs to
140 | * be deregistered when moving to another view.
141 | */
142 | $scope.$on('$destroy', function(){
143 | $(window).off('scroll', scrollHandler);
144 | });
145 |
146 | /**
147 | * Handler called when a scrolling event is fired. It checks if the bottom of the window
148 | * is reaching and calls updatePhotos() if so.
149 | */
150 | var scrollHandler = function() {
151 | if (isBottomReaching() && !$scope.isUpdatingPhotos) {
152 | $log.debug('Fetching more photos');
153 | $scope.updatePhotos();
154 | }
155 | activateTimelineItemOnceWindowReached();
156 | };
157 |
158 | /**
159 | * Judges if the bottom of window is reaching. Used to decide if more photos
160 | * should be fetched upon a scrolling event or not.
161 | */
162 | var isBottomReaching = function(){
163 | return $(window).scrollTop() >= ($(document).height() - $(window).height() - timelinePosition.top);
164 | };
165 |
166 |
167 | /**
168 | * Private function which triggers to display a time line item if the window
169 | * scrolls to the item.
170 | */
171 | var activateTimelineItemOnceWindowReached = function() {
172 | var inactiveItems = $('.timeline-item:not(.active)');
173 | if(inactiveItems.length > 0){
174 | var item = inactiveItems.first();
175 | var itemHead = timelinePosition.top + item.position().top + item.outerHeight() / 3;
176 | var windowBottom = $(window).scrollTop() + $(window).height();
177 | if (windowBottom > itemHead) {
178 | item.addClass('active');
179 | }
180 | }
181 | };
182 | });
183 |
--------------------------------------------------------------------------------
/viewer/app/scripts/controllers/top-voted.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | global $
4 | */
5 |
6 | /**
7 | * @ngdoc function
8 | * @name MSLImageExplorerApp.controller:TopVotedCtrl
9 | * @description
10 | * # TopVotedCtrl
11 | * Controller of the MSLImageExplorerApp
12 | */
13 | angular.module('MSLImageExplorerApp').
14 | controller('TopVotedCtrl', function ($scope, $routeParams, $timeout, $location, $log, MarsPhotosDBAccess, Blueimp) {
15 |
16 | $scope.title = 'Top Voted Mars Images';
17 | $scope.description = 'Photos taken by sorted by # of votes by viewers.';
18 | $scope.photos = [];
19 |
20 | /*
21 | * Initializes instrument in the scope according to $routeParams. It
22 | * uses the default instrument if none specified.
23 | */
24 | if ($routeParams.instrument) {
25 | $scope.instrument = $routeParams.instrument;
26 | } else {
27 | $scope.instrument = MarsPhotosDBAccess.defaultInstrument;
28 | }
29 | $scope.mission = MarsPhotosDBAccess.defaultMission;
30 | $scope.missionInstrument = $scope.mission + '+' + $scope.instrument;
31 | $scope.instrumentList = MarsPhotosDBAccess.instrumentList;
32 |
33 | /**
34 | * Updates the location path and reloads the page. Called from the child
35 | * controller, SideMenuCtrl.
36 | */
37 | $scope.reload = function(){
38 | var path = '/topVoted/' + $scope.instrument;
39 | $log.debug('Updating path to ' + path);
40 | $timeout(function(){
41 | $scope.$apply(function(){
42 | $location.path(path);
43 | });
44 | });
45 | };
46 |
47 | /**
48 | * Fetches top voted photos via MarsPhotosDBAccess service and replaces
49 | * $scope.photos with the results. It also triggers Blueimp
50 | * image gallery to prepare for the slideshow.
51 | */
52 | $scope.updatePhotos = function(){
53 | MarsPhotosDBAccess.queryWithVoteIndex({
54 | hashKey: $scope.missionInstrument,
55 | lastEvaluatedKey: $scope.lastEvaluatedKey
56 | }, function(error, data){
57 | if (!error) {
58 | var photos = data.Items;
59 | $scope.lastEvaluatedKey = data.LastEvaluatedKey;
60 | $scope.$apply(function(){
61 | $scope.photos = photos;
62 | });
63 | } else {
64 | $log.error(error);
65 | }
66 | });
67 | };
68 |
69 | $scope.updatePhotos();
70 | Blueimp.Gallery($('#links a'), $('#blueimp-gallery').data());
71 | });
72 |
73 |
--------------------------------------------------------------------------------
/viewer/app/scripts/services/AWS.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* global AWS, DynamoDB */
3 | /**
4 | * @ngdoc function
5 | * @name MSLImageExplorerApp.service:AWS
6 | * @description
7 | * # AWS
8 | * Service of the MSLImageExplorerApp. It initializes AWS SDK with providing credentials
9 | * either from Cognito Identity service or environment variables as specified in ENV service.
10 | * It initializes a DynamoDB client instance. The instance is exposed as AWS.dynamoDB.
11 | */
12 | angular.module('MSLImageExplorerApp').
13 | service('AWS', function ($log, ENV){
14 | if (ENV.useCognitoIdentity) { // Uses Cognito Idenity to get AWS credentials
15 | // Needs to set us-east-1 as the default to get credentials by Cognito
16 | AWS.config.update({region: 'us-east-1'});
17 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({
18 | AccountId: ENV.awsAccountId,
19 | IdentityPoolId: ENV.identityPoolId,
20 | RoleArn: ENV.unauthRoleArn
21 | });
22 |
23 | AWS.config.credentials.getId(function(){
24 | localStorage.setItem('userid', AWS.config.credentials.params.IdentityId);
25 | $log.info('AWS credentials initialized with Cognito Identity');
26 | });
27 | } else { // Uses environment variables to get AWS credentials.
28 | AWS.config.credentials = new AWS.Credentials({
29 | accessKeyId: ENV.accessKeyId,
30 | secretAccessKey: ENV.secretAccessKey
31 | });
32 | $log.info('AWS credentials initialized with environment variables');
33 | $log.debug('AWS Accesss Key is ' + ENV.accessKeyId);
34 | localStorage.setItem('userid', ENV.userId);
35 | $log.debug('User ID is set to ' + localStorage.getItem('userid'));
36 | }
37 |
38 | /**
39 | * DynamoDB client object.
40 | */
41 | AWS.dynamoDB = new DynamoDB(
42 | new AWS.DynamoDB({
43 | region: ENV.dynamoDBRegion,
44 | endpoint: ENV.dynamoDBEndpoint
45 | }));
46 |
47 | return AWS;
48 | });
49 |
--------------------------------------------------------------------------------
/viewer/app/scripts/services/blueimp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*
3 | global blueimp
4 | */
5 | /**
6 | * @ngdoc function
7 | * @name MSLImageExplorerApp.service:Blueimp
8 | * @description
9 | * # Blueimp
10 | * Service of the MSLImageExplorerApp. Exposes blueimp Javascript object as a service.
11 | */
12 | angular.module('MSLImageExplorerApp').
13 | service('Blueimp', function (){
14 | return blueimp;
15 | });
--------------------------------------------------------------------------------
/viewer/app/scripts/services/mars-photos.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /**
3 | * @ngdoc function
4 | * @name MSLImageExplorerApp.service:MarsPhotosDBAccess
5 | * @description
6 | * # MarsPhotosDBAccess
7 | * Service of the MSLImageExplorerApp. The service offers utility functions to
8 | * access DynamoDB tables.
9 | */
10 | angular.module('MSLImageExplorerApp').service('MarsPhotosDBAccess', function ($log, ENV, AWS){
11 | var MarsPhotosDBAccess = {
12 | /**
13 | * Queries photos table with the date index and give the result
14 | * to callback function.
15 | * @param {object} queryParams - The query parameters used in the query. The parameter
16 | * hashKey is required. The lastEvaluatedKey and rangeKey are optional.
17 | * @param {function} callback - The callback function used to return the result.
18 | */
19 | queryWithDateIndex: function(queryParams, callback){
20 | assertHashKey(queryParams);
21 | assertFunction(callback);
22 |
23 | var params = {
24 | TableName: ENV.photosTable,
25 | KeyConditions: [
26 | AWS.dynamoDB.Condition('Mission+InstrumentID', 'EQ', queryParams.hashKey)
27 | ],
28 | IndexName: 'date-gsi',
29 | Limit: 5,
30 | ScanIndexForward: false
31 | };
32 | if(hasLastEvaluatedKey(queryParams)) {
33 | params.ExclusiveStartKey = queryParams.lastEvaluatedKey;
34 | }
35 | if(hasRangeKey(queryParams)){
36 | params.KeyConditions.push (AWS.dynamoDB.Condition('TimeStamp', 'LE', queryParams.rangeKey));
37 | }
38 | logRequest('query', params);
39 | AWS.dynamoDB.query(params, callback);
40 | },
41 |
42 | /**
43 | * Queries photos table with the vote index and give the result
44 | * to callback function.
45 | * @param {object} queryParams - The query parameters used in the query. The parameter
46 | * hashKey is required. The lastEvaluatedKey and rangeKey are optional.
47 | * @param {function} callback - The callback function used to return the result.
48 | */
49 | queryWithVoteIndex: function(queryParams, callback) {
50 | assertHashKey(queryParams);
51 | assertFunction(callback);
52 |
53 | var params = {
54 | TableName: ENV.photosTable,
55 | KeyConditions: [
56 | AWS.dynamoDB.Condition('Mission+InstrumentID', 'EQ', queryParams.hashKey)
57 | ],
58 | IndexName: 'vote-gsi',
59 | ScanIndexForward: false
60 | };
61 | if(hasLastEvaluatedKey(queryParams)) {
62 | params.ExclusiveStartKey = queryParams.lastEvaluatedKey;
63 | }
64 | if(hasRangeKey(queryParams)){
65 | params.KeyConditions.push (AWS.dynamoDB.Condition('votes', 'LE', queryParams.rangeKey));
66 | }
67 | logRequest('query', params);
68 | AWS.dynamoDB.query(params, callback);
69 | },
70 |
71 | /**
72 | * Queries user votes table and give the result to the callback function.
73 | * @param {object} queryParams - The query parameters used in the query. The parameter
74 | * hashKey is required. The lastEvaluatedKey is optional.
75 | * @param {function} callback - The callback function used to return the result.
76 | */
77 | queryUserVotedPhotos: function(queryParams, callback) {
78 | assertHashKey(queryParams);
79 | assertFunction(callback);
80 |
81 | var params = {
82 | TableName: ENV.userVotesTable,
83 | KeyConditions: AWS.dynamoDB.Condition('userid', 'EQ', queryParams.hashKey),
84 | ScanIndexForward: false
85 | };
86 | if(hasLastEvaluatedKey(queryParams)) {
87 | params.ExclusiveStartKey = queryParams.lastEvaluatedKey;
88 | }
89 | logRequest('query', params);
90 | AWS.dynamoDB.query(params, callback);
91 | },
92 |
93 | /**
94 | * Votes on the specified photo and notes that in the votes table.
95 | * It uses conditional update
96 | * and succeeds only if the user votes on the photo for the first time.
97 | * @param {String} userid - The ID of the user who votes on the photo.
98 | * @param {Object} photo - The photo object to vote on.
99 | * @param {function} callback - The callback function used to return the result or error.
100 | */
101 | voteOnPhoto: function(userid, photo, callback) {
102 | assertUserid(userid);
103 | assertPhoto(photo);
104 | assertFunction(callback);
105 |
106 | var item = {};
107 | for (var key in photo){
108 | // Skip thumbnail data and project other metadata
109 | if(key === 'data'){
110 | continue;
111 | }
112 | item[key] = photo[key];
113 | }
114 | item.userid = userid;
115 | var params = {
116 | TableName: ENV.userVotesTable,
117 | Item: item,
118 | Expected: AWS.dynamoDB.Condition('imageid', 'NULL')
119 | };
120 | logRequest('putItem', params);
121 | AWS.dynamoDB.putItem(params, function(error) {
122 | if (!error) {
123 | $log.debug('Liked image successfully');
124 | incrementVotesCount(photo.imageid, callback);
125 | } else {
126 | if(error.code === 'ConditionalCheckFailedException'){
127 | callback('You have already voted on this image');
128 | } else {
129 | $log.error(error);
130 | }
131 | }
132 | });
133 | },
134 |
135 |
136 | /**
137 | * List of instruments and its display names.
138 | */
139 | instrumentList: {
140 | 'fcam': {id: 'fcam', name: 'Front Hazcam'},
141 | 'ccam': {id: 'ccam', name: 'Chemcam RMI'},
142 | 'mastcam_right': {id: 'mastcam_right', name: 'Right Mastcam'},
143 | 'mastcam_left': {id: 'mastcam_left', name: 'Left Mastcam'},
144 | 'mahli': {id: 'mahli', name: 'MAHLI'},
145 | 'mardi': {id: 'mardi', name: 'MARDI'}
146 | },
147 |
148 | /**
149 | * Default mission ID
150 | */
151 | defaultMission: 'curiosity',
152 |
153 | /**
154 | * Default instrument ID
155 | */
156 | defaultInstrument: 'fcam'
157 | };
158 |
159 | /**
160 | * Increments the voting count in the photo table. It gets the updated
161 | * votes count in return. The function is meant to be used privately
162 | * in MarsPhoto service.
163 | * @param {String} imageid - The ID of the photo to increment vote count.
164 | * @param {function} callback - The callback function used to return the result or error.
165 | */
166 | var incrementVotesCount = function(imageid, callback) {
167 | var params = {
168 | TableName: ENV.photosTable,
169 | Key: { imageid: imageid },
170 | UpdateExpression: 'add votes :v',
171 | ExpressionAttributeValues: {':v': 1},
172 | ReturnValues: 'UPDATED_NEW'
173 | };
174 |
175 | logRequest('updateItem', params);
176 | AWS.dynamoDB.updateItem(params, callback);
177 | };
178 |
179 | var assertHashKey = function(queryParams){
180 | if(!queryParams || typeof queryParams.hashKey === 'undefined'){
181 | throw 'A required parameter, hash key is missing.';
182 | }
183 | };
184 |
185 | var assertPhoto = function(photo) {
186 | if(! photo || typeof photo.imageid === 'undefined'){
187 | throw 'Invalid object was given as a photo: ' + photo;
188 | }
189 | };
190 |
191 | var assertUserid = function(userid) {
192 | if(typeof userid === 'undefined'){
193 | throw 'User ID was invalid: ' + userid;
194 | }
195 | };
196 |
197 | var assertFunction = function(callback){
198 | if(typeof callback !== 'function'){
199 | throw 'No valid callback function was given: ' + callback;
200 | }
201 | };
202 |
203 | var hasLastEvaluatedKey = function(queryParams){
204 | return queryParams && typeof queryParams.lastEvaluatedKey !== 'undefined';
205 | };
206 |
207 | var hasRangeKey = function(queryParams){
208 | return queryParams && typeof queryParams.rangeKey !== 'undefined';
209 | };
210 |
211 | var logRequest = function(method, params){
212 | $log.debug('Requesting DynamoDB to ' + method + ' with the following parameters');
213 | $log.debug(params);
214 | };
215 |
216 | return MarsPhotosDBAccess;
217 | });
218 |
--------------------------------------------------------------------------------
/viewer/app/styles/main.scss:
--------------------------------------------------------------------------------
1 | $icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/";
2 | // bower:scss
3 | @import "bootstrap-sass-official/assets/stylesheets/_bootstrap.scss";
4 | // endbower
5 |
6 | .browsehappy {
7 | margin: 0.2em 0;
8 | background: #ccc;
9 | color: #000;
10 | padding: 0.2em 0;
11 | }
12 |
13 | /* Space out content a bit */
14 | body {
15 | background: url('../images/mars.jpg') no-repeat center center fixed;
16 | -webkit-background-size: cover;
17 | -moz-background-size: cover;
18 | -o-background-size: cover;
19 | background-size: cover;
20 | color:#fff;
21 | background-color:#333;
22 | font-family: 'Open Sans',Arial,Helvetica,Sans-Serif;
23 | padding-top: 70px;
24 | }
25 |
26 | .clickable {
27 | cursor: pointer;
28 | }
29 |
30 | .in-parent {
31 | width: 100%;
32 | }
33 |
34 | /* Everything but the jumbotron gets side spacing for mobile first views */
35 | .header,
36 | .marketing,
37 | .footer {
38 | padding-left: 15px;
39 | padding-right: 15px;
40 | }
41 |
42 | /* Custom page header */
43 | .header {
44 | border-bottom: 1px solid #e5e5e5;
45 |
46 | /* Make the masthead heading the same height as the navigation */
47 | h3 {
48 | margin-top: 0;
49 | margin-bottom: 0;
50 | line-height: 40px;
51 | padding-bottom: 19px;
52 | }
53 | }
54 |
55 | /* Custom page footer */
56 | .footer {
57 | padding-top: 19px;
58 | color: #777;
59 | border-top: 1px solid #e5e5e5;
60 | }
61 |
62 | .container-narrow > hr {
63 | margin: 30px 0;
64 | }
65 |
66 | /* Main marketing message and sign up button */
67 | .jumbotron {
68 | text-align: center;
69 | border-bottom: 1px solid #e5e5e5;
70 | background: url(../images/rover_jumbotron.jpg) no-repeat ;
71 | -webkit-background-size: cover;
72 | -moz-background-size: cover;
73 | -o-background-size: cover;
74 | background-size: cover;
75 | height: 480px;
76 |
77 | .btn {
78 | font-size: 21px;
79 | padding: 14px 24px;
80 | }
81 | }
82 |
83 | .title {
84 | text-align: center;
85 | color: grey;
86 | font-size: 32px;
87 | }
88 |
89 | .full-width {
90 | width: 100%;
91 | }
92 |
93 | /* Supporting marketing content */
94 | .marketing {
95 | margin: 40px 0;
96 |
97 | p + h4 {
98 | margin-top: 28px;
99 | }
100 | }
101 |
102 | .fill-parent {
103 | width: 100%;
104 | }
105 |
106 | .rewind {
107 | position: fixed;
108 | top: 4em;
109 | left: 0px;
110 | text-decoration: none;
111 | color: #000000;
112 | background-color: rgba(235, 235, 235, 0.60);
113 | font-size: 12px;
114 | padding: 1em;
115 | transition: 0.5s linear all;
116 | }
117 |
118 | .fast-forward {
119 | position: fixed;
120 | bottom: 4em;
121 | left: 0px;
122 | text-decoration: none;
123 | color: #000000;
124 | background-color: rgba(235, 235, 235, 0.60);
125 | font-size: 12px;
126 | padding: 1em;
127 | transition: 0.5s linear all;
128 | }
129 |
130 | .copyright {
131 | position: fixed;
132 | bottom: 0px;
133 | right: 0px;
134 | text-decoration: none;
135 | color: rgba(235, 235, 235, 0.60);
136 | font-size: 12px;
137 | padding: 0em;
138 | }
139 |
140 | /* Responsive: Portrait tablets and up */
141 | @media screen and (min-width: 768px) {
142 | .container {
143 | max-width: 730px;
144 | }
145 |
146 | /* Remove the padding we set earlier */
147 | .header,
148 | .marketing,
149 | .footer {
150 | padding-left: 0;
151 | padding-right: 0;
152 | }
153 | /* Space out the masthead */
154 | .header {
155 | margin-bottom: 30px;
156 | }
157 | /* Remove the bottom border on the jumbotron for visual effect */
158 | .jumbotron {
159 | border-bottom: 0;
160 | }
161 | }
162 |
163 | .glyphicon-2x{
164 | font-size: 40px;
165 | }
166 | .glyphicon-3x{
167 | font-size: 60px;
168 | }
169 |
170 | .modal-dialog {
171 | color: black;
172 | }
--------------------------------------------------------------------------------
/viewer/app/styles/timeline.scss:
--------------------------------------------------------------------------------
1 | .timeline {
2 | list-style: none;
3 | padding: 20px 0 20px;
4 | position: relative;
5 | }
6 |
7 | .timeline-item {
8 | opacity: 0;
9 | -webkit-transition: all 0.8s;
10 | -moz-transition: all 0.8s;
11 | transition: all 0.8s;
12 | }
13 |
14 | .timeline-item.active {
15 | opacity: 1.0;
16 | }
17 |
18 | .timeline:before {
19 | top: 0;
20 | bottom: 0;
21 | position: absolute;
22 | content: " ";
23 | width: 3px;
24 | background-color: #eeeeee;
25 | left: 50%;
26 | margin-left: -1.5px;
27 | }
28 |
29 | .timeline > li {
30 | margin-bottom: 20px;
31 | position: relative;
32 | }
33 |
34 | .timeline > li:before,
35 | .timeline > li:after {
36 | content: " ";
37 | display: table;
38 | }
39 |
40 | .timeline > li:after {
41 | clear: both;
42 | }
43 |
44 | .timeline > li:before,
45 | .timeline > li:after {
46 | content: " ";
47 | display: table;
48 | }
49 |
50 | .timeline > li:after {
51 | clear: both;
52 | }
53 |
54 | .timeline > li > .timeline-panel {
55 | width: 32%;
56 | margin-left: 15%;
57 | float: left;
58 | border: 1px solid #d4d4d4;
59 | border-radius: 2px;
60 | padding: 20px;
61 | background-color: #ffffff;
62 | color: black;
63 | position: relative;
64 | -webkit-box-shadow: 0 1px 6px rgba(255, 255, 255, 0.175);
65 | box-shadow: 0 1px 6px rgba(255, 255, 255, 0.175);
66 | }
67 |
68 | .timeline > li > .timeline-panel:before {
69 | position: absolute;
70 | top: 26px;
71 | right: -15px;
72 | display: inline-block;
73 | border-top: 15px solid transparent;
74 | border-left: 15px solid #ccc;
75 | border-right: 0 solid #ccc;
76 | border-bottom: 15px solid transparent;
77 | content: " ";
78 | }
79 |
80 | .timeline > li > .timeline-panel:after {
81 | position: absolute;
82 | top: 27px;
83 | right: -14px;
84 | display: inline-block;
85 | border-top: 14px solid transparent;
86 | border-left: 14px solid #fff;
87 | border-right: 0 solid #fff;
88 | border-bottom: 14px solid transparent;
89 | content: " ";
90 | }
91 |
92 | .timeline > li > .timeline-date {
93 | line-height: 50px;
94 | font-size: 1.4em;
95 | position: absolute;
96 | top: 16px;
97 | left: 50%;
98 | margin-left: 40px;
99 | }
100 |
101 | .timeline > li > .timeline-badge {
102 | color: #fff;
103 | width: 50px;
104 | height: 50px;
105 | line-height: 50px;
106 | font-size: 1.4em;
107 | text-align: center;
108 | position: absolute;
109 | top: 16px;
110 | left: 50%;
111 | margin-left: -25px;
112 | background-color: #999999;
113 | z-index: 100;
114 | border-top-right-radius: 50%;
115 | border-top-left-radius: 50%;
116 | border-bottom-right-radius: 50%;
117 | border-bottom-left-radius: 50%;
118 | }
119 |
120 |
121 | .timeline > li.timeline-inverted > .timeline-panel {
122 | float: right;
123 | margin-right: 15%;
124 | }
125 |
126 | .timeline > li.timeline-inverted > .timeline-panel:before {
127 | border-left-width: 0;
128 | border-right-width: 15px;
129 | left: -15px;
130 | right: auto;
131 | }
132 |
133 | .timeline > li.timeline-inverted > .timeline-panel:after {
134 | border-left-width: 0;
135 | border-right-width: 14px;
136 | left: -14px;
137 | right: auto;
138 | }
139 |
140 | .timeline > li.timeline-inverted > .timeline-date {
141 | line-height: 50px;
142 | font-size: 1.4em;
143 | position: absolute;
144 | text-align: right;
145 | top: 16px;
146 | left: 0%;
147 | margin-left: 0px;
148 | margin-right: 40px;
149 | right: 50%;
150 | }
151 |
152 |
153 | .timeline-badge.primary {
154 | background-color: #2e6da4 !important;
155 | }
156 |
157 | .timeline-badge.success {
158 | background-color: #3f903f !important;
159 | }
160 |
161 | .timeline-badge.warning {
162 | background-color: #f0ad4e !important;
163 | }
164 |
165 | .timeline-badge.danger {
166 | background-color: #d9534f !important;
167 | }
168 |
169 | .timeline-badge.info {
170 | background-color: #5bc0de !important;
171 | }
172 |
173 | .timeline-title {
174 | margin-top: 0;
175 | color: inherit;
176 | }
177 |
178 | .timeline-body > p,
179 | .timeline-body > ul {
180 | margin-bottom: 0;
181 | }
182 |
183 | .timeline-body > p + p {
184 | margin-top: 5px;
185 | }
186 |
187 | @media (max-width: 767px) {
188 | ul.timeline:before {
189 | left: 40px;
190 | }
191 |
192 | ul.timeline > li > .timeline-panel {
193 | width: calc(100% - 90px);
194 | width: -moz-calc(100% - 90px);
195 | width: -webkit-calc(100% - 90px);
196 | }
197 |
198 | ul.timeline > li > .timeline-badge {
199 | left: 15px;
200 | margin-left: 0;
201 | top: 16px;
202 | }
203 |
204 | ul.timeline > li > .timeline-panel {
205 | float: right;
206 | margin-left: 0%;
207 | }
208 |
209 | .timeline > li.timeline-inverted > .timeline-panel {
210 | float: right;
211 | margin-right: 0%;
212 | }
213 |
214 |
215 | ul.timeline > li > .timeline-panel:before {
216 | border-left-width: 0;
217 | border-right-width: 15px;
218 | left: -15px;
219 | right: auto;
220 | }
221 |
222 | ul.timeline > li > .timeline-panel:after {
223 | border-left-width: 0;
224 | border-right-width: 14px;
225 | left: -14px;
226 | right: auto;
227 | }
228 | }
--------------------------------------------------------------------------------
/viewer/app/views/includes/head.jade:
--------------------------------------------------------------------------------
1 | title= title
2 | meta(charset='utf-8')
3 | // build:css(.) styles/vendor.css
4 | link(rel='stylesheet', href='bower_components/bootstrap/dist/css/bootstrap.min.css')
5 | link(rel='stylesheet', href='bower_components/blueimp-gallery/css/blueimp-gallery.css')
6 | link(rel='stylesheet', href='bower_components/blueimp-bootstrap-image-gallery/css/bootstrap-image-gallery.css')
7 | // bower:css
8 | link(rel='stylesheet', href='bower_components/font-awesome/css/font-awesome.min.css')
9 | // endbower
10 | // endbuild
11 |
12 | // build:css(.tmp) styles/main.css
13 | link(rel='stylesheet', href='styles/timeline.css')
14 | link(rel='stylesheet', href='styles/main.css')
15 | // endbuild
--------------------------------------------------------------------------------
/viewer/app/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang='en')
3 | head
4 | include includes/head
5 |
6 | body(ng-app="MSLImageExplorerApp")
7 |
8 |
9 | .container-fluid
10 |
11 | block content
12 |
13 | block footer
14 |
15 | // build:js(.) scripts/vendor.js
16 | // bower:js
17 | // endbower
18 | script(src='bower_components/blueimp-gallery/js/jquery.blueimp-gallery.min.js')
19 | script(src='bower_components/blueimp-bootstrap-image-gallery/js/bootstrap-image-gallery.js')
20 | script(src='bower_components/dynamodb-doc/dynamodb-doc.min.js')
21 | // endbuild
22 |
23 | script(src='scripts/app.js')
24 | script(src='scripts/config.js')
25 | script(src='scripts/controllers/timeline.js')
26 | script(src='scripts/controllers/top-voted.js')
27 | script(src='scripts/controllers/favorites.js')
28 | script(src='scripts/controllers/sidemenu.js')
29 | script(src='scripts/controllers/dialog.js')
30 | script(src='scripts/services/AWS.js')
31 | script(src='scripts/services/mars-photos.js')
32 | script(src='scripts/services/blueimp.js')
33 |
34 | block scripts
35 |
36 |
--------------------------------------------------------------------------------
/viewer/app/views/partials/dialog.jade:
--------------------------------------------------------------------------------
1 | .modal-header
2 | h4 {{title}}
3 | .modal-body
4 | p {{message}}
5 | .modal-footer
6 | button.btn.btn-primary(ng-click="close()") OK
7 |
--------------------------------------------------------------------------------
/viewer/app/views/partials/image-gallery.jade:
--------------------------------------------------------------------------------
1 | div(ng-controller="SideMenuCtrl" ng-init="initSidr()"
2 | ng-include="'views/partials/sidemenu.html'" )
3 |
4 |
5 | .col-md-8.col-md-offset-2
6 | h1 {{title}}
7 | blockquote
8 | p {{description}}
9 |
10 | #links
11 | ul
12 | li(ng-repeat="photo in photos track by photo.imageid" style="list-style: none;")
13 | a(href="{{photo.url}}" title="{{photo.imageid}}" data-gallery)
14 | //img(style="width:500px;" ng-src="{{photo.url}}")
15 | img(style="width:500px;" ng-src="http://s3.amazonaws.com/aws-dynamodb-mars-json-demo/thumbnails/{{photo.imageid}}.jpg")
16 | h3.pull-right(ng-show="photo.votes > 0") {{photo.votes}} votes
17 |
18 | // The Bootstrap Image Gallery lightbox, should be a child element of the document body
19 | #blueimp-gallery.blueimp-gallery.blueimp-gallery-controls(data-use-bootstrap-modal="false")
20 | // The container for the modal slides
21 | .slides
22 | // Controls for the borderless lightbox
23 | h3.title
24 | a.prev
25 | a.next
26 | a.close
27 | a.play-pause
28 | ol.indicator
29 |
--------------------------------------------------------------------------------
/viewer/app/views/partials/rover-detail.jade:
--------------------------------------------------------------------------------
1 | .modal-header
2 | h4 Instrument Selection
3 | .modal-body
4 | img.slide.in-parent(ng-src="/images/rover.jpg")
5 | .modal-footer
6 |
--------------------------------------------------------------------------------
/viewer/app/views/partials/sidemenu.jade:
--------------------------------------------------------------------------------
1 | nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation")
2 | .container-fluid
3 | .navbar-header
4 | a.navbar-brand(href="/#/") MSL Image Explorer
5 | ul.nav.navbar-nav.navbar-right
6 | li.active
7 | a#sidemenu.clickable(ng-click="openSidr()") Mission Control
8 |
9 | #sidr(ng-show="isSidrInitialized")
10 | ul
11 | li
12 | #close-sidemenu.clickable(ng-click="closeSidr()")
13 | span.pull-right ×
14 | span Mission Control
15 | li
16 | a.sidemenu-link(href="/#/timeline/{{instrument}}/{{time}}") Timeline
17 | ul#datepicker(ng-show="showDatePicker")
18 | li
19 | span
20 | .input-append.date.datepicker(data-orientation="left" data-autoclose="true")
21 | input(type="text" data-orientation="left" data-autoclose="true")
22 | span.add-on
23 | i.icon-th
24 |
25 | li
26 | a.sidemenu-link(href="/#/topVoted/{{instrument}}") Top Voted
27 | li
28 | a.sidemenu-link(href="/#/favorites") My Favorites
29 | li
30 | span.clickable(ng-click="showRoverDetails()") Select Instrument
31 | ul
32 | li
33 | span(ng-hide="showInstrumentList") Current: {{instrumentList[instrument].name}}
34 | ul#instrumentList(ng-show="showInstrumentList")
35 | li(ng-repeat="instrument in instrumentList" ng-class="{active: isActive(instrument)}"
36 | ng-click="setInstrument(instrument.id, $event)")
37 | span.clickable {{instrument.name}}
38 |
--------------------------------------------------------------------------------
/viewer/app/views/partials/timeline.jade:
--------------------------------------------------------------------------------
1 | div(ng-controller="SideMenuCtrl" ng-include="'views/partials/sidemenu.html'")
2 |
3 | ul.timeline
4 | li.timeline-item(ng-repeat="photo in photos" ng-class-even="'timeline-inverted'")
5 | .timeline-badge
6 | i.fa.fa-camera
7 | .timeline-date
8 | p {{photo.time.creation_time}}
9 | .timeline-panel
10 | .timeline-title
11 | h4 {{photo.imageid}}
12 | p {{photo.time.creation_time}}
13 | .timeline-body(id="{{photo.imageid}}")
14 | a.mars-photos(href="{{photo.url}}" title="{{photo.imageid}}" data-gallery)
15 | img.img-responsive.fill-parent(ng-src="{{photo.url}}")
16 | p.description
17 | | Image from the rover's {{instrumentList[photo.instrument].name}} during
18 | | mission {{photo.mission ? photo.mission : mission}}. It was taken at
19 | | {{photo.time.creation_time}} and received on Earth
20 | | {{photo.time.received_in}} later.
21 | br
22 | h4.pull-left # of votes: {{photo.votes ? photo.votes : 0}}
23 | a.clickable.pull-right(ng-click="vote(photo)")
24 | span.glyphicon.glyphicon-2x.glyphicon-thumbs-up
25 |
26 | // The Bootstrap Image Gallery lightbox, should be a child element of the document body
27 | #blueimp-gallery.blueimp-gallery.blueimp-gallery-controls(data-use-bootstrap-modal="false")
28 | // The container for the modal slides
29 | .slides
30 | // Controls for the borderless lightbox
31 | h3.title
32 | a.prev
33 | a.next
34 | a.close
35 | a.play-pause
36 | ol.indicator
37 |
--------------------------------------------------------------------------------
/viewer/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MSLImageExplorerApp",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "angular": "1.2.16",
6 | "json3": "~3.3.1",
7 | "es5-shim": "~3.1.0",
8 | "bootstrap-sass-official": "~3.2.0",
9 | "angular-resource": "1.2.16",
10 | "angular-sanitize": "1.2.16",
11 | "angular-touch": "1.2.16",
12 | "angular-route": "1.2.16",
13 | "font-awesome": "~4.1.0",
14 | "angular-bootstrap": "~0.11.0",
15 | "sidr": "~1.2.1",
16 | "bootstrap-datepicker": "~1.3.0",
17 | "blueimp-gallery": "~2.15.1",
18 | "blueimp-bootstrap-image-gallery": "~3.1.1",
19 | "ui.bootstrap": "~0.11.0",
20 | "moment": "~2.8.3",
21 | "aws-sdk": "~2.0.22",
22 | "dynamodb-doc": "git://github.com/awslabs/dynamodb-document-js-sdk.git#4ee5768d3ec3fc036122d999f9e6749bba923d8c"
23 | },
24 | "devDependencies": {
25 | "angular-mocks": "1.2.16"
26 | },
27 | "appPath": "app"
28 | }
29 |
--------------------------------------------------------------------------------
/viewer/lib/mynconf.coffee:
--------------------------------------------------------------------------------
1 | nconf = require 'nconf'
2 |
3 | nconf.argv()
4 | .env()
5 |
6 | nconf.defaults
7 | DYNAMODB_ENDPOINT_DEV: 'http://localhost:9000/dynamodb/'
8 | DYNAMODB_REGION_DEV: 'us-east-1'
9 | USE_COGNITO_DEV: false
10 | DYNAMODB_ENDPOINT_TEST: 'http://localhost:8080/dynamodb/'
11 | DYNAMODB_REGION_TEST: 'us-east-1'
12 | USE_COGNITO_TEST: false
13 | DYNAMODB_ENDPOINT_PROD: 'http://dynamodb.us-east-1.amazonaws.com/'
14 | DYNAMODB_REGION_PROD: 'us-east-1'
15 | USE_COGNITO_PROD: true
16 | AWS_ACCOUNT_ID: 'DummyAWSAccountID'
17 | COGNITO_IDENTITY_POOL_ID: 'DummyCognitoIdenityPoolID'
18 | COGNITO_UNAUTH_ROLE_ARN: 'DummyCognitoUnauthRoleARN'
19 | TABLE_PHOTOS: 'marsDemoImages'
20 | TABLE_USER_VOTES: 'userVotes'
21 | TABLE_RESOURCES: 'marsDemoResources'
22 | READ_CAPACITY_PHOTOS: 1
23 | WRITE_CAPACITY_PHOTOS: 1
24 |
25 | module.exports = nconf
26 |
--------------------------------------------------------------------------------
/viewer/lib/prepare_tables.coffee:
--------------------------------------------------------------------------------
1 | util = require('./util')
2 | nconf = require('./mynconf')
3 |
4 | tables = [
5 | {
6 | TableName: nconf.get('TABLE_PHOTOS')
7 | AttributeDefinitions: [
8 | { AttributeName: 'imageid', AttributeType: 'S' }
9 | { AttributeName: 'Mission+InstrumentID', AttributeType: 'S' }
10 | { AttributeName: 'TimeStamp', AttributeType: 'N' }
11 | { AttributeName: 'votes', AttributeType: 'N' }
12 | ]
13 | KeySchema: [
14 | { AttributeName: 'imageid', KeyType: 'HASH' }
15 | ]
16 | GlobalSecondaryIndexes: [
17 | {
18 | IndexName: 'date-gsi'
19 | KeySchema: [
20 | {AttributeName: 'Mission+InstrumentID', KeyType: 'HASH' }
21 | {AttributeName: 'TimeStamp', KeyType: 'RANGE' }
22 | ]
23 | Projection:
24 | ProjectionType: 'INCLUDE'
25 | NonKeyAttributes: [ 'url', 'time', 'instrument', 'votes', 'mission' ]
26 | ProvisionedThroughput:
27 | ReadCapacityUnits: nconf.get('READ_CAPACITY_PHOTOS')
28 | WriteCapacityUnits: nconf.get('WRITE_CAPACITY_PHOTOS')
29 | }
30 | {
31 | IndexName: 'vote-gsi'
32 | KeySchema: [
33 | {AttributeName: 'Mission+InstrumentID', KeyType: 'HASH' }
34 | {AttributeName: 'votes', KeyType: 'RANGE' }
35 | ]
36 | Projection:
37 | ProjectionType: 'INCLUDE'
38 | NonKeyAttributes: [ 'url', 'time', 'instrument', 'TimeStamp', 'mission' ]
39 | ProvisionedThroughput:
40 | ReadCapacityUnits: nconf.get('READ_CAPACITY_PHOTOS')
41 | WriteCapacityUnits: nconf.get('WRITE_CAPACITY_PHOTOS')
42 | }
43 | ]
44 | ProvisionedThroughput:
45 | ReadCapacityUnits: nconf.get('READ_CAPACITY_PHOTOS')
46 | WriteCapacityUnits: nconf.get('WRITE_CAPACITY_PHOTOS')
47 | }
48 | {
49 | TableName: nconf.get('TABLE_USER_VOTES')
50 | AttributeDefinitions: [
51 | { AttributeName: 'userid', AttributeType: 'S' }
52 | { AttributeName: 'imageid', AttributeType: 'S' }
53 | ]
54 | KeySchema: [
55 | { AttributeName: 'userid', KeyType: 'HASH' }
56 | { AttributeName: 'imageid', KeyType: 'RANGE' }
57 | ]
58 | ProvisionedThroughput:
59 | ReadCapacityUnits: nconf.get('READ_CAPACITY_PHOTOS')
60 | WriteCapacityUnits: nconf.get('WRITE_CAPACITY_PHOTOS')
61 | }
62 | {
63 | TableName: nconf.get('TABLE_RESOURCES')
64 | AttributeDefinitions: [
65 | { AttributeName: 'resource', AttributeType: 'S' }
66 | ]
67 | KeySchema: [
68 | { AttributeName: 'resource', KeyType: 'HASH' }
69 | ]
70 | ProvisionedThroughput:
71 | ReadCapacityUnits: nconf.get('READ_CAPACITY_PHOTOS')
72 | WriteCapacityUnits: nconf.get('WRITE_CAPACITY_PHOTOS')
73 | }
74 | ]
75 |
76 | for table in tables
77 | util.createTable(table, nconf.get('delete_table_if_exists')
78 | (error) ->
79 | console.error error
80 | (tableName) ->
81 | console.log "Table #{tableName} is ready"
82 | )
83 |
--------------------------------------------------------------------------------
/viewer/lib/random_votes.coffee:
--------------------------------------------------------------------------------
1 | util = require('./util')
2 | nconf = require('./mynconf')
3 |
4 |
5 | fetchPhotos = (lastEvaluatedKey, done) ->
6 | params =
7 | TableName: nconf.get('TABLE_PHOTOS')
8 | AttributesToGet: ['imageid']
9 |
10 | if lastEvaluatedKey
11 | params.ExclusiveStartKey = lastEvaluatedKey
12 |
13 | util.dynamoDB.scan(
14 | params
15 | (error, data) ->
16 | unless error
17 | done(data)
18 | else
19 | console.error error
20 | )
21 |
22 | voteRandom = (data) ->
23 | for photo in data.Items
24 | votes = Math.floor((Math.random() * 1000) + 1)
25 | console.log "Voting #{votes} on #{photo.imageid}"
26 | params =
27 | TableName: nconf.get('TABLE_PHOTOS')
28 | Key:
29 | imageid: photo.imageid
30 | AttributeUpdates:
31 | votes:
32 | Action: 'ADD'
33 | Value: votes
34 |
35 | util.dynamoDB.updateItem(
36 | params
37 | (error, data) ->
38 | console.error error if error
39 | )
40 | if data.LastEvaluatedKey
41 | fetchPhotos(data.LastEvaluatedKey, voteRandom)
42 |
43 |
44 |
45 |
46 |
47 | fetchPhotos(null, voteRandom)
48 |
--------------------------------------------------------------------------------
/viewer/lib/util.coffee:
--------------------------------------------------------------------------------
1 | AWS = require('aws-sdk')
2 | DOC = require('dynamodb-doc')
3 | nconf = require('./mynconf')
4 |
5 | AWS.config.credentials = new AWS.Credentials (
6 | accessKeyId: nconf.get('AWS_ACCESS_KEY')
7 | secretAccessKey: nconf.get('AWS_SECRET_ACCESS_KEY')
8 | )
9 |
10 | dynamoDB = new DOC.DynamoDB(
11 | new AWS.DynamoDB(
12 | endpoint: nconf.get('DYNAMODB_ENDPOINT')
13 | region: nconf.get('DYNAMODB_REGION')
14 | )
15 | )
16 |
17 | isReady = (tableName, callback) ->
18 | dynamoDB.describeTable({ TableName: tableName }, (error, data) ->
19 | unless error
20 | callback(data.Table.TableStatus == 'ACTIVE')
21 | else
22 | console.error(error.message)
23 | )
24 |
25 | notExists = (tableName, callback) ->
26 | dynamoDB.describeTable({ TableName: tableName }, (error, data) ->
27 | if error and error.code == 'ResourceNotFoundException'
28 | callback(true)
29 | else
30 | callback(false)
31 | )
32 |
33 | waitUntil = (args, cond, ready) ->
34 | repeat = -> waitUntil(args, cond, ready)
35 | cond(args, (conditionMet) ->
36 | if conditionMet
37 | console.log("Done")
38 | ready(args)
39 | else
40 | console.log("Waiting for operation to complete...")
41 | setTimeout(repeat, 200)
42 | )
43 |
44 | createTable = (tableParams, deleteIfExists, err, done) ->
45 | if deleteIfExists
46 | deleteTableAndWaitUntilRemoved(tableParams.TableName,
47 | (error) ->
48 | console.error error
49 | () ->
50 | createTableAndWaitUntilReady(tableParams, err, done)
51 | )
52 | else
53 | createTableAndWaitUntilReady(tableParams, err, done)
54 |
55 | createTableAndWaitUntilReady = (tableParams, err, done) ->
56 | console.log "creating table #{tableParams.TableName} on #{nconf.get('DYNAMODB_ENDPOINT')}"
57 | dynamoDB.createTable(tableParams, (error, data) ->
58 | unless error
59 | waitUntil(tableParams.TableName, isReady, done)
60 | else
61 | if error.code == 'ResourceInUseException'
62 | console.info "Table #{tableParams.TableName} already exists"
63 | waitUntil(tableParams.TableName, isReady, done)
64 | else
65 | console.error error.message
66 | err(error.message) if err
67 | )
68 |
69 | deleteTableAndWaitUntilRemoved = (tableName, err, done) ->
70 | console.log("Deleting table #{tableName}")
71 | dynamoDB.deleteTable({TableName: tableName}, (error, data) ->
72 | unless error
73 | waitUntil(tableName, notExists, done)
74 | else
75 | if error.code == 'ResourceNotFoundException'
76 | waitUntil(tableName, notExists, done)
77 | else
78 | err(error) if err
79 | )
80 |
81 | putItem = (tableName, item, err, done) ->
82 | params =
83 | TableName: tableName
84 | Item: item
85 |
86 | dynamoDB.putItem(params, (error, data) ->
87 | unless error
88 | done(item) if done
89 | else
90 | err(error) if err
91 | )
92 |
93 |
94 | module.exports.createTable = createTable
95 | module.exports.putItem = putItem
96 | module.exports.dynamoDB = dynamoDB
97 |
--------------------------------------------------------------------------------
/viewer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MSLImageExplorerApp",
3 | "version": "0.1.0",
4 | "dependencies": {},
5 | "devDependencies": {
6 | "aws-sdk": "^2.0.22",
7 | "dynamodb-doc": "git://github.com/awslabs/dynamodb-document-js-sdk.git#4ee5768d3ec3fc036122d999f9e6749bba923d8c",
8 | "coffee-script": "^1.8.0",
9 | "grunt": "^0.4.1",
10 | "grunt-autoprefixer": "^0.7.2",
11 | "grunt-bg-shell": "^2.3.1",
12 | "grunt-concurrent": "^0.5.0",
13 | "grunt-connect-proxy": "^0.1.11",
14 | "grunt-contrib-clean": "^0.5.0",
15 | "grunt-contrib-compass": "^0.7.2",
16 | "grunt-contrib-concat": "^0.4.0",
17 | "grunt-contrib-connect": "^0.7.1",
18 | "grunt-contrib-copy": "^0.5.0",
19 | "grunt-contrib-cssmin": "^0.9.0",
20 | "grunt-contrib-htmlmin": "^0.3.0",
21 | "grunt-contrib-imagemin": "^0.7.0",
22 | "grunt-contrib-jade": "^0.12.0",
23 | "grunt-contrib-jshint": "^0.10.0",
24 | "grunt-contrib-uglify": "^0.4.0",
25 | "grunt-contrib-watch": "^0.6.1",
26 | "grunt-curl": "^2.0.2",
27 | "grunt-filerev": "^0.2.1",
28 | "grunt-google-cdn": "^0.4.0",
29 | "grunt-if-missing": "^1.0.0",
30 | "grunt-karma": "~0.8.3",
31 | "grunt-newer": "^0.7.0",
32 | "grunt-ng-annotate": "^0.4.0",
33 | "grunt-ng-constant": "^1.0.0",
34 | "grunt-svgmin": "^0.4.0",
35 | "grunt-tar.gz": "0.0.3",
36 | "grunt-usemin": "^2.1.1",
37 | "grunt-wiredep": "^1.7.0",
38 | "jshint-stylish": "^0.2.0",
39 | "karma": "~0.12.21",
40 | "karma-jasmine": "^0.2.0",
41 | "karma-ng-html2js-preprocessor": "^0.1.0",
42 | "karma-phantomjs-launcher": "~0.1.4",
43 | "load-grunt-tasks": "^0.4.0",
44 | "nconf": "^0.6.9",
45 | "time-grunt": "^0.3.1"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "https://github.com/awslabs/aws-dynamodb-mars-json-demo"
50 | },
51 | "engines": {
52 | "node": ">=0.10.0"
53 | },
54 | "scripts": {
55 | "start": "grunt serve",
56 | "test": "grunt test"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/viewer/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "regexp": true,
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "smarttabs": true,
21 | "globals": {
22 | "after": false,
23 | "afterEach": false,
24 | "angular": false,
25 | "before": false,
26 | "beforeEach": false,
27 | "browser": false,
28 | "describe": false,
29 | "expect": false,
30 | "inject": false,
31 | "it": false,
32 | "jasmine": false,
33 | "spyOn": false
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/viewer/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // http://karma-runner.github.io/0.12/config/configuration-file.html
3 | // Generated on 2014-08-19 using
4 | // generator-karma 0.8.3
5 |
6 | module.exports = function(config) {
7 | 'use strict';
8 |
9 | config.set({
10 | // enable / disable watching file and executing tests whenever any file changes
11 | autoWatch: true,
12 |
13 | // base path, that will be used to resolve files and exclude
14 | basePath: '../',
15 |
16 | // testing framework to use (jasmine/mocha/qunit/...)
17 | frameworks: ['jasmine'],
18 |
19 | preprocessors: {
20 | '**/*.html': ['ng-html2js']
21 | },
22 |
23 | // list of files / patterns to load in the browser
24 | files: [
25 | // bower:js
26 | 'bower_components/jquery/dist/jquery.js',
27 | 'bower_components/es5-shim/es5-shim.js',
28 | 'bower_components/angular/angular.js',
29 | 'bower_components/json3/lib/json3.js',
30 | 'bower_components/angular-resource/angular-resource.js',
31 | 'bower_components/angular-sanitize/angular-sanitize.js',
32 | 'bower_components/angular-touch/angular-touch.js',
33 | 'bower_components/angular-route/angular-route.js',
34 | 'bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
35 | 'bower_components/sidr/jquery.sidr.min.js',
36 | 'bower_components/bootstrap/dist/js/bootstrap.js',
37 | 'bower_components/bootstrap-datepicker/js/bootstrap-datepicker.js',
38 | 'bower_components/blueimp-gallery/js/blueimp-helper.js',
39 | 'bower_components/blueimp-gallery/js/blueimp-gallery.js',
40 | 'bower_components/blueimp-gallery/js/blueimp-gallery-fullscreen.js',
41 | 'bower_components/blueimp-gallery/js/blueimp-gallery-indicator.js',
42 | 'bower_components/blueimp-gallery/js/blueimp-gallery-video.js',
43 | 'bower_components/blueimp-gallery/js/blueimp-gallery-vimeo.js',
44 | 'bower_components/blueimp-gallery/js/blueimp-gallery-youtube.js',
45 | 'bower_components/blueimp-bootstrap-image-gallery/js/bootstrap-image-gallery.js',
46 | 'bower_components/moment/moment.js',
47 | 'bower_components/aws-sdk/dist/aws-sdk.js',
48 | 'bower_components/angular-mocks/angular-mocks.js',
49 | // endbower
50 | 'app/scripts/**/*.js',
51 | '.tmp/scripts/**/*.js',
52 | 'test/spec/**/*.js',
53 | 'dist/**/*.html'
54 | ],
55 |
56 | // list of files / patterns to exclude
57 | exclude: [
58 | 'node_modules/**/*.html',
59 | 'bower_components/**/*.html'
60 | ],
61 |
62 | // web server port
63 | port: 8080,
64 |
65 | // Start these browsers, currently available:
66 | // - Chrome
67 | // - ChromeCanary
68 | // - Firefox
69 | // - Opera
70 | // - Safari (only Mac)
71 | // - PhantomJS
72 | // - IE (only Windows)
73 | browsers: [
74 | 'PhantomJS'
75 | ],
76 |
77 | // Which plugins to enable
78 | plugins: [
79 | 'karma-phantomjs-launcher',
80 | 'karma-ng-html2js-preprocessor',
81 | 'karma-jasmine'
82 | ],
83 |
84 | // Continuous Integration mode
85 | // if true, it capture browsers, run tests and exit
86 | singleRun: false,
87 |
88 | colors: true,
89 |
90 | // level of logging
91 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
92 | logLevel: config.LOG_INFO,
93 |
94 | proxies: {
95 | '/dynamodb/': 'http://localhost:8000/'
96 | },
97 |
98 | ngHtml2JsPreprocessor: {
99 | cacheIdFromPath: function(filepath) {
100 | filepath = filepath.replace(/^dist\//, '');
101 | filepath = filepath.replace(/^app\//, '');
102 | return filepath;
103 | }
104 | }
105 | });
106 | };
107 |
--------------------------------------------------------------------------------
/viewer/test/spec/controllers/favorites.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Controller: FavoritesCtrl', function () {
4 |
5 | // load the controller's module
6 | beforeEach(module('MSLImageExplorerApp'));
7 |
8 |
9 | var FavoritesCtrl, scope;
10 |
11 | var data = {
12 | Items: [
13 | {imageid: '123'},
14 | {imageid: '456'}
15 | ]
16 | };
17 |
18 | var marsPhotos = {
19 | queryUserVotedPhotos: function(queryParams, callback){
20 | callback(null, data);
21 | },
22 | getThumbnails: function(photos, callback) {
23 | callback();
24 | }
25 | };
26 |
27 | var Blueimp = {
28 | Gallery: function() {}
29 | };
30 |
31 | // Initialize the controller and a mock scope
32 | beforeEach(inject(function ($controller, $rootScope, $log) {
33 | scope = $rootScope.$new();
34 | FavoritesCtrl = $controller('FavoritesCtrl', {
35 | $scope: scope,
36 | $log: $log,
37 | MarsPhotosDBAccess: marsPhotos,
38 | Blueimp: Blueimp
39 | });
40 | }));
41 |
42 | it('should attach a list of user voted photos to the scope', function () {
43 | expect(scope.photos.length).toBe(2);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/viewer/test/spec/controllers/sidemenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Controller: SideMenuCtrl', function () {
4 |
5 | // load the controller's module
6 | beforeEach(module('MSLImageExplorerApp'));
7 |
8 | var SideMenuCtrl, scope;
9 |
10 | var modal = {
11 | open: function(){
12 | var modalInstance = {
13 | shown: true,
14 | dismiss: function(){
15 | modalInstance.shown = false;
16 | }
17 | };
18 | console.log(modalInstance);
19 | return modalInstance;
20 | }
21 | };
22 |
23 | var marsPhotos = {
24 | instrumentList: {
25 | 'fcam': {id: 'fcam', name: 'Chemcam RMI'},
26 | 'mastcam_right': {id: 'mastcam_right', name: 'Right Mastcam'},
27 | 'mastcam_left': {id: 'mastcam_left', name: 'Left Mastcam'},
28 | 'mahli': {id: 'mahli', name: 'MAHLI'}
29 | }
30 | };
31 |
32 | var parentReloaded = false;
33 |
34 | // Initialize the controller and a mock scope
35 | beforeEach(function(){
36 | inject(function ($controller, $rootScope, $log) {
37 | scope = $rootScope.$new();
38 | scope.$parent.reload = function(){
39 | parentReloaded = true;
40 | };
41 |
42 | SideMenuCtrl = $controller('SideMenuCtrl', {
43 | $scope: scope,
44 | $log: $log,
45 | $modal: modal,
46 | MarsPhotosDBAccess: marsPhotos
47 | });
48 | });
49 | });
50 |
51 | it('should initialize the sidemenu at the first time the user opens', function () {
52 | expect(scope.isSidrInitialized).toBe(false);
53 | scope.openSidr();
54 | expect(scope.isSidrInitialized).toBe(true);
55 | });
56 |
57 | it('should show rover details modal window when asked', function(){
58 | scope.showRoverDetails();
59 | expect(scope.modalInstance.shown).toBe(true);
60 | expect(scope.showInstrumentList).toBe(true);
61 | });
62 |
63 | it('should set instrument to its parent view, dismiss modal window and reload its parent view', function(){
64 | scope.showRoverDetails();
65 | scope.setInstrument('fcam', {});
66 | expect(scope.$parent.instrument).toEqual('fcam');
67 | expect(scope.modalInstance.shown).toBe(false);
68 | expect(parentReloaded).toBe(true);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/viewer/test/spec/controllers/timeline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Controller: TimelineCtrl', function () {
4 |
5 | // load the controller's module
6 | beforeEach(module('MSLImageExplorerApp'));
7 |
8 | var TimelineCtrl, scope, log, controller, timeout, location, controllerArgs, path;
9 |
10 | var data = {
11 | /*jshint camelcase: false */
12 | Items: [
13 | {imageid: '123', time: {creation_timestamp_utc: 0, received_timestamp_utc: 0}},
14 | {imageid: '456', time: {creation_timestamp_utc: 0, received_timestamp_utc: 0}}
15 | ]
16 | };
17 |
18 | var marsPhotos = {
19 | queryWithDateIndex: function(queryParams, callback){
20 | setTimeout(function(){
21 | data.LastEvaluatedKey = {
22 | 'Mission+InstrumentID': queryParams.hashKey
23 | };
24 | callback(null, data);
25 | }, 5);
26 | },
27 | voteOnPhoto: function(userid, photo, callback){
28 | if(photo.imageid === 'already_voted'){
29 | callback('You have already voted');
30 | } else {
31 | callback(null, {Attributes: {votes: photo.votes + 1}});
32 | }
33 | },
34 | getThumbnails: function(photos, callback) {
35 | callback();
36 | },
37 | defaultInstrument: 'fcam',
38 | defaultMission: 'curiosity'
39 | };
40 |
41 | beforeEach(function(){
42 | inject(function ($controller, $rootScope, $log) {
43 | $log.debug = function(msg) { console.log(msg); };
44 | $log.error = function(msg) { console.error(msg); };
45 | scope = $rootScope.$new();
46 | log = $log;
47 | location = {
48 | path: function(_path){
49 | path = _path;
50 | }
51 | };
52 | controller = $controller;
53 | timeout = function(callback) {
54 | callback();
55 | };
56 |
57 | controllerArgs = {
58 | $scope: scope,
59 | $log: log,
60 | $routeParams: {},
61 | $timeout: timeout,
62 | $location: location,
63 | MarsPhotosDBAccess: marsPhotos
64 | };
65 | TimelineCtrl = controller('TimelineCtrl', controllerArgs);
66 | });
67 | });
68 |
69 | it('should not be updating photos just after loading page', function(){
70 | expect(scope.isUpdatingPhotos).toBe(false);
71 | expect(scope.photos.length).toBe(0);
72 | });
73 |
74 | it('should show date picker on the mission control', function(){
75 | expect(scope.showDatePicker).toBe(true);
76 | });
77 |
78 | it('should attach a list of timeline photos to the scope once updatePhotos() is called', function(done){
79 | scope.updatePhotos();
80 | setTimeout(function(){
81 | expect(scope.photos.length).toBe(2);
82 | done();
83 | }, 10);
84 | });
85 |
86 |
87 | it('should set isUpdatingPhotos flag while loading photos', function(done){
88 | scope.updatePhotos();
89 | expect(scope.isUpdatingPhotos).toBe(true);
90 | setTimeout(function(){
91 | expect(scope.isUpdatingPhotos).toBe(false);
92 | done();
93 | }, 10);
94 | });
95 |
96 | it('should set instrument to default if it is not specified in routeParams', function (){
97 | expect(scope.instrument).toBe(marsPhotos.defaultInstrument);
98 | expect(scope.missionInstrument).toBe(marsPhotos.defaultMission + '+' + marsPhotos.defaultInstrument);
99 | });
100 |
101 | it('should set instrument to the one specified in routeParams', function (){
102 | controllerArgs.$routeParams = {instrument: 'mastcam_right'};
103 | TimelineCtrl = controller('TimelineCtrl', controllerArgs);
104 | expect(scope.instrument).toBe('mastcam_right');
105 | expect(scope.missionInstrument).toBe(marsPhotos.defaultMission + '+mastcam_right');
106 | });
107 |
108 | it('should set time to if specified in routeParams', function (){
109 | var time = new Date().getTime();
110 | controllerArgs.$routeParams = {time: time};
111 | TimelineCtrl = controller('TimelineCtrl', controllerArgs);
112 | expect(scope.time).toBe(time);
113 | });
114 |
115 | it('should not set time if route param is not a valid integer', function (){
116 | var time = 'not_integer';
117 | controllerArgs.$routeParams = {time: time};
118 | TimelineCtrl = controller('TimelineCtrl', controllerArgs);
119 | expect(scope.time).toBe(undefined);
120 | });
121 |
122 | it('should set a function to reload', function(){
123 | expect(typeof(scope.reload)).toBe('function');
124 | });
125 |
126 | it('should set path to /timeline/ + instrument ID when it is reloaded', function(){
127 | scope.instrument = 'mastcam_left';
128 | scope.reload();
129 | expect(path).toBe('/timeline/mastcam_left');
130 | });
131 |
132 | it('should set path to /timeline/instrumentID/time if time is specified and reload is called', function(){
133 | scope.instrument = 'mastcam_left';
134 | var time = new Date().getTime();
135 | scope.time = time;
136 | scope.reload();
137 | expect(path).toBe('/timeline/mastcam_left/' + time);
138 | });
139 |
140 | it('should update photos and set lastEvaluatedKey', function(done){
141 | scope.missionInstrument = marsPhotos.defaultMission + '+mastcam_left';
142 | scope.updatePhotos();
143 | setTimeout(function(){
144 | expect(scope.lastEvaluatedKey['Mission+InstrumentID']).toBe(marsPhotos.defaultMission + '+mastcam_left');
145 | done();
146 | }, 10);
147 | });
148 |
149 | it('should increase # of votes for a photo which is not yet voted by the user', function(){
150 | var photo = {
151 | imageid: 'not_voted_yet',
152 | votes: 1000
153 | };
154 | scope.vote(photo);
155 | expect(photo.votes).toBe(1001);
156 | });
157 |
158 | it('should not increase # of votes for a photo which is already voted by the user', function(){
159 | var photo = {
160 | imageid: 'already_voted',
161 | votes: 1000
162 | };
163 | scope.vote(photo);
164 | expect(photo.votes).toBe(1000);
165 | });
166 |
167 | });
168 |
--------------------------------------------------------------------------------
/viewer/test/spec/controllers/top-voted.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Controller: TopVotedCtrl', function () {
4 |
5 | // load the controller's module
6 | beforeEach(module('MSLImageExplorerApp'));
7 |
8 | var TopVotedCtrl, scope, log, controller, timeout, location, controllerArgs, path;
9 |
10 | var data = {
11 | Items: [
12 | {imageid: '123'},
13 | {imageid: '456'}
14 | ]
15 | };
16 |
17 | var marsPhotos = {
18 | queryWithVoteIndex: function(queryParams, callback){
19 | data.LastEvaluatedKey = {
20 | 'Mission+InstrumentID': queryParams.hashKey,
21 | 'votes': 100
22 | };
23 | callback(null, data);
24 | },
25 | getThumbnails: function(photos, callback){
26 | callback();
27 | },
28 | defaultInstrument: 'fcam',
29 | defaultMission: 'curiosity'
30 | };
31 |
32 |
33 | var Blueimp = {
34 | Gallery: function() {}
35 | };
36 |
37 | beforeEach(function(){
38 | inject(function ($controller, $rootScope, $log) {
39 | $log.debug = function(msg) { console.log(msg); };
40 | scope = $rootScope.$new();
41 | log = $log;
42 | location = {
43 | path: function(_path){
44 | path = _path;
45 | }
46 | };
47 | controller = $controller;
48 | timeout = function(callback) {
49 | callback();
50 | };
51 |
52 | controllerArgs = {
53 | $scope: scope,
54 | $log: log,
55 | $routeParams: {},
56 | $timeout: timeout,
57 | $location: location,
58 | MarsPhotosDBAccess: marsPhotos,
59 | Blueimp: Blueimp
60 | };
61 | TopVotedCtrl = controller('TopVotedCtrl', controllerArgs);
62 | });
63 | });
64 |
65 | it('should attach a list of user voted photos to the scope', function(){
66 | expect(scope.photos.length).toBe(2);
67 | });
68 |
69 | it('should set instrument to default if it is not specified in routeParams', function (){
70 | expect(scope.instrument).toBe(marsPhotos.defaultInstrument);
71 | expect(scope.missionInstrument).toBe(marsPhotos.defaultMission + '+' + marsPhotos.defaultInstrument);
72 | });
73 |
74 | it('should set instrument to the one specified in routeParams', function (){
75 | controllerArgs.$routeParams = {instrument: 'mastcam_right'};
76 | TopVotedCtrl = controller('TopVotedCtrl', controllerArgs);
77 | expect(scope.instrument).toBe('mastcam_right');
78 | expect(scope.missionInstrument).toBe(marsPhotos.defaultMission + '+mastcam_right');
79 | });
80 |
81 | it('should set a function to reload', function(){
82 | expect(typeof(scope.reload)).toBe('function');
83 | });
84 |
85 | it('should set path to /topVoted/ + instrument ID when it is reloaded', function(){
86 | scope.instrument = 'mastcam_left';
87 | scope.reload();
88 | expect(path).toBe('/topVoted/mastcam_left');
89 | });
90 |
91 | it('should update photos and set lastEvaluatedKey', function(){
92 | scope.missionInstrument = marsPhotos.defaultMission + '+mastcam_left';
93 | scope.updatePhotos();
94 | expect(scope.lastEvaluatedKey['Mission+InstrumentID']).toBe(marsPhotos.defaultMission + '+mastcam_left');
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/viewer/test/spec/views/image-gallery.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('View: ImageGallery', function() {
4 | beforeEach(module('MSLImageExplorerApp'));
5 | beforeEach(module('views/partials/image-gallery.html'));
6 | beforeEach(module('views/partials/sidemenu.html'));
7 |
8 | var FavoritesCtrl, scope, view;
9 |
10 | var data = {
11 | Items: [
12 | {imageid: '123'},
13 | {imageid: '456'}
14 | ]
15 | };
16 |
17 | var marsPhotos = {
18 | queryUserVotedPhotos: function(queryParams, callback){
19 | callback(null, data);
20 | },
21 | getThumbnails: function(photos, callback) {
22 | callback();
23 | }
24 | };
25 | var Blueimp = {
26 | Gallery: function() {}
27 | };
28 |
29 | // Initialize the controller and a mock scope
30 | beforeEach(inject(function ($templateCache, $compile, $controller, $rootScope, $log) {
31 | var html = $templateCache.get('views/partials/image-gallery.html');
32 | scope = $rootScope.$new();
33 |
34 | FavoritesCtrl = $controller('FavoritesCtrl', {
35 | $scope: scope,
36 | $log: $log,
37 | MarsPhotosDBAccess: marsPhotos,
38 | Blueimp: Blueimp
39 | });
40 | view = $compile(angular.element(html))(scope);
41 | scope.$digest();
42 | }));
43 |
44 | it('should show mars images the user liked', function() {
45 | expect(view.find('h1').text()).toEqual('Mars Images You Liked');
46 | });
47 |
48 | it('should show list of favorite images', function() {
49 | expect(view.find('#links img').length).toBe(2);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/viewer/test/spec/views/sidemenu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('View: Sidemenu', function() {
4 | beforeEach(module('MSLImageExplorerApp'));
5 | beforeEach(module('views/partials/sidemenu.html'));
6 |
7 | var Ctrl, scope, view;
8 |
9 | // Initialize the controller and a mock scope
10 | beforeEach(inject(function ($templateCache, $compile, $controller, $rootScope, $modal, $log) {
11 | var html = $templateCache.get('views/partials/sidemenu.html');
12 | scope = $rootScope.$new();
13 |
14 | Ctrl = $controller('SideMenuCtrl', {
15 | $scope: scope,
16 | $log: $log,
17 | $modal: $modal
18 | });
19 | view = $compile(angular.element(html))(scope);
20 | scope.$digest();
21 | }));
22 |
23 |
24 | it('should have 3 side menu links', function(){
25 | expect(view.find('.sidemenu-link').length).toBe(3);
26 | });
27 |
28 | it('should have 4 instruments in the instrument list', function() {
29 | expect(view.find('#instrumentList li').length).toBe(4);
30 | });
31 |
32 | });
33 |
34 |
35 |
--------------------------------------------------------------------------------
/viewer/test/spec/views/timeline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('View: Timeline', function() {
4 | beforeEach(module('MSLImageExplorerApp'));
5 | beforeEach(module('views/partials/timeline.html'));
6 | beforeEach(module('views/partials/sidemenu.html'));
7 |
8 | var Ctrl, scope, view;
9 |
10 | var data = {
11 | Items: [
12 | /*jshint camelcase: false */
13 | {imageid: '123', instrument: 'fcam', time: {creation_timestamp_utc: 0, received_timestamp_utc: 0}},
14 | {imageid: '456', instrument: 'fcam', votes: 290, time: {creation_timestamp_utc: 0, received_timestamp_utc: 0}}
15 | ]
16 | };
17 |
18 | var marsPhotos = {
19 | queryWithDateIndex: function(queryParams, callback){
20 | callback(null, data);
21 | },
22 | getThumbnails: function(photos, callback) {
23 | callback();
24 | },
25 | instrumentList: {
26 | fcam: {id: 'fcam', name: 'Chemcam RMI'}
27 | }
28 | };
29 |
30 | // Initialize the controller and a mock scope
31 | beforeEach(inject(function ($templateCache, $compile, $controller, $rootScope, $modal, $log) {
32 | var html = $templateCache.get('views/partials/timeline.html');
33 | scope = $rootScope.$new();
34 |
35 | Ctrl = $controller('TimelineCtrl', {
36 | $scope: scope,
37 | $log: $log,
38 | MarsPhotosDBAccess: marsPhotos
39 | });
40 |
41 | view = $compile(angular.element(html))(scope);
42 | scope.$digest();
43 | scope.updatePhotos();
44 | }));
45 |
46 | it('should show 2 images under timeline content div', function(){
47 | expect(view.find('.timeline-item .img-responsive').length).toBe(2);
48 | });
49 |
50 | it('should give imageid as the id for the corresponding panel div', function(){
51 | expect(view.find('#123').length).toBe(1);
52 | expect(view.find('#456').length).toBe(1);
53 | });
54 |
55 | it('should show # of votes on the panel if defined', function(){
56 | expect(view.find('#456 h4').text()).toBe('# of votes: 290');
57 | });
58 |
59 | it('should show 0 if # of votes is not set in the photo object', function(){
60 | expect(view.find('#123 h4').text()).toBe('# of votes: 0');
61 | });
62 |
63 | it('should contain instrument name in the photo description', function(){
64 | expect(view.find('#123 .description').text()).toContain('Chemcam RMI');
65 | });
66 |
67 | });
68 |
69 |
70 |
--------------------------------------------------------------------------------