From f34efda19b3dd8d64708f622684915e95938641f Mon Sep 17 00:00:00 2001 From: Loris Sauter Date: Wed, 18 May 2022 13:12:06 +0200 Subject: [PATCH] Textual Competition Submission & Configuration Adjustements (#136) --- README.md | 7 ++ angular.json | 1 + src/app/app.component.html | 113 ++++++++++-------- src/app/app.component.ts | 23 ++-- src/app/app.config.ts | 16 ++- src/app/core/vbs/vbs-submission.service.ts | 66 ++++++---- .../objectdetails/quick-viewer.component.html | 2 +- ...result-segment-preview-tile.component.html | 2 +- .../preferences/preferences.component.html | 102 ++++++++-------- .../preferences/preferences.component.ts | 36 +++--- src/app/settings/settings.component.html | 38 +++--- src/app/settings/settings.component.ts | 1 + src/app/settings/settings.module.ts | 5 +- .../textual-submission.component.css | 3 + .../textual-submission.component.html | 12 ++ .../textual-submission.component.ts | 22 ++++ .../advanced-media-player.component.html | 2 +- .../video/advanced-media-player.component.ts | 1 - src/app/shared/model/config/config.model.ts | 79 +++++++----- src/app/shared/pipes/pipes.module.ts | 5 +- .../pipes/util/competition-enabled.pipe.ts | 18 ++- src/app/shared/pipes/util/format-time.pipe.ts | 25 ++++ src/config.json | 24 ++-- 23 files changed, 379 insertions(+), 224 deletions(-) create mode 100644 src/app/settings/textual-submission/textual-submission.component.css create mode 100644 src/app/settings/textual-submission/textual-submission.component.html create mode 100644 src/app/settings/textual-submission/textual-submission.component.ts create mode 100644 src/app/shared/pipes/util/format-time.pipe.ts diff --git a/README.md b/README.md index 129bd9fa..d0e1d970 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,13 @@ Vitrivr NG is a web-based user interface developed to be used with the latest ve For setup information, consult our [Wiki](https://github.com/vitrivr/vitrivr-ng/wiki) +## Config + +There is a `config.json` which enables configuration of the UI to a certain extend. +While we provide a sensible [default config](src/config.json), the +some default values are better explored in the [code](src/app/shared/model/config/config.model.ts). +Information about the configuration can be found in [the wiki](https://github.com/vitrivr/vitrivr-ng/wiki/Configuration). + ## Development server From the project folder, run `ng serve` to start a development server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. diff --git a/angular.json b/angular.json index b96acf69..3bff455a 100644 --- a/angular.json +++ b/angular.json @@ -30,6 +30,7 @@ "styles": [ "node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet-geosearch/dist/geosearch.css", + "node_modules/@videogular/ngx-videogular/fonts/videogular.css", "src/vitrivr-theme.scss" ], "scripts": [], diff --git a/src/app/app.component.html b/src/app/app.component.html index 15d45db1..30a82772 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,73 +1,88 @@ - + - + - + - + - - grid_on - + + grid_on + - + - - view_comfy - + + view_comfy + - + - - list - + + list + - + - - filter_list - + + filter_list + - + - + - + + - - av timer - +
+ +
+ +
+ + + + + av timer +
- +
- - - - - - - - - - - + + + + + + + + + + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 4773b580..e52f17f7 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -23,17 +23,13 @@ export class AppComponent implements OnInit, AfterViewInit { settingsbadge = ''; _config: Config; - - /** Observable that return the loading state of the QueryService. */ - private readonly _loading: Observable; - _loadBool = false - + textualSubmissionOpen = false; /** Variable to safe currently selected view */ public _active_view: View; - competitionHost = ((c: Config) => c._config.competition.host); - + /** Observable that return the loading state of the QueryService. */ + private readonly _loading: Observable; /** * Default constructor. Subscribe for PING messages at the CineastWebSocketFactoryService. @@ -50,10 +46,10 @@ export class AppComponent implements OnInit, AfterViewInit { } }) this._loading = _queryService.observable.pipe( - filter(msg => ['STARTED', 'ENDED', 'ERROR'].indexOf(msg) > -1), - map(() => { - return _queryService.running; - }) + filter(msg => ['STARTED', 'ENDED', 'ERROR'].indexOf(msg) > -1), + map(() => { + return _queryService.running; + }) ); _configService.configAsObservable.subscribe(c => this._config = c) this._active_view = View.GALLERY; @@ -96,4 +92,9 @@ export class AppComponent implements OnInit, AfterViewInit { public setActiveView(view: View) { this._active_view = view; } + + isCompetitionActive() { + return this._configService.configAsObservable; + return this._config.dresEndpointRest && (this._config.get('competition.vbs') || this._config.get('competition.lsc')) + } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 1c861165..ec641a71 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,6 +4,7 @@ import {Config} from './shared/model/config/config.model'; import {HttpClient} from '@angular/common/http'; import {UUIDGenerator} from './shared/util/uuid-generator.util'; import {MatSnackBar} from '@angular/material/snack-bar'; +import {Title} from '@angular/platform-browser'; /** * A service providing the application's configuration and means to (re) load it. @@ -23,7 +24,12 @@ export class AppConfig { } } as ProxyHandler; - constructor(private http: HttpClient, protected _snackBar: MatSnackBar) { + constructor(private http: HttpClient, protected _snackBar: MatSnackBar, private titleService: Title) { + AppConfig.settingsSubject.subscribe(settings => { + if (settings) { + this.titleService.setTitle(settings.get('title')); + } + }); } /** @@ -33,10 +39,6 @@ export class AppConfig { return AppConfig.settings; } - public publishChanges() { - AppConfig.settingsSubject.next(AppConfig.settings) - } - /** * Returns the current configuration as observable. Can be used to monitor changes. */ @@ -44,6 +46,10 @@ export class AppConfig { return AppConfig.settingsSubject.asObservable(); } + public publishChanges() { + AppConfig.settingsSubject.next(AppConfig.settings) + } + /** * Loads the default configuration from a JSON file. */ diff --git a/src/app/core/vbs/vbs-submission.service.ts b/src/app/core/vbs/vbs-submission.service.ts index 5d0f293b..9617720f 100644 --- a/src/app/core/vbs/vbs-submission.service.ts +++ b/src/app/core/vbs/vbs-submission.service.ts @@ -285,30 +285,54 @@ export class VbsSubmissionService { mergeMap(([segment, frame]) => { /* Submit, do some logging and catch HTTP errors. */ return this._dresSubmit.getApiV1Submit(null, segment, null, frame).pipe( - tap((status: SuccessfulSubmissionsStatus) => { - switch (status.submission) { - case 'CORRECT': - this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-success'}); - break; - case 'WRONG': - this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-warning'}); - break; - default: - this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION}); - break; - } - }), - catchError(err => { - if (err.error) { - this._snackBar.open(`Submissions error: ${err.error.description}`, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-error'}) - } else { - this._snackBar.open(`Submissions error: ${err.message}`, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-error'}) - } - return of(null) - }) + tap((status: SuccessfulSubmissionsStatus) => { + this.handleSubmissionResponse(status); + }), + catchError(err => { + return this.handleSubmissionError(err); + }) ) }) ).subscribe() + + /* Setup submission subscription, which is triggered manually. */ + this._submitTextSubscription = this._submitTextSubject.pipe( + mergeMap((text) => { + /* Submit, do some logging and catch HTTP errors. */ + return this._dresSubmit.getApiV1Submit(null, null, text).pipe( + tap((status: SuccessfulSubmissionsStatus) => { + this.handleSubmissionResponse(status); + }), + catchError(err => { + return this.handleSubmissionError(err); + }) + ) + }) + ).subscribe() + } + + + private handleSubmissionError(err) { + if (err.error) { + this._snackBar.open(`Submissions error: ${err.error.description}`, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-error'}) + } else { + this._snackBar.open(`Submissions error: ${err.message}`, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-error'}) + } + return of(null) + } + + private handleSubmissionResponse(status: SuccessfulSubmissionsStatus) { + switch (status.submission) { + case 'CORRECT': + this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-success'}); + break; + case 'WRONG': + this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION, panelClass: 'snackbar-warning'}); + break; + default: + this._snackBar.open(status.description, null, {duration: Config.SNACKBAR_DURATION}); + break; + } } /** diff --git a/src/app/objectdetails/quick-viewer.component.html b/src/app/objectdetails/quick-viewer.component.html index 87d84781..489535f7 100644 --- a/src/app/objectdetails/quick-viewer.component.html +++ b/src/app/objectdetails/quick-viewer.component.html @@ -11,7 +11,7 @@

{{mediaobject.objectid}} ({{mediaobject.name}}) - diff --git a/src/app/results/result-segment-preview-tile/result-segment-preview-tile.component.html b/src/app/results/result-segment-preview-tile/result-segment-preview-tile.component.html index 0701809d..eff9f108 100644 --- a/src/app/results/result-segment-preview-tile/result-segment-preview-tile.component.html +++ b/src/app/results/result-segment-preview-tile/result-segment-preview-tile.component.html @@ -19,7 +19,7 @@ -

Temporal mode

@@ -63,49 +69,49 @@

Max sequence length

-

Temporal Query Defaults

- Default container distance: {{_config|GetConfigVariablePipe:defaultContainerDist}} +

Temporal Query Defaults

+ Default container distance: {{_config|GetConfigVariablePipe:defaultContainerDist}} -

Reset settings

- - - +

Reset settings

+ + +

Logs

-

Interaction log

- - - - - - -

Results log

- - - - - -

Submission log

- - - - +

Interaction log

+ + + + + + +

Results log

+ + + + + +

Submission log

+ + + +
diff --git a/src/app/settings/preferences/preferences.component.ts b/src/app/settings/preferences/preferences.component.ts index c9f5bb5b..a76506dc 100644 --- a/src/app/settings/preferences/preferences.component.ts +++ b/src/app/settings/preferences/preferences.component.ts @@ -88,7 +88,7 @@ export class PreferencesComponent implements AfterContentInit { from(this._interactionLogTable.orderBy('id').each((o, c) => { data.push(o) })) - .pipe( + .pipe( first(), map(h => { const zip = new JSZip(); @@ -98,17 +98,17 @@ export class PreferencesComponent implements AfterContentInit { } return zip }) - ) - .subscribe(zip => { - zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( + ) + .subscribe(zip => { + zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( (result) => { window.open(window.URL.createObjectURL(result)); }, (error) => { console.log(error); } - ) - }); + ) + }); } /** @@ -119,7 +119,7 @@ export class PreferencesComponent implements AfterContentInit { from(this._resultsLogTable.orderBy('id').each((o, c) => { data.push(o) })) - .pipe( + .pipe( first(), map(() => { const zip = new JSZip(); @@ -129,17 +129,17 @@ export class PreferencesComponent implements AfterContentInit { } return zip }) - ) - .subscribe(zip => { - zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( + ) + .subscribe(zip => { + zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( (result) => { window.open(window.URL.createObjectURL(result)); }, (error) => { console.log(error); } - ) - }); + ) + }); } public onDownloadSubmissionLog() { @@ -147,7 +147,7 @@ export class PreferencesComponent implements AfterContentInit { from(this._submissionLogTable.orderBy('id').each((o, c) => { data.push(o) })) - .pipe( + .pipe( first(), map(() => { const zip = new JSZip(); @@ -157,17 +157,17 @@ export class PreferencesComponent implements AfterContentInit { } return zip }) - ) - .subscribe(zip => { - zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( + ) + .subscribe(zip => { + zip.generateAsync({type: 'blob', compression: 'DEFLATE'}).then( (result) => { window.open(window.URL.createObjectURL(result)); }, (error) => { console.log(error); } - ) - }); + ) + }); } /** diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 8680eede..de26ae44 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -1,23 +1,25 @@ - - - storage - - - - - - settings - - - + + + storage + + + + + + + settings + + + + + + + album + + + - - - album - - - diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 797c5f50..180e85e6 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -25,4 +25,5 @@ export class SettingsComponent implements AfterContentInit { this._badgeValue = el }) } + } diff --git a/src/app/settings/settings.module.ts b/src/app/settings/settings.module.ts index 7dacb619..382ff5af 100644 --- a/src/app/settings/settings.module.ts +++ b/src/app/settings/settings.module.ts @@ -12,11 +12,12 @@ import {PipesModule} from '../shared/pipes/pipes.module'; import { NgxSliderModule } from '@angular-slider/ngx-slider'; import {MatBadgeModule} from '@angular/material/badge'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import { TextualSubmissionComponent } from './textual-submission/textual-submission.component'; @NgModule({ imports: [MaterialModule, BrowserModule, FormsModule, FlexLayoutModule, NgxSliderModule, PipesModule, MatButtonToggleModule, MatBadgeModule, ReactiveFormsModule, ], - declarations: [RefinementComponent, SettingsComponent, SelectionManagementComponent, PreferencesComponent, WeightDistributionComponent], - exports: [RefinementComponent, SettingsComponent, SelectionManagementComponent, PreferencesComponent] + declarations: [RefinementComponent, SettingsComponent, SelectionManagementComponent, PreferencesComponent, WeightDistributionComponent, TextualSubmissionComponent], + exports: [RefinementComponent, SettingsComponent, SelectionManagementComponent, PreferencesComponent, TextualSubmissionComponent] }) export class SettingsModule { diff --git a/src/app/settings/textual-submission/textual-submission.component.css b/src/app/settings/textual-submission/textual-submission.component.css new file mode 100644 index 00000000..8c22c7ff --- /dev/null +++ b/src/app/settings/textual-submission/textual-submission.component.css @@ -0,0 +1,3 @@ +.small-font { + font-size: 12px; +} diff --git a/src/app/settings/textual-submission/textual-submission.component.html b/src/app/settings/textual-submission/textual-submission.component.html new file mode 100644 index 00000000..19bd5b57 --- /dev/null +++ b/src/app/settings/textual-submission/textual-submission.component.html @@ -0,0 +1,12 @@ +
+ + Textual Submission + + + + +
diff --git a/src/app/settings/textual-submission/textual-submission.component.ts b/src/app/settings/textual-submission/textual-submission.component.ts new file mode 100644 index 00000000..37c70353 --- /dev/null +++ b/src/app/settings/textual-submission/textual-submission.component.ts @@ -0,0 +1,22 @@ +import {AfterViewInit, Component, Input, OnInit} from '@angular/core'; +import {VbsSubmissionService} from '../../core/vbs/vbs-submission.service'; + +@Component({ + selector: 'app-textual-submission', + templateUrl: './textual-submission.component.html', + styleUrls: ['./textual-submission.component.css'] +}) +export class TextualSubmissionComponent { + + constructor(private _submissionService: VbsSubmissionService) { } + + public value: string; + + @Input() smallFont = false; + + + submit(){ + this._submissionService.submitText(this.value); + } + +} diff --git a/src/app/shared/components/video/advanced-media-player.component.html b/src/app/shared/components/video/advanced-media-player.component.html index 95c0af75..cbf2c90c 100644 --- a/src/app/shared/components/video/advanced-media-player.component.html +++ b/src/app/shared/components/video/advanced-media-player.component.html @@ -41,7 +41,7 @@
- send diff --git a/src/app/shared/components/video/advanced-media-player.component.ts b/src/app/shared/components/video/advanced-media-player.component.ts index 7c0444e8..abc746a0 100644 --- a/src/app/shared/components/video/advanced-media-player.component.ts +++ b/src/app/shared/components/video/advanced-media-player.component.ts @@ -72,7 +72,6 @@ export class AdvancedMediaPlayerComponent implements AfterViewChecked { * https://github.com/videogular/videogular2/issues/720 */ ngAfterViewChecked() { - console.log('detecting changes') this._cdRef.detectChanges(); } diff --git a/src/app/shared/model/config/config.model.ts b/src/app/shared/model/config/config.model.ts index c48575aa..784bc54e 100644 --- a/src/app/shared/model/config/config.model.ts +++ b/src/app/shared/model/config/config.model.ts @@ -8,17 +8,16 @@ import {MetadataType} from '../messages/queries/metadata-type.model'; export class Config { /** Context of the Cineast API. */ public static readonly CONTEXT = 'api'; - /** Version of the Cineast API. */ public static readonly VERSION = 'v1'; - /** The key under which the main configuration will be saved. */ public static readonly DB_KEY = 'main'; - /** Default display duration for Snackbar messages. */ public static SNACKBAR_DURATION = 2500; - + /** A handy port checking regex based on https://stackoverflow.com/a/12968117 */ + private static readonly PORT_REGEX = /:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])\//; _config = { + title: 'vitrivr', api: { host: window.location.hostname, /* IP address or hostname (no scheme), pointing to the API endpoint; defaults to hostname of window. */ port: 4567, /* Port for the API. */ @@ -27,16 +26,19 @@ export class Config { ping_interval: 5000 /* Default ping interval in milliseconds. */ }, resources: { - host_thumbnails: window.location.protocol + '//' + window.location.hostname + '/vitrivr/thumbnails', /** Path / URL to location where media object thumbnails will be stored. */ - host_objects: window.location.protocol + '//' + window.location.hostname + '/vitrivr/objects', + host_thumbnails: window.location.protocol + '//' + window.location.hostname + ':80/vitrivr/thumbnails', /** Path / URL to location where media object's will be stored. */ - suffix_default: '.jpg', + host_objects: window.location.protocol + '//' + window.location.hostname + ':80/vitrivr/objects', /** Default suffix for thumbnails. */ + suffix_default: '.jpg', + /** Per-mediatype suffix definition for thumbnails. */ suffix: { 'IMAGE': 'png', 'VIDEO': 'png' - } /** Per-mediatype suffix definition for thumbnails. */ + }, + /** Options for the resources port: $host -> use the host's port, $api -> use the api port, number: specify the port */ + port: '$host' // string to enable overrides }, competition: { /* Toggles VBS mode; determines type of information that is submitted. */ @@ -103,7 +105,7 @@ export class Config { neighboringSegmentLookupAllCount: 200000 }, text: { - categories: ["visualtextcoembedding", "Text Co-Embedding"] + categories: ['visualtextcoembedding', 'Text Co-Embedding'] }, boolean: [], temporal_mode: 'TEMPORAL_DISTANCE', @@ -119,27 +121,11 @@ export class Config { } }; - /** - * Deserializes a Config object from a given JavaScript object or string. - * - * @param {{} | string} object The object that should be parsed. - * @return {Config} The resulting config object. - */ - public static deserialize(object: {} | string): Config { - if (typeof object === 'string') { - object = JSON.parse(object); - } - if (object['api'] || object['resources'] || object['query'] || object['competition'] || object['tags'] || object['mlt'] || object['refinement']) { - return new Config(object['api'], object['resources'], object['query'], object['competition'], object['tags'], object['mlt'], object['refinement']); - } else { - return null; - } - } - /** * Default constructor for configuration object. The different configuration type can be passed to this constructor and the will be merged with * the default configuration. * + * @param title Optional title for the application as, e.g. loaded from a file. * @param api Optional Cineast API configuration as, e.g. loaded from a file. * @param resources Optional resources configuration as, e.g. loaded from a file. * @param query Optional query configuration, e.g. loaded from a file. @@ -148,8 +134,11 @@ export class Config { * @param mlt Optional More-Like-This categories as, e.g. loaded from a file. * @param refinement Optional refinement configuration */ - constructor(api?: any, resources?: any, query?: QuerySettings, competition?: any, tags?: Tag[], mlt?: FeatureCategories[], refinement?: any) { + constructor(title?: string, api?: any, resources?: any, query?: QuerySettings, competition?: any, tags?: Tag[], mlt?: FeatureCategories[], refinement?: any) { const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray; + if (title) { + this._config.title = title; + } if (api) { this._config.api = DEEPMERGE(this._config.api, api, {arrayMerge: overwriteMerge}); } @@ -171,11 +160,24 @@ export class Config { if (refinement) { this._config.refinement = DEEPMERGE(this._config.refinement, refinement, {arrayMerge: overwriteMerge}); } - if (this._config.api.host === 'default') { + if (this._config.api.host === '$host') { this._config.api.host = window.location.hostname } - this._config.resources.host_objects = this._config.resources.host_objects.replace('/default/', '/' + window.location.hostname + '/'); - this._config.resources.host_thumbnails = this._config.resources.host_thumbnails.replace('/default/', '/' + window.location.hostname + '/'); + this._config.resources.host_objects = this._config.resources.host_objects.replace('/$host/', '/' + window.location.hostname + '/'); + this._config.resources.host_thumbnails = this._config.resources.host_thumbnails.replace('/$host/', '/' + window.location.hostname + '/'); + if (this._config.resources.port) { + const providedPort = this._config.resources.port; + let port = window.location.port; + if (providedPort === '$api') { + port = '' + this._config.api.port; + } else if (providedPort.match(Config.PORT_REGEX)?.length > 0) { + port = '' + providedPort; + } else if (providedPort === '$host') { + // default + } // no else, as this was the default. + this._config.resources.host_objects = this._config.resources.host_objects.replace(Config.PORT_REGEX, ':' + port + '/'); + this._config.resources.host_thumbnails = this._config.resources.host_thumbnails.replace(Config.PORT_REGEX, ':' + port + '/'); + } } /** @@ -223,6 +225,23 @@ export class Config { return spec; } + /** + * Deserializes a Config object from a given JavaScript object or string. + * + * @param {{} | string} object The object that should be parsed. + * @return {Config} The resulting config object. + */ + public static deserialize(object: {} | string): Config { + if (typeof object === 'string') { + object = JSON.parse(object); + } + if (object['api'] || object['resources'] || object['query'] || object['competition'] || object['tags'] || object['mlt'] || object['refinement']) { + return new Config(object['title'], object['api'], object['resources'], object['query'], object['competition'], object['tags'], object['mlt'], object['refinement']); + } else { + return null; + } + } + /** * Accesses and returns the config value specified by the given path. Path components are separated by a '.' * If the value does not exist, then null is returned! diff --git a/src/app/shared/pipes/pipes.module.ts b/src/app/shared/pipes/pipes.module.ts index 5e0e1ec1..98429d4d 100644 --- a/src/app/shared/pipes/pipes.module.ts +++ b/src/app/shared/pipes/pipes.module.ts @@ -27,11 +27,12 @@ import {QueryStageIndexPipe} from './query/query-stage-index.pipe'; import {QueryStageLast} from './query/query-stage-last.pipe'; import {ModelHasTermPipe} from './util/model-has-term.pipe'; import {FilterTagsPipe} from './containers/filter-tags.pipe'; +import {FormatTimePipe} from './util/format-time.pipe'; @NgModule({ imports: [], - declarations: [FilterTagsPipe, ModelHasTermPipe, QueryStageLast, QueryStageIndexPipe, BackgroundScorePipe, ScorePercentagePipe, SortTagsPipe, TextWithLinkPipe, ColorForRelevancePipe, CompetitionEnabledPipe, SegmentPathPipe, IiifResourceUrlPipe, ObjectPathPipe, ThumbnailPathPipe, ObjectFilterTemporalPipe, GetConfigVariablePipe, DresEnabledPipe, OrderBySegmentPipe, FlattenPathsPipe, OrderByScorePipe, FilterPipe, LimitPipe, ArrayObjectSortPipe, SetStringSortPipe, LimitObjectsPipe, LimitPathsPipe, OrderBySegmentIdPipe, OrderByPipe], - exports: [FilterTagsPipe, ModelHasTermPipe, QueryStageLast, QueryStageIndexPipe, BackgroundScorePipe, ScorePercentagePipe, SortTagsPipe, TextWithLinkPipe, ColorForRelevancePipe, CompetitionEnabledPipe, SegmentPathPipe, IiifResourceUrlPipe, ObjectPathPipe, ThumbnailPathPipe, ObjectFilterTemporalPipe, GetConfigVariablePipe, DresEnabledPipe, OrderBySegmentPipe, FlattenPathsPipe, OrderByScorePipe, FilterPipe, LimitPipe, ArrayObjectSortPipe, SetStringSortPipe, LimitObjectsPipe, LimitPathsPipe, OrderBySegmentIdPipe, OrderByPipe] + declarations: [FilterTagsPipe, ModelHasTermPipe, QueryStageLast, QueryStageIndexPipe, BackgroundScorePipe, ScorePercentagePipe, SortTagsPipe, TextWithLinkPipe, ColorForRelevancePipe, CompetitionEnabledPipe, SegmentPathPipe, IiifResourceUrlPipe, ObjectPathPipe, ThumbnailPathPipe, ObjectFilterTemporalPipe, GetConfigVariablePipe, DresEnabledPipe, OrderBySegmentPipe, FlattenPathsPipe, OrderByScorePipe, FilterPipe, LimitPipe, ArrayObjectSortPipe, SetStringSortPipe, LimitObjectsPipe, LimitPathsPipe, OrderBySegmentIdPipe, OrderByPipe, FormatTimePipe], + exports: [FilterTagsPipe, ModelHasTermPipe, QueryStageLast, QueryStageIndexPipe, BackgroundScorePipe, ScorePercentagePipe, SortTagsPipe, TextWithLinkPipe, ColorForRelevancePipe, CompetitionEnabledPipe, SegmentPathPipe, IiifResourceUrlPipe, ObjectPathPipe, ThumbnailPathPipe, ObjectFilterTemporalPipe, GetConfigVariablePipe, DresEnabledPipe, OrderBySegmentPipe, FlattenPathsPipe, OrderByScorePipe, FilterPipe, LimitPipe, ArrayObjectSortPipe, SetStringSortPipe, LimitObjectsPipe, LimitPathsPipe, OrderBySegmentIdPipe, OrderByPipe, FormatTimePipe] }) export class PipesModule { } diff --git a/src/app/shared/pipes/util/competition-enabled.pipe.ts b/src/app/shared/pipes/util/competition-enabled.pipe.ts index 3c53b027..5d4fe94b 100644 --- a/src/app/shared/pipes/util/competition-enabled.pipe.ts +++ b/src/app/shared/pipes/util/competition-enabled.pipe.ts @@ -5,7 +5,7 @@ import {AppConfig} from '../../../app.config'; import { Observable } from 'rxjs'; @Pipe({ - name: 'CompetitionEnabledPipe' + name: 'competitionEnabledPipe' // should be camelCase https://angular.io/guide/styleguide }) export class CompetitionEnabledPipe implements PipeTransform { @@ -13,11 +13,21 @@ export class CompetitionEnabledPipe implements PipeTransform { } /** - * Returns true uf VBS mode is active and properly configured (i.e. endpoint and team ID is specified). + * Returns true if the competition host is set. False otherwise. Doesn't require an input + * @param type The type to check. empty to generically check for competition * * @return {boolean} */ - public transform(submissionService: VbsSubmissionService): Observable { - return this._config.configAsObservable.pipe(map(c => c.get('competition.host'))); + public transform(type?: string): Observable { + if(type){ + if(type === 'vbs' || type === 'lsc'){ + return this._config.configAsObservable.pipe(map(c => c.get('competition.'+type))); + } else if(type.length === 0){ + return this._config.configAsObservable.pipe(map(c => c.get('competition.host'))); + }else{ + throw Error(`Invalid competition type: ${type}`); + } + } + return this._config.configAsObservable.pipe(map(c => c.get('competition.host'))); } } diff --git a/src/app/shared/pipes/util/format-time.pipe.ts b/src/app/shared/pipes/util/format-time.pipe.ts new file mode 100644 index 00000000..5f0f3ca9 --- /dev/null +++ b/src/app/shared/pipes/util/format-time.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Transforms a given number as hours:minutes:seconds + */ +@Pipe({ + name: 'formatTime', +}) +export class FormatTimePipe implements PipeTransform { + transform(value: number): string { + const hrs = Math.floor(value / 3600); + const mins = Math.floor((value % 3600) / 60); + const secs = Math.floor(value % 60); + let out = ''; + /* Hours if present */ + if (hrs > 0) { + out += '' + (hrs < 10 ? '0' : '') + hrs + ':'; + } + /* Minutes */ + out += '' + (mins < 10 ? '0' : '') + mins + ':'; + /* seconds */ + out += '' + (secs < 10 ? '0' : '') + secs; + return out; + } +} diff --git a/src/config.json b/src/config.json index 0eb00ee1..0af6498a 100644 --- a/src/config.json +++ b/src/config.json @@ -1,21 +1,18 @@ { - "api": { - "host": "default" - }, - "resources": { - "host_thumbnails": "http://localhost:4567/thumbnails/:s", - "host_objects": "http://localhost:4567/objects/:o" - }, "competition": { "host": "", - "tls": true, - "log": false, + "tls": false, + "log": true, "lsc": false, "vbs": false }, "mlt": { - "VIDEO": ["visualtextcoembedding"], - "IMAGE": ["visualtextcoembedding"] + "VIDEO": [ + "visualtextcoembedding" + ], + "IMAGE": [ + "visualtextcoembedding" + ] }, "query": { "history": 0, @@ -31,7 +28,10 @@ }, "text": { "categories": [ - ["visualtextcoembedding", "Text Co-Embedding"] + [ + "visualtextcoembedding", + "Text Co-Embedding" + ] ] }, "boolean": [