From f44b6aa12a3705b9dbd447f2f1a5372c727ec1ac Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 2 May 2014 20:15:37 +1000 Subject: [PATCH] Addressing feedback on abstractions and functionality. --- .../annotationLibrary/_annotationLibrary.scss | 11 +- .../annotationLibrary/annotationItem.tpl.html | 61 ++-- .../annotationLibrary/annotationLibrary.js | 92 +++--- .../annotationLibrary.tpl.html | 34 +- src/app/listen/listen.js | 79 +---- src/common/functions.js | 7 + src/components/models/media.js | 49 +++ src/components/models/media.spec.js | 87 +++++ src/components/services/services.js | 303 +++++++++--------- 9 files changed, 414 insertions(+), 309 deletions(-) create mode 100644 src/components/models/media.js create mode 100644 src/components/models/media.spec.js diff --git a/src/app/annotationLibrary/_annotationLibrary.scss b/src/app/annotationLibrary/_annotationLibrary.scss index 43b45ccb..e1f83869 100644 --- a/src/app/annotationLibrary/_annotationLibrary.scss +++ b/src/app/annotationLibrary/_annotationLibrary.scss @@ -12,8 +12,8 @@ $loadGifPath: $IMAGE_ASSET_PATH + 'assets/img/load.gif'; padding: 10px 10px 0 40px; margin: 5px; position: relative; - max-height:500px; - height:385px; + max-height:410px; + height:410px; min-width: 180px; & .library-item-info { @@ -32,8 +32,11 @@ $loadGifPath: $IMAGE_ASSET_PATH + 'assets/img/load.gif'; & .info-entry { font-size: small; - - margin: 5px 10px; + display:block; + margin: 3px 5px; + overflow-x: hidden; + min-width:110px; + max-width:300px; } } } diff --git a/src/app/annotationLibrary/annotationItem.tpl.html b/src/app/annotationLibrary/annotationItem.tpl.html index 7488b272..3152a68c 100644 --- a/src/app/annotationLibrary/annotationItem.tpl.html +++ b/src/app/annotationLibrary/annotationItem.tpl.html @@ -4,20 +4,19 @@

Annotation {{model.audioEventId}}

+ data-ng-style="{width:annotation.converters.conversions.enforcedImageWidth + 100}">
- - - + +
+ data-ng-style="{top: annotation.bounds.top, left: annotation.bounds.left, width: annotation.bounds.width, height: annotation.bounds.height}">
@@ -27,11 +26,11 @@

Annotation {{model.audioEventId}}

@@ -45,8 +44,8 @@

Details

@@ -86,9 +81,11 @@

Details

-

Comments

+

 Comments

