Commit d2915619 authored by Anton Koch's avatar Anton Koch

Merge branch 'elan' into 'release_2_1'

Add ELAN export for media entries

See merge request !134
parents 46dbd7cf cb2ffd7e
Pipeline #63961 passed with stage
in 4 minutes and 5 seconds
......@@ -19,6 +19,7 @@ reference date, if available (e.g. live streams)
- Duration field on edit media screen
- Add media screen refuses to add inaccessible videos
- Metadata store relays 'not found' and 'access denied' errors
- [ELAN](https://archive.mpi.nl) `.eaf` file export for media entries
### Changed
......
......@@ -14845,8 +14845,7 @@
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"schema-object": {
"version": "4.0.11",
......@@ -18415,6 +18414,14 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"xmldom": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz",
......
......@@ -27,6 +27,7 @@ export default {
delete: 'Delete',
download_archive: 'Download Archive',
download_csv: 'Download CSV',
download_eaf: 'Download EAF',
download_package: 'Download Package',
download: 'Download',
done: 'Done',
......@@ -34,6 +35,7 @@ export default {
export_grid: 'Export Grid',
export_timeline: 'Export Timeline',
export_timeline_csv: 'Export Timeline as CSV',
export_media_eaf: 'Export Media as EAF',
forgot_password: 'Forgot Password',
help: 'Help',
here: 'Here',
......
const eafTemplate = {
_declaration: {
_attributes: {
version: '1.0',
encoding: 'UTF-8'
}
},
ANNOTATION_DOCUMENT: {
_attributes: {
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:noNamespaceSchemaLocation': 'http://www.mpi.nl/tools/elan/EAFv3.0.xsd',
FORMAT: '3.0',
VERSION: '3.0'
},
LINGUISTIC_TYPE: {
_attributes: {
GRAPHIC_REFERENCES: 'false',
LINGUISTIC_TYPE_ID: 'piecemaker-lt',
TIME_ALIGNABLE: 'true'
}
},
CONSTRAINT: [
{
_attributes: {
DESCRIPTION: 'Time subdivision of parent annotation\'s time interval, no time gaps allowed within this interval',
STEREOTYPE: 'Time_Subdivision'
}
},
{
_attributes: {
DESCRIPTION: 'Symbolic subdivision of a parent annotation. Annotations refering to the same parent are ordered',
STEREOTYPE: 'Symbolic_Subdivision'
}
},
{
_attributes: {
DESCRIPTION: '1-1 association with a parent annotation',
STEREOTYPE: 'Symbolic_Association'
}
},
{
_attributes: {
DESCRIPTION: 'Time alignable annotations within the parent annotation\'s time interval, gaps are allowed',
STEREOTYPE: 'Included_In'
}
}
],
HEADER: {
_attributes: {
TIME_UNITS: 'milliseconds'
}
},
TIER: {
_attributes: {
LINGUISTIC_TYPE_REF: 'piecemaker-lt'
}
},
TIME_ORDER: {
TIME_SLOT: []
}
}
}
export default eafTemplate
......@@ -17,6 +17,9 @@
content-paragraph
form-main(v-if="acl.put", v-model.lazy="payload", :schema="schema", ref="mediaForm")
div(slot="form-buttons-add", :class="{'full-width row q-mb-sm': isMobile}")
q-btn(v-if="$route.params.uuid", @click="exportEAF", color="grey", :disabled="!canExportEAF"
:class="[!isMobile ? 'q-mr-sm' : '']", :label="exportLabelEAF")
p(v-if="acl.put === false") {{ $t('errors.editing_forbidden') }}
</template>
......@@ -35,6 +38,7 @@
import constants from 'mbjs-data-models/src/constants'
import { parseURI } from 'mbjs-data-models/src/lib'
import { ObjectUtil } from 'mbjs-utils'
import { DateTime } from 'luxon'
export default {
......@@ -56,6 +60,36 @@
this.acl = Object.assign({}, this.acl, acl)
this.timeline = await this.$store.dispatch('maps/get', parseURI(this.media.target.id).uuid)
this.$root.$emit('setBackButton', '/piecemaker/timelines/' + parseURI(this.media.target.id).uuid + '/media')
},
async exportEAF () {
if (this.downloadUrlEAF) return this.downloadUrlEAF.click()
this.$q.loading.show()
const query = {
'target.id': this.timeline.id,
'target.type': constants.mapTypes.MAP_TYPE_TIMELINE,
'target.selector._valueMillis': { $gte: this.media.target.selector._valueMillis },
'body.type': { $in: ['TextualBody', 'VocabularyEntry'] }
}
if (this.media.target.selector._valueDuration) {
query['target.selector._valueMillis']['$lte'] = this.media.target.selector._valueMillis +
this.media.target.selector._valueDuration
}
const results = await this.$store.dispatch('annotations/find', query)
const metadata = await this.$store.dispatch('metadata/getLocal', this.media)
const doc = await this.$store.dispatch('elan/generateEAF', {
annotations: results.items,
map: this.timeline,
media: this.media,
metadata
})
const eafData = `data:text/x-eaf+xml;charset=utf-8,${doc}`
const download = document.createElement('a')
download.setAttribute('href', encodeURI(eafData))
download.setAttribute('download', `media_${ObjectUtil.slug(metadata.title)}-${this.timeline._uuid}.eaf`)
this.downloadUrlEAF = download
this.exportLabelEAF = this.$t('buttons.download_eaf')
this.$q.loading.hide()
}
},
computed: {
......@@ -76,6 +110,9 @@
const duration = this.annotation.target.selector.getDuration()
if (duration) return duration.toFormat(constants.config.TIMECODE_FORMAT_DURATION)
}
},
canExportEAF () {
return this.annotation && ['video/mp4', 'audio/m4a'].indexOf(this.annotation.body.source.type) > -1
}
},
watch: {
......@@ -120,6 +157,8 @@
],
acl: {},
apiPayload: undefined,
downloadUrlEAF: undefined,
exportLabelEAF: this.$t('buttons.export_media_eaf'),
selectorOverride: undefined,
showDurationOverride: false,
titlePayload: undefined,
......
......@@ -19,6 +19,7 @@ import {
/** Import custom modules */
import {
auth0,
elan,
auth,
acl,
files,
......@@ -107,6 +108,7 @@ const getRequestConfig = () => {
const modules = {
/** Custom stores */
auth0,
elan,
auth,
mosys,
notifications,
......
import { js2xml } from 'xml-js'
import { ObjectUtil } from 'mbjs-utils'
import parseURI from 'mbjs-data-models/src/lib/parse-uri'
import { DateTime, Interval } from 'luxon'
import eafTemplate from '../../lib/eaf-template'
const elan = {
namespaced: true,
state: {},
actions: {
generateEAF (context, { annotations, map, media, metadata }) {
const mediaDateTime = DateTime.fromMillis(media.target.selector._valueMillis)
let TIME_SLOT = []
const ANNOTATION = annotations.map((annotation, i) => {
const
annoStart = DateTime.fromMillis(annotation.target.selector._valueMillis),
annoEnd = annoStart.plus(annotation.target.selector._valueDuration || 1),
TIME_SLOT_REF1 = `ts${((i + 1) * 2) - 1}`,
TIME_SLOT_REF2 = `ts${(i + 1) * 2}`
TIME_SLOT = TIME_SLOT.concat([
{
_attributes: {
TIME_SLOT_ID: TIME_SLOT_REF1,
TIME_VALUE: Interval.fromDateTimes(mediaDateTime, annoStart)
.toDuration()
.as('milliseconds')
}
},
{
_attributes: {
TIME_SLOT_ID: TIME_SLOT_REF2,
TIME_VALUE: Interval.fromDateTimes(mediaDateTime, annoEnd)
.toDuration()
.as('milliseconds')
}
}
])
return {
ALIGNABLE_ANNOTATION: {
_attributes: {
ANNOTATION_ID: `a${i + 1}`,
TIME_SLOT_REF1,
TIME_SLOT_REF2
},
ANNOTATION_VALUE: {
_text: annotation.body.value
}
}
}
})
const doc = ObjectUtil.merge({}, eafTemplate, {
ANNOTATION_DOCUMENT: {
_attributes: { AUTHOR: map.creator.name },
TIER: {
_attributes: {
TIER_ID: metadata.title || media.body.source.id
},
ANNOTATION
},
TIME_ORDER: {
TIME_SLOT
},
HEADER: {
MEDIA_DESCRIPTOR: {
_attributes: {
MEDIA_URL: media.body.source.id,
MIME_TYPE: media.body.source.type
}
},
PROPERTY: [
{
_attributes: { name: 'URN' },
_text: `urn:nl-mpi-tools-elan-eaf:${parseURI(media.id).uuid}`
},
{
_attributes: { name: 'lastUsedAnnotationId' },
_text: `${ANNOTATION.length}`
}
]
}
}
})
return js2xml(doc, {
compact: true,
spaces: '\t'
})
}
}
}
export default elan
import auth0 from './auth0'
import elan from './elan'
import mosys from './mosys'
import notifications from './notifications'
import auth from './auth'
......@@ -13,6 +14,7 @@ import vocabularies from './vocabularies'
export {
auth0,
elan,
auth,
acl,
files,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment