From eca8d25a8e93a60a9789ea10d66f85d2fe7b4e5d Mon Sep 17 00:00:00 2001 From: Anthony Truskinger Date: Mon, 7 Oct 2013 17:20:14 +1000 Subject: [PATCH] Added in juqery-ui dependency. Jshint issues fixed. Fixed bug that meant annotation IDs were returned as strings (opposed to numbers). started splitting directives out into their own files - for sanity. removed uncessary anonymous function initiation wrappers. --- bower.json | 3 +- build.config.js | 2 + src/app/app.js | 90 +- src/common/100-String.format.js | 2 +- src/common/jquery.drawabox.js | 2 +- .../directives/bawAnnotationViewer.js | 267 ++++ src/components/directives/directives.js | 1073 +++++------------ src/components/directives/ngAudio.js | 100 ++ src/components/directives/ngGoogleMaps.js | 139 +++ src/components/filters/filters.js | 5 +- src/components/models/annotation.js | 3 - .../{audio_event_tag.js => audioEventTag.js} | 5 +- src/components/services/url.js | 4 + 13 files changed, 852 insertions(+), 843 deletions(-) create mode 100644 src/components/directives/bawAnnotationViewer.js create mode 100644 src/components/directives/ngAudio.js create mode 100644 src/components/directives/ngGoogleMaps.js rename src/components/models/{audio_event_tag.js => audioEventTag.js} (95%) diff --git a/bower.json b/bower.json index 0bc39969..fd1126db 100644 --- a/bower.json +++ b/bower.json @@ -12,7 +12,8 @@ "hint.css": "https://github.com/chinchang/hint.css.git", "underscore": "~1.5.2", "angular-resource": "~1.0.8", - "modernizr": "~2.6.2" + "modernizr": "~2.6.2", + "jquery-ui": "~1.10.3" }, "dependencies": {} } diff --git a/build.config.js b/build.config.js index 3119082f..668c947b 100644 --- a/build.config.js +++ b/build.config.js @@ -64,6 +64,8 @@ module.exports = { vendor_files: { js: [ 'vendor/jquery/jquery.js', + // TODO: THIS IS TERRIBLE! REMOVE UI ASAP... OR AT LEAST ONLY INCLUDE RELEVANT COMPONENTS + 'vendor/jquery-ui/ui/jquery-ui.js', 'vendor/angular/angular.js', 'vendor/angular-route/angular-route.js', 'vendor/angular-resource/angular-resource.js', diff --git a/src/app/app.js b/src/app/app.js index 56db7a37..59567837 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -54,8 +54,8 @@ var app = angular.module('baw', 'ui.utils', /* angular-ui project */ 'bawApp.configuration', /* a mapping of all static path configurations - and a module that contains all app configuration */ - 'url', /* a custom uri formatter */ + and a module that contains all app configuration */ + 'url', /* a custom uri formatter */ 'templates-app', /* these are the precompiled templates */ 'templates-common', @@ -86,62 +86,62 @@ var app = angular.module('baw', 'bawApp.users' ]) - .config(['$routeProvider', '$locationProvider', '$httpProvider', 'conf.paths', '$sceDelegateProvider' - , function ($routeProvider, $locationProvider, $httpProvider, paths, $sceDelegateProvider) { - // adjust security whitelist for resource urls - var currentWhitelist = $sceDelegateProvider.resourceUrlWhitelist(); - currentWhitelist.push(paths.api.root); - $sceDelegateProvider.resourceUrlWhitelist(currentWhitelist); + .config(['$routeProvider', '$locationProvider', '$httpProvider', 'conf.paths', '$sceDelegateProvider', + function ($routeProvider, $locationProvider, $httpProvider, paths, $sceDelegateProvider) { + // adjust security whitelist for resource urls + var currentWhitelist = $sceDelegateProvider.resourceUrlWhitelist(); + currentWhitelist.push(paths.api.root); + $sceDelegateProvider.resourceUrlWhitelist(currentWhitelist); - $routeProvider.whenDefaults = whenDefaults; - $routeProvider.fluidIf = baw.fluidIf; + $routeProvider.whenDefaults = whenDefaults; + $routeProvider.fluidIf = baw.fluidIf; - // routes - $routeProvider. - when('/home', {templateUrl: '/assets/home.html', controller: 'HomeCtrl'}). + // routes + $routeProvider. + when('/home', {templateUrl: '/assets/home.html', controller: 'HomeCtrl'}). - whenDefaults("projects", "project", ":projectId", 'ProjectsCtrl', 'ProjectCtrl'). - whenDefaults("sites", "site", ":siteId", 'SitesCtrl', 'SiteCtrl'). - whenDefaults("photos", "photo", ":photoId", 'PhotosCtrl', 'PhotoCtrl'). - whenDefaults("bookmarks", "bookmark", ":bookmarkId", 'BookmarksCtrl', 'BookmarkCtrl'). - whenDefaults("searches", "search", ":searchId", 'SearchesCtrl', 'SearchCtrl'). - whenDefaults("tags", "tag", ":tagId", 'TagsCtrl', 'TagCtrl'). - whenDefaults("audioEvents", "audioEvent", ":audioEventId", 'AudioEventsCtrl', 'AudioEventCtrl'). - whenDefaults("users", "user", ":userId", 'UsersCtrl', 'UserCtrl'). + whenDefaults("projects", "project", ":projectId", 'ProjectsCtrl', 'ProjectCtrl'). + whenDefaults("sites", "site", ":siteId", 'SitesCtrl', 'SiteCtrl'). + whenDefaults("photos", "photo", ":photoId", 'PhotosCtrl', 'PhotoCtrl'). + whenDefaults("bookmarks", "bookmark", ":bookmarkId", 'BookmarksCtrl', 'BookmarkCtrl'). + whenDefaults("searches", "search", ":searchId", 'SearchesCtrl', 'SearchCtrl'). + whenDefaults("tags", "tag", ":tagId", 'TagsCtrl', 'TagCtrl'). + whenDefaults("audioEvents", "audioEvent", ":audioEventId", 'AudioEventsCtrl', 'AudioEventCtrl'). + whenDefaults("users", "user", ":userId", 'UsersCtrl', 'UserCtrl'). - when('/recordings', {templateUrl: '/assets/recordings.html', controller: 'RecordingsCtrl' }). - when('/recordings/:recordingId', {templateUrl: '/assets/recording.html', controller: 'RecordingCtrl' }). + when('/recordings', {templateUrl: '/assets/recordings.html', controller: 'RecordingsCtrl' }). + when('/recordings/:recordingId', {templateUrl: '/assets/recording.html', controller: 'RecordingCtrl' }). - when('/listen', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). - when('/listen/:recordingId', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). - //when('/listen/:recordingId/start=:start/end=:end', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). + when('/listen', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). + when('/listen/:recordingId', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). + //when('/listen/:recordingId/start=:start/end=:end', {templateUrl: paths.site.files.listen, controller: 'ListenCtrl'}). - when('/accounts', {templateUrl: '/assets/accounts_sign_in.html', controller: 'AccountsCtrl'}). - when('/accounts/:action', {templateUrl: '/assets/accounts_sign_in.html', controller: 'AccountsCtrl'}). + when('/accounts', {templateUrl: '/assets/accounts_sign_in.html', controller: 'AccountsCtrl'}). + when('/accounts/:action', {templateUrl: '/assets/accounts_sign_in.html', controller: 'AccountsCtrl'}). - when('/attribution', {templateUrl: '/assets/attributions.html'}). + when('/attribution', {templateUrl: '/assets/attributions.html'}). - // experiments - when('/experiments/:experiment', {templateUrl: '/assets/experiment_base.html', controller: 'ExperimentsCtrl'}). + // experiments + when('/experiments/:experiment', {templateUrl: '/assets/experiment_base.html', controller: 'ExperimentsCtrl'}). - // missing route page - when('/', {templateUrl: paths.site.files.home, controller: 'HomeCtrl'}). - when('/404', {templateUrl: paths.site.files.error404, controller: 'ErrorCtrl'}). - when('/404?path=:errorPath', {templateUrl: paths.site.files.error404, controller: 'ErrorCtrl'}). - otherwise({ - redirectTo: function (params, location, search) { - return '/404?path=' + location; - } - }); + // missing route page + when('/', {templateUrl: paths.site.files.home, controller: 'HomeCtrl'}). + when('/404', {templateUrl: paths.site.files.error404, controller: 'ErrorCtrl'}). + when('/404?path=:errorPath', {templateUrl: paths.site.files.error404, controller: 'ErrorCtrl'}). + otherwise({ + redirectTo: function (params, location, search) { + return '/404?path=' + location; + } + }); - // location config - $locationProvider.html5Mode(true); + // location config + $locationProvider.html5Mode(true); - // http default configuration - $httpProvider.defaults.withCredentials = true; - }]) + // http default configuration + $httpProvider.defaults.withCredentials = true; + }]) .run(['$rootScope', '$location', '$route', '$http', 'AudioEvent', function ($rootScope, $location, $route, $http, AudioEvent) { diff --git a/src/common/100-String.format.js b/src/common/100-String.format.js index be430e9a..0edb4791 100644 --- a/src/common/100-String.format.js +++ b/src/common/100-String.format.js @@ -216,7 +216,7 @@ returns: 'some string with first value and second value injected using {property var diff = {}; if (trimObject) { - if (Object.keys(params).length === 1 && params["0"] == "") { + if (Object.keys(params).length === 1 && params["0"] === "") { diff = null; trimObject = false; } diff --git a/src/common/jquery.drawabox.js b/src/common/jquery.drawabox.js index 39e704f0..b10d7dd9 100644 --- a/src/common/jquery.drawabox.js +++ b/src/common/jquery.drawabox.js @@ -275,7 +275,7 @@ var selectedAttr = $element.attr(SELECTED_ATTRIBUTE); return { - id: $element.attr(dataIdKey), + id: parseInt($element.attr(dataIdKey), 10), left: removePx($element.css("left")), top: removePx($element.css("top")), width: removePx($element.css("width")) + BORDER_MODEL_DIFFERANCE, // box model - border not included in widths diff --git a/src/components/directives/bawAnnotationViewer.js b/src/components/directives/bawAnnotationViewer.js new file mode 100644 index 00000000..66a2f2c2 --- /dev/null +++ b/src/components/directives/bawAnnotationViewer.js @@ -0,0 +1,267 @@ +var bawds = bawds || angular.module('bawApp.directives', ['bawApp.configuration']); + +bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { + + function variance(x, y) { + var fraction = x / y; + return Math.abs(fraction - 1); + } + + function unitConversions(sampleRate, window, imageWidth, imageHeight) { + if (sampleRate === undefined || window === undefined || !imageWidth || !imageHeight) { + Console.warn("not enough information to calculate unit conversions"); + return { pixelsPerSecond: NaN, pixelsPerHertz: NaN}; + } + + // based on meta data only + var nyquistFrequency = (sampleRate / 2.0), + idealPps = sampleRate / window, + idealPph = (window / 2.0) / nyquistFrequency; + + // intentionally use width to ensure the image is correct + var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate; + var spectrogramPps = imageWidth / spectrogramBasedAudioLength; + + // intentionally use width to ensure image is correct + // SEE https://github.com/QutBioacousticsResearchGroup/bioacoustic-workbench/issues/86 + imageHeight = (imageHeight % 2) === 1 ? imageHeight - 1 : imageHeight; + var imagePph = imageHeight / nyquistFrequency; + + // do consistency check (tolerance = 2%) + if (variance(idealPph, imagePph) > 0.02) { + console.warn("the image height does not conform well with the meta data"); + } + if (variance(idealPps, spectrogramPps) > 0.02) { + console.warn("the image width does not3 conform well with the meta data"); + } + + return { pixelsPerSecond: spectrogramPps, pixelsPerHertz: imagePph}; + } + + function updateUnitConversions(scope, imageWidth, imageHeight) { + var conversions = unitConversions(scope.model.media.sampleRate, scope.model.media.window, imageWidth, imageHeight); + + var PRECISION = 6; + + return { + conversions: conversions, + pixelsToSeconds: function pixelsToSeconds(pixels) { + var seconds = pixels / conversions.pixelsPerSecond; + return seconds; + }, + pixelsToHertz: function pixelsToHertz(pixels) { + var hertz = pixels / conversions.pixelsPerHertz; + return hertz; + }, + secondsToPixels: function secondsToPixels(seconds) { + var pixels = seconds * conversions.pixelsPerSecond; + return pixels; + }, + hertzToPixels: function hertzToPixels(hertz) { + var pixels = hertz * conversions.pixelsPerHertz; + return pixels; + } + }; + } + + /** + * + * @param audioEvent + * @param box + * @param scope + */ + function resizeOrMove(audioEvent, box, scope) { + var boxId = baw.parseInt(box.id); + + if (audioEvent.__temporaryId__ === boxId) { + audioEvent.startTimeSeconds = scope.model.converters.pixelsToSeconds(box.left || 0); + audioEvent.highFrequencyHertz = scope.model.converters.pixelsToHertz(box.top || 0); + + audioEvent.endTimeSeconds = audioEvent.startTimeSeconds + scope.model.converters.pixelsToSeconds(box.width || 0); + audioEvent.lowFrequencyHertz = audioEvent.highFrequencyHertz + scope.model.converters.pixelsToHertz(box.height || 0); + } + else { + console.error("Box ids do not match on resizing or move event", audioEvent.__temporaryId__, boxId); + } + } + + function resizeOrMoveWithApply(scope, audioEvent, box) { + scope.$apply(function () { + scope.__lastDrawABoxEditId__ = audioEvent.__temporaryId__; + resizeOrMove(audioEvent, box, scope); + + }); + } + + function touchUpdatedField(audioEvent) { + audioEvent.updatedAt = new Date(); + } + + function create(simpleBox, audioRecordingId, scope) { + + var audioEvent = new baw.Annotation(baw.parseInt(simpleBox.id), audioRecordingId); + + resizeOrMove(audioEvent, simpleBox, scope); + touchUpdatedField(audioEvent); + + return audioEvent; + } + + /** + * Create an watcher for an audio event model. + * The purpose is to allow for reverse binding from model -> drawabox + * NB: interestingly, these watchers are bound to array indexes... not the objects in them. + * this means the object is not coupled to the watcher and is not affected by any operation on it. + * @param scope + * @param array + * @param index + * @param drawaboxInstance + */ + function registerWatcher(scope, array, index, drawaboxInstance) { + + // create the watcher + var watcherFunc = function () { + return array[index]; + }; + + // create the listener - the actual callback + var listenerFunc = function audioEventToBoxWatcher(value) { + + if (value) { + if (scope.__lastDrawABoxEditId__ === value.__temporaryId__) { + scope.__lastDrawABoxEditId__ = undefined; + return; + } + + console.log("audioEvent watcher fired", value.__temporaryId__, value._selected); + + // TODO: SET UP CONVERSIONS HERE + var top = scope.model.converters.hertzToPixels(value.highFrequencyHertz), + left = scope.model.converters.secondsToPixels(value.startTimeSeconds), + width = scope.model.converters.secondsToPixels(value.endTimeSeconds - value.startTimeSeconds), + height = scope.model.converters.hertzToPixels(value.highFrequencyHertz - value.lowFrequencyHertz); + + drawaboxInstance.drawabox('setBox', value.__temporaryId__, top, left, height, width, value._selected); + } + }; + + // tag both for easy removal later + var tag = "index" + index.toString(); + watcherFunc.__drawaboxWatcherForAudioEvent = tag; + listenerFunc.__drawaboxWatcherForAudioEvent = tag; + + // don't know if I need deregisterer or not - use this to stop listening... + // -- + // note the last argument sets up the watcher for compare equality (not reference). + // this may cause memory / performance issues if the model gets too big later on + var deregisterer = scope.$watch(watcherFunc, listenerFunc, true); + } + + return { + restrict: 'AE', + scope: { + model: '=model' + }, + controller: 'AnnotationViewerCtrl', + require: '', // ngModel? + templateUrl: paths.site.files.annotationViewer, +// compile: function(element, attributes, transclude) { +// // transform DOM +// }, + link: function (scope, $element, attributes, controller) { + + // assign a unique id to scope + scope.id = Number.Unique(); + + scope.$canvas = $element.find(".annotation-viewer img + div").first(); + scope.$image = $element.find("img"); + + + // init unit conversion + function updateConverters() { + scope.model.converters = updateUnitConversions(scope, scope.$image.width(), scope.$image.height()); + } + + scope.$watch(function () { + return scope.model.media.imageUrl; + }, updateConverters); + scope.$image[0].addEventListener('load', updateConverters, false); + updateConverters(); + + // init drawabox + scope.model.audioEvents = scope.model.audioEvents || []; + //scope.model.selectedAudioEvents = scope.model.selectedAudioEvents || []; + + + scope.$canvas.drawabox({ + "selectionCallbackTrigger": "mousedown", + "newBox": function (element, newBox) { + var newAudioEvent = create(newBox, "a dummy id!", scope); + + + scope.$apply(function () { + scope.model.audioEvents.push(newAudioEvent); + + var annotationViewerIndex = scope.model.audioEvents.length - 1; + element[0].annotationViewerIndex = annotationViewerIndex; + + // register for reverse binding + registerWatcher(scope, scope.model.audioEvents, annotationViewerIndex, scope.$canvas); + + console.log("newBox", newBox, newAudioEvent); + }); + }, + "boxSelected": function (element, selectedBox) { + console.log("boxSelected", selectedBox); + + // support for multiple selections - remove the clear + scope.$apply(function () { + //scope.model.selectedAudioEvents.length = 0; + //scope.model.selectedAudioEvents.push(scope.model.audioEvents[element[0].annotationViewerIndex]); + + angular.forEach(scope.model.audioEvents, function (value, key) { + value._selected = false; + }); + + // new form of selecting + scope.model.audioEvents[element[0].annotationViewerIndex]._selected = true; + }); + }, + "boxResizing": function (element, box) { + console.log("boxResizing"); + resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); + + }, + "boxResized": function (element, box) { + console.log("boxResized"); + resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); + }, + "boxMoving": function (element, box) { + console.log("boxMoving"); + resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); + }, + "boxMoved": function (element, box) { + console.log("boxMoved"); + resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); + }, + "boxDeleted": function (element, deletedBox) { + console.log("boxDeleted"); + + scope.$apply(function () { + // TODO: i'm not sure how I should handle 'deleted' items yet + var itemToDelete = scope.model.audioEvents[element[0].annotationViewerIndex]; + itemToDelete.deletedAt = (new Date()); + +// if (scope.model.selectedAudioEvents.length > 0) { +// var index = scope.model.selectedAudioEvents.indexOf(itemToDelete); +// +// if (index >= 0) { +// scope.model.selectedAudioEvents.splice(index, 1); +// } +// } + }); + } + }); + } + }; +}]); \ No newline at end of file diff --git a/src/components/directives/directives.js b/src/components/directives/directives.js index b518d250..ce747b09 100644 --- a/src/components/directives/directives.js +++ b/src/components/directives/directives.js @@ -1,847 +1,350 @@ -(function (undefined) { - var bawds = angular.module('bawApp.directives', []); +var bawds = bawds || angular.module('bawApp.directives', ['bawApp.configuration']); - bawds.directive('bawRecordInformation', function () { - - return { - restrict: 'AE', - scope: false, - /* priority: ??? */ -// controller: 'RecordInformationCtrl', - /* require: ??? */ - /*template: "
",*/ - templateUrl: "/assets/record_information.html", - replace: false, - /*compile: function(tElement, tAttrs, transclude) { - - },*/ - link: function (scope, iElement, iAttrs, controller) { - scope.name = scope[iAttrs.ngModel]; - - - } - - }; - }); - - bawds.directive('bawDebugInfo', function () { - return { - restrict: 'AE', - replace: true, - template: '
Debug info {{showOrHideDebugInfo}}
', - link: function (scope, element, attrs) { - if (!scope.print) { - //console.warn("baw-debug-info missing parent scope, no print function"); - scope.print = bawApp.print; - } - } - }; - }); - - bawds.directive('bawJsonBinding', function () { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, element, attr, ngModel) { - - function catchParseErrors(viewValue) { - var result; - try { - result = angular.fromJson(viewValue); - } catch (e) { - ngModel.$setValidity('bawJsonBinding', false); - return ''; - } - ngModel.$setValidity('bawJsonBinding', true); - return result; - } - - ngModel.$parsers.push(catchParseErrors); - ngModel.$formatters.push(angular.toJson); - } - }; - }); - - // ensures formatters are run on input blur - bawds.directive('renderOnBlur', function () { - return { - require: 'ngModel', - restrict: 'A', - link: function (scope, elm, attrs, ctrl) { - elm.bind('blur', function () { - var viewValue = ctrl.$modelValue; - for (var i in ctrl.$formatters) { - viewValue = ctrl.$formatters[i](viewValue); - } - ctrl.$viewValue = viewValue; - ctrl.$render(); - }); - } - }; - }); +bawds.directive('bawRecordInformation', function () { - bawds.directive('isGuid', function () { - return { - - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - var isList = typeof attrs.ngList !== "undefined"; - - // push rather than unshift... we want to test last - ctrl.$parsers.push(function (viewValue) { - var valid = true; - if (isList) { - for (var i = 0; i < viewValue.length && valid; i++) { - valid = baw.GUID_REGEXP.test(viewValue[i]); - } - } - else { - valid = baw.GUID_REGEXP.test(viewValue); - } - - if (valid) { - // it is valid - ctrl.$setValidity('isGuid', true); - return viewValue; - } else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('isGuid', false); - return undefined; - } - }); - } - }; - }); - - // implements infinite scrolling - // http://jsfiddle.net/vojtajina/U7Bz9/ - bawds.directive('whenScrolled', function () { - return function (scope, elm, attr) { - var raw = elm[0]; - - elm.bind('scroll', function () { - console.log('scrolled'); - if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) { - scope.$apply(attr.whenScrolled); - } - }); - }; - }); + return { + restrict: 'AE', + scope: false, + /* priority: ??? */ +// controller: 'RecordInformationCtrl', + /* require: ??? */ + /*template: "
",*/ + templateUrl: "/assets/record_information.html", + replace: false, + /*compile: function(tElement, tAttrs, transclude) { + },*/ + link: function (scope, iElement, iAttrs, controller) { + scope.name = scope[iAttrs.ngModel]; - bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) { - function variance(x, y) { - var fraction = x / y; - return Math.abs(fraction - 1); } - function unitConversions(sampleRate, window, imageWidth, imageHeight) { - if (sampleRate === undefined || window === undefined || !imageWidth || !imageHeight) { - return { pixelsPerSecond: NaN, pixelsPerHertz: NaN}; + }; +}); + +bawds.directive('bawDebugInfo', function () { + return { + restrict: 'AE', + replace: true, + template: '
Debug info {{showOrHideDebugInfo}}
', + link: function (scope, element, attrs) { + if (!scope.print) { + //console.warn("baw-debug-info missing parent scope, no print function"); + scope.print = bawApp.print; } - - // based on meta data only - var nyquistFrequency = (sampleRate / 2.0), - idealPps = sampleRate / window, - idealPph = (window / 2.0) / nyquistFrequency; - - // intentionally use width to ensure the image is correct - var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate; - var spectrogramPps = imageWidth / spectrogramBasedAudioLength; - - // intentionally use width to ensure image is correct - // SEE https://github.com/QutBioacousticsResearchGroup/bioacoustic-workbench/issues/86 - imageHeight = (imageHeight % 2) === 1 ? imageHeight - 1 : imageHeight; - var imagePph = imageHeight / nyquistFrequency; - - // do consistency check (tolerance = 2%) - if (variance(idealPph, imagePph) > 0.02) { - console.warn("the image height does not conform well with the meta data"); - } - if (variance(idealPps, spectrogramPps) > 0.02) { - console.warn("the image width does not3 conform well with the meta data"); - } - - return { pixelsPerSecond: spectrogramPps, pixelsPerHertz: imagePph}; } - - function updateUnitConversions(scope, imageWidth, imageHeight) { - var conversions = unitConversions(scope.model.media.sampleRate, scope.model.media.window, imageWidth, imageHeight); - - var PRECISION = 6; - - return { - conversions: conversions, - pixelsToSeconds: function pixelsToSeconds(pixels) { - var seconds = pixels / conversions.pixelsPerSecond; - return seconds; - }, - pixelsToHertz: function pixelsToHertz(pixels) { - var hertz = pixels / conversions.pixelsPerHertz; - return hertz; - }, - secondsToPixels: function secondsToPixels(seconds) { - var pixels = seconds * conversions.pixelsPerSecond; - return pixels; - }, - hertzToPixels: function hertzToPixels(hertz) { - var pixels = hertz * conversions.pixelsPerHertz; - return pixels; + }; +}); + +bawds.directive('bawJsonBinding', function () { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attr, ngModel) { + + function catchParseErrors(viewValue) { + var result; + try { + result = angular.fromJson(viewValue); + } catch (e) { + ngModel.$setValidity('bawJsonBinding', false); + return ''; } - }; - } - - /** - * - * @param audioEvent - * @param box - * @param scope - */ - function resizeOrMove(audioEvent, box, scope) { - var boxId = baw.parseInt(box.id); - - if (audioEvent.__temporaryId__ === boxId) { - audioEvent.startTimeSeconds = scope.model.converters.pixelsToSeconds(box.left || 0); - audioEvent.highFrequencyHertz = scope.model.converters.pixelsToHertz(box.top || 0); - - audioEvent.endTimeSeconds = audioEvent.startTimeSeconds + scope.model.converters.pixelsToSeconds(box.width || 0); - audioEvent.lowFrequencyHertz = audioEvent.highFrequencyHertz + scope.model.converters.pixelsToHertz(box.height || 0); - } - else { - console.error("Box ids do not match on resizing or move event", audioEvent.__temporaryId__, boxId); + ngModel.$setValidity('bawJsonBinding', true); + return result; } - } - - function resizeOrMoveWithApply(scope, audioEvent, box) { - scope.$apply(function () { - scope.__lastDrawABoxEditId__ = audioEvent.__temporaryId__; - resizeOrMove(audioEvent, box, scope); - }); + ngModel.$parsers.push(catchParseErrors); + ngModel.$formatters.push(angular.toJson); } - - function touchUpdatedField(audioEvent) { - audioEvent.updatedAt = new Date(); + }; +}); + +// ensures formatters are run on input blur +bawds.directive('renderOnBlur', function () { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, elm, attrs, ctrl) { + elm.bind('blur', function () { + var viewValue = ctrl.$modelValue; + for (var i in ctrl.$formatters) { + viewValue = ctrl.$formatters[i](viewValue); + } + ctrl.$viewValue = viewValue; + ctrl.$render(); + }); } + }; +}); - function create(simpleBox, audioRecordingId, scope) { - var audioEvent = new baw.Annotation(baw.parseInt(simpleBox.id), audioRecordingId); +bawds.directive('isGuid', function () { + return { - resizeOrMove(audioEvent, simpleBox, scope); - touchUpdatedField(audioEvent); - - return audioEvent; - } + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + var isList = typeof attrs.ngList !== "undefined"; - /** - * Create an watcher for an audio event model. - * The purpose is to allow for reverse binding from model -> drawabox - * NB: interestingly, these watchers are bound to array indexes... not the objects in them. - * this means the object is not coupled to the watcher and is not affected by any operation on it. - * @param scope - * @param array - * @param index - * @param drawaboxInstance - */ - function registerWatcher(scope, array, index, drawaboxInstance) { - - // create the watcher - var watcherFunc = function () { - return array[index]; - }; - - // create the listener - the actual callback - var listenerFunc = function audioEventToBoxWatcher(value) { - - if (value) { - if (scope.__lastDrawABoxEditId__ === value.__temporaryId__) { - scope.__lastDrawABoxEditId__ = undefined; - return; + // push rather than unshift... we want to test last + ctrl.$parsers.push(function (viewValue) { + var valid = true; + if (isList) { + for (var i = 0; i < viewValue.length && valid; i++) { + valid = baw.GUID_REGEXP.test(viewValue[i]); } - - console.log("audioEvent watcher fired", value.__temporaryId__, value._selected); - - // TODO: SET UP CONVERSIONS HERE - var top = scope.model.converters.hertzToPixels(value.highFrequencyHertz), - left = scope.model.converters.secondsToPixels(value.startTimeSeconds), - width = scope.model.converters.secondsToPixels(value.endTimeSeconds - value.startTimeSeconds), - height = scope.model.converters.hertzToPixels(value.highFrequencyHertz - value.lowFrequencyHertz); - - drawaboxInstance.drawabox('setBox', value.__temporaryId__, top, left, height, width, value._selected); } - }; - - // tag both for easy removal later - var tag = "index" + index.toString(); - watcherFunc.__drawaboxWatcherForAudioEvent = tag; - listenerFunc.__drawaboxWatcherForAudioEvent = tag; - - // don't know if I need deregisterer or not - use this to stop listening... - // -- - // note the last argument sets up the watcher for compare equality (not reference). - // this may cause memory / performance issues if the model gets too big later on - var deregisterer = scope.$watch(watcherFunc, listenerFunc, true); - } - - return { - restrict: 'AE', - scope: { - model: '=model' - }, - controller: 'AnnotationViewerCtrl', - require: '', // ngModel? - templateUrl: paths.site.files.annotationViewer, -// compile: function(element, attributes, transclude) { -// // transform DOM -// }, - link: function (scope, $element, attributes, controller) { - - // assign a unique id to scope - scope.id = Number.Unique(); - - scope.$canvas = $element.find(".annotation-viewer img + div").first(); - scope.$image = $element.find("img"); - - - // init unit conversion - function updateConverters() { - scope.model.converters = updateUnitConversions(scope, scope.$image.width(), scope.$image.height()); - } - - scope.$watch(function () { - return scope.model.media.imageUrl; - }, updateConverters); - scope.$image[0].addEventListener('load', updateConverters, false); - updateConverters(); - - // init drawabox - scope.model.audioEvents = scope.model.audioEvents || []; - //scope.model.selectedAudioEvents = scope.model.selectedAudioEvents || []; - - - scope.$canvas.drawabox({ - "selectionCallbackTrigger": "mousedown", - "newBox": function (element, newBox) { - var newAudioEvent = create(newBox, "a dummy id!", scope); - - - scope.$apply(function () { - scope.model.audioEvents.push(newAudioEvent); - - var annotationViewerIndex = scope.model.audioEvents.length - 1; - element[0].annotationViewerIndex = annotationViewerIndex; - - // register for reverse binding - registerWatcher(scope, scope.model.audioEvents, annotationViewerIndex, scope.$canvas); - - console.log("newBox", newBox, newAudioEvent); - }); - }, - "boxSelected": function (element, selectedBox) { - console.log("boxSelected", selectedBox); - - // support for multiple selections - remove the clear - scope.$apply(function () { - //scope.model.selectedAudioEvents.length = 0; - //scope.model.selectedAudioEvents.push(scope.model.audioEvents[element[0].annotationViewerIndex]); - - angular.forEach(scope.model.audioEvents, function (value, key) { - value._selected = false; - }); - - // new form of selecting - scope.model.audioEvents[element[0].annotationViewerIndex]._selected = true; - }); - }, - "boxResizing": function (element, box) { - console.log("boxResizing"); - resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); - - }, - "boxResized": function (element, box) { - console.log("boxResized"); - resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); - }, - "boxMoving": function (element, box) { - console.log("boxMoving"); - resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); - }, - "boxMoved": function (element, box) { - console.log("boxMoved"); - resizeOrMoveWithApply(scope, scope.model.audioEvents[element[0].annotationViewerIndex], box); - }, - "boxDeleted": function (element, deletedBox) { - console.log("boxDeleted"); - - scope.$apply(function () { - // TODO: i'm not sure how I should handle 'deleted' items yet - var itemToDelete = scope.model.audioEvents[element[0].annotationViewerIndex]; - itemToDelete.deletedAt = (new Date()); - -// if (scope.model.selectedAudioEvents.length > 0) { -// var index = scope.model.selectedAudioEvents.indexOf(itemToDelete); -// -// if (index >= 0) { -// scope.model.selectedAudioEvents.splice(index, 1); -// } -// } - }); - } - }); - } - }; - }]); - - /** - * A directive for binding the model to data off an audio element. - * Most things are oneway bindings - */ - bawds.directive('ngAudio', ['$parse', function ($parse) { - - return { - restrict: 'A', - link: function (scope, elements, attributes, controller) { - var element = elements[0]; - if (element.nodeName !== "AUDIO") { - throw 'Cannot put ngAudio element on an element that is not a