You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@climate.apache.org by jo...@apache.org on 2014/08/28 05:07:49 UTC

[05/28] CLIMATE-512 - Replace frontend with new Yeoman/Grunt/Bower frontend

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/directives/onblur.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/directives/onblur.js b/ocw-ui/frontend/app/scripts/directives/onblur.js
new file mode 100644
index 0000000..313dbe0
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/directives/onblur.js
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name ocwUiApp.directive:onBlur
+ * @description
+ * # onBlur
+ */
+angular.module('ocwUiApp')
+.directive('onBlur', function() {
+	return {
+        restrict: 'A',
+        link: function($scope, $elem, $attrs) {
+            $elem.bind('blur', function() {
+				$scope.$eval($attrs.onBlur);
+			});
+        },
+    };
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/directives/predictivefilebrowserinput.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/directives/predictivefilebrowserinput.js b/ocw-ui/frontend/app/scripts/directives/predictivefilebrowserinput.js
new file mode 100644
index 0000000..7142c15
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/directives/predictivefilebrowserinput.js
@@ -0,0 +1,316 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name ocwUiApp.directive:predictiveFileBrowserInput
+ * @description
+ * # predictiveFileBrowserInput
+ */
+angular.module('ocwUiApp')
+.directive('predictiveFileBrowserInput', function() {
+	var link = function($scope, $elem, $attrs) {
+		$scope.autocomplete = [];
+		
+		// Set id to use this directive correctly in multiple places
+		$scope.id = 'autoCompletePath'+ $elem.context.id
+		/*
+		 * We need a place to dump our auto-completion options
+		 */
+		$($elem).parent().append('<ul id="' + $scope.id +'"><ul>');
+
+		// Handle user clicks on auto-complete path options
+		$(document).on('click', '#' +$scope.id+ ' li span', function(e) {
+			// Set the input text box's value to that of the user selected path
+			var val = $(e.target).text();
+			$($elem).val(val);
+			// Need to trigger the input box's "input" event so Angular updates the model!
+			$elem.trigger('input'); 
+			
+			// If the user selected a directory, find more results..
+			if (val[val.length - 1] == '/') {
+				$scope.fetchFiles($($elem).val());
+			// Otherwise, remove the auto-complete options...
+			} else {
+				$('#' +$scope.id+ ' li').remove();
+			}
+		});
+
+		/*
+		 * Handle key-down events on the input box
+		 *
+		 * We need to ignore <TAB> presses here so we can auto-complete with <TAB>.
+		 * If we don't ignore here then <TAB> will move the user to the next field
+		 * in the form and our common-prefix-fill won't work later.
+		 */
+		$($elem).keydown(function(e) {
+			var code = e.keyCode || e.which;
+			var BACKSPACE = 8,
+				TAB = 9;
+
+			if (code == TAB)
+				return false;
+		});
+
+		/*
+		 * Handle key-up events on the input box
+		 */
+		$($elem).keyup(function(e) {
+			var code = e.keyCode || e.which;
+			var BACKSPACE = 8,
+				TAB = 9,
+				FORWARD_SLASH = 191;
+
+			if (code === FORWARD_SLASH) {
+				// Fetch new directory information from the server.
+				$scope.fetchFiles($(this).val());
+			} else if (code === TAB) {
+				// Attempt to auto-fill for the user.
+				$scope.handleTabPress();
+			} else if (code == BACKSPACE) {
+				// Need to properly handle the removal of directory information
+				// and the displaying of auto-complete options
+				$scope.handleBackSpace();
+			} else {
+				// Filter auto-complete options based on user input..
+				$scope.handleMiscKeyPress();
+			}
+
+			// This is being used so we can handle backspacing. The user might hold
+			// down the backspace key or select a section of text and delete. This allows
+			// us to compare the result to its prior state, which makes handling
+			// backspaces easier.
+			$scope.lastInputContents = $elem.val();
+		});
+
+		/*
+		 * Grab additional path information from the web-server
+		 *
+		 * Params:
+		 *		path - The path to get a directory listing of.
+		 */
+		// TODO Make this use $HTTP
+		$scope.fetchFiles = function(path) {
+			$.get($scope.baseURL + '/dir/list/' + path, {},
+				 function(data) {
+					 data = data['listing']
+					 $scope.setNewData(data);
+					 $scope.updateAutoComplete();
+				 }, 'json');
+		};
+
+		/*
+		 * Grab additional path information from the web-server and filter the
+		 * results based on the current input text.
+		 *
+		 * Params:
+		 *		path - The path to get a directory listing of.
+		 *
+		 * This is needed to handle deletion of selected text. It is possible that
+		 * the user will select text and delete only part of a word. The results
+		 * need to be filtered based on this partial input.
+		 */
+		// TODO Why isn't this using $http?!?!?! Because I copy and pasted!!!!
+		$scope.fetchFilesAndFilter = function(path) {
+			$.get($scope.baseURL + '/dir/list/' + path, {},
+				 function(data) {
+					 data = data['listing']
+					 $scope.setNewData(data);
+					 $scope.filterResults();
+					 $scope.updateAutoComplete();
+				 }, 'json');
+		};
+
+		/*
+		 * Handle directory data from the server.
+		 *
+		 * We store the entire directory information along with the remaining
+		 * possible options given the users current input. This lets us avoid
+		 * unnecessary calls to the server for directory information every time
+		 * the user deletes something.
+		 */
+		$scope.setNewData = function(data) {
+			$scope.autocomplete = data.sort();
+			$scope.possibleCompletes = $scope.autocomplete;
+		};
+
+		/* 
+		 * Handle <TAB> presses.
+		 *
+		 * Attempt to auto-complete options when the user presses <TAB>.
+		 */
+		$scope.handleTabPress = function() {
+			// If there's only one option available there's no points in trying to
+			// find a common prefix! Just set the value!
+			if ($scope.possibleCompletes.length === 1) {
+				$elem.val($scope.possibleCompletes[0]);
+
+				// Make sure more options are displayed if a directory was selected.
+				$scope.checkForMoreOptions();
+				$scope.updateAutoComplete();
+				return;
+			}
+
+			// Find the greatest common prefix amongst the remaining choices and set
+			// the input text to it.
+			var prefix = $scope.getLargestCommonPrefix($scope.possibleCompletes);
+			$elem.val(prefix);
+			$scope.updateAutoComplete();
+		};
+
+		/*
+		 * Handle Backspacing and option displaying.
+		 *
+		 * The auto-complete options needs to be displayed correctly when the user
+		 * removes directory information.
+		 */
+		$scope.handleBackSpace = function() {
+			var curInputVal = $elem.val();
+
+			// If the user deletes everything in the input box all we need to do
+			// is make sure that the auto-complete options aren't displayed.
+			if (curInputVal.length === 0) {
+				$('#' +$scope.id+ ' li').remove();
+				return;
+			}
+
+			// Figure out how much text the user removed from the input box.
+			var lengthDiff = $scope.lastInputContents.length - curInputVal.length;
+			// Grab the removed text.
+			var removedText = $scope.lastInputContents.substr(-lengthDiff);
+
+			// If the user deleted over a directory we need to fetch information on the
+			// previous directory for auto-completion.
+			if (removedText.indexOf('/') !== -1) {
+				var lastSlashIndex = curInputVal.lastIndexOf('/');
+
+				// If the remaining path still contains a directory...
+				if (lastSlashIndex !== -1) {
+					// Grab the section of the path that points to a valid directory,
+					// fetch the listing, and update the results.
+					var pathToSearch = curInputVal.slice(0, lastSlashIndex + 1);
+					$scope.fetchFilesAndFilter(pathToSearch);
+				} else {
+					// Delete the old auto-complete information in the case where the user
+					// completely removed path information.
+					$('#' +$scope.id+ ' li').remove();
+				}
+			} else {
+				// Otherwise, we just need to filter results based on the remaining input.
+				$scope.filterResults();
+				$scope.updateAutoComplete();
+			}
+		};
+
+		/* 
+		 * Handle all other key presses in the input box
+		 *
+		 * Filter the auto-complete options as the user types to ensure that only options
+		 * which are possible given the current input text are still displayed.
+		 */
+		$scope.handleMiscKeyPress = function() {
+			// Safely exit when there are no options available.
+			if ($scope.autocomplete === [])
+				return;
+
+			// Otherwise, filter the results.
+			$scope.filterResults();
+			$scope.updateAutoComplete();
+		};
+
+		/* 
+		 * When a path is auto-completed with <TAB> we need to check to see if it points
+		 * to a directory. If it does, we still need to fetch results!
+		 */
+		$scope.checkForMoreOptions = function() {
+			var path = $elem.val();
+			if (path[path.length - 1] === '/') {
+				$scope.fetchFiles(path);
+			}
+		};
+
+		/* 
+		 * Calculate the greatest common prefix of the passed options.
+		 *
+		 * Params:
+		 *		Options - An array of strings in which the greatest common prefix
+		 *				  should be found
+		 *
+		 * Returns:
+		 *		The greatest common prefix of the strings.
+		 *
+		 *
+		 * Note - For us, there will always be a prefix of at least '/' since this can't
+		 * possible be called without the users entering a starting directory. As a result,
+		 * we don't explicitly handle the case where there is 0 length common prefix.
+		 */
+		$scope.getLargestCommonPrefix = function(options) {
+			var index = 1;
+			var shortestString = options.reduce(function(a, b) { return a.length < b.length ? a : b; });
+			var longestString = options.reduce(function(a, b) { return a.length > b.length ? a : b; });
+			var	substringToCheck = shortestString[0];
+
+			while (longestString.indexOf(substringToCheck) !== -1) {
+				substringToCheck = shortestString.slice(0, ++index);
+			}
+
+			return longestString.slice(0, index - 1);
+		};
+
+		/* 
+		 * Filter the auto-complete options based on the current input.
+		 */
+		$scope.filterResults = function() {
+			$scope.possibleCompletes = $scope.autocomplete.filter(function(item, index, array) {
+				return (~item.indexOf($($elem).val()));
+			});
+
+			$scope.possibleCompletes.sort();
+		};
+
+		/*
+		 * Update the display of auto-complete options.
+		 */
+		$scope.updateAutoComplete = function() {
+			// Remove all the existing options
+			$('#' +$scope.id+ ' li').remove();
+
+			// We don't need to show anything if the user has completely selected
+			// the only existing option available.
+			if ($scope.possibleCompletes.length === 1) {
+				if ($scope.possibleCompletes[0] === $elem.val()) {
+					return;
+				}
+			}
+
+			// Display all the possible completes
+			$.each($scope.possibleCompletes, function(i, v) {
+				$('#' +$scope.id+ '').append($('<li>').html($('<span>').text(v)));
+			});
+		};
+	};
+
+	return {
+		link: link,
+		scope: true,
+		restrict: 'A'
+	};
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/directives/previewmap.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/directives/previewmap.js b/ocw-ui/frontend/app/scripts/directives/previewmap.js
new file mode 100644
index 0000000..78aae8c
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/directives/previewmap.js
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name ocwUiApp.directive:previewMap
+ * @description
+ * # previewMap
+ */
+angular.module('ocwUiApp')
+.directive('previewMap', function($rootScope) {
+	return {
+		restrict: 'A',
+		scope: {dataset: '=previewMap', index: '=index'},
+		template: '<div id="{{dataset.name}}" class="preview-map"></div>',
+		replace: true,
+		link: function(scope, element, attrs) {
+
+			// Any attribute that contains {{}} interpolation will be set to null in the attrs
+			// parameter during the link function since the first $digest since the compilation
+			// has yet to run to evaluate it! We can't run a $digest in the middle of compilation,
+			// so using an $observe (or $watch) is the best way to get the values.
+			attrs.$observe('id', function(newId) {
+				var map = L.map(attrs.id, {
+					zoom: 0,
+					scrollWheelZoom: false,
+					zoomControl: false,
+					attributionControl: false,
+					worldCopyJump: true,
+				});
+
+				L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {}).addTo(map);
+
+				// Zoom the map to the dataset bound regions (or at least try our best to do so)
+				var datasetBounds = [[scope.dataset.latlonVals.latMax, scope.dataset.latlonVals.lonMin], 
+									 [scope.dataset.latlonVals.latMin, scope.dataset.latlonVals.lonMax]];
+				map.fitBounds(datasetBounds, {});
+
+				// Draw a colored overlay on the region of the map
+				var maplatlon = scope.dataset.latlonVals;
+				var bounds = [[maplatlon.latMax, maplatlon.lonMin], [maplatlon.latMin, maplatlon.lonMax]];
+
+				var polygon = L.rectangle(bounds,{
+					stroke: false,
+					fillColor: $rootScope.fillColors[1],
+					fillOpacity: 0.6
+				});
+
+				// Add layer to Group
+				var rectangleGroup = L.layerGroup();
+				rectangleGroup.addLayer(polygon);
+
+				// Add the overlay to the map
+				rectangleGroup.addTo(map);
+			});
+		}
+	};
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/directives/timeline.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/directives/timeline.js b/ocw-ui/frontend/app/scripts/directives/timeline.js
new file mode 100644
index 0000000..760a819
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/directives/timeline.js
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name ocwUiApp.directive:timeline
+ * @description
+ * # timeline
+ */
+angular.module('ocwUiApp')
+.directive('timeline', function($rootScope, $window) {
+	return {
+		restrict: 'C',
+		replace: true,
+		transclude: true,
+		template: '<div id="OCWtimeline"></div>',
+		link: function(scope, element, attrs) {
+			// Instantiate timeline object.
+			$rootScope.timeline = new links.Timeline(document.getElementById('OCWtimeline'));
+
+			// Redraw the timeline whenever the window is resized
+			angular.element($window).bind('resize', function() {
+				$rootScope.timeline.checkResize();
+			});
+
+			var options = {
+				"width": "100%",
+				"showCurrentTime": false,
+				"moveable": false,
+				"zoomable": false
+			};
+
+			$rootScope.timeline.draw([], options);
+		}
+	}
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/filters/isodatetomiddleendian.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/filters/isodatetomiddleendian.js b/ocw-ui/frontend/app/scripts/filters/isodatetomiddleendian.js
new file mode 100644
index 0000000..7fcd2a9
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/filters/isodatetomiddleendian.js
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc filter
+ * @name ocwUiApp.filter:ISODateToMiddleEndian
+ * @function
+ * @description
+ * # ISODateToMiddleEndian
+ * Filter in the ocwUiApp.
+ */
+angular.module('ocwUiApp')
+.filter('ISODateToMiddleEndian', function() {
+	return function(input) {
+		var original = input;
+
+		// Strip whitespace from the start and end of the string
+		input = input.replace(/(^\s+|\s+$)/g, '');
+
+		// ISO Standard says time is separated from Date with a 'T'. Our timestamps
+		// slightly modify that and use a space. We'll check for both here and prefer
+		// to split on a 'T' if it's available.
+		if (input.indexOf('T') != -1 || input.indexOf(' ') != -1) {
+			input = (input.indexOf('T') != -1) ? input.split('T')[0] : input.split(' ')[0];
+		} 
+		
+		// The components of the date should be split with hyphens. If we can't find them
+		// then the string is poorly formed.
+		if (input.indexOf('-') == -1 || input.split('-').length - 1 != 2) {
+			return original;
+		}
+
+		// At this point the date is probably valid and we should try to convert it!
+		var components = input.split('-');
+		return (components[1] + "/" + components[2] + "/" + components[0]);
+	};
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/services/evaluationsettings.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/services/evaluationsettings.js b/ocw-ui/frontend/app/scripts/services/evaluationsettings.js
new file mode 100644
index 0000000..b53842c
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/services/evaluationsettings.js
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name ocwUiApp.evaluationSettings
+ * @description
+ * # evaluationSettings
+ * Service in the ocwUiApp.
+ */
+angular.module('ocwUiApp')
+  .service('evaluationSettings', function($rootScope, $http) {
+    $http.get($rootScope.baseURL + '/processing/metrics/').then(function(data) {
+        var metrics_data = data['data']['metrics'];
+        var metrics = [];
+
+        for (var i = 0; i < metrics_data.length; ++i) {
+            metrics.push({'name': metrics_data[i], 'select': false});
+        }
+
+        settings['metrics'] = metrics;
+    });
+
+    var settings = {
+      'metrics': [],
+      'temporal': {
+        'options': ['daily', 'monthly', 'yearly'],
+        'selected': 'yearly',
+      },
+      'spatialSelect': null,
+    };
+
+    return {
+      getSettings: function() {
+        return settings;
+      }
+    };
+  });

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/services/regionselectparams.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/services/regionselectparams.js b/ocw-ui/frontend/app/scripts/services/regionselectparams.js
new file mode 100644
index 0000000..cb3f4f8
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/services/regionselectparams.js
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name ocwUiApp.regionSelectParams
+ * @description
+ * # regionSelectParams
+ * Service in the ocwUiApp.
+ */
+angular.module('ocwUiApp')
+.service('regionSelectParams', function() {
+	var parameters = {
+		"areValid" : true,
+		"latMin"   : "",
+		"latMax"   : "",
+		"lonMin"   : "",
+		"lonMax"   : "",
+		"start"    : "",
+		"end"      : "",
+	};
+
+	return {
+		getParameters: function() {
+			return parameters;
+		},
+	};
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/scripts/services/selecteddatasetinformation.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/scripts/services/selecteddatasetinformation.js b/ocw-ui/frontend/app/scripts/services/selecteddatasetinformation.js
new file mode 100644
index 0000000..613cfb8
--- /dev/null
+++ b/ocw-ui/frontend/app/scripts/services/selecteddatasetinformation.js
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name ocwUiApp.selectedDatasetInformation
+ * @description
+ * # selectedDatasetInformation
+ * Service in the ocwUiApp.
+ */
+angular.module('ocwUiApp')
+.service('selectedDatasetInformation', function() {
+	var datasets = [];
+
+	return {
+		getDatasets: function() {
+			return datasets;
+		},
+		getDatasetCount: function() {
+			return datasets.length;
+		},
+		// TODO: Define the structure of the objects that are added with addDataset.
+		addDataset: function(dataset) {
+			// All datasets need a shouldDisplay attribute that is used when rendering
+			// the overlays on the map!
+			dataset.shouldDisplay = false;
+			// The regrid attribute indicates which dataset should be used for spatial regridding
+			dataset.regrid = false;
+
+			datasets.push(dataset);
+		},
+		removeDataset: function(index) {
+			datasets.splice(index, 1);
+		},
+		clearDatasets: function() {
+			datasets.length = 0;
+		},
+	};
+});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/styles/main.css
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/styles/main.css b/ocw-ui/frontend/app/styles/main.css
new file mode 100644
index 0000000..fb42ccd
--- /dev/null
+++ b/ocw-ui/frontend/app/styles/main.css
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *    http: *www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+	
+}
+
+#header-img {
+	
+}
+
+#header-title {
+	
+}
+
+#OCW-powered {
+	
+}
+
+#main-container {
+    margin-top: 20px;
+	min-height: 400px;
+	height: auto !important;
+	height: 400px;
+}
+
+#datasetDiv {
+	height: 750px;
+	overflow-y: auto;
+	overflow-x: hidden;
+}
+
+#ocw-navbar {
+    margin-bottom: 0;
+}
+
+#map { height: 500px; }
+
+/* Small preview map that is displayed alongside dataset information */
+.preview-map {
+	height: 100px;
+	width: 100px;
+}
+
+.small-alert {
+	font-size: 12px;
+	color: green;
+	margin-top: 4px;
+	margin-left: 10px;
+}
+
+.colorSquare {
+	margin-top: 3px;
+	height: 10px;
+	width: 10px;
+}
+
+ul { list-style-type: none; }
+
+.no-color-link { color: #000000; }
+.no-color-link:hover { color: #000000; text-decoration: none; }
+.no-color-link:visited { color: #000000; }
+.no-color-link:active { color: #000000; }
+
+/* Remove the grayed out close button in modal headers */
+.modal-header .close { opacity: 1; }
+
+/* Remove the grayed out close button in modal footers */
+.modal-footer .close { opacity: 1; }
+
+/** 
+  * Timeline 
+  */
+div#OCWtimeline {
+	margin-top: 20px;
+	padding-bottom: 20px;
+}
+
+div.timeline-event {
+	border: none;
+	background: none;
+}
+
+div.timeline-event-content { margin: 0; }
+
+div.ocw-bar { height: 5px; }
+
+/**
+  * Results
+  */
+#results-sidebar {
+	min-height: 400px;
+	height: auto !important;
+	height: 400px;
+}
+
+#results-sidebar-header { font-size: 14px; }
+
+/* Helpers for vertical offsets */
+.top3 { margin-top: 3%; }
+.top7 { margin-top: 7%; }
+.top14 { margin-top:14%; }
+.top21 { margin-top:21%; }
+.top42 { margin-top:42%; }

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/main.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/main.html b/ocw-ui/frontend/app/views/main.html
new file mode 100644
index 0000000..2f428e5
--- /dev/null
+++ b/ocw-ui/frontend/app/views/main.html
@@ -0,0 +1,274 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<!-- Modal for evaluation settings -->
+<div class="modal fade" id="evaluationSettingsModal" role="dialog" aria-labelledby="datasetSelectModalLabel" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button class="close" data-dismiss="modal">&times;</button>
+        <h3>Settings</h3>
+      </div>
+      <div class="modal-body" ng-controller="SettingsCtrl">
+        <h4>Select the metrics you would like to run.</h4>
+        <div ng-repeat="metric in settings.metrics">
+          <label><input type="checkbox" ng-model="metric.select"> {{metric.name}}</label>
+        </div>
+        <hr />
+        <h4>Select how you would like to temporally re-grid the datasets.</h4>
+        <select class="form-control" ng-model="settings.temporal.selected" ng-options="opt for opt in settings.temporal.options"></select>
+        <hr />
+        <h4>Select which dataset to use as the reference.</h4>
+        <select class="form-control" ng-model="settings.spatialSelect" ng-options="dataset as dataset.name for dataset in datasets"></select>
+        <hr />
+        <!-- Temporarily hidden for work on CLIMATE-365.-->
+        <div ng-hide=true>
+        <h4>Select a file which will define the bounds of subregions.</h4>
+        <form class="form-inline" autocomplete="off">
+          <input id="subregionFileInput" predictive-file-browser-input ng-model="settings.subregionFile" type="text" class="input-xlarge" autocomplete="off" />
+        </form>
+        </div>
+        <!--End hidden section for CLIMATE-365-->
+      </div>
+      <div class="modal-footer">
+        <button class="btn btn-warning cancel" data-dismiss="modal">Close</button>
+      </div>
+    </div>
+  </div>
+</div>
+<!-- END - Modal for evaluation settings -->
+
+<div class="row">
+  <div class="col-md-12">
+    <div class="row">
+      <!-- Dataset Select and Display Column -->
+      <div class="col-md-6">
+        <!--Dataset Select Controls-->
+        <div ng-controller="DatasetSelectCtrl">
+          <div class="row">
+            <div class="col-md-1 col-md-offset-10">
+              <button class="btn btn-link no-color-link" ng-click="clearDatasets()" ng-disabled="shouldDisableClearButton()">
+                <span tooltip-placement="left" tooltip-popup-delay="700" tooltip="Clear Datasets">
+                  <i class="fa fa-trash-o fa-2x"></i>
+                </span>
+              </button>
+            </div>
+            <div class="col-md-1">
+              <button class="btn btn-link no-color-link" data-toggle="modal" data-target="#datasetSelect">
+                <span tooltip-placement="left" tooltip-popup-delay="700" tooltip="Add Dataset">
+                  <i class="fa fa-plus fa-2x"></i>
+                </span>
+              </button>
+            </div>
+          </div>
+          <!-- Modal for dataset selection -->
+          <div class="modal fade" id="datasetSelect" role="dialog" aria-labelledby="datasetSelectModalLabel" aria-hidden="true">
+            <div class="modal-dialog">
+              <div class="modal-content">
+                <div class="modal-header">
+                  <h3>Dataset Select</h3>
+                </div>
+                <div class="modal-body">
+                  <tabset>
+                    <tab ng-repeat="tab in templates" heading="{{tab.title}}" active="tab.active" disabled="tab.disabled">
+                      <div ng-include src="tab.url"></div>
+                    </tab>
+                    <li class="pull-right">Queued Datasets: {{datasetCount.length}}</li>
+                  </tabset>
+                </div>
+                <div class="modal-footer">
+                  <button class="btn btn-warning cancel" data-dismiss="modal">Close</button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <!-- END - Modal for dataset selection -->
+          <div class="row">
+            <div class="col-md-12">
+            <hr />
+            </div>
+          </div>
+        </div>
+        <!--Dataset display-->
+        <div ng-controller="DatasetDisplayCtrl" id="datasetDiv">
+            <div ng-repeat="dataset in datasets">
+              <div class="row">
+                <!--Data section-->
+                <div class="col-md-8 col-md-offset-1 muted">
+                  {{dataset.name}}
+                </div>
+                <div class="col-md-1 col-md-offset-2">
+                  <span tooltip-placement="left" tooltip-popup-delay="700" tooltip="Remove Dataset">  
+                    <a class="no-color-link" href="#" ng-click="removeDataset($index)">
+                      <i class="fa fa-remove"></i>
+                    </a>
+                  </span>
+                </div>
+              </div>
+              <!--Time Values!-->
+              <div class="row">
+                <!--Dataset Info Section-->
+                <div class="col-md-9">
+                  <div class="row">
+                    <div class="col-md-2 col-md-offset-1 text-center">Start:</div>
+                    <div class="col-md-2">
+                      <div class="col-md-2 text-center">{{dataset.timeVals.start | ISODateToMiddleEndian}}</div>
+                    </div>
+                    <div class="col-md-2 text-center">End:</div>
+                    <div class="col-md-2">
+                      <div class="col-md-2 text-center">{{dataset.timeVals.end | ISODateToMiddleEndian}}</div>
+                    </div>
+                  </div>
+                  <!--Lat/Long Values!-->
+                  <div class="row">
+                    <div class="col-md-2 col-md-offset-1 text-center">North:</div>
+                    <div class="col-md-2 text-center">
+                      {{dataset.latlonVals.latMax | number:2}}
+                    </div>
+                    <div class="col-md-2 text-center">West:</div>
+                    <div class="col-md-2 text-center">
+                      {{dataset.latlonVals.lonMin | number:2}}
+                    </div>
+                  </div>
+                  <div class="row">
+                    <div class="col-md-2 col-md-offset-1 text-center">South:</div>
+                    <div class="col-md-2 text-center">
+                      {{dataset.latlonVals.latMin | number:2}}
+                    </div>
+                    <div class="col-md-2 text-center">East:</div>
+                    <div class="col-md-2 text-center">
+                      {{dataset.latlonVals.lonMax | number:2}}
+                    </div>
+                  </div>
+                </div>
+                <!--Preview Map Section-->
+                <div class="col-md-3">
+                  <!--If the dataset is global we show a picture of a globe instead of the actual map-->
+                  <div ng-hide="dataset.latlonVals.lonMin == -180 && dataset.latlonVals.lonMax == 180 && 
+                                dataset.latlonVals.latMin == -90 && dataset.latlonVals.latMax == 90" 
+                                preview-map="dataset" index="$index"></div>
+                  <div ng-show="dataset.latlonVals.lonMin == -180 && dataset.latlonVals.lonMax == 180 &&
+                                dataset.latlonVals.latMin == -90 && dataset.latlonVals.latMax == 90">
+                    <img src="img/globe.png" class="preview-map">
+                  </div>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-6 col-md-offset-3"><hr /></div>
+              </div>
+            </div>
+          </div>
+      </div>
+
+      <!-- Map, Timeline, and Parameter Control Column -->
+      <div class="col-md-6">
+        <!--Map-->
+        <div class="row"  ng-controller="WorldMapCtrl">
+          <div class="col-md-12">
+            <leaflet-map id="map"></leaflet-map>
+          </div>
+        </div>
+        
+        <!--Timeline-->
+        <div class="row">
+          <div class="col-md-12" ng-controller="TimelineCtrl">
+            <div class="timeline"></div>
+          </div>
+        </div>
+        
+        <div class="row">
+          <div class="col-md-12" ng-controller="ParameterSelectCtrl">
+            <div class="row top3">
+              <div class="col-md-2 text-center">Start Date:</div>
+              <div class="col-md-4">
+                <form>
+                  <!--This styling HAD to be done inline. Using a class wouldn't work and for some -->
+                  <!--reason the input boxes refused to be 100% wide when their span size was set.-->
+                  <input ng-disabled="shouldDisableControls()" on-blur="checkParameters();" ng-model="displayParams.start" ui-date="datepickerSettings" ui-date-format="yy-mm-dd" type="text" class="col-md-4 text-center" style="width:100%" />
+                </form>
+              </div>
+              <div class="col-md-2 text-center">End Date:</div>
+              <div class="col-md-4">
+                <form>
+                  <!--This styling HAD to be done inline. Using a class wouldn't work and for some -->
+                  <!--reason the input boxes refused to be 100% wide when their span size was set.-->
+                  <input ng-disabled="shouldDisableControls()" on-blur="checkParameters();" ng-model="displayParams.end" ui-date="datepickerSettings" ui-date-format="yy-mm-dd" type="text" class="col-md-4 text-center" style="width:100%"/>
+                </form>
+              </div>
+            </div>
+            <div class="row top3">
+              <div class="col-md-2 text-center">North:</div>
+              <div class="col-md-4">
+                <form action="">
+                  <input ng-disabled="shouldDisableControls()" ng-model="displayParams.latMax"  on-blur="checkParameters();" type="text" class="col-md-4 text-center" style="width:100%"/>
+                </form>
+              </div>
+              <div class="col-md-2 text-center">South:</div>
+              <div class="col-md-4">
+                <form action="">
+                  <!--This styling HAD to be done inline. Using a class wouldn't work and for some -->
+                  <!--reason the input boxes refused to be 100% wide when their span size was set.-->
+                  <input ng-disabled="shouldDisableControls()" ng-model="displayParams.latMin" on-blur="checkParameters();" type="text" class="col-md-4 text-center" style="width:100%"/>
+                </form>
+              </div>
+            </div>
+            <div class="row top3">
+              <div class="col-md-2 text-center">East:</div>
+              <div class="col-md-4">
+                <form>
+                  <!--This styling HAD to be done inline. Using a class wouldn't work and for some -->
+                  <!--reason the input boxes refused to be 100% wide when their span size was set.-->
+                  <input ng-disabled="shouldDisableControls()" ng-model="displayParams.lonMax" on-blur="checkParameters();" type="text" class="col-md-4 text-center" style="width:100%"/>
+                </form>
+              </div>
+              <div class="col-md-2 text-center">West:</div>
+              <div class="col-md-4">
+                <form>
+                  <!--This styling HAD to be done inline. Using a class wouldn't work and for some -->
+                  <!--reason the input boxes refused to be 100% wide when their span size was set.-->
+                  <input ng-disabled="shouldDisableControls()" ng-model="displayParams.lonMin" on-blur="checkParameters();"; type="text" class="col-md-4 text-center" style="width:100%"/>
+                </form>
+              </div>
+            </div>
+            <div class="row top3">
+              <div class="col-md-2 col-md-offset-6">
+                <!--<button class="btn btn-link no-color-link pull-right" bootstrap-modal-open="evaluationSettings">-->
+                <button class="btn btn-link no-color-link pull-right" data-toggle="modal" data-target="#evaluationSettingsModal">
+                  <span tooltip-placement="left" tooltip-popup-delay="700" tooltip="Settings">
+                    <span class="fa-stack fa-lg">
+                      <i class="fa fa-square-o fa-stack-2x"></i>
+                      <i class="fa fa-cogs fa-stack-1x"></i>
+                    </span>
+                  </span>
+                </button>
+              </div>
+              <div class="col-md-4">
+                <button ng-click="runEvaluation()" ng-disabled="shouldDisableEvaluateButton()" class="btn btn-block btn-primary">
+                  <div ng-hide="runningEval">Evaluate</div>
+                  <div ng-show="runningEval"><i class="fa fa-spinner fa-spin"></i></div>
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      </div>
+  </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/modelselect.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/modelselect.html b/ocw-ui/frontend/app/views/modelselect.html
new file mode 100644
index 0000000..9bc128f
--- /dev/null
+++ b/ocw-ui/frontend/app/views/modelselect.html
@@ -0,0 +1,87 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="container">
+  <div class="row">
+    <div class="span10 offset1 columns">
+      <div class="row">
+        <div class="span10">
+          <center>
+            <form id="modelSelectorForm">
+              <input type="file" name="modelSelector">
+            </form>
+          </center>
+        </div>
+      </div>
+      <div class="row">
+        <div class="span10 offset2">
+          <form class="form-horizontal" id="parameterSelectorForm">
+            <div class="control-group">
+              <label class="control-label" for="paramSelect">Parameter Value</label>
+              <div class="controls">
+                <select id="paramSelect">
+                  <option ng-repeat="param in modelParameters">
+                    {{param.text}}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="control-group">
+              <label class="control-label" for="latSelect">Latitude Variable</label>
+              <div class="controls">
+                <select id="latSelect">
+                  <option ng-repeat="lat in latVariables">
+                    {{lat.text}}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="control-group">
+              <label class="control-label" for="lonSelect">Longitude Variable</label>
+              <div class"controls">
+                <select id="lonSelect">
+                  <option ng-repeat="lon in lonVariables">
+                    {{lon.text}}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="control-group">
+              <label class="control-label" for="dateTimeSelect">Date/Time Variable</label>
+              <div class="controls">
+                <select id="dateTimeSelect">
+                  <option ng-repeat="dateTime in dateTimeVariables">
+                    {{dateTime.text}}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="control-group">
+              <div class="controls">
+                <button type="submit" class="btn btn-warn">Cancel</button>
+                <button type="submit" class="btn">Add Model</button>
+              </div>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/results.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/results.html b/ocw-ui/frontend/app/views/results.html
new file mode 100644
index 0000000..432bba1
--- /dev/null
+++ b/ocw-ui/frontend/app/views/results.html
@@ -0,0 +1,34 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="row">
+  <div class="col-md-3">
+    <div id="results-sidebar" class="pa-sidebar well well-small">
+      <ul class="nav nav-list">
+        <li id="results-sidebar-header" class="nav-header">Latest Run Results</li>
+        <li ng-repeat="result in results"
+            ng-class="{ active: $state.includes('results.detail') && $stateParams.resultId == result }">
+          <a href="#/results/{{result.replace('/', '')}}" >{{result}}</a>
+        </li>
+      </ul>
+      <div ui-view="menu"></div>
+    </div>
+  </div>
+  <div class="col-md-9" ui-view ng-animate="{enter:'fade-enter'}"></div>
+</div>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/resultsdetail.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/resultsdetail.html b/ocw-ui/frontend/app/views/resultsdetail.html
new file mode 100644
index 0000000..4083ff3
--- /dev/null
+++ b/ocw-ui/frontend/app/views/resultsdetail.html
@@ -0,0 +1,30 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div>
+  <h2>{{result}}</h2>
+  <div class="row text-center">
+    <div class="{{alertClass}}">{{alertMessage}}</div>
+    <ul>
+      <li ng-repeat="figure in figures">
+        <img class="img-responsive" ng-src="{{baseURL}}/static/eval_results/{{figure}}" alt="" />
+      </li>
+    </ul>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/resultslist.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/resultslist.html b/ocw-ui/frontend/app/views/resultslist.html
new file mode 100644
index 0000000..f4664c6
--- /dev/null
+++ b/ocw-ui/frontend/app/views/resultslist.html
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<h4>Select one of your previous runs. If none are displayed, please return
+  to the main page and start one!</h4>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/selectobservation.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/selectobservation.html b/ocw-ui/frontend/app/views/selectobservation.html
new file mode 100644
index 0000000..fd10ff8
--- /dev/null
+++ b/ocw-ui/frontend/app/views/selectobservation.html
@@ -0,0 +1,80 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div ng-controller="ObservationSelectCtrl">
+  <div class="row">
+    <div class="col-md-12">
+      <div class="row">
+        <!--<form class="form-inline" autocomplete="off">-->
+        <form class="form" autocomplete="off">
+          <div class="form-group">
+            <div class="col-md-9">
+              <input id="observationFileInput" predictive-file-browser-input ng-model="filePathInput" type="text" class="form-control" autocomplete="off" placeholder="Navigate to a .nc file. Start by typing '/'" />
+            </div>
+            <div class="col-md-2">
+              <button class="btn" ng-click="uploadLocalFile()" ng-disabled="shouldDisableLoadButton()">
+                <div ng-hide="loadingFile">Parse File</div>
+                <div ng-show="loadingFile"><i class="icon-spinner icon-spin"></i></div>
+              </button>
+            </div>
+          </div>
+        </form>
+      </div>
+      <hr />
+      <div class="row top3">
+        <div class="col-md-5 text-center">
+          Evaluation Variable
+        </div>
+        <div class="col-md-6">
+          <select class="form-control" ng-model="paramSelect" ng-options="param for param in params"></select>
+        </div>
+      </div>
+      <div class="row top3">
+        <div class="col-md-5 text-center">
+          Latitude Variable
+        </div>
+        <div class="col-md-6">
+          <select class="form-control" ng-model="latsSelect" ng-options="lat for lat in lats"></select>
+        </div>
+      </div>
+      <div class="row top3">
+        <div class="col-md-5 text-center">
+          Longitude Variable
+        </div>
+        <div class="col-md-6">
+          <select class="form-control" ng-model="lonsSelect" ng-options="lon for lon in lons"></select>
+        </div>
+      </div>
+      <div class="row top3">
+        <div class="col-md-5 text-center">
+          Date/Time Variable
+        </div>
+        <div class="col-md-6">
+          <select class="form-control" ng-model="timeSelect" ng-options="time for time in times"></select>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="row top3">
+    <div class="col-md-3 col-md-offset-1">
+      <button class="btn btn-primary btn-block" ng-click="addDataSet()">Add Dataset</button>
+    </div>
+    <div class="pull-left small-alert" ng-show="fileAdded">Successfully added dataset...</div>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/app/views/selectrcmed.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/app/views/selectrcmed.html b/ocw-ui/frontend/app/views/selectrcmed.html
new file mode 100644
index 0000000..6b09f48
--- /dev/null
+++ b/ocw-ui/frontend/app/views/selectrcmed.html
@@ -0,0 +1,40 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div ng-controller="RcmedSelectionCtrl">
+  <div class="row">
+    <div class="col-md-10 col-md-offset-1">
+      <div class="row top3">Select the RCMED dataset that you would like use.</div>
+      <div class="row top3">
+        <select ng-change="dataSelectUpdated()" class="form-control" ng-model="datasetSelection" ng-options="obs as obs.longname for obs in availableObs"></select>
+      </div>
+      <div class="row top3">Select the dataset parameter that you would like to test.</div>
+      <div class="row top3">
+        <select class="form-control" ng-model="parameterSelection" ng-options="param as param.shortname for param in retrievedObsParams"></select>
+      </div>
+
+      <div class="row top3">
+        <div class="col-md-3">
+          <button class="btn btn-primary btn-block-left" ng-click="addObservation()">Add Observation</button>
+        </div>
+        <div class="pull-left small-alert" ng-show="fileAdded">Successfully added dataset...</div>
+      </div>
+    </div>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/bower.json
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/bower.json b/ocw-ui/frontend/bower.json
new file mode 100644
index 0000000..d288a19
--- /dev/null
+++ b/ocw-ui/frontend/bower.json
@@ -0,0 +1,25 @@
+{
+  "name": "ocw-ui",
+  "version": "0.0.0",
+  "dependencies": {
+    "angular": "1.2.16",
+    "json3": "~3.3.1",
+    "es5-shim": "~3.1.0",
+    "bootstrap": "~3.2.0",
+    "angular-resource": "1.2.16",
+    "angular-cookies": "1.2.16",
+    "angular-animate": "1.2.16",
+    "angular-route": "1.2.16",
+    "angular-ui-router": "angular-ui/ui-router#~0.2.10",
+    "leaflet": "~0.7.3",
+    "chap-links-timeline": "~2.6.1",
+    "components-font-awesome": "~4.1.0",
+    "angular-bootstrap": "~0.11.0",
+    "angular-ui-date": "~0.0.3"
+  },
+  "devDependencies": {
+    "angular-mocks": "1.2.16",
+    "angular-scenario": "1.2.16"
+  },
+  "appPath": "app"
+}

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/config/karma-e2e.conf.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/config/karma-e2e.conf.js b/ocw-ui/frontend/config/karma-e2e.conf.js
deleted file mode 100755
index 746879e..0000000
--- a/ocw-ui/frontend/config/karma-e2e.conf.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
-**/
-basePath = '../';
-
-files = [
-  ANGULAR_SCENARIO,
-  ANGULAR_SCENARIO_ADAPTER,
-  'test/e2e/**/*.js'
-];
-
-autoWatch = false;
-
-browsers = ['Chrome'];
-
-singleRun = true;
-
-proxies = {
-  '/': 'http://localhost:8000/'
-};
-
-junitReporter = {
-  outputFile: 'test_out/e2e.xml',
-  suite: 'e2e'
-};

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/config/karma.conf.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/config/karma.conf.js b/ocw-ui/frontend/config/karma.conf.js
deleted file mode 100755
index 65b5013..0000000
--- a/ocw-ui/frontend/config/karma.conf.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
-**/
-basePath = '../';
-
-files = [
-  JASMINE,
-  JASMINE_ADAPTER,
-  'app/js/lib/jquery/jquery-1.10.1.js',
-  'app/js/lib/jquery/jquery-ui/jquery-ui-1.10.3.min.js',
-  'app/js/lib/bootstrap/bootstrap.js',
-  'app/js/lib/angular/angular.js',
-  'app/js/lib/angular/angular-*.js',
-  'app/js/lib/angular-ui/*.js',
-  'test/lib/angular/angular-mocks.js',
-  'app/js/lib/jquery/jquery-ui/datepicker-wrapper/date.js',
-  'app/js/lib/leaflet/leaflet-0.5.js',
-  'app/js/app.js',
-  'app/js/controllers/*.js',
-  'app/js/directives/*.js',
-  'app/js/services/*.js',
-  'app/js/filters/*.js',
-  'test/unit/**/*.js'
-];
-
-autoWatch = true;
-
-browsers = ['Chrome'];
-
-junitReporter = {
-  outputFile: 'test_out/unit.xml',
-  suite: 'unit'
-};

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/package.json
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/package.json b/ocw-ui/frontend/package.json
new file mode 100644
index 0000000..be2f597
--- /dev/null
+++ b/ocw-ui/frontend/package.json
@@ -0,0 +1,40 @@
+{
+  "name": "ocwui",
+  "version": "0.0.0",
+  "dependencies": {},
+  "devDependencies": {
+    "grunt": "^0.4.1",
+    "grunt-autoprefixer": "^0.7.3",
+    "grunt-concurrent": "^0.5.0",
+    "grunt-contrib-clean": "^0.5.0",
+    "grunt-contrib-concat": "^0.4.0",
+    "grunt-contrib-connect": "^0.7.1",
+    "grunt-contrib-copy": "^0.5.0",
+    "grunt-contrib-cssmin": "^0.9.0",
+    "grunt-contrib-htmlmin": "^0.3.0",
+    "grunt-contrib-imagemin": "^0.7.0",
+    "grunt-contrib-jshint": "^0.10.0",
+    "grunt-contrib-uglify": "^0.4.0",
+    "grunt-contrib-watch": "^0.6.1",
+    "grunt-filerev": "^0.2.1",
+    "grunt-google-cdn": "^0.4.0",
+    "grunt-karma": "^0.8.3",
+    "grunt-newer": "^0.7.0",
+    "grunt-ngmin": "^0.0.3",
+    "grunt-svgmin": "^0.4.0",
+    "grunt-usemin": "^2.1.1",
+    "grunt-wiredep": "^1.8.0",
+    "jshint-stylish": "^0.2.0",
+    "karma": "^0.12.17",
+    "karma-jasmine": "^0.1.5",
+    "karma-phantomjs-launcher": "^0.1.4",
+    "load-grunt-tasks": "^0.4.0",
+    "time-grunt": "^0.3.1"
+  },
+  "engines": {
+    "node": ">=0.10.0"
+  },
+  "scripts": {
+    "test": "grunt test"
+  }
+}

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/scripts/e2e-test.sh
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/scripts/e2e-test.sh b/ocw-ui/frontend/scripts/e2e-test.sh
deleted file mode 100755
index dfca333..0000000
--- a/ocw-ui/frontend/scripts/e2e-test.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-BASE_DIR=`dirname $0`
-
-echo ""
-echo "Starting Karma Server"
-echo "-------------------------------------------------------------------"
-
-karma start $BASE_DIR/../config/karma-e2e.conf.js $*

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/scripts/test.sh
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/scripts/test.sh b/ocw-ui/frontend/scripts/test.sh
deleted file mode 100755
index 7094550..0000000
--- a/ocw-ui/frontend/scripts/test.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-BASE_DIR=`dirname $0`
-
-echo ""
-echo "Starting Karma Server"
-echo "-------------------------------------------------------------------"
-
-karma start $BASE_DIR/../config/karma.conf.js $*

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/scripts/web-server.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/scripts/web-server.js b/ocw-ui/frontend/scripts/web-server.js
deleted file mode 100755
index e137ad2..0000000
--- a/ocw-ui/frontend/scripts/web-server.js
+++ /dev/null
@@ -1,313 +0,0 @@
-#!/usr/bin/env node
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
-**/
-
-var util = require('util'),
-    http = require('http'),
-    fs = require('fs'),
-    url = require('url'),
-    events = require('events');
-
-var DEFAULT_PORT = 8000;
-
-function main(argv) {
-  new HttpServer({
-    'GET': createServlet(StaticServlet),
-    'HEAD': createServlet(StaticServlet)
-  }).start(Number(argv[2]) || DEFAULT_PORT);
-}
-
-function escapeHtml(value) {
-  return value.toString().
-    replace('<', '&lt;').
-    replace('>', '&gt;').
-    replace('"', '&quot;');
-}
-
-function createServlet(Class) {
-  var servlet = new Class();
-  return servlet.handleRequest.bind(servlet);
-}
-
-/**
- * An Http server implementation that uses a map of methods to decide
- * action routing.
- *
- * @param {Object} Map of method => Handler function
- */
-function HttpServer(handlers) {
-  this.handlers = handlers;
-  this.server = http.createServer(this.handleRequest_.bind(this));
-}
-
-HttpServer.prototype.start = function(port) {
-  this.port = port;
-  this.server.listen(port);
-  util.puts('Http Server running at http://localhost:' + port + '/');
-};
-
-HttpServer.prototype.parseUrl_ = function(urlString) {
-  var parsed = url.parse(urlString);
-  parsed.pathname = url.resolve('/', parsed.pathname);
-  return url.parse(url.format(parsed), true);
-};
-
-HttpServer.prototype.handleRequest_ = function(req, res) {
-  var logEntry = req.method + ' ' + req.url;
-  if (req.headers['user-agent']) {
-    logEntry += ' ' + req.headers['user-agent'];
-  }
-  util.puts(logEntry);
-  req.url = this.parseUrl_(req.url);
-  var handler = this.handlers[req.method];
-  if (!handler) {
-    res.writeHead(501);
-    res.end();
-  } else {
-    handler.call(this, req, res);
-  }
-};
-
-/**
- * Handles static content.
- */
-function StaticServlet() {}
-
-StaticServlet.MimeMap = {
-  'txt': 'text/plain',
-  'html': 'text/html',
-  'css': 'text/css',
-  'xml': 'application/xml',
-  'json': 'application/json',
-  'js': 'application/javascript',
-  'jpg': 'image/jpeg',
-  'jpeg': 'image/jpeg',
-  'gif': 'image/gif',
-  'png': 'image/png',
-  'svg': 'image/svg+xml'
-};
-
-StaticServlet.prototype.handleRequest = function(req, res) {
-  var self = this;
-  var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){
-    return String.fromCharCode(parseInt(hex, 16));
-  });
-  var parts = path.split('/');
-  if (parts[parts.length-1].charAt(0) === '.')
-    return self.sendForbidden_(req, res, path);
-  if (~path.indexOf("dirlist"))
-    return self.getDirList_(req, res, path);
-  fs.stat(path, function(err, stat) {
-    if (err)
-      return self.sendMissing_(req, res, path);
-    if (stat.isDirectory())
-      return self.sendDirectory_(req, res, path);
-    return self.sendFile_(req, res, path);
-  });
-}
-
-StaticServlet.prototype.sendError_ = function(req, res, error) {
-  res.writeHead(500, {
-      'Content-Type': 'text/html'
-  });
-  res.write('<!doctype html>\n');
-  res.write('<title>Internal Server Error</title>\n');
-  res.write('<h1>Internal Server Error</h1>');
-  res.write('<pre>' + escapeHtml(util.inspect(error)) + '</pre>');
-  util.puts('500 Internal Server Error');
-  util.puts(util.inspect(error));
-};
-
-StaticServlet.prototype.sendMissing_ = function(req, res, path) {
-  path = path.substring(1);
-  res.writeHead(404, {
-      'Content-Type': 'text/html'
-  });
-  res.write('<!doctype html>\n');
-  res.write('<title>404 Not Found</title>\n');
-  res.write('<h1>Not Found</h1>');
-  res.write(
-    '<p>The requested URL ' +
-    escapeHtml(path) +
-    ' was not found on this server.</p>'
-  );
-  res.end();
-  util.puts('404 Not Found: ' + path);
-};
-
-StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
-  path = path.substring(1);
-  res.writeHead(403, {
-      'Content-Type': 'text/html'
-  });
-  res.write('<!doctype html>\n');
-  res.write('<title>403 Forbidden</title>\n');
-  res.write('<h1>Forbidden</h1>');
-  res.write(
-    '<p>You do not have permission to access ' +
-    escapeHtml(path) + ' on this server.</p>'
-  );
-  res.end();
-  util.puts('403 Forbidden: ' + path);
-};
-
-StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
-  res.writeHead(301, {
-      'Content-Type': 'text/html',
-      'Location': redirectUrl
-  });
-  res.write('<!doctype html>\n');
-  res.write('<title>301 Moved Permanently</title>\n');
-  res.write('<h1>Moved Permanently</h1>');
-  res.write(
-    '<p>The document has moved <a href="' +
-    redirectUrl +
-    '">here</a>.</p>'
-  );
-  res.end();
-  util.puts('301 Moved Permanently: ' + redirectUrl);
-};
-
-StaticServlet.prototype.getDirList_ = function(req, res, path) {
-  res.writeHead(200, {
-	'Content-Type': 'json'
-  });
-
-  // Grab the passed path value
-  var pathQuery = url.parse(req.url, true).query.path
-  // Using the supplied path, grab directory information
-  var dirList = fs.readdirSync(pathQuery);
-
-  // Filter out any hidden files or current/previous directory references
-  dirList = dirList.filter(function(item, index, array) {
-	return (item[0] !== ".");
-  });
-  
-  // Generate the full path names for all the items found when 'ls'-ing 
-  // the passed directory.
-  dirList = dirList.map(function(item, index, array) {
-    var temp = item; 
-
-	// Make sure the path is joined properly. Sometimes there will be a trailing
-	// '/' in the path and sometimes there won't. Don't want to end up with '//'.
-    if (pathQuery[pathQuery.length - 1] === "/") {
-      temp = pathQuery + item;
-    } else {
-  	  temp = pathQuery + "/" + item;
-    }
-  
-	// We want the directories that are found to have a trailing '/'. Let's make sure
-	// that we do that!
-    var ret = temp;
-    if (fs.existsSync(temp + "/")) {
-     ret = temp + "/";
-    }  
-
-    return ret;
-  });
-  
-  // Sort all the results alphabetically ignoring case.
-  dirList = dirList.sort(function(a, b) {
-    if (a.toLowerCase() < b.toLowerCase()) return -1;
-    if (a.toLowerCase() > b.toLowerCase()) return 1;
-    return 0;
-  });
-
-  res.write(JSON.stringify(dirList));
-  res.end();
-}
-
-StaticServlet.prototype.sendFile_ = function(req, res, path) {
-  var self = this;
-  var file = fs.createReadStream(path);
-  res.writeHead(200, {
-    'Content-Type': StaticServlet.
-      MimeMap[path.split('.').pop()] || 'text/plain'
-  });
-  if (req.method === 'HEAD') {
-    res.end();
-  } else {
-    file.on('data', res.write.bind(res));
-    file.on('close', function() {
-      res.end();
-    });
-    file.on('error', function(error) {
-      self.sendError_(req, res, error);
-    });
-  }
-};
-
-StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
-  var self = this;
-  if (path.match(/[^\/]$/)) {
-    req.url.pathname += '/';
-    var redirectUrl = url.format(url.parse(url.format(req.url)));
-    return self.sendRedirect_(req, res, redirectUrl);
-  }
-  fs.readdir(path, function(err, files) {
-    if (err)
-      return self.sendError_(req, res, error);
-
-    if (!files.length)
-      return self.writeDirectoryIndex_(req, res, path, []);
-
-    var remaining = files.length;
-    files.forEach(function(fileName, index) {
-      fs.stat(path + '/' + fileName, function(err, stat) {
-        if (err)
-          return self.sendError_(req, res, err);
-        if (stat.isDirectory()) {
-          files[index] = fileName + '/';
-        }
-        if (!(--remaining))
-          return self.writeDirectoryIndex_(req, res, path, files);
-      });
-    });
-  });
-};
-
-StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
-  path = path.substring(1);
-  res.writeHead(200, {
-    'Content-Type': 'text/html'
-  });
-  if (req.method === 'HEAD') {
-    res.end();
-    return;
-  }
-  res.write('<!doctype html>\n');
-  res.write('<title>' + escapeHtml(path) + '</title>\n');
-  res.write('<style>\n');
-  res.write('  ol { list-style-type: none; font-size: 1.2em; }\n');
-  res.write('</style>\n');
-  res.write('<h1>Directory: ' + escapeHtml(path) + '</h1>');
-  res.write('<ol>');
-  files.forEach(function(fileName) {
-    if (fileName.charAt(0) !== '.') {
-      res.write('<li><a href="' +
-        escapeHtml(fileName) + '">' +
-        escapeHtml(fileName) + '</a></li>');
-    }
-  });
-  res.write('</ol>');
-  res.end();
-};
-
-// Must be last,
-main(process.argv);

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/test/.jshintrc
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/test/.jshintrc b/ocw-ui/frontend/test/.jshintrc
new file mode 100644
index 0000000..b1be025
--- /dev/null
+++ b/ocw-ui/frontend/test/.jshintrc
@@ -0,0 +1,36 @@
+{
+  "node": true,
+  "browser": true,
+  "esnext": true,
+  "bitwise": true,
+  "camelcase": true,
+  "curly": true,
+  "eqeqeq": true,
+  "immed": true,
+  "indent": 2,
+  "latedef": true,
+  "newcap": true,
+  "noarg": true,
+  "quotmark": "single",
+  "regexp": true,
+  "undef": true,
+  "unused": true,
+  "strict": true,
+  "trailing": true,
+  "smarttabs": true,
+  "globals": {
+    "after": false,
+    "afterEach": false,
+    "angular": false,
+    "before": false,
+    "beforeEach": false,
+    "browser": false,
+    "describe": false,
+    "expect": false,
+    "inject": false,
+    "it": false,
+    "jasmine": false,
+    "spyOn": false
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/test/e2e/runner.html
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/test/e2e/runner.html b/ocw-ui/frontend/test/e2e/runner.html
deleted file mode 100755
index 73c7733..0000000
--- a/ocw-ui/frontend/test/e2e/runner.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
- ~ Licensed to the Apache Software Foundation (ASF) under one
- ~ or more contributor license agreements.  See the NOTICE file
- ~ distributed with this work for additional information
- ~ regarding copyright ownership.  The ASF licenses this file
- ~ to you under the Apache License, Version 2.0 (the
- ~ "License"); you may not use this file except in compliance
- ~ with the License.  You may obtain a copy of the License at
- ~
- ~    http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing,
- ~ software distributed under the License is distributed on an
- ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- ~ KIND, either express or implied.  See the License for the
- ~ specific language governing permissions and limitations
- ~ under the License.
- -->
-<!doctype html>
-<html lang="en">
-  <head>
-    <title>End2end Test Runner</title>
-    <script src="../lib/angular/angular-scenario.js" ng-autotest></script>
-    <script src="scenarios.js"></script>
-  </head>
-  <body>
-  </body>
-</html>

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/test/e2e/scenarios.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/test/e2e/scenarios.js b/ocw-ui/frontend/test/e2e/scenarios.js
deleted file mode 100755
index c7c0e6c..0000000
--- a/ocw-ui/frontend/test/e2e/scenarios.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
-**/
-'use strict';
-
-/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
-
-describe('my app', function() {
-
-  beforeEach(function() {
-    browser().navigateTo('../../app/index.html');
-  });
-
-
-  it('should automatically redirect to /view1 when location hash/fragment is empty', function() {
-    expect(browser().location().url()).toBe("/view1");
-  });
-
-
-  describe('view1', function() {
-
-    beforeEach(function() {
-      browser().navigateTo('#/view1');
-    });
-
-
-    it('should render view1 when user navigates to /view1', function() {
-      expect(element('[ng-view] p:first').text()).
-        toMatch(/partial for view 1/);
-    });
-
-  });
-
-
-  describe('view2', function() {
-
-    beforeEach(function() {
-      browser().navigateTo('#/view2');
-    });
-
-
-    it('should render view2 when user navigates to /view2', function() {
-      expect(element('[ng-view] p:first').text()).
-        toMatch(/partial for view 2/);
-    });
-
-  });
-});

http://git-wip-us.apache.org/repos/asf/climate/blob/652ea657/ocw-ui/frontend/test/karma.conf.js
----------------------------------------------------------------------
diff --git a/ocw-ui/frontend/test/karma.conf.js b/ocw-ui/frontend/test/karma.conf.js
new file mode 100644
index 0000000..744d927
--- /dev/null
+++ b/ocw-ui/frontend/test/karma.conf.js
@@ -0,0 +1,86 @@
+// Karma configuration
+// http://karma-runner.github.io/0.12/config/configuration-file.html
+// Generated on 2014-07-15 using
+// generator-karma 0.8.3
+
+module.exports = function(config) {
+  'use strict';
+
+  config.set({
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: true,
+
+    // base path, that will be used to resolve files and exclude
+    basePath: '../',
+
+    // testing framework to use (jasmine/mocha/qunit/...)
+    frameworks: ['jasmine'],
+
+    // list of files / patterns to load in the browser
+    files: [
+      'bower_components/angular/angular.js',
+      'bower_components/angular-mocks/angular-mocks.js',
+      'bower_components/angular-animate/angular-animate.js',
+      'bower_components/angular-cookies/angular-cookies.js',
+      'bower_components/angular-resource/angular-resource.js',
+      'bower_components/angular-route/angular-route.js',
+      'bower_components/angular-animate/angular-animate.js',
+      'bower_components/angular-bootstrap/ui-bootstrap.js',
+      // For some reason this causes the tests to completely fail to run
+      // if it is uncommented.
+      //'bower_components/angular-scenario/angular-scenario.js',
+      'bower_components/angular-ui-date/src/date.js',
+      'bower_components/angular-ui-router/release/angular-ui-router.js',
+      'bower_components/chap-links-timeline/timeline.js',
+      'bower_components/jquery/dist/jquery.js',
+      'bower_components/jquery-ui/jquery-ui.js',
+      'bower_components/leaflet/dist/leaflet.js',
+      'app/scripts/**/*.js',
+      'test/mock/**/*.js',
+      'test/spec/**/*.js'
+    ],
+
+    // list of files / patterns to exclude
+    exclude: [],
+
+    // web server port
+    port: 8080,
+
+    // Start these browsers, currently available:
+    // - Chrome
+    // - ChromeCanary
+    // - Firefox
+    // - Opera
+    // - Safari (only Mac)
+    // - PhantomJS
+    // - IE (only Windows)
+    browsers: [
+      'PhantomJS',
+      //'Chrome'
+    ],
+
+    // Which plugins to enable
+    plugins: [
+      'karma-phantomjs-launcher',
+      'karma-chrome-launcher',
+      'karma-jasmine'
+    ],
+
+    // Continuous Integration mode
+    // if true, it capture browsers, run tests and exit
+    singleRun: false,
+
+    colors: true,
+
+    // level of logging
+    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+    // Uncomment the following lines if you are using grunt's server to run the tests
+    // proxies: {
+    //   '/': 'http://localhost:9000/'
+    // },
+    // URL root prevent conflicts with the site root
+    // urlRoot: '_karma_'
+  });
+};