Skip to content

Commit

Permalink
Fixed the event model, got updates working!
Browse files Browse the repository at this point in the history
- Added some packages to make unit testing easier. Loaded by the karma config
- added some formatting for the details table on the listen page
- implemented isDirty and isNew. Dirty properties are tracked with JavaScript getters/setters
- added a ton of unit tests for the Annotation class - the only way I could sort out the nightmare of JS props
- Converted some exceptions to assertions in bawAnnotationViewer.js. Also
	- Also changed unit/converter calculation to work solely off metadata. This means it can be calculated before an image resource is loaded. Images are now stretched to fit. Warnings are still included for badly sized images.
	- FIxed by in invertPixels function
	- fleshed out the modelUpdatesServer stub - now with real live updating! w00t!
	- converted lastUpdate to $lastUpdate - this means the prop is ignored for equality checks. If included the model goes through an extra update loop... very inefficient
  • Loading branch information
atruskie committed Nov 16, 2013
1 parent 3b46dd1 commit 78f8722
Show file tree
Hide file tree
Showing 8 changed files with 445 additions and 145 deletions.
4 changes: 3 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"angular-resource": "~1.2.0",
"modernizr": "~2.6.2",
"jquery-ui": "~1.10.3",
"momentjs": "~2.3.0"
"momentjs": "~2.3.0",
"jasmine-matchers": "https://github.com/JamieMason/Jasmine-Matchers.git",
"objectdiff": "https://github.com/NV/objectDiff.js.git"
},
"dependencies": {}
}
14 changes: 8 additions & 6 deletions karma/karma-unit.tpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ module.exports = function (config) {
/**
* This is the list of file patterns to load into the browser during testing.
*/
files: JSON.parse(fileJson).concat([
'src/**/*.js',
'src/**/*.coffee'
]),
files: [
"vendor/objectdiff/objectDiff.js",
"vendor/jasmine-matchers/dist/jasmine-matchers.js"
].concat(JSON.parse(fileJson).concat([
'src/**/*.js'
])),
exclude: [
'src/assets/**/*.js'
],
Expand All @@ -33,9 +35,9 @@ module.exports = function (config) {
},

/**
* How to report, by default.
* How to report, by default. 'dots', 'progress'
*/
reporters: 'dots',
reporters: ['dots'],

/**
* On which port should the browser connect, on which port is the test runner
Expand Down
68 changes: 34 additions & 34 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
{
"author": "Anthony Truskinger",
"name": "baw-client",
"version": "0.0.4",
"description": "The AngularJS client for the QUT Bioacoustics server",
"licenses": {
"type": "Apache",
"url": "https://github.com/QutBioacoustics/baw-client/blob/master/LICENSE"
},
"bugs": "https://github.com/QutBioacoustics/baw-client/issues",
"repository": {
"type": "git",
"url": "https://github.com/QutBioacoustics/baw-client.git"
},
"//": "THE karma-chrome-launcher IS NEEDED TO LAUNCH CHROME PROPERLY ON WINDOWS. REMOVE WHEN PULL REQUEST COMPLETED https://github.com/karma-runner/karma-chrome-launcher",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-jshint": "~0.4.3",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-watch": "~0.4.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-karma": "~0.7.0",
"karma-chrome-launcher": "https://github.com/EE/karma-chrome-launcher/tarball/windows",
"grunt-ngmin": "0.0.2",
"grunt-html2js": "~0.1.3",
"grunt-conventional-changelog": "~0.1.1",
"grunt-bump": "0.0.6",
"grunt-contrib-connect": "~0.5.0",
"connect-modrewrite": "~0.5.7",
"grunt-sass": "~0.7.0",
"lodash": "~2.2.1"
},
"private": true
"author": "Anthony Truskinger",
"name": "baw-client",
"version": "0.0.4",
"description": "The AngularJS client for the QUT Bioacoustics server",
"licenses": {
"type": "Apache",
"url": "https://github.com/QutBioacoustics/baw-client/blob/master/LICENSE"
},
"bugs": "https://github.com/QutBioacoustics/baw-client/issues",
"repository": {
"type": "git",
"url": "https://github.com/QutBioacoustics/baw-client.git"
},
"//": "THE karma-chrome-launcher IS NEEDED TO LAUNCH CHROME PROPERLY ON WINDOWS. REMOVE WHEN PULL REQUEST COMPLETED https://github.com/karma-runner/karma-chrome-launcher",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-jshint": "~0.4.3",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-watch": "~0.4.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-karma": "~0.7.0",
"karma-chrome-launcher": "https://github.com/EE/karma-chrome-launcher/tarball/windows",
"grunt-ngmin": "0.0.2",
"grunt-html2js": "~0.1.3",
"grunt-conventional-changelog": "~0.1.1",
"grunt-bump": "0.0.6",
"grunt-contrib-connect": "~0.5.0",
"connect-modrewrite": "~0.5.7",
"grunt-sass": "~0.7.0",
"lodash": "~2.2.1"
},
"private": true
}
12 changes: 12 additions & 0 deletions src/app/listen/_listen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,16 @@




}


.details-table {
@extend .table;
@extend .table-striped;
@extend .table-bordered;

tbody {

}
}
8 changes: 6 additions & 2 deletions src/app/listen/listen.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ <h3>
</div>
<div class="debug-ui">
<h3>Annotations</h3>
<table>
<table class="details-table">
<thead>
<tr>
<th>Selected</th>
<th>Unsaved</th>
<th>Jump to</th>
<th>Annotation ID</th>
<th>Audio Recording</th>
Expand All @@ -156,7 +157,10 @@ <h3>Annotations</h3>
<td>
<!--<input type="radio" ng-checked="ae.selected" ng-model="ae.selected" name="selectionRadioGroup" >-->
<input type="radio" baw-checked="ae.selected" name="selectionRadioGroup">
SELECTED:{{ae.selected}}
{{ae.selected && '✓' || '✗'}}
</td>
<td>
{{ae.isDirty && '✓' || '✗'}}
</td>
<td>
<!-- TODO -->
Expand Down
133 changes: 77 additions & 56 deletions src/components/directives/bawAnnotationViewer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var bawds = bawds || angular.module('bawApp.directives', ['bawApp.configuration']);

bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
bawds.directive('bawAnnotationViewer', [ 'conf.paths', 'AudioEvent', function (paths, AudioEvent) {

function variance(x, y) {
var fraction = x / y;
Expand Down Expand Up @@ -38,34 +38,37 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
throw "AnnotationEditor:calculateUnitConversions: can't determine natural height or natural width of source image!";
}

// crop images that are too tall - specifically for removing DC values
if (!baw.isPowerOfTwo(imageHeight)) {
var croppedHeight = baw.closestPowerOfTwoBelow(imageHeight);
console.error("AnnotationEditor:calculateUnitConversions: The natural height (" + imageHeight +
"px) for image " + image.src +
" is not a power of two. The image has been STRETCHED to " + croppedHeight + "px! ALL MEASUREMENTS ON THIS SPECTROGRAM WILL BE WRONG!");

// squish image into the nearest 'correct height' to minimise damage
result.enforcedImageHeight = imageHeight = croppedHeight;
}
// only process if image is loaded
if (imageHeight && imageWidth) {
// crop images that are too tall - specifically for removing DC values
if (!baw.isPowerOfTwo(imageHeight)) {
var croppedHeight = baw.closestPowerOfTwoBelow(imageHeight);
console.error("AnnotationEditor:calculateUnitConversions: The natural height (" + imageHeight +
"px) for image " + image.src +
" is not a power of two. The image has been STRETCHED to " + croppedHeight + "px! ALL MEASUREMENTS ON THIS SPECTROGRAM WILL BE WRONG!");

// squish image into the nearest 'correct height' to minimise damage
result.enforcedImageHeight = imageHeight = croppedHeight;
}


// use the image width to estimate the actual shown duration
var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate,
spectrogramPps = imageWidth / spectrogramBasedAudioLength;
// use the image width to estimate the actual shown duration
var spectrogramBasedAudioLength = (imageWidth * window) / sampleRate,
spectrogramPps = imageWidth / spectrogramBasedAudioLength;

// use the image height to estimate the actual shown frequency bounds
var spectrogramPph = imageHeight / nyquistFrequency;
// use the image height to estimate the actual shown frequency bounds
var spectrogramPph = imageHeight / nyquistFrequency;

// do consistency check (tolerance = 2%)
var tolerance = 0.02;
if (variance(idealPph, spectrogramPph) > tolerance) {
console.warn("AnnotationEditor:calculateUniConversions: the image height does not conform well with the meta data. The image will be stretched to fit!",
idealPph, spectrogramPph);
}
if (variance(idealPps, spectrogramPps) > tolerance) {
console.warn("AnnotationEditor:calculateUniConversions: the image width does not conform well with the meta data. The image will be stretched to fit!",
idealPps, spectrogramPps);
// do consistency check (tolerance = 2%)
var tolerance = 0.02;
if (variance(idealPph, spectrogramPph) > tolerance) {
console.warn("AnnotationEditor:calculateUniConversions: the image height does not conform well with the meta data. The image will be stretched to fit!",
idealPph, spectrogramPph);
}
if (variance(idealPps, spectrogramPps) > tolerance) {
console.warn("AnnotationEditor:calculateUniConversions: the image width does not conform well with the meta data. The image will be stretched to fit!",
idealPps, spectrogramPps);
}
}
}

Expand Down Expand Up @@ -104,7 +107,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
return Math.abs(conversions.nyquistFrequency - hertz);
},
invertPixels: function invertPixels(pixels) {
return Math.abs(conversions.imageHeight - pixels);
return Math.abs(conversions.enforcedImageHeight - pixels);
}
};
}
Expand Down Expand Up @@ -145,7 +148,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {

function watchForSpectrogramChanges(scope, imageElement) {
function updateUnitConverters() {
console.debug("AnnotationEditor:watchForSpectrogramChanges:");
console.debug("AnnotationEditor:watchForSpectrogramChanges:updateUnitConverters");

scope.model.converters = calculateUnitConverters(scope.model.media, scope.$image[0]);

Expand All @@ -162,7 +165,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
updateUnitConverters();
});
}, false);
updateUnitConverters();
//updateUnitConverters();
}

/**ȻɌɄƉ**/
Expand All @@ -179,23 +182,23 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
* Handles emitted create, update, delete
*/
function drawaboxUpdatesModel(scope, annotation, box, action) {
console.debug("AnnotationEditor:drawaboxUpdatesModel:");
console.debug("AnnotationEditor:drawaboxUpdatesModel:", action);

// invariants
if (action === DRAWABOX_ACTION_SELECT && box.selected !== true) {
throw "AnnotationEditor:drawaboxUpdatesModel: Invariant failed for selection action";
}
if (action !== DRAWABOX_ACTION_SELECT && box.selected !== annotation.selected) {
if (action !== DRAWABOX_ACTION_SELECT && action !== DRAWABOX_ACTION_CREATE && box.selected !== annotation.selected) {
throw "AnnotationEditor:drawaboxUpdatesModel: Invariant failed for non-selection action";
}
if (action !== DRAWABOX_ACTION_CREATE && !annotation) {
throw "AnnotationEditor:drawaboxUpdatesModel: Invariant failed: annotation must be null when creating a new box";
}

// pre assertion
var wasDirty = annotation.isDirty;
var wasDirty = annotation === undefined ? null : annotation.isDirty;
var boxId = baw.parseInt(box.id);
if (annotation.__localId__ !== boxId) {
if (annotation && annotation.__localId__ !== boxId) {
console.error("Box ids do not match on resizing or move event", annotation.__localId__, boxId);
return;
}
Expand All @@ -204,11 +207,11 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
// create
if (action === DRAWABOX_ACTION_CREATE) {
//noinspection AssignmentToFunctionParameterJS
annotation = new baw.Annotation(baw.parseInt(box.id), audioRecordingId);
annotation = new baw.Annotation(baw.parseInt(box.id), scope.model.media.id);
scope.model.audioEvents.push(annotation);
}

annotation.lastUpdater = UPDATER_DRAWABOX;
annotation.$lastUpdater = UPDATER_DRAWABOX;

// only the select action selects, and only the select action does not update the bounds of the annotation
if (action === DRAWABOX_ACTION_SELECT) {
Expand All @@ -235,11 +238,17 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
});

// post assertion
if (action === DRAWABOX_ACTION_SELECT && annotation.isDirty) {
throw "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for selection triggering a isDirty state";
if (action === DRAWABOX_ACTION_SELECT) {
console.assert(annotation.isDirty == wasDirty,
"AnnotationEditor:drawaboxUpdatesModel: Post condition failed for selection triggering a isDirty state");
}
else {
console.assert(annotation.isDirty,
"AnnotationEditor:drawaboxUpdatesModel: Post condition failed for action not triggering a isDirty state");
}
if (action === DRAWABOX_ACTION_DELETE && annotation.toBeDeleted !== true) {
throw "AnnotationEditor:drawaboxUpdatesModel: Post condition failed for ensuring a annotation is deleted";
if (action === DRAWABOX_ACTION_DELETE) {
console.assert(annotation.toBeDeleted === true,
"AnnotationEditor:drawaboxUpdatesModel: Post condition failed for ensuring a annotation is deleted");
}
}

Expand All @@ -248,7 +257,7 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
* Handles model updates (not created or deleted events)
*/
function modelUpdatesDrawabox(scope, annotation) {
console.debug("AnnotationEditor:modelUpdatesDrawabox:");
console.debug("AnnotationEditor:modelUpdatesDrawabox:", annotation.__localId__);

var drawaboxInstance = scope.$drawaboxElement,
top = scope.model.converters.invertPixels(scope.model.converters.hertzToPixels(annotation.highFrequencyHertz)),
Expand All @@ -260,15 +269,32 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
}

function modelUpdatesServer(annotation) {

// invariants
if (!annotation) {
throw "AnnotationEditor:modelUpdatesServer: Invalid state! Cannot call this method with a falsy value!";
console.assert(annotation,
"AnnotationEditor:modelUpdatesServer: Invalid state! Cannot call this method with a falsy value!");
console.assert(annotation.isDirty === true,
"AnnotationEditor:modelUpdatesServer: Invalid state! The annotation should be dirty (but isn't)!");

var postData = annotation.exportObj();
var parameters = {recordingId: postData.audioRecordingId, audioEventId: postData.id};
if (annotation.isNew()) {
console.debug("AnnotationEditor:modelUpdatesServer: create!", parameters.__localId__);
}
if (annotation.isDirty !== true) {
throw "AnnotationEditor:modelUpdatesServer: Invalid state! The annotation should be dirty (but isn't)!";
else if (annotation.toBeDeleted === true) {
console.debug("AnnotationEditor:modelUpdatesServer: delete!", parameters.__localId__);
}
else {
// update!
console.debug("AnnotationEditor:modelUpdatesServer: update!", parameters.__localId__);
AudioEvent.update(parameters, postData,
function success(value, headers) {
console.debug("AnnotationEditor:modelUpdatesServer: update success", value);
},
function error(response) {
console.debug("AnnotationEditor:modelUpdatesServer: update FAILURE");
});
}

console.debug("AnnotationEditor:modelUpdatesServer: stub");
}

function serverUpdatesModel() {
Expand Down Expand Up @@ -296,20 +322,15 @@ bawds.directive('bawAnnotationViewer', [ 'conf.paths', function (paths) {
console.debug("AnnotationEditor:modelUpdated:", changedAnnotation.__localId__, changedAnnotation.selected);

// invariants
if (changedAnnotation.lastUpdater === UPDATER_DRAWABOX && changedAnnotation.isDirty !== true) {
throw "AnnotationEditor:modelUpdated: Invalid state! If the last update came from drawabox then the the annotation must be dirty!";
}
if (changedAnnotation.lastUpdater === UPDATER_PAGE_LOAD && changedAnnotation.isDirty !== false) {
throw "AnnotationEditor:modelUpdated: Invalid state! If the last update came from page load then the the annotation must NOT be dirty!";
}
if (changedAnnotation.toBeDeleted && changedAnnotation.isDirty !== true) {
throw "AnnotationEditor:modelUpdated: Invalid state! If the the delete flag is set the annotation must be dirty!";
}
console.assert(changedAnnotation.$lastUpdater !== UPDATER_PAGE_LOAD || changedAnnotation.isDirty !== false,
"AnnotationEditor:modelUpdated: Invalid state! If the last update came from page load then the the annotation must NOT be dirty!");
console.assert(!changedAnnotation.toBeDeleted || changedAnnotation.toBeDeleted && changedAnnotation.isDirty !== true,
"AnnotationEditor:modelUpdated: Invalid state! If the the delete flag is set the annotation must be dirty!");

// if the last update was done by the drawabox control, do not propagate it back to drawabox
if (changedAnnotation.lastUpdater === UPDATER_DRAWABOX) {
if (changedAnnotation.$lastUpdater === UPDATER_DRAWABOX) {
// reset flag
changedAnnotation.lastUpdater = null;
changedAnnotation.$lastUpdater = null;
}
else {
modelUpdatesDrawabox(scope, changedAnnotation);
Expand Down
Loading

0 comments on commit 78f8722

Please sign in to comment.