+
+
+ Coming soon...
-
diff --git a/src/app/annotationLibrary/annotationLibrary.js b/src/app/annotationLibrary/annotationLibrary.js index 138f8e60..de3a9f2b 100644 --- a/src/app/annotationLibrary/annotationLibrary.js +++ b/src/app/annotationLibrary/annotationLibrary.js @@ -1,6 +1,8 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) - .controller('AnnotationLibraryCtrl', ['$scope', '$location', '$resource', '$routeParams', '$url', 'conf.paths', 'AudioEvent', 'Media', - function ($scope, $location, $resource, $routeParams, $url, paths, AudioEvent, Media) { + .controller('AnnotationLibraryCtrl', ['$scope', '$location', '$resource', '$routeParams', '$url', + 'conf.paths', 'conf.constants', 'bawApp.unitConverter', + 'AudioEvent', 'Tag', + function ($scope, $location, $resource, $routeParams, $url, paths, constants, unitConverter, AudioEvent, Tag) { $scope.status = 'idle'; @@ -13,9 +15,7 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) }; $scope.clearFilter = function clearFilter() { - $scope.filterSettings = getEmptyFilterSettings(); - $scope.setFilter(); }; @@ -29,10 +29,11 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) return '/library/?' + $url.toKeyValue(paramObj); }; - function getEmptyFilterSettings(){ + function getEmptyFilterSettings() { return { tagsPartial: null, reference: '', // set to empty string to match value of radio button + userId: null, annotationDuration: null, freqMin: null, freqMax: null, @@ -41,8 +42,7 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) }; } - function loadFilter() { - $scope.status = 'loading'; + function normaliseFilterSettings() { $scope.filterSettings = angular.extend({}, $scope.filterSettings, $routeParams); // ensure properties that need to be numeric are actually numbers @@ -58,17 +58,6 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) $scope.filterSettings[currentvalue] = stringValue === null ? null : Number(stringValue); } ); - - $scope.library = AudioEvent.library($scope.filterSettings, null, function librarySuccess(value, responseHeaders) { - value.entries.map(Media.getMediaItem); - value.audioElement = null; - $scope.paging = getPagingSettings($scope.library.page, $scope.library.items, $scope.library.total); - $scope.status = 'loaded'; - $scope.responseDetails = responseHeaders; - }, function libraryError(httpResponse){ - $scope.status = 'error'; - console.error('Failed to load library response.', $scope.filterSettings, httpResponse); - }); } function getPagingSettings(page, items, total) { @@ -135,35 +124,66 @@ angular.module('bawApp.annotationLibrary', ['bawApp.configuration']) return paging; } + + function loadFilter() { + $scope.status = 'loading'; + + normaliseFilterSettings(); + + AudioEvent.library($scope.filterSettings, null, function librarySuccess(value, responseHeaders) { + + $scope.status = 'loaded'; + $scope.responseDetails = responseHeaders; + $scope.paging = getPagingSettings(value.page, value.items, value.total); + $scope.annotations = []; + + angular.forEach(value.entries, function (audioEventValue, key) { + var annotation = angular.extend({}, audioEventValue); + annotation.priorityTag = Tag.selectSinglePriorityTag(annotation.tags); + AudioEvent.addCalculatedProperties(annotation); + AudioEvent.getBoundSettings(annotation); + this.push(annotation); + }, $scope.annotations); + + }, function libraryError(httpResponse) { + $scope.status = 'error'; + console.error('Failed to load library response.', $scope.filterSettings, httpResponse); + }); + } }]) - .controller('AnnotationItemCtrl', ['$scope', '$location', '$resource', '$routeParams', '$url', 'conf.paths', 'AudioEvent', 'Media', - function ($scope, $location, $resource, $routeParams, $url, paths, AudioEvent, Media) { + .controller('AnnotationItemCtrl', + ['$scope', '$location', '$resource', '$routeParams', '$url', + 'conf.paths', 'conf.constants', + 'AudioEvent', 'Tag', + function ($scope, $location, $resource, $routeParams, $url, paths, constants, AudioEvent, Tag) { var parameters = { audioEventId: $routeParams.audioEventId, recordingId: $routeParams.recordingId }; - $scope.model = AudioEvent.get(parameters, - function annotationShowSuccess(value, responseHeaders) { - Media.getMediaItem(value); - value.audioElement = null; - $scope.model = value; + AudioEvent.get(parameters, + function annotationShowSuccess(audioEventValue, responseHeaders) { - if ($scope.model.paging.nextEvent.hasOwnProperty('audioEventId')) { - $scope.model.paging.nextEvent.link = '/library/' + - $scope.model.paging.nextEvent.audioRecordingId + - '/audio_events/' + $scope.model.paging.nextEvent.audioEventId; - } + var annotation = angular.extend({}, audioEventValue); + annotation.priorityTag = Tag.selectSinglePriorityTag(annotation.tags); + AudioEvent.addCalculatedProperties(annotation); + AudioEvent.getBoundSettings(annotation); - if ($scope.model.paging.prevEvent.hasOwnProperty('audioEventId')) { - $scope.model.paging.prevEvent.link = '/library/' + - $scope.model.paging.prevEvent.audioRecordingId + - '/audio_events/' + $scope.model.paging.prevEvent.audioEventId; - } + $scope.annotation = annotation; - $scope.model.audioEventDuration = Math.round10(value.endTimeSeconds - value.startTimeSeconds, -3); + // paging + if ($scope.annotation.paging.nextEvent.hasOwnProperty('audioEventId')) { + $scope.annotation.paging.nextEvent.link = '/library/' + + $scope.annotation.paging.nextEvent.audioRecordingId + + '/audio_events/' + $scope.annotation.paging.nextEvent.audioEventId; + } + if ($scope.annotation.paging.prevEvent.hasOwnProperty('audioEventId')) { + $scope.annotation.paging.prevEvent.link = '/library/' + + $scope.annotation.paging.prevEvent.audioRecordingId + + '/audio_events/' + $scope.annotation.paging.prevEvent.audioEventId; + } }, function annotationShowError(httpResponse) { console.error('Failed to load library single item response.', parameters, httpResponse); }); diff --git a/src/app/annotationLibrary/annotationLibrary.tpl.html b/src/app/annotationLibrary/annotationLibrary.tpl.html index 521ca9f7..b8c1442f 100644 --- a/src/app/annotationLibrary/annotationLibrary.tpl.html +++ b/src/app/annotationLibrary/annotationLibrary.tpl.html @@ -60,17 +60,24 @@

Annotation Library

-
+
- + -
+
+ + + +
+ +
@@ -120,11 +127,10 @@

Annotation Library

-
+
- -
@@ -134,33 +140,33 @@

Annotation Library

data-ng-style="{'min-width':150, width:item.converters.conversions.enforcedImageWidth}"> - + {{item.priorityTag.text}} - + {{item.siteName}} - - + + {{item.audioEventStartDate | date: 'MMM d, yyyy HH:mm'}} - + More info diff --git a/src/app/listen/listen.js b/src/app/listen/listen.js index 29735e32..c711c9af 100644 --- a/src/app/listen/listen.js +++ b/src/app/listen/listen.js @@ -42,16 +42,6 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead']) var CHUNK_DURATION_SECONDS = constants.listen.chunkDurationSeconds; - function getMediaParameters(format) { - return { - start_offset: $routeParams.start, - end_offset: $routeParams.end, - // this one is different, it is encoded into the path of the request by angular - recordingId: $routeParams.recordingId, - format: format - }; - } - $scope.errorState = !(baw.isNumber($routeParams.recordingId) && baw.parseInt($routeParams.recordingId) >= 0); @@ -106,67 +96,24 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead']) profileLoaded(null, UserProfile); } - - var formatPaths = function () { - if ($scope.model.media && $scope.model.media.hasOwnProperty('id')) { - //var authToken = $scope.authTokenQuery(); - - var imgKeys = Object.keys($scope.model.media.availableImageFormats); - if (imgKeys.length > 1) { - throw "don't know how to handle more than one image format!"; - } - - $scope.model.media.availableImageFormats[imgKeys[0]].url = - paths.joinFragments(paths.api.root, - $scope.model.media.availableImageFormats[imgKeys[0]].url); - $scope.model.media.spectrogram = $scope.model.media.availableImageFormats[imgKeys[0]]; - - angular.forEach($scope.model.media.availableAudioFormats, function (value, key) { - - // just update the url so it is an absolute uri - this[key].url = paths.joinFragments(paths.api.root, value.url); - - }, $scope.model.media.availableAudioFormats); - - } - }; - /* // NOT NECESSARY - we aren't using auth keys atm */ - $scope.$on('event:auth-loginRequired', formatPaths); - $scope.$on('event:auth-loginConfirmed', formatPaths); + $scope.$on('event:auth-loginRequired', function(){ Media.formatPaths($scope.model.media); }); + $scope.$on('event:auth-loginConfirmed', function(){ Media.formatPaths($scope.model.media); }); - $scope.model.media = Media.get(getMediaParameters("json"), {}, - function mediaGetSuccess() { + $scope.model.media = Media.get( + { + recordingId: $routeParams.recordingId, + start_offset: $routeParams.start, + end_offset: $routeParams.end, + format: "json" + }, + function mediaGetSuccess(value, responseHeaders) { // reformat urls - formatPaths(); - - // fixMediaApi(); - - // additionally do a check on the sample rate - // the sample rate is used in the unit calculations. - // it must be exposed and must be consistent for all sub-resources. - var sampleRate = null; - var sampleRateChecker = function (value, key) { - if (sampleRate === null) { - sampleRate = value.sampleRate; - } - else { - if (value.sampleRate !== sampleRate) { - throw "The sample rates are not consistent for the media.json request. At the current time all sub-resources returned must be equal!"; - } - } - }; + Media.formatPaths($scope.model.media); - angular.forEach($scope.model.media.availableAudioFormats, sampleRateChecker); - angular.forEach($scope.model.media.availableImageFormats, sampleRateChecker); - - if (angular.isNumber(sampleRate)) { - $scope.model.media.sampleRate = sampleRate; - } - else { - throw "The provided sample rate for the Media json must be a number!"; - } + value = new baw.Media(value); + // fixMediaApi(); var // moment works by reference - need to parse the date twice - sigh absoluteStartChunk = moment($scope.model.media.datetime).add('s', parseFloat($scope.model.media.startOffset)), diff --git a/src/common/functions.js b/src/common/functions.js index 039a96d1..23ea50bb 100644 --- a/src/common/functions.js +++ b/src/common/functions.js @@ -246,6 +246,13 @@ if (!Array.prototype.filter) { return output; }; + // http://stackoverflow.com/a/1199420 + baw.stringTrunc = function (str, n, useWordBoundary) { + var toLong = str.length > n, + s_ = toLong ? str.substr(0, n - 1) : str; + s_ = useWordBoundary && toLong ? s_.substr(0, s_.lastIndexOf(' ')) : s_; + return toLong ? s_ + '…' : s_; + }; /** * A custom formatter for TimeSpans - accepts seconds only diff --git a/src/components/models/media.js b/src/components/models/media.js new file mode 100644 index 00000000..00866f09 --- /dev/null +++ b/src/components/models/media.js @@ -0,0 +1,49 @@ +var baw = window.baw = window.baw || {}; + +baw.Media = (function () { + + var module = function Media(resource) { + + if (!(this instanceof Media)) { + throw new Error("Constructor called as a function"); + } + + if (!angular.isObject(resource)) { + throw "Media must be constructed with a valid resource."; + } + + angular.extend(this, resource); + + // additionally do a check on the sample rate + // the sample rate is used in the unit calculations. + // it must be exposed and must be consistent for all sub-resources. + var sampleRate = null; + var sampleRateChecker = function (value, key) { + if (sampleRate === null) { + sampleRate = value.sampleRate; + } + else { + if (value.sampleRate !== sampleRate) { + throw "The sample rates are not consistent for the media.json request. At the current time all sub-resources returned must be equal!"; + } + } + }; + + angular.forEach(resource.availableAudioFormats, sampleRateChecker); + angular.forEach(resource.availableImageFormats, sampleRateChecker); + + if (angular.isNumber(sampleRate)) { + resource.sampleRate = sampleRate; + } + else { + throw "The provided sample rate for the Media json must be a number!"; + } + + }; + + module.make = function (arg) { + return new baw.Media(arg); + }; + + return module; +})(); \ No newline at end of file diff --git a/src/components/models/media.spec.js b/src/components/models/media.spec.js new file mode 100644 index 00000000..a7cadc4c --- /dev/null +++ b/src/components/models/media.spec.js @@ -0,0 +1,87 @@ +describe("The Media object", function () { + + var existingMedia; + var resource = { + "datetime": "2007-10-21T14:46:07+10:00", + "originalFormat": ".asf", + "originalSampleRate": 22050, + "startOffset": 47.0, + "endOffset": 51.0, + "uuid": "5151512c-332b-470a-8a29-b7d46b0a9791", + "id": 3952, + "mediaType": "application/json", + "availableAudioFormats": { + "mp3": { + "extension": "mp3", + "channel": 0, + "sampleRate": 22050, + "maxDurationSeconds": 300.0, + "minDurationSeconds": 0.5, + "mimeType": "audio/mp3", + "url": "/audio_recordings/3952/media.mp3?end_offset=51&start_offset=47"}, + "webm": { + "extension": "webm", + "channel": 0, + "sampleRate": 22050, + "maxDurationSeconds": 300.0, + "minDurationSeconds": 0.5, + "mimeType": "audio/webm", + "url": "/audio_recordings/3952/media.webm?end_offset=51&start_offset=47"}, + "ogg": { + "extension": "ogg", + "channel": 0, + "sampleRate": 22050, + "maxDurationSeconds": 300.0, + "minDurationSeconds": 0.5, + "mimeType": "audio/ogg", + "url": "/audio_recordings/3952/media.ogg?end_offset=51&start_offset=47"}, + "flac": { + "extension": "flac", + "channel": 0, + "sampleRate": 22050, + "maxDurationSeconds": 300.0, + "minDurationSeconds": 0.5, + "mimeType": "audio/x-flac", + "url": "/audio_recordings/3952/media.flac?end_offset=51&start_offset=47"}, + "wav": { + "extension": "wav", + "channel": 0, + "sampleRate": 22050, + "maxDurationSeconds": 300.0, + "minDurationSeconds": 0.5, + "mimeType": "audio/wav", + "url": "/audio_recordings/3952/media.wav?end_offset=51&start_offset=47"} + }, + "availableImageFormats": { + "png": { + "extension": "png", + "channel": 0, + "sampleRate": 22050, + "window": 512, + "windowFunction": "Hamming", + "colour": "g", + "ppms": 0.045, + "maxDurationSeconds": 120.0, + "minDurationSeconds": 0.5, + "mimeType": "image/png", + "url": "/audio_recordings/3952/media.png?end_offset=51&start_offset=47"} + }, + "availableTextFormats": { + "json": { + "extension": "json", + "mimeType": "application/json", + "url": "/audio_recordings/3952/media.json?end_offset=51&start_offset=47" + } + }, + "format": "json"}; + + + beforeEach(function () { + existingMedia = new baw.Media(resource); + }); + + it("should be found globally", function () { + var type = typeof baw.Media; + expect(type).toEqual("function"); + }); +}); \ No newline at end of file diff --git a/src/components/services/services.js b/src/components/services/services.js index e463ac9e..4019e53d 100644 --- a/src/components/services/services.js +++ b/src/components/services/services.js @@ -3,8 +3,8 @@ * Helper method for adding a put request onto the standard angular resource service * @param $resource - the stub resource * @param {string} path - the web server path - * @param {Object} paramDefaults - * @param {Object} [actions] a set of actions to also add (extend) + * @param {Object} paramDefaults - the default parameters + * @param {Object} [actions] - a set of actions to also add (extend) * @return {*} */ function resourcePut($resource, path, paramDefaults, actions) { @@ -12,8 +12,14 @@ a.update = a.update || { method: 'PUT' }; return $resource(path, paramDefaults, a); } - + /** + * + * @param uri + * @returns {*} + */ function uriConvert(uri) { + // find all place holders in this form: '{identifier}' + // replace with placeholder in this form: ':identifier' return uri.replace(/(\{([^{}]*)\})/g, ":$2"); } @@ -36,7 +42,8 @@ {projectId: "@projectId", siteId: "@siteId", recordingId: '@recordingId'}); }]); - bawss.factory('AudioEvent', [ '$resource', 'conf.paths', function ($resource, paths) { + bawss.factory('AudioEvent', [ '$resource', '$url', 'conf.paths', 'conf.constants', 'bawApp.unitConverter', 'Media', + function ($resource, $url, paths, constants, unitConverter, Media) { var baseCsvUri = paths.api.routes.audioEvent.csvAbsolute; // TODO: move this to paths conf object @@ -74,22 +81,120 @@ return formattedUrl; } - var baseLibraryUri = paths.api.routes.audioEvent.libraryAbsolute; - var baseAnnotationShowUri = paths.api.routes.audioEvent.showAbsolute; - //'http://localhost:3000'+paths.api.routes.audioEvent.library; - //paths.api.routes.audioEvent.libraryAbsolute; - var resource = resourcePut($resource, uriConvert(paths.api.routes.audioEvent.showAbsolute), {recordingId: '@recordingId',audioEventId: '@audioEventId'}, { library: { method:'GET', - url: uriConvert(baseLibraryUri) + url: uriConvert(paths.api.routes.audioEvent.libraryAbsolute) } } ); resource.csvLink = makeCsvLink; + resource.addCalculatedProperties = function addCalculatedProperties(audioEvent){ + + audioEvent.annotationDuration = audioEvent.endTimeSeconds - audioEvent.startTimeSeconds; + audioEvent.annotationDurationRounded = Math.round10(audioEvent.endTimeSeconds - audioEvent.startTimeSeconds, -3); + audioEvent.annotationFrequencyRange = audioEvent.highFrequencyHertz - audioEvent.lowFrequencyHertz; + audioEvent.calcOffsetStart = Math.floor(audioEvent.startTimeSeconds / 30) * 30; + audioEvent.calcOffsetEnd = (Math.floor(audioEvent.startTimeSeconds / 30) * 30) + 30; + + audioEvent.urls = { + site: '/projects/' + audioEvent.projects[0].id + + '/sites/' + audioEvent.siteId, + user: '/user_accounts/' + audioEvent.ownerId, + tagSearch: '/library?' + $url.toKeyValue({tagsPartial: audioEvent.priorityTag.text}), + similar: '/library?' + $url.toKeyValue( + { + annotationDuration: Math.round10(audioEvent.annotationDuration, -3), + freqMin: Math.round(audioEvent.lowFrequencyHertz), + freqMax: Math.round(audioEvent.highFrequencyHertz) + }), + singleItem: '/library/' + audioEvent.audioRecordingId + + '/audio_events/' + audioEvent.audioEventId, + listen: '/listen/' + audioEvent.audioRecordingId + + '?start=' + audioEvent.calcOffsetStart + + '&end=' + audioEvent.calcOffsetEnd, + listenWithoutPadding: '/listen/' + audioEvent.audioRecordingId + + '?start=' + audioEvent.startTimeSeconds + + '&end=' + audioEvent.endTimeSeconds + }; + + return audioEvent; + }; + + resource.getBoundSettings = function getBoundSettings(audioEvent){ + var mediaItemParameters = { + recordingId: audioEvent.audioRecordingId, + start_offset: Math.floor(audioEvent.startTimeSeconds - constants.annotationLibrary.paddingSeconds), + end_offset: Math.ceil(audioEvent.endTimeSeconds + constants.annotationLibrary.paddingSeconds), + format: "json" + }; + + audioEvent.media = Media.get( + mediaItemParameters, + function mediaGetSuccess(mediaValue, responseHeaders) { + + Media.formatPaths(mediaValue); + mediaValue = new baw.Media(mediaValue); + + // create properties that depend on Media + audioEvent.converters = unitConverter.getConversions({ + sampleRate: audioEvent.media.sampleRate, + spectrogramWindowSize: audioEvent.media.availableImageFormats.png.window, + endOffset: audioEvent.media.endOffset, + startOffset: audioEvent.media.startOffset, + imageElement: null + }); + + audioEvent.bounds = { + top: audioEvent.converters.toTop(audioEvent.highFrequencyHertz), + left: audioEvent.converters.toLeft(audioEvent.startTimeSeconds), + width: audioEvent.converters.toWidth(audioEvent.endTimeSeconds, audioEvent.startTimeSeconds), + height: audioEvent.converters.toHeight(audioEvent.highFrequencyHertz, audioEvent.lowFrequencyHertz) + }; + + // set common/sensible defaults, but hide the elements + audioEvent.gridConfig = { + y: { + showGrid: true, + showScale: true, + max: audioEvent.converters.conversions.nyquistFrequency, + min: 0, + step: 1000, + height: audioEvent.converters.conversions.enforcedImageHeight, + labelFormatter: function (value, index, min, max) { + return (value / 1000).toFixed(1); + }, + title: "Frequency (KHz)" + }, + x: { + showGrid: true, + showScale: true, + max: audioEvent.media.endOffset, + min: audioEvent.media.startOffset, + step: 1, + width: audioEvent.converters.conversions.enforcedImageWidth, + labelFormatter: function (value, index, min, max) { + // show 'absolute' time.... i.e. seconds of the minute + var offset = (value % 60); + + return (offset).toFixed(0); + }, + title: "Time offset (seconds)" + } + }; + + + }, function mediaGetFailure(httpResponse) { + console.error("Failed to get Media.", httpResponse); + } + ); + + return audioEvent; + }; + return resource; }]); @@ -225,85 +330,45 @@ return resource; }]); - bawss.factory('Media', [ '$resource', '$url', 'conf.paths', 'conf.constants', 'bawApp.unitConverter', 'AudioEvent', 'Tag', - function ($resource, $url, paths, constants, unitConverter, AudioEvent, Tag) { - var mediaResource = $resource(uriConvert(paths.api.routes.media.showAbsolute), + bawss.factory('Media', [ '$resource', '$url', 'conf.paths', + function ($resource, $url, paths) { + + // create resource for rest requests to media api + var mediaResource = $resource(uriConvert(paths.api.routes.media.showAbsolute), { recordingId: '@recordingId', format: '@format' }); - // this is a read only service, remove unnecessary methods - delete mediaResource.save; - delete mediaResource.remove; - delete mediaResource["delete"]; - //delete mediaResource.update; - - // TODO: should be in AnnotationLibrary service - function getMediaParameters(value) { - return { - start_offset: Math.floor(value.startTimeSeconds - constants.annotationLibrary.paddingSeconds), - end_offset: Math.ceil(value.endTimeSeconds + constants.annotationLibrary.paddingSeconds), - // this one is different, it is encoded into the path of the request by angular - recordingId: value.audioRecordingId, - format: "json" - }; - } - - mediaResource.formatPaths = function formatPaths(mediaItem) { - - var imgKeys = Object.keys(mediaItem.availableImageFormats); - if (imgKeys.length > 1) { - throw "don't know how to handle more than one image format!"; - } - - mediaItem.availableImageFormats[imgKeys[0]].url = - paths.joinFragments( - paths.api.root, - mediaItem.availableImageFormats[imgKeys[0]].url); - - angular.forEach(mediaItem.availableAudioFormats, function (value, key) { - - // just update the url so it is an absolute uri - this[key].url = paths.joinFragments(paths.api.root, value.url); - - }, mediaItem.availableAudioFormats); - }; - - mediaResource.getMediaItem = function getMedia(value, index, array) { - mediaResource.get(getMediaParameters(value), null, function getMediaSuccess(mediaValue) { - // adds a media resource to each audio event - mediaResource.formatPaths(mediaValue); - value.media = mediaValue; - - value.media.sampleRate = value.media.availableAudioFormats.mp3.sampleRate; - - var audioRecordingIdValue = value.audioRecordingId; - var calcOffsetStartValue = AudioEvent.calcOffsetStart(value.startTimeSeconds); - var calcOffsetEndValue = AudioEvent.calcOffsetEnd(value.startTimeSeconds); - - value.priorityTag = Tag.selectSinglePriorityTag(value.tags); - - value.converters = unitConverter.getConversions({ - sampleRate: value.media.sampleRate, - spectrogramWindowSize: value.media.availableImageFormats.png.window, - endOffset: value.media.endOffset, - startOffset: value.media.startOffset, - imageElement: null - }); + // this is a read only service, remove unnecessary methods + // keep get + delete mediaResource["save"]; + delete mediaResource["query"]; + delete mediaResource["remove"]; + delete mediaResource["delete"]; + + /** + * Change relative image and audio urls into absolute urls + * @param {Object} mediaItem + */ + mediaResource.formatPaths = function formatPaths(mediaItem) { + + var imgKeys = Object.keys(mediaItem.availableImageFormats); + if (imgKeys.length > 1) { + throw "don't know how to handle more than one image format!"; + } - value.bounds = { - top: value.converters.toTop(value.highFrequencyHertz), - left: value.converters.toLeft(value.startTimeSeconds), - width: value.converters.toWidth(value.endTimeSeconds, value.startTimeSeconds), - height: value.converters.toHeight(value.highFrequencyHertz, value.lowFrequencyHertz) - }; + var imageKey = imgKeys[0]; + var imageFormat = mediaItem.availableImageFormats[imageKey]; + mediaItem.availableImageFormats[imageKey].url = paths.joinFragments(paths.api.root, imageFormat.url); + mediaItem.spectrogram = imageFormat; - value.annotationDuration = value.endTimeSeconds - value.startTimeSeconds; + angular.forEach(mediaItem.availableAudioFormats, function (value, key) { + // just update the url so it is an absolute uri + this[key].url = paths.joinFragments(paths.api.root, value.url); - //console.debug(value.media); - }); - }; + }, mediaItem.availableAudioFormats); + }; return mediaResource; }]); @@ -342,82 +407,6 @@ return birdWalkService; }]); - bawss.factory('AnnotationLibrary', [ '$resource', 'conf.paths', 'conf.constants', 'bawApp.unitConverter', 'AudioEvent', 'Tag', - function ($resource, paths, constants, unitConverter, AudioEvent, Tag) { - - var libraryService = {}; - - libraryService.getItem = function GetItem(){ - - }; - - function getGridConfig(){ - // set common/sensible defaults, but hide the elements - return { - y: { - showGrid: true, - showScale: true, - max: value.converters.conversions.nyquistFrequency, - min: 0, - step: 1000, - height: value.converters.conversions.enforcedImageHeight, - labelFormatter: function (value, index, min, max) { - return (value / 1000).toFixed(1); - }, - title: "Frequency (KHz)" - }, - x: { - showGrid: true, - showScale: true, - max: value.media.endOffset, - min: value.media.startOffset, - step: 1, - width: value.converters.conversions.enforcedImageWidth, - labelFormatter: function (value, index, min, max) { - // show 'absolute' time.... i.e. seconds of the minute - var offset = (value % 60); - - return (offset).toFixed(0); - }, - title: "Time offset (seconds)" - } - }; - } - - function calcOffsetStart(startOffset) { - return Math.floor(startOffset / 30) * 30; - } - - function calcOffsetEnd(startOffset) { - return (Math.floor(startOffset / 30) * 30) + 30; - } - - function getUrls(){ - // urls - value.listenUrl = '/listen/' + audioRecordingIdValue + - '?start=' + calcOffsetStartValue + - '&end=' + calcOffsetEndValue; - value.siteUrl = '/projects/' + value.projects[0].id + - '/sites/' + value.siteId; - value.userUrl = '/user_accounts/' + value.ownerId; - - // updateFilter({tagsPartial:item.priorityTag.text}) - value.tagSearchUrl = '/library?tagsPartial=' + - baw.angularCopies.fixedEncodeURI(value.priorityTag.text); - - // updateFilter({annotationDuration:Math.round10(value.annotationDuration, -3), ...}) - value.similarUrl = '/library?annotationDuration=' + Math.round10(value.annotationDuration, -3) + - '&freqMin=' + Math.round(value.lowFrequencyHertz) + - '&freqMax=' + Math.round(value.highFrequencyHertz); - - value.singleItemUrl = '/library/' + - value.audioRecordingId + '/audio_events/' + - value.audioEventId; - } - - return libraryService; - }]); - // breadcrumbs - from https://github.com/angular-app/angular-app/blob/master/client/src/common/services/breadcrumbs.js bawss.factory('breadcrumbs', ['$rootScope', '$location', '$route', '$routeParams', 'conf.paths', function ($rootScope, $location, $route, $routeParams, paths) {