-
![]()
-
@@ -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) {