123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- /**
- * @typedef {Object} ElanPluginParams
- * @property {string|HTMLElement} container CSS selector or HTML element where
- * the ELAN information should be renderer.
- * @property {string} url The location of ELAN XML data
- * @property {?boolean} deferInit Set to true to manually call
- * @property {?Object} tiers If set only shows the data tiers with the `TIER_ID`
- * in this map.
- */
- /**
- * Downloads and renders ELAN audio transcription documents alongside the
- * waveform.
- *
- * @implements {PluginClass}
- * @extends {Observer}
- * @example
- * // es6
- * import ElanPlugin from 'wavesurfer.elan.js';
- *
- * // commonjs
- * var ElanPlugin = require('wavesurfer.elan.js');
- *
- * // if you are using <script> tags
- * var ElanPlugin = window.WaveSurfer.elan;
- *
- * // ... initialising wavesurfer with the plugin
- * var wavesurfer = WaveSurfer.create({
- * // wavesurfer options ...
- * plugins: [
- * ElanPlugin.create({
- * // plugin options ...
- * })
- * ]
- * });
- */
- export default class ElanPlugin {
- /**
- * Elan plugin definition factory
- *
- * This function must be used to create a plugin definition which can be
- * used by wavesurfer to correctly instantiate the plugin.
- *
- * @param {ElanPluginParams} params parameters use to initialise the plugin
- * @return {PluginDefinition} an object representing the plugin
- */
- static create(params) {
- return {
- name: 'elan',
- deferInit: params && params.deferInit ? params.deferInit : false,
- params: params,
- instance: ElanPlugin
- };
- }
- Types = {
- ALIGNABLE_ANNOTATION: 'ALIGNABLE_ANNOTATION',
- REF_ANNOTATION: 'REF_ANNOTATION'
- }
- constructor(params, ws) {
- this.data = null;
- this.params = params;
- this.container = 'string' == typeof params.container ?
- document.querySelector(params.container) : params.container;
- if (!this.container) {
- throw Error('No container for ELAN');
- }
- }
- init() {
- this.bindClick();
- if (this.params.url) {
- this.load(this.params.url);
- }
- }
- destroy() {
- this.container.removeEventListener('click', this._onClick);
- this.container.removeChild(this.table);
- }
- load(url) {
- this.loadXML(url, xml => {
- this.data = this.parseElan(xml);
- this.render();
- this.fireEvent('ready', this.data);
- });
- }
- loadXML(url, callback) {
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'document';
- xhr.send();
- xhr.addEventListener('load', e => {
- callback && callback(e.target.responseXML);
- });
- }
- parseElan(xml) {
- const _forEach = Array.prototype.forEach;
- const _map = Array.prototype.map;
- const data = {
- media: {},
- timeOrder: {},
- tiers: [],
- annotations: {},
- alignableAnnotations: []
- };
- const header = xml.querySelector('HEADER');
- const inMilliseconds = header.getAttribute('TIME_UNITS') == 'milliseconds';
- const media = header.querySelector('MEDIA_DESCRIPTOR');
- data.media.url = media.getAttribute('MEDIA_URL');
- data.media.type = media.getAttribute('MIME_TYPE');
- const timeSlots = xml.querySelectorAll('TIME_ORDER TIME_SLOT');
- const timeOrder = {};
- _forEach.call(timeSlots, slot => {
- let value = parseFloat(slot.getAttribute('TIME_VALUE'));
- // If in milliseconds, convert to seconds with rounding
- if (inMilliseconds) {
- value = Math.round(value * 1e2) / 1e5;
- }
- timeOrder[slot.getAttribute('TIME_SLOT_ID')] = value;
- });
- data.tiers = _map.call(xml.querySelectorAll('TIER'), tier => ({
- id: tier.getAttribute('TIER_ID'),
- linguisticTypeRef: tier.getAttribute('LINGUISTIC_TYPE_REF'),
- defaultLocale: tier.getAttribute('DEFAULT_LOCALE'),
- annotations: _map.call(
- tier.querySelectorAll('REF_ANNOTATION, ALIGNABLE_ANNOTATION'), node => {
- const annot = {
- type: node.nodeName,
- id: node.getAttribute('ANNOTATION_ID'),
- ref: node.getAttribute('ANNOTATION_REF'),
- value: node.querySelector('ANNOTATION_VALUE')
- .textContent.trim()
- };
- if (this.Types.ALIGNABLE_ANNOTATION == annot.type) {
- // Add start & end to alignable annotation
- annot.start = timeOrder[node.getAttribute('TIME_SLOT_REF1')];
- annot.end = timeOrder[node.getAttribute('TIME_SLOT_REF2')];
- // Add to the list of alignable annotations
- data.alignableAnnotations.push(annot);
- }
- // Additionally, put into the flat map of all annotations
- data.annotations[annot.id] = annot;
- return annot;
- }
- )
- }));
- // Create JavaScript references between annotations
- data.tiers.forEach(tier => {
- tier.annotations.forEach(annot => {
- if (null != annot.ref) {
- annot.reference = data.annotations[annot.ref];
- }
- });
- });
- // Sort alignable annotations by start & end
- data.alignableAnnotations.sort((a, b) => {
- let d = a.start - b.start;
- if (d == 0) {
- d = b.end - a.end;
- }
- return d;
- });
- data.length = data.alignableAnnotations.length;
- return data;
- }
- render() {
- // apply tiers filter
- let tiers = this.data.tiers;
- if (this.params.tiers) {
- tiers = tiers.filter(tier => tier.id in this.params.tiers);
- }
- // denormalize references to alignable annotations
- const backRefs = {};
- let indeces = {};
- tiers.forEach((tier, index) => {
- tier.annotations.forEach(annot => {
- if (annot.reference && annot.reference.type == this.Types.ALIGNABLE_ANNOTATION) {
- if (!(annot.reference.id in backRefs)) {
- backRefs[annot.ref] = {};
- }
- backRefs[annot.ref][index] = annot;
- indeces[index] = true;
- }
- });
- });
- indeces = Object.keys(indeces).sort();
- this.renderedAlignable = this.data.alignableAnnotations.filter(alignable => backRefs[alignable.id]);
- // table
- const table = this.table = document.createElement('table');
- table.className = 'wavesurfer-annotations';
- // head
- const thead = document.createElement('thead');
- const headRow = document.createElement('tr');
- thead.appendChild(headRow);
- table.appendChild(thead);
- const th = document.createElement('th');
- th.textContent = 'Time';
- th.className = 'wavesurfer-time';
- headRow.appendChild(th);
- indeces.forEach(index => {
- const tier = tiers[index];
- const th = document.createElement('th');
- th.className = 'wavesurfer-tier-' + tier.id;
- th.textContent = tier.id;
- th.style.width = this.params.tiers[tier.id];
- headRow.appendChild(th);
- });
- // body
- const tbody = document.createElement('tbody');
- table.appendChild(tbody);
- this.renderedAlignable.forEach(alignable => {
- const row = document.createElement('tr');
- row.id = 'wavesurfer-alignable-' + alignable.id;
- tbody.appendChild(row);
- const td = document.createElement('td');
- td.className = 'wavesurfer-time';
- td.textContent = alignable.start.toFixed(1) + '–' +
- alignable.end.toFixed(1);
- row.appendChild(td);
- const backRef = backRefs[alignable.id];
- indeces.forEach(index => {
- const tier = tiers[index];
- const td = document.createElement('td');
- const annotation = backRef[index];
- if (annotation) {
- td.id = 'wavesurfer-annotation-' + annotation.id;
- td.dataset.ref = alignable.id;
- td.dataset.start = alignable.start;
- td.dataset.end = alignable.end;
- td.textContent = annotation.value;
- }
- td.className = 'wavesurfer-tier-' + tier.id;
- row.appendChild(td);
- });
- });
- this.container.innerHTML = '';
- this.container.appendChild(table);
- }
- bindClick() {
- this._onClick = e => {
- const ref = e.target.dataset.ref;
- if (null != ref) {
- const annot = this.data.annotations[ref];
- if (annot) {
- this.fireEvent('select', annot.start, annot.end);
- }
- }
- };
- this.container.addEventListener('click', this._onClick);
- }
- getRenderedAnnotation(time) {
- let result;
- this.renderedAlignable.some(annotation => {
- if (annotation.start <= time && annotation.end >= time) {
- result = annotation;
- return true;
- }
- return false;
- });
- return result;
- }
- getAnnotationNode(annotation) {
- return document.getElementById(
- 'wavesurfer-alignable-' + annotation.id
- );
- }
- }
|