diff --git a/web/src/audio/hatnote_audio.ts b/web/src/audio/hatnote_audio.ts index 2a87691..d478cb1 100644 --- a/web/src/audio/hatnote_audio.ts +++ b/web/src/audio/hatnote_audio.ts @@ -12,11 +12,9 @@ export class HatnoteAudio { private readonly swells: Howl[] = [] private readonly harp: Howl[] = [] private lastAudioPlayed: number = 0 - private readonly showAudioInfoboxSubject: Subject private readonly settings_data: SettingsData - constructor(settings_data: SettingsData, showAudioInfoboxSubject: Subject ) { - this.showAudioInfoboxSubject = showAudioInfoboxSubject + constructor(settings_data: SettingsData ) { this.settings_data = settings_data let loaded_sounds = 0 @@ -47,10 +45,7 @@ export class HatnoteAudio { console.log("Number of audio playing (subtract): " + thisAudio.current_notes) } }, - onload: sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, + onload: sound_load })) this.clav.push(new Howl({ @@ -63,10 +58,7 @@ export class HatnoteAudio { console.log("Number of audio playing (subtract): " + thisAudio.current_notes) } }, - onload: sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, + onload: sound_load })) } @@ -82,10 +74,7 @@ export class HatnoteAudio { console.log("Number of audio playing (subtract): " + thisAudio.current_notes) } }, - onload : sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, + onload : sound_load })) } @@ -93,26 +82,17 @@ export class HatnoteAudio { this.harp.push(new Howl({ src : [sound_map.get('sounds/ConcertHarp-small/samples/C3_mf3.wav')], volume : 0.2, - onload : sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, + onload : sound_load })) this.harp.push(new Howl({ src : [sound_map.get('sounds/ConcertHarp-small/samples/F2_f1.wav')], volume : 0.2, - onload : sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, + onload : sound_load })) this.harp.push(new Howl({ src : [sound_map.get('sounds/ConcertHarp-small/samples/A2_mf1.wav')], volume : 0.2, onload : sound_load, - onunlock: function() { - thisAudio.showAudioInfoboxSubject.next(false) - }, })) } diff --git a/web/src/canvas/banner.ts b/web/src/canvas/banner.ts index f27f17e..2efee82 100644 --- a/web/src/canvas/banner.ts +++ b/web/src/canvas/banner.ts @@ -11,7 +11,7 @@ export class Banner{ this.bannerLayer = bannerLayer this.root = bannerLayer.appendSVGElement('g') - .attr('transform', 'translate(0, ' + bannerLayer.canvas.theme.header_height +')'); + .attr('transform', 'translate(0, ' + bannerLayer.canvas.visDirector.hatnoteTheme.header_height +')'); this.user_container = this.root.append('g') @@ -28,7 +28,7 @@ export class Banner{ this.user_container.append('rect') .attr('opacity', 0) - .attr('fill', bannerLayer.canvas.theme.getThemeColor(bannerData.serviceEvent)) + .attr('fill', bannerLayer.canvas.visDirector.getThemeColor(bannerData.serviceEvent)) .attr('width', bannerLayer.canvas.width) .attr('height', 35) .transition() diff --git a/web/src/canvas/banner_layer.ts b/web/src/canvas/banner_layer.ts index f7ca218..7db32ca 100644 --- a/web/src/canvas/banner_layer.ts +++ b/web/src/canvas/banner_layer.ts @@ -1,13 +1,13 @@ import {Selection} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import {BannerData} from "../observable/model"; import {Banner} from "./banner"; +import {Canvas} from "./canvas"; export class BannerLayer{ - public readonly canvas: ListenToCanvas + public readonly canvas: Canvas private readonly root: Selection; private banner: Banner | null - constructor(canvas: ListenToCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'banner_layer') this.banner = null; @@ -18,7 +18,7 @@ export class BannerLayer{ } public addBanner(bannerData: BannerData){ - if(this.banner !== null || this.canvas.theme.current_service_theme?.id_name !== this.canvas.theme.getHatnoteService(bannerData.serviceEvent)) { + if(this.banner !== null || this.canvas.visDirector.current_service_theme?.id_name !== this.canvas.visDirector.getHatnoteService(bannerData.serviceEvent)) { return; } diff --git a/web/src/canvas/canvas.ts b/web/src/canvas/canvas.ts index e519271..e9092bc 100644 --- a/web/src/canvas/canvas.ts +++ b/web/src/canvas/canvas.ts @@ -1,27 +1,46 @@ import {select, Selection} from "d3"; import {BehaviorSubject, Subject} from "rxjs"; -import {CircleData, DatabaseInfo, NetworkInfoboxData} from "../observable/model"; +import {BannerData, CircleData, DatabaseInfo, NetworkInfoboxData} from "../observable/model"; import {SettingsData} from "../configuration/hatnote_settings"; import {InfoBox, InfoboxType} from "./info_box"; -import {CirclesLayer} from "./listen/circles_layer"; import {Header} from "./header"; -import {Theme} from "../theme/theme"; +import {VisualisationDirector} from "../theme/visualisationDirector"; import {HatnoteVisService} from "../service_event/model"; +import {Navigation} from "./navigation"; +import {BannerLayer} from "./banner_layer"; +import {QRCode} from "./qr_code"; +import {MuteIcon} from "./mute_icon"; +import {Carousel} from "./carousel"; +import {ListenToVisualisation} from "./listen/listenToVisualisation"; +import {GeoVisualisation} from "./geo/geoVisualisation"; +import {Visualisation} from "../theme/model"; +import {set} from "lodash"; -export abstract class Canvas { - abstract header: Header; - public readonly theme: Theme; - abstract info_box_websocket: InfoBox; - abstract info_box_legend: InfoBox; +export class Canvas { + public readonly banner_layer: BannerLayer; + public readonly qr_code: QRCode; + public readonly header: Header; + public readonly visDirector: VisualisationDirector; + public readonly mute_icon: MuteIcon; + public readonly carousel: Carousel | undefined + public readonly navigation: Navigation | undefined; + public readonly info_box_websocket: InfoBox; + public readonly info_box_legend: InfoBox; public readonly isMobileScreen: boolean = false; public readonly settings: SettingsData; public readonly showNetworkInfoboxObservable: Subject public readonly updateDatabaseInfoSubject: Subject - public readonly hatnoteVisServiceChangedSubject: BehaviorSubject + public readonly onCarouselTransitionStart: BehaviorSubject<[HatnoteVisService, Visualisation]> + public readonly onThemeHasChanged: BehaviorSubject<[HatnoteVisService, Visualisation]> + public readonly onCarouselTransitionEnd: BehaviorSubject<[HatnoteVisService, Visualisation]> public readonly updateVersionSubject: Subject<[string, number]> public readonly newCircleSubject: Subject + public readonly newBannerSubject: Subject + private readonly listenToVis: ListenToVisualisation; + public readonly geoPopUpContainer: Selection + private readonly geoVis: GeoVisualisation; public readonly appContainer: Selection; - protected abstract _root: Selection; + private readonly _root: Selection; public get root(): Selection { return this._root; } @@ -40,30 +59,129 @@ export abstract class Canvas { this._height = value; } - protected constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject, + constructor(theme: VisualisationDirector, settings: SettingsData, newCircleSubject: Subject, showNetworkInfoboxObservable: Subject, updateVersionSubject: Subject<[string, number]>, - hatnoteVisServiceChangedSubject: BehaviorSubject, + onCarouselTransitionStart: BehaviorSubject<[HatnoteVisService, Visualisation]>, + onCarouselTransitionMid: BehaviorSubject<[HatnoteVisService, Visualisation]>, + onCarouselTransitionEnd: BehaviorSubject<[HatnoteVisService, Visualisation]>, updateDatabaseInfoSubject: Subject, + newBannerSubject: Subject, appContainer: Selection) { this._width = window.innerWidth; this._height = window.innerHeight; - this.theme = theme; + this.visDirector = theme; this.settings = settings - this.hatnoteVisServiceChangedSubject = hatnoteVisServiceChangedSubject + this.onCarouselTransitionStart = onCarouselTransitionStart + this.onThemeHasChanged = onCarouselTransitionMid + this.onCarouselTransitionEnd = onCarouselTransitionEnd this.newCircleSubject = newCircleSubject this.showNetworkInfoboxObservable = showNetworkInfoboxObservable this.updateDatabaseInfoSubject = updateDatabaseInfoSubject this.updateVersionSubject = updateVersionSubject this.appContainer = appContainer; + this.newBannerSubject = newBannerSubject if (this.width <= 430 || this.height <= 430) { // iPhone 12 Pro Max 430px viewport width this.isMobileScreen = true; } + + // draw order matters in this function. Do not change without checking the result. + this._root = this.appContainer.append("svg") + .attr("width", this.width) + .attr("height", this.height) + .attr('fill', this.visDirector.hatnoteTheme.svg_background_color) + .style('background-color', '#1c2733'); + this.geoPopUpContainer = this.appContainer.append('div') + .attr("id","geo-visualisation-popup-container").attr("style", "opacity: 1;") + + this.qr_code = new QRCode(this) + + this.listenToVis = new ListenToVisualisation(this) + this.geoVis = new GeoVisualisation(this) + + this.banner_layer = new BannerLayer(this) + this.header = new Header(this) + // needs to be added last to the svg because it should draw over everything else + this.info_box_websocket = new InfoBox(this, InfoboxType.network_websocket_connecting) + this.info_box_legend = new InfoBox(this, InfoboxType.legend) + + if(settings.carousel_mode && !this.isMobileScreen){ + this.carousel = new Carousel(this) + } + + // needs to be added after the carousel transition because the transition layer spans over the entire screen + // which captures mouse clicks that otherwise would not arrive at the navigation buttons + if (this.isMobileScreen && !this.settings.embedded_mode) { + this.navigation = new Navigation(this) + } + + // needs to be here because otherwise the transition animation and mobile navigation layer of the carousel would lay above the mute icon + // and block the cursor event of the mute icon + this.mute_icon = new MuteIcon(this) + + this.onThemeHasChanged.subscribe({ + next: (value) => { + this.renderCurrentTheme() + } + }) + + this.renderCurrentTheme(); + + if(!settings.kiosk_mode && !settings.audio_mute && !(!settings.carousel_mode && settings.map)){ + this.mute_icon.show() + } + + window.onresize = (_) => this.windowUpdate(); } public appendSVGElement(type: string): Selection { return this._root.append(type) } - protected abstract windowUpdate() : void + public renderCurrentTheme(){ + // remove circles and visualisation from other services + this.listenToVis.renderCurrentTheme() + this.geoVis.renderCurrentTheme() + + // remove banner + this.banner_layer.removeBanner(); + + // update qr code + this.qr_code.themeUpdate(this.visDirector.current_service_theme) + + // update header logo + this.header.themeUpdate(this.visDirector.current_service_theme) + + this.navigation?.themeUpdate(this.visDirector.current_service_theme) + } + + // This method does not cover all ui elements. There is no requirement for this nor a need for a mobile version. People + // will use the website as a background animation. If you resize the window it is easier to just reload the page for a moment. + public windowUpdate() { + // update canvas root dimensions + this.width = window.innerWidth; + this.height = window.innerHeight; + this._root.attr("width", this.width).attr("height", this.height); + + // update canvas header dimensions + this.header.windowUpdate() + + // update banner + this.banner_layer.windowUpdate() + + // update progress indicator + this.carousel?.windowUpdate() + + // update qr_code + this.qr_code?.windowUpdate() + + // update navigation + this.navigation?.windowUpdate() + + // update websocket info box + this.info_box_websocket.windowUpdate() + + // update mute icon + this.mute_icon.windowUpdate() + } } \ No newline at end of file diff --git a/web/src/canvas/carousel.ts b/web/src/canvas/carousel.ts index 273d74e..df8cfa0 100644 --- a/web/src/canvas/carousel.ts +++ b/web/src/canvas/carousel.ts @@ -1,11 +1,10 @@ -import {ListenToCanvas} from "./listen/listenToCanvas"; import {Transition} from "./transition"; import {ProgressIndicator} from "./progress_indicator"; import {DatabaseInfo} from "../observable/model"; -import {Subject} from "rxjs"; +import {BehaviorSubject, Subject} from "rxjs"; import {HatnoteVisService} from "../service_event/model"; -import {ServiceTheme} from "../theme/model"; -import {GeoCanvas} from "./geo/geoCanvas"; +import {ServiceTheme, Visualisation} from "../theme/model"; +import {Canvas} from "./canvas"; export class Carousel { public readonly transition: Transition; @@ -15,21 +14,32 @@ export class Carousel { public serviceError: Map public allServicesHaveError: boolean private startCarouselService: HatnoteVisService | null - private readonly canvas: ListenToCanvas | GeoCanvas + private readonly canvas: Canvas; + private nextTheme: ServiceTheme | undefined; private currentCarouselOrderIndex; - constructor(canvas: ListenToCanvas | GeoCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.transition = new Transition(this.canvas) - this.transition.onTransitionMid.subscribe(_ => this.canvas.hatnoteVisServiceChangedSubject.next(this.canvas.theme.current_service_theme.id_name)) - this.transition.onTransitionEnd.subscribe(_ => this.continueCarousel()) + this.transition.onTransitionStart.subscribe(_ => this.canvas.onCarouselTransitionStart.next( + [this.canvas.visDirector.current_service_theme.id_name,this.canvas.visDirector.current_visualisation])) + this.transition.onTransitionMid.subscribe(_ => { + this.initNextTheme(); + this.canvas.onThemeHasChanged.next( + [this.canvas.visDirector.current_service_theme.id_name,this.canvas.visDirector.current_visualisation]); + }) + this.transition.onTransitionEnd.subscribe(_ => { + this.canvas.onCarouselTransitionEnd.next( + [this.canvas.visDirector.current_service_theme.id_name,this.canvas.visDirector.current_visualisation]) + this.continueCarousel(); + }) this.progess_indicator = new ProgressIndicator(this.canvas) this.updateDatabaseInfoSubject = this.canvas.updateDatabaseInfoSubject this.serviceError = new Map() this.currentCarouselOrderIndex = 0 this.allServicesHaveError = false - this.startCarouselService = this.canvas.theme.carousel_service_order[0].id_name + this.startCarouselService = this.canvas.visDirector.carousel_service_order[0].id_name this.databaseInfo = new Map() - this.canvas.theme.service_themes.forEach((serviceTheme => { + this.canvas.visDirector.service_themes.forEach((serviceTheme => { this.serviceError.set(serviceTheme.id_name, false) this.databaseInfo.set(serviceTheme.id_name, { service: serviceTheme.id_name, @@ -57,8 +67,8 @@ export class Carousel { let serviceErrors = 0 this.startCarouselService = null - for (let i = 0; i < this.canvas.theme.carousel_service_order.length; i++) { - let serviceTheme = this.canvas.theme.carousel_service_order[i] + for (let i = 0; i < this.canvas.visDirector.carousel_service_order.length; i++) { + let serviceTheme = this.canvas.visDirector.carousel_service_order[i] if(this.serviceError.get(serviceTheme.id_name)){ serviceErrors++ } else { @@ -68,7 +78,7 @@ export class Carousel { } } - if(serviceErrors === this.canvas.theme.service_themes.size){ + if(serviceErrors === this.canvas.visDirector.service_themes.size){ this.allServicesHaveError = true } @@ -76,9 +86,9 @@ export class Carousel { if(this.serviceError.get(dbInfo.service)){ this.progess_indicator.service_indicators.get(dbInfo.service)?.setError() - if(dbInfo.service === this.canvas.theme.current_service_theme.id_name && !this.allServicesHaveError) { + if(dbInfo.service === this.canvas.visDirector.current_service_theme.id_name && !this.allServicesHaveError) { this.initNextTheme() - this.transition.startTransition(this.canvas.theme.current_service_theme) + this.transition.startTransition(this.canvas.visDirector.current_service_theme) } return; } @@ -90,15 +100,15 @@ export class Carousel { let nextTheme: ServiceTheme | undefined; let iterationNumber = 0 - let iterationNumberLimit = this.canvas.theme.carousel_service_order.length; + let iterationNumberLimit = this.canvas.visDirector.carousel_service_order.length; let iterationIndex = (this.currentCarouselOrderIndex + 1) % iterationNumberLimit while(iterationNumber < iterationNumberLimit){ - if(this.serviceError.get(this.canvas.theme.carousel_service_order[iterationIndex].id_name)){ + if(this.serviceError.get(this.canvas.visDirector.carousel_service_order[iterationIndex].id_name)){ iterationIndex = (iterationIndex + 1) % iterationNumberLimit iterationNumber++; } else { this.currentCarouselOrderIndex = iterationIndex - nextTheme = this.canvas.theme.carousel_service_order[iterationIndex] + nextTheme = this.canvas.visDirector.carousel_service_order[iterationIndex] break; } } @@ -107,10 +117,11 @@ export class Carousel { } private initNextTheme(){ - let nextTheme: ServiceTheme | undefined = this.getNextServiceTheme() - if(nextTheme){ - this.canvas.theme.set_current_theme(nextTheme); - this.progess_indicator.setCurrentServiceIndicator(nextTheme) + let nextVisualisation = this.canvas.visDirector.getNextVisualisation() + if(this.nextTheme){ + this.canvas.visDirector.set_current_theme(this.nextTheme); + this.canvas.visDirector.setCurrentVisualisation(nextVisualisation); + this.progess_indicator.setCurrentServiceIndicator(this.nextTheme) } } @@ -123,7 +134,7 @@ export class Carousel { if(this.canvas.settings.carousel_mode) { let indicator = this.progess_indicator.currentServiceIndicator; - if (this.canvas.theme.current_service_theme.id_name === this.startCarouselService) { + if (this.canvas.visDirector.current_service_theme.id_name === this.startCarouselService) { this.serviceError.forEach((error, service) => { if (!error) { this.progess_indicator.service_indicators.get(service)?.reset() @@ -133,8 +144,10 @@ export class Carousel { indicator?.start(() => { if (!this.allServicesHaveError) { - this.initNextTheme() - this.transition.startTransition(this.canvas.theme.current_service_theme) + this.nextTheme = this.getNextServiceTheme() + if(this.nextTheme !== undefined) { + this.transition.startTransition(this.nextTheme) + } } }) } diff --git a/web/src/canvas/geo/circle.ts b/web/src/canvas/geo/geoCircle.ts similarity index 75% rename from web/src/canvas/geo/circle.ts rename to web/src/canvas/geo/geoCircle.ts index d4f6eab..489a2bd 100644 --- a/web/src/canvas/geo/circle.ts +++ b/web/src/canvas/geo/geoCircle.ts @@ -1,19 +1,22 @@ -import {CirclesLayer} from "./circles_layer"; import {HatnoteVisService} from "../../service_event/model"; import {BaseType, select, Selection} from "d3"; import {CircleData} from "../../observable/model"; +import {GeoCirclesLayer} from "./geoCirclesLayer"; +import {Canvas} from "../canvas"; -export class Circle{ - private readonly circlesLayer: CirclesLayer +export class GeoCircle { + private readonly circlesLayer: GeoCirclesLayer private readonly root: Selection; + private readonly canvas: Canvas; - constructor(circlesLayer: CirclesLayer, circleData: CircleData, + constructor(circlesLayer: GeoCirclesLayer, circleData: CircleData, svgCircle: SVGCircleElement, service: HatnoteVisService) { this.circlesLayer = circlesLayer + this.canvas = this.circlesLayer.geoVis.canvas // init circle values this.root = select(svgCircle) let point; - if(this.circlesLayer.canvas.theme.current_service_theme.id_name === HatnoteVisService.Bloxberg){ + if(this.canvas.visDirector.current_service_theme.id_name === HatnoteVisService.Bloxberg){ point = this.circlesLayer.worldProjection([circleData.location?.coordinate.long ?? 0, circleData.location?.coordinate.lat?? 0]) } else { point = this.circlesLayer.germanyProjection([circleData.location?.coordinate.long ?? 0, circleData.location?.coordinate.lat?? 0]) @@ -30,7 +33,7 @@ export class Circle{ .attr('cy', y) .attr("data-hatnote-event-type", circleData.type) .attr("data-hatnote-service-name", service) - .style('fill', this.circlesLayer.canvas.theme.getThemeColor(circleData.type)) + .style('fill', this.canvas.visDirector.getThemeColor(circleData.type)) .attr('fill-opacity', 0.75) .attr('r', 0) .transition() @@ -43,30 +46,30 @@ export class Circle{ .on('interrupt', _ => { highlightedArea.interrupt() popUpContainer.remove(); - if(this.circlesLayer.canvas.settings.debug_mode){ + if(this.canvas.settings.debug_mode){ console.log('Circle removed for ' + circleData.type) } }) .remove() .each( _ => { - if(this.circlesLayer.canvas.settings.debug_mode){ + if(this.canvas.settings.debug_mode){ console.log('Circle removed for ' + circleData.type) } }) let highlightedArea: Selection; // highlight region - if(this.circlesLayer.canvas.theme.current_service_theme.id_name === HatnoteVisService.Bloxberg){ + if(this.canvas.visDirector.current_service_theme.id_name === HatnoteVisService.Bloxberg){ highlightedArea = this.highlightCountry(circleData.location.countryId) } else { highlightedArea = this.highlightState(circleData.location.stateId) } // add pop up - const popUpContainer = this.circlesLayer.canvas.appContainer.append("div"); + const popUpContainer = this.canvas.geoPopUpContainer.append("div"); popUpContainer .style('position', 'absolute') - .style('top', `${y - this.circlesLayer.canvas.theme.header_height + 10}px`) + .style('top', `${y - this.canvas.visDirector.hatnoteTheme.header_height + 10}px`) .style('left', `${x + 10}px`) .style('padding', '4px') .style('border-radius', '1px') @@ -83,7 +86,7 @@ export class Circle{ } private highlightCountry(countryId: string): Selection { - let country = this.circlesLayer.canvas.root.select(`path[data-country-id="${countryId}"]`) + let country = this.canvas.root.select(`path[data-country-id="${countryId}"]`) .style('fill', '#eddc4e') country.transition() .duration(5000) @@ -92,7 +95,7 @@ export class Circle{ }; private highlightState(stateId: string): Selection { - let country = this.circlesLayer.canvas.root.select(`path[data-state-id="${stateId}"]`) + let country = this.canvas.root.select(`path[data-state-id="${stateId}"]`) .style('fill', '#eddc4e') country.transition() .duration(5000) diff --git a/web/src/canvas/geo/circles_layer.ts b/web/src/canvas/geo/geoCirclesLayer.ts similarity index 60% rename from web/src/canvas/geo/circles_layer.ts rename to web/src/canvas/geo/geoCirclesLayer.ts index 9397efa..7213ddb 100644 --- a/web/src/canvas/geo/circles_layer.ts +++ b/web/src/canvas/geo/geoCirclesLayer.ts @@ -1,22 +1,24 @@ import {GeoProjection, Selection} from "d3"; import {ServiceTheme} from "../../theme/model"; -import {Circle} from "./circle"; import {CircleData} from "../../observable/model"; import {Canvas} from "../canvas"; import {ServiceEvent} from "../../service_event/model"; +import {GeoCircle} from "./geoCircle"; +import {GeoVisualisation} from "./geoVisualisation"; -export class CirclesLayer{ +export class GeoCirclesLayer{ private readonly root: Selection; - public readonly canvas: Canvas + public readonly geoVis: GeoVisualisation public readonly germanyProjection: GeoProjection public readonly worldProjection: GeoProjection + private readonly rootId = "geo-vis-circle-layer" - constructor(canvas: Canvas, germanyProjection: GeoProjection, worldProjection: GeoProjection) { - this.canvas = canvas + constructor(geoVis: GeoVisualisation, germanyProjection: GeoProjection, worldProjection: GeoProjection) { + this.geoVis = geoVis this.germanyProjection = germanyProjection; this.worldProjection = worldProjection; - this.root = canvas.appendSVGElement('g').attr('id', 'circle_layer') - canvas.newCircleSubject.subscribe({ + this.root = geoVis.appendSVGElement('g').attr('id', this.rootId) + geoVis.canvas.newCircleSubject.subscribe({ next: (value) => this.addCircle(value) }) } @@ -30,21 +32,24 @@ export class CirclesLayer{ .enter() .append('circle') .each(function (circleData, _) { - let service = that.canvas.theme.getHatnoteService(circleData.type) - if (that.canvas.theme.current_service_theme?.id_name !== service){ + let service = that.geoVis.canvas.visDirector.getHatnoteService(circleData.type) + if (that.geoVis.canvas.visDirector.current_service_theme?.id_name !== service){ return } - new Circle(that,circleData, + new GeoCircle(that,circleData, this, service) }) } public removeOtherServiceCircles(currentServiceTheme: ServiceTheme) { - for (let serviceTheme of this.canvas.theme.service_themes.values()) { + for (let serviceTheme of this.geoVis.canvas.visDirector.service_themes.values()) { if(serviceTheme.id_name !== currentServiceTheme.id_name) { this.root.select(`circle[data-hatnote-service-name="${serviceTheme.id_name}"]`).interrupt().remove() } } + let circleLayer = document.getElementById(this.rootId); + circleLayer?.replaceChildren() + this.geoVis.canvas.geoPopUpContainer.selectChildren().remove() } } \ No newline at end of file diff --git a/web/src/canvas/geo/geoCanvas.ts b/web/src/canvas/geo/geoVisualisation.ts similarity index 54% rename from web/src/canvas/geo/geoCanvas.ts rename to web/src/canvas/geo/geoVisualisation.ts index a198528..ce88e35 100644 --- a/web/src/canvas/geo/geoCanvas.ts +++ b/web/src/canvas/geo/geoVisualisation.ts @@ -1,13 +1,6 @@ import {geoAlbers, geoBounds, geoEqualEarth, geoPath, GeoProjection, Selection} from "d3"; import '../../style/normalize.css'; import '../../style/main.css'; -import {CirclesLayer} from "./circles_layer"; -import {Header} from "../header"; -import {InfoBox, InfoboxType} from "../info_box"; -import {Theme} from "../../theme/theme"; -import {BehaviorSubject, Subject} from "rxjs"; -import {CircleData, DatabaseInfo, NetworkInfoboxData} from "../../observable/model"; -import {SettingsData} from "../../configuration/hatnote_settings"; import {feature, mesh} from "topojson"; import countriesJson from '../../../assets/countries-50m.json' import germanyJson from '../../../assets/germany.json' @@ -15,65 +8,49 @@ import {GeometryObject, Topology} from 'topojson-specification'; import {FeatureCollection, GeoJsonProperties} from 'geojson'; import {Canvas} from "../canvas"; import {HatnoteVisService} from "../../service_event/model"; -import {Carousel} from "../carousel"; - -export class GeoCanvas extends Canvas{ - public readonly circles_layer: CirclesLayer - public readonly header: Header; - protected readonly _root: Selection; - public readonly info_box_websocket: InfoBox; - public readonly info_box_legend: InfoBox; +import {GeoCirclesLayer} from "./geoCirclesLayer"; +import {Visualisation} from "../../theme/model"; + +export class GeoVisualisation { + public readonly circles_layer: GeoCirclesLayer + public readonly canvas: Canvas; + protected readonly root: Selection; public readonly worldMap: Selection; public readonly worldMapProjection: GeoProjection; public readonly germanyMap: Selection; public readonly germanyMapProjection: GeoProjection; - public readonly carousel: Carousel | undefined - constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject, - showNetworkInfoboxObservable: Subject, - updateVersionSubject: Subject<[string, number]>, - hatnoteVisServiceChangedSubject: BehaviorSubject, - updateDatabaseInfoSubject: Subject, - appContainer: Selection) { - super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject, appContainer) + constructor(canvas: Canvas) { + this.canvas = canvas; + this.root = canvas.appendSVGElement('g').attr("id", "geo-visualisation") // draw order matters in this function. Do not change without checking the result. - this._root = appContainer.append("svg") - .attr("id", 'hatnote-canvas') - .attr("width", this.width) - .attr("height", this.height) + this.root .attr("style", "max-width: 100%; height: auto;"); - this.worldMap = this._root.append("g").attr("id", "world-map") - this.germanyMap = this._root.append("g").attr("id", "germany-map") + this.worldMap = this.root.append("g").attr("id", "world-map") + this.germanyMap = this.root.append("g").attr("id", "germany-map") this.worldMapProjection = this.initWorldMapSvg() this.germanyMapProjection = this.initGermanyMapSvg() - this.circles_layer = new CirclesLayer(this, this.germanyMapProjection, this.worldMapProjection) - this.header = new Header(this, false) - // needs to be added last to the svg because it should draw over everything else - this.info_box_websocket = new InfoBox(this, InfoboxType.network_websocket_connecting, false, undefined, undefined) - this.info_box_legend = new InfoBox(this, InfoboxType.legend, false, undefined, undefined) - - if(settings.carousel_mode && !this.isMobileScreen){ - this.carousel = new Carousel(this) - } - - this.hatnoteVisServiceChangedSubject.subscribe({ - next: (value) => { - this.renderCurrentTheme() - } - }) + this.circles_layer = new GeoCirclesLayer(this, this.germanyMapProjection, this.worldMapProjection) this.renderCurrentTheme(); - window.onresize = (_) => this.windowUpdate(); + this.canvas.onCarouselTransitionStart.subscribe({ + next: (_) => this.canvas.geoPopUpContainer.attr("style", "opacity: 0;") + }) + + this.canvas.onCarouselTransitionEnd.subscribe({ + next: (_) => this.canvas.geoPopUpContainer.attr("style", "opacity: 1;") + }) } private germanyProjection(states: any): GeoProjection{ - const width = this.width; - const marginTop = this.theme.header_height; - const height = this.height; + const width = this.canvas.width; + const marginTop = this.canvas.visDirector.hatnoteTheme.header_height + 10; + const height = this.canvas.height; + const carouselProgressIndicatorSafeZone = this.canvas.visDirector.hatnoteTheme.progress_indicator_y_padding + 10; // from https://observablehq.com/@sto3psl/map-of-germany-in-d3-js const [bottomLeft, topRight] = geoBounds(states); @@ -93,7 +70,7 @@ export class GeoCanvas extends Canvas{ .translate([width / 2, height / 2]) .rotate([lambda, 0, 0]) .center(center) - .scale(scale * 200).fitExtent([[2, marginTop + 2], [width - 2, height - 2 ]], states); + .scale(scale * 200).fitExtent([[2, marginTop + 2], [width - 2, height - 2 - carouselProgressIndicatorSafeZone ]], states); } private initGermanyMapSvg(){ @@ -132,26 +109,17 @@ export class GeoCanvas extends Canvas{ } private initWorldMapSvg(){ - const width = this.width; - const marginTop = this.theme.header_height; - const height = this.height; + const width = this.canvas.width; + const marginTop = this.canvas.visDirector.hatnoteTheme.header_height + 10; + const height = this.canvas.height; + const carouselProgressIndicatorSafeZone = this.canvas.visDirector.hatnoteTheme.progress_indicator_y_padding + 10; // create projection - let projection = geoEqualEarth().fitExtent([[2, marginTop + 2], [width - 2, height - 2 ]], {type: "Sphere"}) + let projection = geoEqualEarth().fitExtent([[2, marginTop + 2], [width - 2, height - 2 - carouselProgressIndicatorSafeZone ]], {type: "Sphere"}) // Fit the projection. const path = geoPath(projection); - // draw order matters here, check before changing something - // Add a white sphere with a black border. - this.worldMap.append("path") - .attr("id", "black-world-boundary") - .datum({type: "Sphere"}) - .attr("fill", "white") - .attr("stroke", "currentColor") - // @ts-ignore - .attr("d", path); - let world: Topology = (countriesJson as unknown) as Topology let countriesGeometry: GeometryObject = world.objects.countries; let countries = feature(world, countriesGeometry) @@ -179,7 +147,13 @@ export class GeoCanvas extends Canvas{ } public renderCurrentTheme(){ - if (this.theme.current_service_theme.id_name == HatnoteVisService.Bloxberg) { + if(this.canvas.visDirector.current_visualisation === Visualisation.listenTo){ + this.root.attr("opacity", "0") + } else { + this.root.attr("opacity", "1") + } + + if (this.canvas.visDirector.current_service_theme.id_name == HatnoteVisService.Bloxberg) { this.worldMap.attr("opacity", 1) this.germanyMap.attr("opacity", 0) } else { @@ -188,24 +162,10 @@ export class GeoCanvas extends Canvas{ } // remove circles from other services - this.circles_layer.removeOtherServiceCircles(this.theme.current_service_theme) - - // update header logo - this.header.themeUpdate(this.theme.current_service_theme) + this.circles_layer.removeOtherServiceCircles(this.canvas.visDirector.current_service_theme) } - // This method does not cover all ui elements. There is no requirement for this nor a need for a mobile version. People - // will use the website as a background animation. If you resize the window it is easier to just reload the page for a moment. - protected windowUpdate() : void { - // update canvas root dimensions - this.width = window.innerWidth; - this.height = window.innerHeight; - this._root.attr("width", this.width).attr("height", this.height); - - // update canvas header dimensions - this.header.windowUpdate() - - // update websocket info box - this.info_box_websocket.windowUpdate() + public appendSVGElement(type: string): Selection { + return this.root.append(type) } } \ No newline at end of file diff --git a/web/src/canvas/header.ts b/web/src/canvas/header.ts index 531ea39..78726f1 100644 --- a/web/src/canvas/header.ts +++ b/web/src/canvas/header.ts @@ -1,13 +1,12 @@ import {Selection} from "d3"; import MinervaLogo from "../../assets/images/minervamessenger-banner-kussmund+bulb.png"; import {LegendItem} from "./legend_item"; -import {ServiceTheme} from "../theme/model"; +import {ServiceTheme, Visualisation} from "../theme/model"; import {environmentVariables} from "../configuration/environment"; import {Canvas} from "./canvas"; export class Header{ public readonly canvas: Canvas; - private readonly isMobileScreen: boolean; private readonly root: Selection private readonly background_rect: Selection private readonly title: Selection @@ -17,9 +16,8 @@ export class Header{ private readonly updateText: Selection private readonly legend_items: LegendItem[] = []; - constructor(canvas: Canvas, isMobileScreen: boolean) { + constructor(canvas: Canvas) { this.canvas = canvas; - this.isMobileScreen = isMobileScreen; this.root = canvas.appendSVGElement('g') .attr('id', 'header') @@ -28,8 +26,8 @@ export class Header{ this.background_rect = this.root.append('rect') .attr('width', canvas.width) - .attr('height', canvas.theme.header_height) - .attr('fill', canvas.theme.header_bg_color) + .attr('height', canvas.visDirector.hatnoteTheme.header_height) + .attr('fill', canvas.visDirector.hatnoteTheme.header_bg_color) this.logo = this.root.append('image') .attr('x', 10).attr('y', 4) @@ -39,16 +37,16 @@ export class Header{ this.title = this.root.append('text') .text('Hatnote title') .attr('font-family', 'HatnoteVisBold') - .attr('font-size', this.isMobileScreen ? '22px' : '32px') - .attr('fill', canvas.theme.header_text_color) - .attr('x', this.isMobileScreen ? 174 : 224).attr('y', canvas.theme.header_height/2 + 8.5) + .attr('font-size', this.canvas.isMobileScreen ? '22px' : '32px') + .attr('fill', canvas.visDirector.hatnoteTheme.header_text_color) + .attr('x', this.canvas.isMobileScreen ? 174 : 224).attr('y', canvas.visDirector.hatnoteTheme.header_height/2 + 8.5) this.title0 = this.root.append('text') - .text('Listen to') + .text(this.canvas.visDirector.current_visualisation === Visualisation.listenTo ? 'Listen to' : 'Locate') .attr('font-family', 'HatnoteVisNormal') - .attr('font-size', this.isMobileScreen ? '22px' : '32px') - .attr('fill', canvas.theme.header_text_color) - .attr('x', 70).attr('y', canvas.theme.header_height/2 + 8.5) + .attr('font-size', this.canvas.isMobileScreen ? '22px' : '32px') + .attr('fill', canvas.visDirector.hatnoteTheme.header_text_color) + .attr('x', 70).attr('y', canvas.visDirector.hatnoteTheme.header_height/2 + 8.5) this.updateBox = this.root.append('rect') .attr('opacity', 0) @@ -57,7 +55,7 @@ export class Header{ .attr('height', '30') .attr('rx', 7) .attr('ry', 7) - .attr('fill', this.canvas.theme.header_version_update_bg) + .attr('fill', this.canvas.visDirector.hatnoteTheme.header_version_update_bg) this.updateText = this.root.append('text') .attr('opacity', 0) @@ -68,7 +66,7 @@ export class Header{ .attr('font-size', '16px') .attr('fill', '#fff') - if(!this.isMobileScreen){ + if(!this.canvas.isMobileScreen){ for (let i = 0; i < 3; i++) { this.legend_items.push(new LegendItem(this, undefined, this.canvas)) } @@ -106,6 +104,7 @@ export class Header{ this.logo.attr('y', currentServiceTheme.header_y) this.title.text(currentServiceTheme.header_title) + this.title0.text(this.canvas.visDirector.current_visualisation === Visualisation.listenTo ? 'Listen to' : 'Locate') // update legend items this.clearLegendItems() @@ -122,7 +121,7 @@ export class Header{ this.updateBox.attr('transform', `translate(${this.canvas.width/2 - 95}, 8)`) this.updateText.attr('transform', `translate(${this.canvas.width/2}, 28)`) - this.canvas.theme.current_service_theme.legend_items.forEach((theme_legend_item, i) => { + this.canvas.visDirector.current_service_theme.legend_items.forEach((theme_legend_item, i) => { if(i < this.legend_items.length) { this.legend_items[i].windowUpdate(theme_legend_item) } diff --git a/web/src/canvas/icon_button.ts b/web/src/canvas/icon_button.ts index 71ae8ea..8ae9608 100644 --- a/web/src/canvas/icon_button.ts +++ b/web/src/canvas/icon_button.ts @@ -20,7 +20,7 @@ export class IconButton { this.bg = this.root.append('circle') .attr('transform', 'translate(' + xPos + ', ' + yPos+ ')') .attr('r', this.circleRadius) - .attr('stroke', this.navigation.canvas.theme.progress_indicator_fg_color) + .attr('stroke', this.navigation.canvas.visDirector.hatnoteTheme.progress_indicator_fg_color) .attr('stroke-width', 4 ) .attr('fill', '#fff') diff --git a/web/src/canvas/info_box.ts b/web/src/canvas/info_box.ts index 3bdea8d..d702938 100644 --- a/web/src/canvas/info_box.ts +++ b/web/src/canvas/info_box.ts @@ -1,12 +1,10 @@ import {Selection} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import InfoboxAudioImg from "../../assets/images/DancingDoodle.svg"; import LoadingSpinner from "../../assets/images/spinner.svg"; import InfoboxWebsocketConnectingImg from "../../assets/images/SprintingDoodle.svg"; import {NetworkInfoboxData} from "../observable/model"; import InfoboxDbConnectingImg from "../../assets/images/MessyDoodle.svg"; import {Carousel} from "./carousel"; -import {Subject} from "rxjs"; import {Canvas} from "./canvas"; export class InfoBox{ @@ -31,14 +29,12 @@ export class InfoBox{ private readonly isMobileScreen : boolean private readonly carousel : Carousel | undefined private currentType: InfoboxType - public readonly showAudioInfoboxObservable: Subject | undefined - constructor(canvas: Canvas, type: InfoboxType, isMobileScreen: boolean, carousel: Carousel | undefined, showAudioInfoboxObservable: Subject | undefined) { + constructor(canvas: Canvas, type: InfoboxType) { this.canvas = canvas - this.isMobileScreen = isMobileScreen - this.carousel = carousel - this.showAudioInfoboxObservable = showAudioInfoboxObservable + this.isMobileScreen = this.canvas.isMobileScreen + this.carousel = this.canvas.carousel this.currentType = type this.root = canvas.appendSVGElement('g') .attr('opacity', 0) @@ -60,11 +56,11 @@ export class InfoBox{ .text('') .attr('font-family', 'HatnoteVisBold') .attr('font-size', '26px') - .attr('fill', canvas.theme.header_text_color) + .attr('fill', canvas.visDirector.hatnoteTheme.header_text_color) this.text = this.root.append('text') .attr('font-family', 'HatnoteVisNormal') .attr('font-size', '16px') - .attr('fill', canvas.theme.header_text_color) + .attr('fill', canvas.visDirector.hatnoteTheme.header_text_color) let line1 = this.text.append('tspan') let line12 = this.text.append('tspan') let line2 = this.text.append('tspan') @@ -98,16 +94,6 @@ export class InfoBox{ next: (value) => this.show(value.infoboxType, value.show, value) }) break; - case InfoboxType.audio_enable: - line5link = this.text.append('a') - line5 = line5link.append('tspan') - line52 = line5link.append('tspan') - this.line5link = line5link - this.loadingSpinner = undefined - this.showAudioInfoboxObservable?.subscribe({ - next: (value) => this.show(InfoboxType.audio_enable, value) - }) - break; } this.setPosition() @@ -119,20 +105,6 @@ export class InfoBox{ public show(type: InfoboxType, show: boolean, network_infobox_data?: NetworkInfoboxData) { this.currentType = type - if(type=== InfoboxType.audio_enable) { - this.root.attr('opacity', show ? 1 : 0) - this.image?.attr('href', InfoboxAudioImg) - this.title.text('Enable audio') - this.line1[0].text('Your browser autoplay policy may prevent') - this.line2[0].text('audio from being played. Please ') - this.line2[1].text('click ').attr('font-family', 'HatnoteVisBold') - this.line3[0].text('somewhere on this page ').attr('font-family', 'HatnoteVisBold') - this.line3[1].text('to enable audio.') - this.line4[0].text('For more info see:') - this.line5link?.attr('href', 'https://jamonserrano.github.io/state-of-autoplay/') - .attr('target', '_blank') - this.line5[0].text('jamonserrano.github.io/state-of-autoplay') - } if(type === InfoboxType.network_websocket_connecting){ this.root.attr('opacity', show ? 1 : 0) @@ -149,11 +121,11 @@ export class InfoBox{ // current theme service must match with websocket event service, otherwise, when carousel mode is active and e.g. minerva service // is shown, the keeper db infobox might appear - if(type === InfoboxType.network_database_connecting && this.canvas.theme.current_service_theme.id_name === network_infobox_data?.service){ + if(type === InfoboxType.network_database_connecting && this.canvas.visDirector.current_service_theme.id_name === network_infobox_data?.service){ this.root.attr('opacity', show ? 1 : 0) this.image?.attr('href', InfoboxWebsocketConnectingImg) this.title.text('Connecting to database') - this.line1[0].text('Connecting to ' + this.canvas.theme.current_service_theme.name + ' database.') + this.line1[0].text('Connecting to ' + this.canvas.visDirector.current_service_theme.name + ' database.') this.line2[0].text(' ') this.line3[0].text(' ') this.line3[1].text(' ') @@ -162,12 +134,12 @@ export class InfoBox{ this.loadingSpinner?.attr('opacity', 1) } - if(type === InfoboxType.network_database_can_not_connect && this.canvas.theme.current_service_theme.id_name === network_infobox_data?.service && + if(type === InfoboxType.network_database_can_not_connect && this.canvas.visDirector.current_service_theme.id_name === network_infobox_data?.service && (!this.canvas.settings.carousel_mode || this.carousel?.allServicesHaveError)){ this.root.attr('opacity', show ? 1 : 0) this.image?.attr('href', InfoboxDbConnectingImg) this.title.text('Cannot connect to database') - this.line1[0].text('The backend can not connect ' + this.canvas.theme.current_service_theme.name + ' database.') + this.line1[0].text('The backend can not connect ' + this.canvas.visDirector.current_service_theme.name + ' database.') this.line2[0].text(' ') this.line3[0].text('Next reconnect: ').attr('font-family', 'HatnoteVisBold') this.line3[1].text(network_infobox_data?.next_reconnect_date ?? '') @@ -209,9 +181,6 @@ export class InfoBox{ case InfoboxType.network_database_connecting: this.loadingSpinner?.attr('x', this.canvas.width/2 + 40).attr('y', this.canvas.height/2 + 10) break; - case InfoboxType.audio_enable: - this.line5[0].attr('x', text_x).attr('dy', '20px') - break; } } } @@ -220,6 +189,5 @@ export enum InfoboxType { network_websocket_connecting, network_database_connecting, network_database_can_not_connect, - audio_enable, // nowhere used because it was dismissed in favour of the mute icon legend } \ No newline at end of file diff --git a/web/src/canvas/legend_item.ts b/web/src/canvas/legend_item.ts index 4bd6cc3..1317dc7 100644 --- a/web/src/canvas/legend_item.ts +++ b/web/src/canvas/legend_item.ts @@ -2,7 +2,7 @@ import {Selection} from "d3"; import {Header} from "./header"; import {ThemeLegendItem} from "../theme/model"; import {InfoBox} from "./info_box"; -import {Theme} from "../theme/theme"; +import {VisualisationDirector} from "../theme/visualisationDirector"; import {Canvas} from "./canvas"; export class LegendItem{ @@ -14,13 +14,13 @@ export class LegendItem{ private readonly header: Header | undefined; private readonly legendInfoBox: InfoBox | undefined; private readonly canvas: Canvas; - private readonly theme: Theme; + private readonly theme: VisualisationDirector; constructor(header: Header | undefined, legendInfoBox: InfoBox | undefined, canvas: Canvas) { this.header = header; this.legendInfoBox = legendInfoBox; this.canvas = canvas - this.theme = this.canvas.theme + this.theme = this.canvas.visDirector if(this.header) { this.root = this.header?.appendSVGElement('g') @@ -37,33 +37,33 @@ export class LegendItem{ .text('legend item') .attr('font-family', 'HatnoteVisNormal') .attr('font-size', '26px') - .attr('fill', this.theme.header_text_color) - .attr('x', this.theme.legend_item_circle_r + 10) - .attr('y', this.theme.header_height/2 + 8.5) + .attr('fill', this.theme.hatnoteTheme.header_text_color) + .attr('x', this.theme.hatnoteTheme.legend_item_circle_r + 10) + .attr('y', this.theme.hatnoteTheme.header_height/2 + 8.5) .attr('opacity', 1) this.smallTitle1 = this.root?.append('text') .text('legend item small1') .attr('font-family', 'HatnoteVisNormal') .attr('font-size', '16px') - .attr('fill', this.theme.header_text_color) - .attr('x', this.theme.legend_item_circle_r + 10) - .attr('y', this.theme.header_height/2 - 4) + .attr('fill', this.theme.hatnoteTheme.header_text_color) + .attr('x', this.theme.hatnoteTheme.legend_item_circle_r + 10) + .attr('y', this.theme.hatnoteTheme.header_height/2 - 4) .attr('opacity', 0) this.smallTitle2 = this.root?.append('text') .text('legend item small2') .attr('font-family', 'HatnoteVisNormal') .attr('font-size', '16px') - .attr('fill', this.theme.header_text_color) - .attr('x', this.theme.legend_item_circle_r + 10) - .attr('y', this.theme.header_height/2 + 13) + .attr('fill', this.theme.hatnoteTheme.header_text_color) + .attr('x', this.theme.hatnoteTheme.legend_item_circle_r + 10) + .attr('y', this.theme.hatnoteTheme.header_height/2 + 13) .attr('opacity', 0) this.circle = this.root?.append('circle') - .attr('r', this.theme.legend_item_circle_r) + .attr('r', this.theme.hatnoteTheme.legend_item_circle_r) .attr('cx', 0) - .attr('cy', this.theme.header_height/2) + .attr('cy', this.theme.hatnoteTheme.header_height/2) .attr('fill', '#000') // default value } diff --git a/web/src/canvas/listen/listenToCanvas.ts b/web/src/canvas/listen/listenToCanvas.ts deleted file mode 100644 index 4a86464..0000000 --- a/web/src/canvas/listen/listenToCanvas.ts +++ /dev/null @@ -1,136 +0,0 @@ -import {select, Selection} from "d3"; -import '../../style/normalize.css'; -import '../../style/main.css'; -import {CirclesLayer} from "./circles_layer"; -import {BannerLayer} from "../banner_layer"; -import {QRCode} from "../qr_code"; -import {Header} from "../header"; -import {InfoBox, InfoboxType} from "../info_box"; -import {Theme} from "../../theme/theme"; -import {BehaviorSubject, Subject} from "rxjs"; -import {BannerData, CircleData, DatabaseInfo, NetworkInfoboxData} from "../../observable/model"; -import {SettingsData} from "../../configuration/hatnote_settings"; -import {HatnoteVisService} from "../../service_event/model"; -import {Carousel} from "../carousel"; -import {Navigation} from "../navigation"; -import {MuteIcon} from "../mute_icon"; -import {Canvas} from "../canvas"; - -export class ListenToCanvas extends Canvas { - public readonly circles_layer: CirclesLayer; - public readonly banner_layer: BannerLayer; - public readonly qr_code: QRCode | undefined; - public readonly header: Header; - protected readonly _root: Selection; - public readonly navigation: Navigation | undefined; - public readonly info_box_websocket: InfoBox; - public readonly info_box_audio: InfoBox; - public readonly info_box_legend: InfoBox; - public readonly mute_icon: MuteIcon; - public readonly newBannerSubject: Subject - public readonly showAudioInfoboxObservable: Subject - public readonly carousel: Carousel | undefined - - constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject, - newBannerSubject: Subject, - showAudioInfoboxObservable: Subject, - showNetworkInfoboxObservable: Subject, - updateVersionSubject: Subject<[string, number]>, - hatnoteVisServiceChangedSubject: BehaviorSubject, - updateDatabaseInfoSubject: Subject, - appContainer: Selection){ - super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,hatnoteVisServiceChangedSubject,updateDatabaseInfoSubject, appContainer) - - this.newBannerSubject = newBannerSubject - this.showAudioInfoboxObservable = showAudioInfoboxObservable - - // draw order matters in this function. Do not change without checking the result. - this._root = this.appContainer.append("svg") - .attr("width", this.width) - .attr("height", this.height) - .attr('fill', theme.svg_background_color) - .style('background-color', '#1c2733'); - - this.circles_layer = new CirclesLayer(this) - this.banner_layer = new BannerLayer(this) - if (!this.isMobileScreen) { - this.qr_code = new QRCode(this) - } - this.header = new Header(this, this.isMobileScreen) - // needs to be added last to the svg because it should draw over everything else - this.info_box_websocket = new InfoBox(this, InfoboxType.network_websocket_connecting, this.isMobileScreen, this.carousel, this.showAudioInfoboxObservable) - this.info_box_audio = new InfoBox(this, InfoboxType.audio_enable, this.isMobileScreen, this.carousel, this.showAudioInfoboxObservable) - this.info_box_legend = new InfoBox(this, InfoboxType.legend, this.isMobileScreen, this.carousel, this.showAudioInfoboxObservable) - - if(settings.carousel_mode && !this.isMobileScreen){ - this.carousel = new Carousel(this) - } - - // needs to be added after the carousel transition because the transition layer spans over the entire screen - // which captures mouse clicks that otherwise would not arrive at the navigation buttons - if (this.isMobileScreen && !this.settings.embedded_mode) { - this.navigation = new Navigation(this) - } - - // needs to be here because otherwise the transition animation and mobile navigation layer of the carousel would lay above the mute icon - // and block the cursor event of the mute icon - this.mute_icon = new MuteIcon(this) - - this.renderCurrentTheme(); - - if(!settings.kiosk_mode && !settings.audio_mute){ - this.mute_icon.show() - } - - window.onresize = (_) => this.windowUpdate(); - } - - public renderCurrentTheme(){ - // remove circles from other services - this.circles_layer.removeOtherServiceCircles(this.theme.current_service_theme) - - // remove banner - this.banner_layer.removeBanner(); - - // update qr code - this.qr_code?.themeUpdate(this.theme.current_service_theme) - - // update header logo - this.header.themeUpdate(this.theme.current_service_theme) - - this.navigation?.themeUpdate(this.theme.current_service_theme) - } - - // This method does not cover all ui elements. There is no requirement for this nor a need for a mobile version. People - // will use the website as a background animation. If you resize the window it is easier to just reload the page for a moment. - public windowUpdate() { - // update canvas root dimensions - this.width = window.innerWidth; - this.height = window.innerHeight; - this._root.attr("width", this.width).attr("height", this.height); - - // update canvas header dimensions - this.header.windowUpdate() - - // update banner - this.banner_layer.windowUpdate() - - // update progress indicator - this.carousel?.windowUpdate() - - // update qr_code - this.qr_code?.windowUpdate() - - // update navigation - this.navigation?.windowUpdate() - - // update websocket info box - this.info_box_websocket.windowUpdate() - - // update audio info box - this.info_box_audio.windowUpdate() - - // update mute icon - this.mute_icon.windowUpdate() - } -} \ No newline at end of file diff --git a/web/src/canvas/listen/circle.ts b/web/src/canvas/listen/listenToCircle.ts similarity index 86% rename from web/src/canvas/listen/circle.ts rename to web/src/canvas/listen/listenToCircle.ts index c030057..8d4142b 100644 --- a/web/src/canvas/listen/circle.ts +++ b/web/src/canvas/listen/listenToCircle.ts @@ -1,10 +1,10 @@ import {Selection} from "d3"; -import {CirclesLayer} from "./circles_layer"; import {ServiceEvent} from "../../service_event/model"; import {getRandomIntInclusive} from "../../util/random"; +import {ListenToCirclesLayer} from "./listenToCirclesLayer"; -export class Circle{ - private readonly circlesLayer: CirclesLayer +export class ListenToCircle{ + private readonly circlesLayer: ListenToCirclesLayer private readonly root: Selection; private readonly ring: Selection private readonly circle_container: Selection @@ -13,18 +13,18 @@ export class Circle{ private no_label = false; private titleColor = '#fff' - constructor(circlesLayer: CirclesLayer, type: ServiceEvent, label_text: string, circle_radius: number, removeCircle: (serviceEvent: ServiceEvent) => void) { + constructor(circlesLayer: ListenToCirclesLayer, type: ServiceEvent, label_text: string, circle_radius: number, removeCircle: (serviceEvent: ServiceEvent) => void) { this.circlesLayer = circlesLayer // Otherwise the same label text will reset the seed to the same value which results in Math.random() returning the same number over and over //Math.seedrandom(label_text + Date.now()) // give circle spawn a padding of 20 let x = getRandomIntInclusive(20, circlesLayer.canvas.width - 20); - let y = getRandomIntInclusive(20 + circlesLayer.canvas.theme.header_height, circlesLayer.canvas.height - 20) ; + let y = getRandomIntInclusive(20 + circlesLayer.canvas.visDirector.hatnoteTheme.header_height, circlesLayer.canvas.height - 20) ; this.root = circlesLayer.appendSVGElement('g') .attr('transform', 'translate(' + x + ', ' + y + ')') - .attr('fill', circlesLayer.canvas.theme.circle_wave_color) + .attr('fill', circlesLayer.canvas.visDirector.hatnoteTheme.circle_wave_color) .style('opacity', 1.0) this.ring = this.root.append('circle') @@ -41,7 +41,7 @@ export class Circle{ this.circle_container = this.root.append('g') this.circle = this.circle_container.append('circle') - .attr('fill', this.circlesLayer.canvas.theme.getThemeColor(type)) + .attr('fill', this.circlesLayer.canvas.visDirector.getThemeColor(type)) .attr('r', circle_radius) .style('opacity', this.circlesLayer.canvas.settings.circle_start_opacity) this.circle.transition() diff --git a/web/src/canvas/listen/circles_layer.ts b/web/src/canvas/listen/listenToCirclesLayer.ts similarity index 67% rename from web/src/canvas/listen/circles_layer.ts rename to web/src/canvas/listen/listenToCirclesLayer.ts index 22dcc38..09d10ed 100644 --- a/web/src/canvas/listen/circles_layer.ts +++ b/web/src/canvas/listen/listenToCirclesLayer.ts @@ -1,37 +1,40 @@ import {Selection} from "d3"; import {ServiceTheme} from "../../theme/model"; -import {Circle} from "./circle"; import {CircleData} from "../../observable/model"; import {HatnoteVisService, ServiceEvent} from "../../service_event/model"; import {Canvas} from "../canvas"; +import {ListenToVisualisation} from "./listenToVisualisation"; +import {ListenToCircle} from "./listenToCircle"; -export class CirclesLayer{ +export class ListenToCirclesLayer{ private readonly root: Selection; - private readonly service_circles: Map; + private readonly service_circles: Map; + public readonly listenToVisualisation: ListenToVisualisation public readonly canvas: Canvas - constructor(canvas: Canvas) { - this.canvas = canvas - this.root = canvas.appendSVGElement('g').attr('id', 'circle_layer') - this.service_circles = new Map([ + constructor(listenToVisualisation: ListenToVisualisation) { + this.listenToVisualisation = listenToVisualisation + this.canvas = this.listenToVisualisation.canvas + this.root = this.listenToVisualisation.appendSVGElement('g').attr('id', 'listen-to-circle-layer') + this.service_circles = new Map([ [HatnoteVisService.Minerva, []], [HatnoteVisService.Keeper, []], [HatnoteVisService.Bloxberg, []], ]) - canvas.newCircleSubject.subscribe({ + this.canvas.newCircleSubject.subscribe({ next: (value) => this.addCircle(value) }) } public addCircle(circleData: CircleData) { - let service = this.canvas.theme.getHatnoteService(circleData.type) + let service = this.canvas.visDirector.getHatnoteService(circleData.type) if (isNaN(circleData.circle_radius) || - this.canvas.theme.current_service_theme?.id_name !== service){ + this.canvas.visDirector.current_service_theme?.id_name !== service){ return } - let circle = new Circle(this,circleData.type,circleData.label_text,circleData.circle_radius, + let circle = new ListenToCircle(this,circleData.type,circleData.label_text,circleData.circle_radius, (serviceEvent) => this.removeOldestCircle(serviceEvent)) if(service !== undefined){ @@ -46,7 +49,7 @@ export class CirclesLayer{ } public removeOldestCircle(serviceEvent: ServiceEvent){ - let service = this.canvas.theme.getHatnoteService(serviceEvent) + let service = this.canvas.visDirector.getHatnoteService(serviceEvent) if(service !== undefined){ // when adding to the end of the list we can delete the first entry if a circle has finished its animation diff --git a/web/src/canvas/listen/listenToVisualisation.ts b/web/src/canvas/listen/listenToVisualisation.ts new file mode 100644 index 0000000..418dd07 --- /dev/null +++ b/web/src/canvas/listen/listenToVisualisation.ts @@ -0,0 +1,40 @@ +import {Selection} from "d3"; +import '../../style/normalize.css'; +import '../../style/main.css'; +import {Canvas} from "../canvas"; +import {ListenToCirclesLayer} from "./listenToCirclesLayer"; +import {Visualisation} from "../../theme/model"; + +export class ListenToVisualisation { + public readonly circles_layer: ListenToCirclesLayer; + protected readonly root: Selection; + public readonly canvas: Canvas; + + constructor(canvas: Canvas){ + this.canvas = canvas; + + this.root = canvas.appendSVGElement('g').attr("id", "liston-to-visualisation") + + // draw order matters in this function. Do not change without checking the result. + this.root + .attr('fill', this.canvas.visDirector.hatnoteTheme.svg_background_color) + .style('background-color', '#1c2733'); + + this.circles_layer = new ListenToCirclesLayer(this) + + } + + public renderCurrentTheme(){ + if(this.canvas.visDirector.current_visualisation === Visualisation.geo){ + this.root.attr("opacity", "0") + } else { + this.root.attr("opacity", "1") + } + + this.circles_layer.removeOtherServiceCircles(this.canvas.visDirector.current_service_theme) + } + + public appendSVGElement(type: string): Selection { + return this.root.append(type) + } +} \ No newline at end of file diff --git a/web/src/canvas/mute_icon.ts b/web/src/canvas/mute_icon.ts index c59729f..7f45f88 100644 --- a/web/src/canvas/mute_icon.ts +++ b/web/src/canvas/mute_icon.ts @@ -1,6 +1,6 @@ import {Selection} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import QrCodeMinerva from "../../assets/images/volume_off_FILL1_wght400_GRAD0_opsz24.svg"; +import {Canvas} from "./canvas"; export class MuteIcon{ private readonly root: Selection; @@ -10,10 +10,10 @@ export class MuteIcon{ private readonly background: Selection; private readonly image_width = 100; private readonly text_color = '#fff'; - private readonly canvas: ListenToCanvas; + private readonly canvas: Canvas; // consists not only of the icon but also spans a transparent clickable container above everything - constructor(canvas: ListenToCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g') .attr('id', 'qr_code') diff --git a/web/src/canvas/navigation.ts b/web/src/canvas/navigation.ts index ba81a2d..94d3c53 100644 --- a/web/src/canvas/navigation.ts +++ b/web/src/canvas/navigation.ts @@ -1,12 +1,12 @@ import {Selection} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import {IconButton} from "./icon_button"; import {ServiceTheme} from "../theme/model"; import {LegendItem} from "./legend_item"; import {InfoboxType} from "./info_box"; +import {Canvas} from "./canvas"; export class Navigation{ - public readonly canvas: ListenToCanvas; + public readonly canvas: Canvas; public readonly root: Selection; private readonly backButton: IconButton; private readonly nextButton: IconButton; @@ -16,7 +16,7 @@ export class Navigation{ private currentServiceIndex = 0; private readonly legend_items: LegendItem[] = []; - constructor(canvas: ListenToCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'navigation') @@ -47,7 +47,7 @@ export class Navigation{ } private nextService(nav: Navigation){ - nav.currentServiceIndex = (nav.currentServiceIndex + 1) % nav.canvas.theme.carousel_service_order.length + nav.currentServiceIndex = (nav.currentServiceIndex + 1) % nav.canvas.visDirector.carousel_service_order.length nav.changeTheme() } @@ -72,10 +72,11 @@ export class Navigation{ } private changeTheme(){ - let nextService = this.canvas.theme.carousel_service_order[this.currentServiceIndex] - this.canvas.theme.set_current_theme(nextService); + let nextService = this.canvas.visDirector.carousel_service_order[this.currentServiceIndex] + this.canvas.visDirector.set_current_theme(nextService); this.canvas.renderCurrentTheme() - this.canvas.hatnoteVisServiceChangedSubject.next(this.canvas.theme.current_service_theme.id_name) + this.canvas.onThemeHasChanged.next( + [this.canvas.visDirector.current_service_theme.id_name, this.canvas.visDirector.current_visualisation]) } private clearLegendItems(){ @@ -96,7 +97,7 @@ export class Navigation{ public windowUpdate() { this.setPosition() - this.canvas.theme.current_service_theme.legend_items.forEach((theme_legend_item, i) => { + this.canvas.visDirector.current_service_theme.legend_items.forEach((theme_legend_item, i) => { if(i < this.legend_items.length) { this.legend_items[i].windowUpdate(theme_legend_item) } diff --git a/web/src/canvas/progress_indicator.ts b/web/src/canvas/progress_indicator.ts index 0b595ba..b4eea84 100644 --- a/web/src/canvas/progress_indicator.ts +++ b/web/src/canvas/progress_indicator.ts @@ -1,8 +1,7 @@ import { easeLinear, Selection, transition} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import {ServiceTheme} from "../theme/model"; import {HatnoteVisService} from "../service_event/model"; -import {GeoCanvas} from "./geo/geoCanvas"; +import {Canvas} from "./canvas"; export class ProgressIndicator{ public readonly service_indicators: Map; @@ -12,12 +11,12 @@ export class ProgressIndicator{ return this._currentServiceIndicator; } - private readonly canvas: ListenToCanvas | GeoCanvas + private readonly canvas: Canvas - constructor(canvas: ListenToCanvas | GeoCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.service_indicators = new Map() - canvas.theme.service_themes.forEach(service => { + canvas.visDirector.service_themes.forEach(service => { this.service_indicators.set(service.id_name, new ServiceProgressIndicator(canvas, service)) }); this._currentServiceIndicator = this.service_indicators.get(HatnoteVisService.Minerva); @@ -28,7 +27,7 @@ export class ProgressIndicator{ } windowUpdate() { - let progress_indicator_width = this.canvas.theme.progress_indicator_width(this.canvas.width); + let progress_indicator_width = this.canvas.visDirector.progress_indicator_width(this.canvas.width); this.service_indicators.forEach(indicator => { indicator.windowUpdate(progress_indicator_width) }) @@ -42,27 +41,27 @@ class ServiceProgressIndicator{ private readonly textBox: Selection private readonly text: Selection public readonly service_id: HatnoteVisService - private readonly canvas: ListenToCanvas | GeoCanvas + private readonly canvas: Canvas - constructor(canvas: ListenToCanvas | GeoCanvas, service_theme: ServiceTheme) { + constructor(canvas: Canvas, service_theme: ServiceTheme) { this.canvas = canvas; - let progress_indicator_width = canvas.theme.progress_indicator_width(canvas.width); - let pos_x = canvas.theme.progress_indicator_pos_x(service_theme.id_name, canvas.width, progress_indicator_width, canvas.theme.progress_indicator_gap_width); + let progress_indicator_width = canvas.visDirector.progress_indicator_width(canvas.width); + let pos_x = canvas.visDirector.progress_indicator_pos_x(service_theme.id_name, canvas.width, progress_indicator_width, canvas.visDirector.hatnoteTheme.progress_indicator_gap_width); let showIndicator = canvas.settings.carousel_mode this.root = canvas.appendSVGElement('g').attr('id', 'progress_' + service_theme.name) - .attr('transform', 'translate(' + pos_x + ', ' + (canvas.height - canvas.theme.progress_indicator_y_padding) + ')') + .attr('transform', 'translate(' + pos_x + ', ' + (canvas.height - canvas.visDirector.hatnoteTheme.progress_indicator_y_padding) + ')') .attr('opacity', showIndicator ? 1 : 0); this.bg = this.root.append('rect') .attr('width', progress_indicator_width) - .attr('height', canvas.theme.progress_indicator_height) - .attr('fill', canvas.theme.progress_indicator_bg_color) + .attr('height', canvas.visDirector.hatnoteTheme.progress_indicator_height) + .attr('fill', canvas.visDirector.hatnoteTheme.progress_indicator_bg_color) this.fg = this.root.append('rect') .attr('width', 0) - .attr('height', canvas.theme.progress_indicator_height) - .attr('fill', canvas.theme.progress_indicator_fg_color) + .attr('height', canvas.visDirector.hatnoteTheme.progress_indicator_height) + .attr('fill', canvas.visDirector.hatnoteTheme.progress_indicator_fg_color) this.textBox = this.root.append('rect') .attr('opacity', 0) @@ -71,7 +70,7 @@ class ServiceProgressIndicator{ .attr('height', '30') .attr('rx', 7) .attr('ry', 7) - .attr('fill', this.canvas.theme.progress_indicator_error_color) + .attr('fill', this.canvas.visDirector.hatnoteTheme.progress_indicator_error_color) this.text = this.root.append('text') .attr('opacity', 0) @@ -96,29 +95,29 @@ class ServiceProgressIndicator{ public start(onEnd: () => void){ const t = transition() - .duration(this.canvas.theme.current_service_theme.carousel_time) + .duration(this.canvas.visDirector.current_service_theme.carousel_time) .ease(easeLinear); this.showTextBox(false); - this.bg.attr('fill', this.canvas.theme.progress_indicator_bg_color) - let progress_indicator_width = this.canvas.theme.progress_indicator_width(this.canvas.width); + this.bg.attr('fill', this.canvas.visDirector.hatnoteTheme.progress_indicator_bg_color) + let progress_indicator_width = this.canvas.visDirector.progress_indicator_width(this.canvas.width); this.fg.transition(t).attr('width', progress_indicator_width).on('end', onEnd) } public reset() { - this.bg.attr('fill', this.canvas.theme.progress_indicator_bg_color) + this.bg.attr('fill', this.canvas.visDirector.hatnoteTheme.progress_indicator_bg_color) this.fg.attr('width', 0); } public setError(){ this.fg.interrupt(); - this.bg.attr('fill', this.canvas.theme.progress_indicator_error_color); + this.bg.attr('fill', this.canvas.visDirector.hatnoteTheme.progress_indicator_error_color); this.fg.attr('width', 0); this.showTextBox(true); } public windowUpdate(progress_indicator_width: number, ) { - let pos_x = this.canvas.theme.progress_indicator_pos_x(this.service_id, this.canvas.width, progress_indicator_width, this.canvas.theme.progress_indicator_gap_width); - this.root.attr('transform', 'translate(' + pos_x + ', ' + (this.canvas.height - this.canvas.theme.progress_indicator_y_padding) + ')'); + let pos_x = this.canvas.visDirector.progress_indicator_pos_x(this.service_id, this.canvas.width, progress_indicator_width, this.canvas.visDirector.hatnoteTheme.progress_indicator_gap_width); + this.root.attr('transform', 'translate(' + pos_x + ', ' + (this.canvas.height - this.canvas.visDirector.hatnoteTheme.progress_indicator_y_padding) + ')'); this.bg.attr('width', progress_indicator_width) } } \ No newline at end of file diff --git a/web/src/canvas/qr_code.ts b/web/src/canvas/qr_code.ts index 3f99ac1..a4551b0 100644 --- a/web/src/canvas/qr_code.ts +++ b/web/src/canvas/qr_code.ts @@ -1,7 +1,7 @@ import {Selection} from "d3"; import QrCodeMinerva from "../../assets/images/qr-code-minerva.png"; -import {ListenToCanvas} from "./listen/listenToCanvas"; -import {ServiceTheme} from "../theme/model"; +import {ServiceTheme, Visualisation} from "../theme/model"; +import {Canvas} from "./canvas"; export class QRCode{ private readonly root: Selection; @@ -12,9 +12,9 @@ export class QRCode{ private readonly image_width = 100; private readonly image_right_padding = 50; private readonly text_color = '#5d7da1'; - private readonly canvas: ListenToCanvas; + private readonly canvas: Canvas; - constructor(canvas: ListenToCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'qr_code') this.image = this.root.append('image') @@ -36,10 +36,12 @@ export class QRCode{ .attr('text-anchor', 'middle') this.setPosition() + this.setOpacity() } public windowUpdate(){ this.setPosition(); + this.setOpacity() } private setPosition(){ @@ -50,9 +52,18 @@ export class QRCode{ this.line2.attr('x', text_x).attr('dy', 16) } + private setOpacity(){ + if(!this.canvas.isMobileScreen) { + this.root.attr("opacity", 1) + } else { + this.root.attr("opacity", 0) + } + } + public themeUpdate(currentServiceTheme: ServiceTheme) { this.image.attr('href', currentServiceTheme.qr_code.image) this.line1.text(currentServiceTheme.qr_code.line1) this.line2.text(currentServiceTheme.qr_code.line2) + this.setOpacity() } } \ No newline at end of file diff --git a/web/src/canvas/transition.ts b/web/src/canvas/transition.ts index a00c8b7..98207fc 100644 --- a/web/src/canvas/transition.ts +++ b/web/src/canvas/transition.ts @@ -1,11 +1,10 @@ import {easeBackOut, easeCircleOut, easeCubicOut, easeExpOut, easeQuadOut, Selection} from "d3"; -import {ListenToCanvas} from "./listen/listenToCanvas"; import MpdlLogo from "../../assets/images/logo-mpdl-twocolor-dark-var1.png"; import {ServiceTheme} from "../theme/model"; import {HatnoteVisService} from "../service_event/model"; -import {GeoCanvas} from "./geo/geoCanvas"; import {Subject} from "rxjs"; import {NetworkInfoboxData} from "../observable/model"; +import {Canvas} from "./canvas"; export class Transition{ private readonly root: Selection; @@ -20,12 +19,12 @@ export class Transition{ private readonly mpdl_logo: Selection; private readonly text: Selection; private readonly service_logo: Selection; - private readonly canvas: ListenToCanvas | GeoCanvas; + private readonly canvas: Canvas; public readonly onTransitionStart: Subject public readonly onTransitionMid: Subject public readonly onTransitionEnd: Subject - constructor(canvas: ListenToCanvas | GeoCanvas) { + constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'transition_layer').attr('opacity', 0) diff --git a/web/src/configuration/hatnote_settings.ts b/web/src/configuration/hatnote_settings.ts index d869847..b284257 100644 --- a/web/src/configuration/hatnote_settings.ts +++ b/web/src/configuration/hatnote_settings.ts @@ -26,7 +26,8 @@ export class HatnoteSettings { circle_radius_max: window.innerHeight/2, circle_radius_min: 3, help: false, - map: false + map: false, + mixed: false } this.loadUrlParameters() @@ -47,6 +48,10 @@ export class HatnoteSettings { this._settings_data.map = true; } + if(url_search_parameters.has("mixed")){ + this._settings_data.mixed = true; + } + if(url_search_parameters.has("mute")){ this._settings_data.audio_mute = true; } @@ -170,6 +175,10 @@ export class HatnoteSettings { this._settings_data.initialService = HatnoteVisService.Minerva this._settings_data.carousel_mode = true; } + + if (this._settings_data.kiosk_mode){ + this._settings_data.mixed = true; + } } } @@ -191,5 +200,6 @@ export interface SettingsData{ circle_radius_max: number, circle_radius_min: number, help: boolean, - map: boolean + map: boolean, + mixed: boolean } \ No newline at end of file diff --git a/web/src/help/help_page.ts b/web/src/help/help_page.ts index e48de5a..4b8df18 100644 --- a/web/src/help/help_page.ts +++ b/web/src/help/help_page.ts @@ -13,6 +13,8 @@ export class HelpPage { root.append("p").html('Following url parameters are supported:') this.list = root.append("ul") this.createHelpListItem('mute', 'mutes sounds.', this.baseUrl+'?mute', `${settings.audio_mute}`) + this.createHelpListItem('map', 'shows the geographic visualisation.', this.baseUrl+'?map', `${settings.map}`) + this.createHelpListItem('mixed', 'if the carousel mode is activated and map mode deactivated this flag will cause the carousel mix visualisation types.', this.baseUrl+'?mixed', `${settings.mixed}`) this.createHelpListItem('service=${option}', 'displays given service. Disables carousel. Options: keeper, minerva, bloxberg.', this.baseUrl+'?service=bloxberg', `carousel mode enabled`) this.createHelpListItem('carousel-time=${minerva},${keeper},${bloxberg}', 'modifies the carousel display time for individual services. All values must be given and are read in milliseconds.', this.baseUrl+'?carousel-time=20000,10000,14000', `${settings.carousel_time[0]},${settings.carousel_time[1]},${settings.carousel_time[2]}`) this.createHelpListItem('audio-protection=${time}', 'timeframe in which successive events will be muted. Time is read in milliseconds.', this.baseUrl+'?audio-protection=200', `${settings.audioProtection}`) @@ -24,6 +26,8 @@ export class HelpPage { this.createHelpListItem('debug', 'enables debug mode that will generate output in the javascript console.', this.baseUrl+'?debug', `${settings.debug_mode}`) this.createHelpListItem('help', 'shows help page.', ` Example: ${this.baseUrl}?help`, `${settings.help}`) root.append("p").html(`Url parameters can be combined with "&". Order does not matter. Example: ${this.baseUrl}?mute&service=bloxberg`) + root.append("h2").html('GIS (Geographic Information System) for hatnote') + root.append("p").html(`Geographic information for hatnote can be modified at http://gis.hatnote.mpdl.mpg.de`) document.body.setAttribute("style", "overflow: auto") } diff --git a/web/src/main.ts b/web/src/main.ts index dc93133..bc27376 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,15 +1,15 @@ import {BehaviorSubject, Subject} from "rxjs"; import {BannerData, CircleData, DatabaseInfo, NetworkInfoboxData} from "./observable/model"; -import {ListenToCanvas} from "./canvas/listen/listenToCanvas"; import {HatnoteAudio} from "./audio/hatnote_audio"; import {HatnoteSettings} from "./configuration/hatnote_settings"; -import {Theme} from "./theme/theme"; +import {VisualisationDirector} from "./theme/visualisationDirector"; import {EventBridge} from "./service_event/event_bridge"; import {WebsocketManager} from "./websocket/websocket"; import {HatnoteVisService} from "./service_event/model"; import {HelpPage} from "./help/help_page"; -import {GeoCanvas} from "./canvas/geo/geoCanvas"; import {select} from "d3"; +import {Canvas} from "./canvas/canvas"; +import {Visualisation} from "./theme/model"; main(); @@ -27,35 +27,28 @@ function main(){ } // load theme - let theme = new Theme(settings_data); + let theme = new VisualisationDirector(settings_data); // create observables let newCircleSubject: Subject = new Subject() let newBannerSubject: Subject = new Subject() - let hatnoteVisServiceChangedSubject: BehaviorSubject = new BehaviorSubject(theme.current_service_theme.id_name) - let showAudioInfoboxSubject: Subject = new Subject() + let onCarouselTransitionStart: BehaviorSubject<[HatnoteVisService, Visualisation]> = new BehaviorSubject([theme.current_service_theme.id_name, theme.current_visualisation]) + let onCarouselTransitionMid: BehaviorSubject<[HatnoteVisService, Visualisation]> = new BehaviorSubject([theme.current_service_theme.id_name, theme.current_visualisation]) + let onCarouselTransitionEnd: BehaviorSubject<[HatnoteVisService, Visualisation]> = new BehaviorSubject([theme.current_service_theme.id_name, theme.current_visualisation]) let showWebsocketInfoboxSubject: Subject = new Subject() let updateDatabaseInfoSubject: Subject = new Subject() let updateVersionSubject: Subject<[string,number]> = new Subject() // build canvas - if (settings_data.map) { - new GeoCanvas(theme, settings_data, newCircleSubject, - showWebsocketInfoboxSubject, updateVersionSubject, hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject, select(appContainer)) - } else { - new ListenToCanvas(theme, settings_data, newCircleSubject, newBannerSubject, - showAudioInfoboxSubject, showWebsocketInfoboxSubject, updateVersionSubject, hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject,select(appContainer)) - } + new Canvas(theme, settings_data, newCircleSubject, + showWebsocketInfoboxSubject, updateVersionSubject, onCarouselTransitionStart, onCarouselTransitionMid,onCarouselTransitionEnd, updateDatabaseInfoSubject, newBannerSubject, select(appContainer)) // load audio - let audio; - if (!settings_data.map) { - audio = new HatnoteAudio(settings_data, showAudioInfoboxSubject); - } + let audio = new HatnoteAudio(settings_data); // init event bridge let event_bridge = new EventBridge(audio, newCircleSubject, newBannerSubject, updateVersionSubject, - hatnoteVisServiceChangedSubject, settings_data) + onCarouselTransitionStart, onCarouselTransitionMid,onCarouselTransitionEnd, settings_data) // start websocket new WebsocketManager(settings_data, showWebsocketInfoboxSubject, updateDatabaseInfoSubject, event_bridge); diff --git a/web/src/service_event/event_bridge.ts b/web/src/service_event/event_bridge.ts index 85a2641..12529f5 100644 --- a/web/src/service_event/event_bridge.ts +++ b/web/src/service_event/event_bridge.ts @@ -8,12 +8,14 @@ import { BloxbergTransformedData, DelayedCircleEvent, HatnoteVisService, - KeeperTransformedData, MinervaTransformedData + KeeperTransformedData, + MinervaTransformedData } from "./model"; import {EventBuffer} from "./event_buffer"; import {SettingsData} from "../configuration/hatnote_settings"; import {KeeperTransformer} from "./keeper_transformer"; import {MinervaTransformer} from "./minerva_transformer"; +import {Visualisation} from "../theme/model"; export class EventBridge{ private bloxbergTransformer: BloxbergTransformer; @@ -22,38 +24,53 @@ export class EventBridge{ private audio: HatnoteAudio | undefined; private newCircleSubject: Subject; private newBannerSubject: Subject; - private hatnoteVisServiceChangedSubject: BehaviorSubject + private onCarouselTransitionStart: BehaviorSubject<[HatnoteVisService, Visualisation]> + private onThemeHasChanged: BehaviorSubject<[HatnoteVisService, Visualisation]> private eventBuffer: EventBuffer; private updateVersionSubject: Subject<[string,number]> private _currentService: HatnoteVisService + private _currentVisualisation: Visualisation private settings_data: SettingsData private readonly event_delay_protection: number public get currentService(): HatnoteVisService{ return this._currentService; } + + public get currentVisualisation(): Visualisation{ + return this._currentVisualisation; + } constructor(audio: HatnoteAudio | undefined, newCircleSubject: Subject, newBanenrSubject: Subject, - updateVersionSubject: Subject<[string,number]>, hatnoteVisServiceChangedSubject: BehaviorSubject, + updateVersionSubject: Subject<[string,number]>, + onCarouselTransitionStart: BehaviorSubject<[HatnoteVisService, Visualisation]>, + onCarouselTransitionMid: BehaviorSubject<[HatnoteVisService, Visualisation]>, + onCarouselTransitionEnd: BehaviorSubject<[HatnoteVisService, Visualisation]>, settings_data: SettingsData) { this.settings_data = settings_data this.event_delay_protection = settings_data.event_delay_protection this._currentService = settings_data.initialService + this._currentVisualisation = settings_data.map ? Visualisation.geo : Visualisation.listenTo this.updateVersionSubject = updateVersionSubject this.newBannerSubject = newBanenrSubject - this.hatnoteVisServiceChangedSubject = hatnoteVisServiceChangedSubject + this.onCarouselTransitionStart = onCarouselTransitionStart + this.onThemeHasChanged = onCarouselTransitionMid this.minervaTransformer = new MinervaTransformer() this.bloxbergTransformer = new BloxbergTransformer(settings_data) this.keeperTransformer = new KeeperTransformer(settings_data) this.audio = audio this.newCircleSubject = newCircleSubject this.eventBuffer = new EventBuffer(this.settings_data.default_event_buffer_timespan, - (value) => this.publishCircleEvent(value), this.settings_data.map) + (value) => this.publishCircleEvent(value), this) - this.hatnoteVisServiceChangedSubject.subscribe({ + this.onCarouselTransitionStart.subscribe({ next: (value) => { - this._currentService = value - if (settings_data.carousel_mode) { - this.audio?.play_transition_sound() - } + this.audio?.play_transition_sound() + } + }) + + this.onThemeHasChanged.subscribe({ + next: (value) => { + this._currentService = value[0] + this._currentVisualisation = value[1] } }) } @@ -176,7 +193,9 @@ export class EventBridge{ // browser stops animations to save energy and to increase performance when a tab is inactive if (!document.hidden){ for (const delayedCircleEvent of circleEvents) { - this.audio?.play_sound(delayedCircleEvent.radius, delayedCircleEvent.event) + if(this._currentVisualisation === Visualisation.listenTo){ + this.audio?.play_sound(delayedCircleEvent.radius, delayedCircleEvent.event) + } this.newCircleSubject.next({label_text: delayedCircleEvent.title, circle_radius: delayedCircleEvent.radius, type: delayedCircleEvent.event, location: delayedCircleEvent.location}) @@ -186,7 +205,9 @@ export class EventBridge{ public publishBannerEvent(bannerEvent: BannerEvent){ if (!document.hidden){ - this.audio?.play_sound(0, bannerEvent.event) + if(this._currentVisualisation === Visualisation.listenTo) { + this.audio?.play_sound(0, bannerEvent.event) + } this.newBannerSubject.next({message: bannerEvent.title, serviceEvent: bannerEvent.event}) } } diff --git a/web/src/service_event/event_buffer.ts b/web/src/service_event/event_buffer.ts index 9f8c546..6c75a8e 100644 --- a/web/src/service_event/event_buffer.ts +++ b/web/src/service_event/event_buffer.ts @@ -1,22 +1,24 @@ import {EventBufferData} from "./event_buffer_data"; import {DelayedCircleEvent, ServiceEvent} from "./model"; import {getRandomIntInclusive} from "../util/random"; +import {EventBridge} from "./event_bridge"; +import {Visualisation} from "../theme/model"; export class EventBuffer { private readonly eventBuffer: Map; - private readonly hatnote_map: boolean; + public readonly eventBridge: EventBridge; - constructor(default_event_buffer_timespan: number, publishCircleEvent: (circleEvent: DelayedCircleEvent[]) => void, hatnote_map: boolean) { - this.hatnote_map = hatnote_map; + constructor(default_event_buffer_timespan: number, publishCircleEvent: (circleEvent: DelayedCircleEvent[]) => void, eventBridge: EventBridge) { + this.eventBridge = eventBridge; this.eventBuffer = new Map([ - [ServiceEvent.bloxberg_block, new EventBufferData(publishCircleEvent, hatnote_map, default_event_buffer_timespan)], - [ServiceEvent.bloxberg_confirmed_transaction, new EventBufferData(publishCircleEvent, hatnote_map, default_event_buffer_timespan, hatnote_map ? 1000 : 1200)], - [ServiceEvent.keeper_file_create, new EventBufferData(publishCircleEvent, hatnote_map, 1000,1000)], // Keeper only returns time precision of seconds - [ServiceEvent.keeper_file_edit, new EventBufferData(publishCircleEvent, hatnote_map, 1000,1000)], // Keeper only returns time precision of seconds - [ServiceEvent.keeper_new_library, new EventBufferData(publishCircleEvent, hatnote_map, 1000,1000)], // Keeper only returns time precision of seconds - [ServiceEvent.minerva_direct_message, new EventBufferData(publishCircleEvent, hatnote_map, default_event_buffer_timespan)], - [ServiceEvent.minerva_private_message, new EventBufferData(publishCircleEvent, hatnote_map, default_event_buffer_timespan)], - [ServiceEvent.minerva_public_message, new EventBufferData(publishCircleEvent, hatnote_map, default_event_buffer_timespan)], + [ServiceEvent.bloxberg_block, new EventBufferData(publishCircleEvent, this, default_event_buffer_timespan)], + [ServiceEvent.bloxberg_confirmed_transaction, new EventBufferData(publishCircleEvent, this, default_event_buffer_timespan,1200)], + [ServiceEvent.keeper_file_create, new EventBufferData(publishCircleEvent, this, 1000,1000)], // Keeper only returns time precision of seconds + [ServiceEvent.keeper_file_edit, new EventBufferData(publishCircleEvent, this, 1000,1000)], // Keeper only returns time precision of seconds + [ServiceEvent.keeper_new_library, new EventBufferData(publishCircleEvent, this, 1000,1000)], // Keeper only returns time precision of seconds + [ServiceEvent.minerva_direct_message, new EventBufferData(publishCircleEvent, this, default_event_buffer_timespan)], + [ServiceEvent.minerva_private_message, new EventBufferData(publishCircleEvent, this, default_event_buffer_timespan)], + [ServiceEvent.minerva_public_message, new EventBufferData(publishCircleEvent, this, default_event_buffer_timespan)], ]); } @@ -37,7 +39,7 @@ export class EventBuffer { switch (circleEvent) { case ServiceEvent.keeper_file_create: case ServiceEvent.keeper_file_edit: - if (!that.hatnote_map) { + if (that.eventBridge.currentVisualisation !== Visualisation.geo) { let splitRandom = getRandomIntInclusive(1,4) eventBufferData?.splitBufferAndRelease(splitRandom) } else { @@ -45,7 +47,7 @@ export class EventBuffer { } break; case ServiceEvent.bloxberg_confirmed_transaction: - if (!that.hatnote_map) { + if (that.eventBridge.currentVisualisation !== Visualisation.geo) { let splitRandomBloxberg = 3 eventBufferData?.splitBufferAndRelease(splitRandomBloxberg) } else { diff --git a/web/src/service_event/event_buffer_data.ts b/web/src/service_event/event_buffer_data.ts index 29e51ee..6795bf9 100644 --- a/web/src/service_event/event_buffer_data.ts +++ b/web/src/service_event/event_buffer_data.ts @@ -1,19 +1,21 @@ import {DelayedCircleEvent, ServiceEvent} from "./model"; import {getRandomIntInclusive} from "../util/random"; +import {EventBuffer} from "./event_buffer"; +import {Visualisation} from "../theme/model"; export class EventBufferData { private eventCirclesMap: Map private eventCirclesArray: DelayedCircleEvent[] - private readonly hatnote_map: boolean; + private readonly eventBuffer: EventBuffer; private radii: number[] public circleGroupCatchTimespan; // in ms public bufferSplitDelayTimespan; // in ms private readonly publishCircleEvent : (circleEvent: DelayedCircleEvent[]) => void - constructor(publishCircleEvent: (circleEvent: DelayedCircleEvent[]) => void, hatnote_map: boolean, circleGroupCatchTimespan: number, bufferSplitDelayTimespan: number = 1000) { + constructor(publishCircleEvent: (circleEvent: DelayedCircleEvent[]) => void, eventBuffer: EventBuffer, circleGroupCatchTimespan: number, bufferSplitDelayTimespan: number = 1000) { this.circleGroupCatchTimespan = circleGroupCatchTimespan; this.bufferSplitDelayTimespan = bufferSplitDelayTimespan - this.hatnote_map = hatnote_map; + this.eventBuffer = eventBuffer; this.eventCirclesMap = new Map; this.eventCirclesArray = []; this.radii = []; @@ -103,7 +105,7 @@ export class EventBufferData { let thisBufferData = this setTimeout(function(){ - if(thisBufferData.hatnote_map){ + if(thisBufferData.eventBuffer.eventBridge.currentVisualisation === Visualisation.geo){ for (let [key, circles] of thisBufferData.eventCirclesMap) { if(circles[0].location !== undefined){ thisBufferData.publishCircleEvent([{ @@ -139,7 +141,7 @@ export class EventBufferData { for (let i = 0; i < this.eventCirclesArray.length; i += chunkSizeInt) { const chunk = this.eventCirclesArray.slice(i, i + chunkSizeInt); let chunkBufferData = new EventBufferData( - (circleEvent) => this.publishCircleEvent(circleEvent),this.hatnote_map, + (circleEvent) => this.publishCircleEvent(circleEvent),this.eventBuffer, this.circleGroupCatchTimespan, this.bufferSplitDelayTimespan ) chunkBufferData.addCircleEvents(chunk) diff --git a/web/src/theme/hatnote.ts b/web/src/theme/hatnote.ts new file mode 100644 index 0000000..fe17f81 --- /dev/null +++ b/web/src/theme/hatnote.ts @@ -0,0 +1,17 @@ +import {HatnoteTheme} from "./model"; + +export const hatnote_theme: HatnoteTheme = { + svg_background_color: '#1c2733', + header_bg_color: '#fff', + header_height: 45, + header_version_update_bg: '#b62121', + header_text_color: '#000', + legend_item_circle_r: 17, + progress_indicator_bg_color: '#fff', + progress_indicator_fg_color: 'rgb(41, 128, 185)', + progress_indicator_error_color: '#b62121', + progress_indicator_height: 7, + progress_indicator_gap_width: 10, + progress_indicator_y_padding: 20, + circle_wave_color: '#fff' +} \ No newline at end of file diff --git a/web/src/theme/model.ts b/web/src/theme/model.ts index fbb61e0..6767a06 100644 --- a/web/src/theme/model.ts +++ b/web/src/theme/model.ts @@ -1,5 +1,21 @@ import {HatnoteVisService, ServiceEvent} from "../service_event/model"; +export interface HatnoteTheme { + svg_background_color: string, + header_bg_color: string, + header_height: number, + header_version_update_bg: string, + header_text_color: string, + legend_item_circle_r: number, + progress_indicator_bg_color: string, + progress_indicator_fg_color: string, + progress_indicator_error_color: string, + progress_indicator_height: number, + progress_indicator_gap_width: number, + progress_indicator_y_padding: number, + circle_wave_color: string +} + export interface ServiceTheme { name: string, id_name: HatnoteVisService, @@ -29,4 +45,9 @@ export interface ThemeLegendItem { smallTitle1?: string, smallTitle2?: string, event: ServiceEvent +} + +export enum Visualisation { + listenTo, + geo } \ No newline at end of file diff --git a/web/src/theme/theme.ts b/web/src/theme/visualisationDirector.ts similarity index 81% rename from web/src/theme/theme.ts rename to web/src/theme/visualisationDirector.ts index 920f22b..fc27f8d 100644 --- a/web/src/theme/theme.ts +++ b/web/src/theme/visualisationDirector.ts @@ -1,40 +1,40 @@ -import {ServiceTheme} from "./model"; +import {HatnoteTheme, ServiceTheme, Visualisation} from "./model"; import {minerva_service_theme} from "./minerva"; import {SettingsData} from "../configuration/hatnote_settings"; import {HatnoteVisService, ServiceEvent} from "../service_event/model"; import {keeper_service_theme} from "./keeper"; import {bloxberg_service_theme} from "./bloxberg"; +import {hatnote_theme} from "./hatnote"; +import {getRandomIntInclusive} from "../util/random"; -export class Theme { - readonly svg_background_color: string = '#1c2733' - readonly header_bg_color: string = '#fff' - readonly header_height: number = 45 - readonly header_version_update_bg = '#b62121' - readonly header_text_color: string = '#000' - readonly legend_item_circle_r: number = 17 - readonly progress_indicator_bg_color = '#fff' - readonly progress_indicator_fg_color = 'rgb(41, 128, 185)' - readonly progress_indicator_error_color = '#b62121' - readonly progress_indicator_height = 7 - readonly progress_indicator_gap_width = 10 - readonly progress_indicator_y_padding = 20 - readonly circle_wave_color= '#fff' +export class VisualisationDirector { service_themes: Map = new Map() carousel_service_order: ServiceTheme[] current_service_theme: ServiceTheme; readonly minervaTheme: ServiceTheme; readonly keeperTheme: ServiceTheme; readonly bloxbergTheme: ServiceTheme; + readonly hatnoteTheme: HatnoteTheme; + current_visualisation: Visualisation; + private settings_data: SettingsData; constructor(settings_data: SettingsData) { + this.settings_data = settings_data this.minervaTheme = minerva_service_theme this.keeperTheme = keeper_service_theme this.bloxbergTheme = bloxberg_service_theme + this.hatnoteTheme = hatnote_theme this.minervaTheme.carousel_time = settings_data.carousel_time[0] this.keeperTheme.carousel_time = settings_data.carousel_time[1] this.bloxbergTheme.carousel_time = settings_data.carousel_time[2] + if(settings_data.map) { + this.current_visualisation = Visualisation.geo + } else { + this.current_visualisation = Visualisation.listenTo + } + this.service_themes.set(HatnoteVisService.Minerva, this.minervaTheme) this.service_themes.set(HatnoteVisService.Keeper, this.keeperTheme) this.service_themes.set(HatnoteVisService.Bloxberg, this.bloxbergTheme) @@ -44,6 +44,22 @@ export class Theme { this.current_service_theme = this.service_themes.get(settings_data.initialService) ?? this.minervaTheme } + getNextVisualisation(): Visualisation{ + let vis: Visualisation = this.current_visualisation + if(this.settings_data.mixed) { + if(vis === Visualisation.listenTo){ + vis = Visualisation.geo + } else { + vis = Visualisation.listenTo + } + } + return vis + } + + setCurrentVisualisation(vis: Visualisation){ + this.current_visualisation = vis; + } + set_current_theme(serviceTheme: ServiceTheme){ this.current_service_theme = serviceTheme; }