diff --git a/web/src/canvas/header.ts b/web/src/canvas/header.ts deleted file mode 100644 index 78726f1..0000000 --- a/web/src/canvas/header.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {Selection} from "d3"; -import MinervaLogo from "../../assets/images/minervamessenger-banner-kussmund+bulb.png"; -import {LegendItem} from "./legend_item"; -import {ServiceTheme, Visualisation} from "../theme/model"; -import {environmentVariables} from "../configuration/environment"; -import {Canvas} from "./canvas"; - -export class Header{ - public readonly canvas: Canvas; - private readonly root: Selection - private readonly background_rect: Selection - private readonly title: Selection - private readonly title0: Selection - private readonly logo: Selection - private readonly updateBox: Selection - private readonly updateText: Selection - private readonly legend_items: LegendItem[] = []; - - constructor(canvas: Canvas) { - this.canvas = canvas; - - this.root = canvas.appendSVGElement('g') - .attr('id', 'header') - .attr('transform', 'translate(0, 0)') - .style('opacity', 1.0) - - this.background_rect = this.root.append('rect') - .attr('width', canvas.width) - .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) - .attr('href', MinervaLogo) - .attr('width', 44) - - this.title = this.root.append('text') - .text('Hatnote title') - .attr('font-family', 'HatnoteVisBold') - .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(this.canvas.visDirector.current_visualisation === Visualisation.listenTo ? 'Listen to' : 'Locate') - .attr('font-family', 'HatnoteVisNormal') - .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) - .attr('transform', `translate(${canvas.width/2 - 95}, 8)`) - .attr('width', '190') - .attr('height', '30') - .attr('rx', 7) - .attr('ry', 7) - .attr('fill', this.canvas.visDirector.hatnoteTheme.header_version_update_bg) - - this.updateText = this.root.append('text') - .attr('opacity', 0) - .attr('text-anchor', 'middle') - .attr('transform', `translate(${canvas.width/2}, 28)`) - .text('New update available') - .attr('font-family', 'HatnoteVisBold') - .attr('font-size', '16px') - .attr('fill', '#fff') - - if(!this.canvas.isMobileScreen){ - for (let i = 0; i < 3; i++) { - this.legend_items.push(new LegendItem(this, undefined, this.canvas)) - } - } - - canvas.updateVersionSubject.subscribe({ - next: (versions) => this.updateVersionNumbers(versions) - }) - } - - private updateVersionNumbers(versions: [string, number]){ - let expectedFrontendVersion = versions[1] - let browserFrontendVersion: number = Number(environmentVariables.version); - if (!isNaN(browserFrontendVersion) && browserFrontendVersion < expectedFrontendVersion) { - this.updateBox.attr('opacity', 1) - this.updateText.attr('opacity', 1) - } else { - this.updateBox.attr('opacity', 0) - this.updateText.attr('opacity', 0) - } - } - - public appendSVGElement(type: string): Selection { - return this.root.append(type) - } - - private clearLegendItems(){ - this.legend_items.forEach((theme_legend_item, i) => { - this.legend_items[i].hide() - }); - } - - public themeUpdate(currentServiceTheme: ServiceTheme) { - this.logo.attr('href', currentServiceTheme.header_logo) - 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() - currentServiceTheme.legend_items.forEach((theme_legend_item, i) => { - if(i < this.legend_items.length) { - this.legend_items[i].themeUpdate(theme_legend_item) - } - }); - } - - public windowUpdate() { - this.background_rect.attr("width", this.canvas.width); - - this.updateBox.attr('transform', `translate(${this.canvas.width/2 - 95}, 8)`) - this.updateText.attr('transform', `translate(${this.canvas.width/2}, 28)`) - - 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) - } - }); - } -} \ No newline at end of file diff --git a/web/src/canvas/info_box.ts b/web/src/canvas/info_box.ts deleted file mode 100644 index d702938..0000000 --- a/web/src/canvas/info_box.ts +++ /dev/null @@ -1,193 +0,0 @@ -import {Selection} from "d3"; -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 {Canvas} from "./canvas"; - -export class InfoBox{ - private readonly root: Selection - private readonly background_rect: Selection - private readonly image: Selection | undefined - private readonly title: Selection - private readonly text: Selection - private readonly line1: Selection[] - private readonly line2: Selection[] - private readonly line3: Selection[] - private readonly line4: Selection[] - private readonly line5: Selection[] - private readonly line5link: Selection | undefined - private readonly loadingSpinner: Selection | undefined - private readonly infobox_width = 600 - public readonly infobox_height = 190 - private readonly infobox_image_width = 200 - private readonly infobox_image_left_padding = 15 - private readonly infobox_image_top_padding = 20 - private readonly canvas: Canvas - private readonly isMobileScreen : boolean - private readonly carousel : Carousel | undefined - private currentType: InfoboxType - - - constructor(canvas: Canvas, type: InfoboxType) { - this.canvas = canvas - this.isMobileScreen = this.canvas.isMobileScreen - this.carousel = this.canvas.carousel - this.currentType = type - this.root = canvas.appendSVGElement('g') - .attr('opacity', 0) - .attr('id', 'infobox_' + type.toString()) - this.background_rect = this.root.append('rect') - .attr('width', this.isMobileScreen ? this.canvas.width - 40 : this.infobox_width) - .attr('height', this.infobox_height) - .attr('stroke', 'rgb(41, 128, 185)') - .attr('stroke-width', '7') - .attr('rx', 20) - .attr('ry', 20) - .attr('fill', '#fff') - if(!this.isMobileScreen) { - this.image = this.root.append('image') - .attr('href', InfoboxAudioImg) - .attr('width', this.infobox_image_width) - } - this.title = this.root.append('text') - .text('') - .attr('font-family', 'HatnoteVisBold') - .attr('font-size', '26px') - .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.visDirector.hatnoteTheme.header_text_color) - let line1 = this.text.append('tspan') - let line12 = this.text.append('tspan') - let line2 = this.text.append('tspan') - let line22 = this.text.append('tspan') - let line3 = this.text.append('tspan') - let line32 = this.text.append('tspan') - let line4 = this.text.append('tspan') - let line42 = this.text.append('tspan') - - let line5link; - let line5 = this.text.append('tspan') - let line52 = this.text.append('tspan') - - this.line1 = [line1,line12] - this.line2 = [line2,line22] - this.line3 = [line3,line32] - this.line4 = [line4,line42] - this.line5 = [line5,line52] - - switch (type) { - case InfoboxType.network_websocket_connecting: - case InfoboxType.network_database_can_not_connect: - case InfoboxType.network_database_connecting: - let loadingSpinner = this.root.append('image') - .attr('href', LoadingSpinner) - .attr('width', 25) - - this.line5link = undefined - this.loadingSpinner = loadingSpinner - canvas.showNetworkInfoboxObservable.subscribe({ - next: (value) => this.show(value.infoboxType, value.show, value) - }) - break; - } - - this.setPosition() - } - - public appendSVGElement(type: string): Selection { - return this.root.append(type) - } - - public show(type: InfoboxType, show: boolean, network_infobox_data?: NetworkInfoboxData) { - this.currentType = type - - if(type === InfoboxType.network_websocket_connecting){ - this.root.attr('opacity', show ? 1 : 0) - this.image?.attr('href', InfoboxWebsocketConnectingImg) - this.title.text('Connecting to server') - this.line1[0].text('Your browser is connecting to the server.') - this.line2[0].text(' ') - this.line3[0].text('Url: ').attr('font-family', 'HatnoteVisBold') - this.line3[1].text('' + network_infobox_data?.target_url) - this.line4[0].text('Number of reconnects: ').attr('font-family', 'HatnoteVisBold') - this.line4[1].text('' + network_infobox_data?.number_of_reconnects) - this.loadingSpinner?.attr('opacity', 1) - } - - // 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.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.visDirector.current_service_theme.name + ' database.') - this.line2[0].text(' ') - this.line3[0].text(' ') - this.line3[1].text(' ') - this.line4[0].text(' ') - this.line4[1].text(' ') - this.loadingSpinner?.attr('opacity', 1) - } - - 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.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 ?? '') - this.line4[0].text('Number of reconnects: ').attr('font-family', 'HatnoteVisBold') - this.line4[1].text('' + network_infobox_data?.number_of_reconnects) - this.loadingSpinner?.attr('opacity', 0) - } - - if(type=== InfoboxType.legend) { - this.root.attr('opacity', show ? 1 : 0) - } - - this.setPosition() - } - - public windowUpdate(){ - this.setPosition() - } - - private setPosition(){ - this.background_rect.attr('x', this.isMobileScreen ? 20 : this.canvas.width/2 - this.infobox_width/2) - .attr('y', this.canvas.height/2 - this.infobox_height/2) - this.image?.attr('x', this.canvas.width/2 - this.infobox_width/2 + this.infobox_image_left_padding).attr('y', this.canvas.height/2 - this.infobox_height/2 + this.infobox_image_top_padding) - let text_x = this.isMobileScreen ? 40 : this.canvas.width/2 - this.infobox_width/2 + this.infobox_image_width + this.infobox_image_left_padding + 20 - this.title.attr('x', text_x).attr('y', this.canvas.height/2 - this.infobox_height/2 + this.infobox_image_top_padding + 25) - this.text.attr('x', text_x).attr('y', this.canvas.height/2 - this.infobox_height/2 + 80) - this.line2[0].attr('x', text_x).attr('dy', '20px') - this.line3[0].attr('x', text_x).attr('dy', '20px') - this.line4[0].attr('x', text_x).attr('dy', '20px') - this.line5[0].attr('x', text_x).attr('dy', '20px') - - switch (this.currentType) { - case InfoboxType.network_websocket_connecting: - this.loadingSpinner?.attr('x', this.isMobileScreen ? this.canvas.width/2 + 60 : this.canvas.width/2 + 180).attr('y', this.canvas.height/2 + 15) - break; - case InfoboxType.network_database_can_not_connect: - this.loadingSpinner?.attr('x', text_x + 100).attr('y', this.canvas.height/2) - break; - case InfoboxType.network_database_connecting: - this.loadingSpinner?.attr('x', this.canvas.width/2 + 40).attr('y', this.canvas.height/2 + 10) - break; - } - } -} - -export enum InfoboxType { - network_websocket_connecting, - network_database_connecting, - network_database_can_not_connect, - legend -} \ No newline at end of file diff --git a/web/src/canvas/legend_item.ts b/web/src/canvas/legend_item.ts deleted file mode 100644 index 1317dc7..0000000 --- a/web/src/canvas/legend_item.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {Selection} from "d3"; -import {Header} from "./header"; -import {ThemeLegendItem} from "../theme/model"; -import {InfoBox} from "./info_box"; -import {VisualisationDirector} from "../theme/visualisationDirector"; -import {Canvas} from "./canvas"; - -export class LegendItem{ - private readonly root: Selection | undefined; - private readonly title: Selection | undefined; - private readonly smallTitle1: Selection | undefined; - private readonly smallTitle2: Selection | undefined; - private readonly circle: Selection | undefined; - private readonly header: Header | undefined; - private readonly legendInfoBox: InfoBox | undefined; - private readonly canvas: Canvas; - 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.visDirector - - if(this.header) { - this.root = this.header?.appendSVGElement('g') - .attr('id', 'legend_items') - .attr('opacity', 0) - } else { - this.root = this.legendInfoBox?.appendSVGElement('g') - .attr('id', 'legend_items') - .attr('opacity', 0) - } - - - this.title = this.root?.append('text') - .text('legend item') - .attr('font-family', 'HatnoteVisNormal') - .attr('font-size', '26px') - .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.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.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.hatnoteTheme.legend_item_circle_r) - .attr('cx', 0) - .attr('cy', this.theme.hatnoteTheme.header_height/2) - .attr('fill', '#000') // default value - } - - public hide(){ - this.root?.attr('opacity', 0) - } - - public themeUpdate(theme_legend_item: ThemeLegendItem) { - this.root?.attr('opacity', 1) - this.updatePos(theme_legend_item) - if(theme_legend_item.title === undefined){ - this.smallTitle1?.text(theme_legend_item.smallTitle1 ?? 'not defined') - this.smallTitle2?.text(theme_legend_item.smallTitle2 ?? 'not defined') - this.title?.attr('opacity', 0) - this.smallTitle1?.attr('opacity', 1) - this.smallTitle2?.attr('opacity', 1) - } else { - this.title?.text(theme_legend_item.title) - this.title?.attr('opacity', 1) - this.smallTitle1?.attr('opacity', 0) - this.smallTitle2?.attr('opacity', 0) - } - this.circle?.attr('fill', this.theme.getThemeColor(theme_legend_item.event)) - } - - public windowUpdate(theme_legend_item: ThemeLegendItem) { - this.updatePos(theme_legend_item) - } - - private updatePos(theme_legend_item: ThemeLegendItem){ - if(this.header) { - let pos_x = theme_legend_item.position_x ?? theme_legend_item.position_x_small_title ?? 100 - this.root?.attr('transform', - 'translate(' + (this.header.canvas.width - pos_x) + ', ' + 0 + ')') - } - - if(this.legendInfoBox){ - let pos_y = theme_legend_item.position_y_info_box ?? 100 - this.root?.attr('transform', - 'translate(' + 60 + ', ' + (this.canvas.height/2 - this.legendInfoBox.infobox_height/2 + 40 + pos_y) + ')') - } - } -} \ No newline at end of file diff --git a/web/src/canvas/mute_icon.ts b/web/src/canvas/mute_icon.ts deleted file mode 100644 index 7f45f88..0000000 --- a/web/src/canvas/mute_icon.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {Selection} from "d3"; -import QrCodeMinerva from "../../assets/images/volume_off_FILL1_wght400_GRAD0_opsz24.svg"; -import {Canvas} from "./canvas"; - -export class MuteIcon{ - private readonly root: Selection; - private readonly image: Selection; - private readonly text: Selection; - private readonly line1: Selection; - private readonly background: Selection; - private readonly image_width = 100; - private readonly text_color = '#fff'; - private readonly canvas: Canvas; - - // consists not only of the icon but also spans a transparent clickable container above everything - constructor(canvas: Canvas) { - this.canvas = canvas - this.root = canvas.appendSVGElement('g') - .attr('id', 'qr_code') - .attr('opacity', 0) - .attr('cursor', 'auto') - .attr('pointer-events','none') - .attr('id', 'mute_icon') - this.image = this.root.append('image') - .attr('href', QrCodeMinerva) - .attr('width', this.image_width) - this.text = this.root.append('text') - .attr('text-anchor', 'middle') - this.line1 = this.text.append('tspan') - .text('line1') - .attr('font-family', 'HatnoteVisNormal') - .attr('font-size', '12px') - .attr('fill', this.text_color) - .attr('text-anchor', 'middle') - this.line1.text('Click here to unmute') - - // needs to be added last so the click on this layer is not blocked by other elements - this.background = this.root.append('rect') - .attr('opacity', 0) - .attr('width', this.canvas.width) - .attr('height', this.canvas.height) - .attr('id', 'mute_icon_background') - - this.setPosition() - - document.getElementById('mute_icon_background')?.addEventListener("click", (_) => { - this.hide(); - }); - } - - public windowUpdate(){ - this.setPosition(); - this.background.attr('width', this.canvas.width) - this.background.attr('height', this.canvas.height) - } - - public show(){ - this.root.attr('opacity', 0.7).attr('cursor', 'pointer').attr('pointer-events','visiblePainted') - } - - public hide(){ - this.root.attr('opacity', 0).attr('cursor', 'auto').attr('pointer-events','none') - } - - private setPosition(){ - this.image.attr('x', this.canvas.width/2 - this.image_width/2).attr('y', this.canvas.height/2 - this.image_width) - let text_x: number = this.canvas.width/2; - this.text.attr('x', text_x).attr('y', (this.canvas.height/2 + 16)) - this.line1.attr('x', text_x) - } -} \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts index df32b23..50a4747 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -8,8 +8,14 @@ import {WebsocketManager} from "./websocket/websocket"; import {HatnoteVisService} from "./service_event/model"; import {HelpPage} from "./help/help_page"; import {select} from "d3"; -import {Canvas} from "./canvas/canvas"; +import {Canvas} from "./ui/canvas/canvas"; import {Visualisation} from "./theme/model"; +import {Transition} from "./ui/transition"; +import {Header} from "./ui/header"; +import {InfoBox, InfoboxType} from "./ui/info_box"; +import {MuteIcon} from "./ui/mute_icon"; +import './style/normalize.css'; +import './style/main.css'; main(); @@ -27,28 +33,49 @@ function main(){ } // load theme - let theme = new VisualisationDirector(settings_data); + let visDirector = new VisualisationDirector(settings_data); // create observables let newCircleSubject: Subject = new Subject() let newBannerSubject: Subject = new Subject() let onCarouselTransitionStart: Subject<[HatnoteVisService, Visualisation]> = new Subject() - let onCarouselTransitionMid: Subject<[HatnoteVisService, Visualisation]> = new Subject() + let onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]> = new Subject() let onCarouselTransitionEnd: Subject<[HatnoteVisService, Visualisation]> = new Subject() let showWebsocketInfoboxSubject: Subject = new Subject() + let showLegendInfoboxSubject: Subject = new Subject() let updateDatabaseInfoSubject: Subject = new Subject() let updateVersionSubject: Subject<[string,number]> = new Subject() + // create header + new Header(select(appContainer), visDirector, onThemeHasChanged, updateVersionSubject) + + // create transition + const transition = new Transition(select(appContainer), visDirector) + // build canvas - new Canvas(theme, settings_data, newCircleSubject, - showWebsocketInfoboxSubject, updateVersionSubject, onCarouselTransitionStart, onCarouselTransitionMid,onCarouselTransitionEnd, updateDatabaseInfoSubject, newBannerSubject, select(appContainer)) + let canvas = new Canvas(visDirector, settings_data, newCircleSubject, + updateVersionSubject, onCarouselTransitionStart, onThemeHasChanged, + onCarouselTransitionEnd, updateDatabaseInfoSubject, newBannerSubject, select(appContainer), transition, + showLegendInfoboxSubject) + + // build info boxes + new InfoBox(select(appContainer), visDirector, showWebsocketInfoboxSubject, InfoboxType.network_websocket_connecting, + settings_data, onThemeHasChanged, showLegendInfoboxSubject, canvas.carousel) + new InfoBox(select(appContainer), visDirector, showWebsocketInfoboxSubject, InfoboxType.legend, settings_data, + onThemeHasChanged, showLegendInfoboxSubject, canvas.carousel) + + // build mute icon + let mute_icon = new MuteIcon(select(appContainer)) + if(!settings_data.kiosk_mode && !settings_data.audio_mute && !(!settings_data.carousel_mode && settings_data.map)){ + mute_icon.show() + } // load audio let audio = new HatnoteAudio(settings_data); // init event bridge let event_bridge = new EventBridge(audio, newCircleSubject, newBannerSubject, updateVersionSubject, - onCarouselTransitionStart, onCarouselTransitionMid,onCarouselTransitionEnd, settings_data) + onCarouselTransitionStart, onThemeHasChanged,onCarouselTransitionEnd, settings_data) // start websocket new WebsocketManager(settings_data, showWebsocketInfoboxSubject, updateDatabaseInfoSubject, event_bridge); diff --git a/web/src/observable/model.ts b/web/src/observable/model.ts index a635d31..4c72ec5 100644 --- a/web/src/observable/model.ts +++ b/web/src/observable/model.ts @@ -1,4 +1,4 @@ -import {InfoboxType} from "../canvas/info_box"; +import {InfoboxType} from "../ui/info_box"; import {HatnoteVisService, ServiceEvent} from "../service_event/model"; import {Location} from "../websocket/model"; diff --git a/web/src/theme/visualisationDirector.ts b/web/src/theme/visualisationDirector.ts index fc27f8d..a604b27 100644 --- a/web/src/theme/visualisationDirector.ts +++ b/web/src/theme/visualisationDirector.ts @@ -5,7 +5,6 @@ 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 VisualisationDirector { service_themes: Map = new Map() @@ -17,6 +16,9 @@ export class VisualisationDirector { readonly hatnoteTheme: HatnoteTheme; current_visualisation: Visualisation; private settings_data: SettingsData; + public windowWidth: number; + public windowHeight: number; + public isMobileScreen: boolean = false; constructor(settings_data: SettingsData) { this.settings_data = settings_data @@ -42,6 +44,20 @@ export class VisualisationDirector { this.carousel_service_order = [this.minervaTheme, this.keeperTheme, this.bloxbergTheme] this.current_service_theme = this.service_themes.get(settings_data.initialService) ?? this.minervaTheme + + this.windowHeight = window.innerHeight; + this.windowWidth = window.innerWidth; + + if (this.windowWidth <= 430 || this.windowHeight <= 430) { // iPhone 12 Pro Max 430px viewport width + this.isMobileScreen = true; + } + + window.onresize = (_) => this.windowUpdate(); + } + + windowUpdate(){ + this.windowHeight = window.innerHeight; + this.windowWidth = window.innerWidth; } getNextVisualisation(): Visualisation{ diff --git a/web/src/canvas/banner.ts b/web/src/ui/canvas/banner.ts similarity index 92% rename from web/src/canvas/banner.ts rename to web/src/ui/canvas/banner.ts index 2efee82..5d60db2 100644 --- a/web/src/canvas/banner.ts +++ b/web/src/ui/canvas/banner.ts @@ -1,6 +1,6 @@ import {Selection} from "d3"; import {BannerLayer} from "./banner_layer"; -import {BannerData} from "../observable/model"; +import {BannerData} from "../../observable/model"; export class Banner{ private readonly bannerLayer: BannerLayer @@ -11,7 +11,7 @@ export class Banner{ this.bannerLayer = bannerLayer this.root = bannerLayer.appendSVGElement('g') - .attr('transform', 'translate(0, ' + bannerLayer.canvas.visDirector.hatnoteTheme.header_height +')'); + .attr('transform', 'translate(0, 0'); this.user_container = this.root.append('g') diff --git a/web/src/canvas/banner_layer.ts b/web/src/ui/canvas/banner_layer.ts similarity index 95% rename from web/src/canvas/banner_layer.ts rename to web/src/ui/canvas/banner_layer.ts index 7db32ca..1f4c3ea 100644 --- a/web/src/canvas/banner_layer.ts +++ b/web/src/ui/canvas/banner_layer.ts @@ -1,5 +1,5 @@ import {Selection} from "d3"; -import {BannerData} from "../observable/model"; +import {BannerData} from "../../observable/model"; import {Banner} from "./banner"; import {Canvas} from "./canvas"; diff --git a/web/src/canvas/canvas.ts b/web/src/ui/canvas/canvas.ts similarity index 70% rename from web/src/canvas/canvas.ts rename to web/src/ui/canvas/canvas.ts index bc3de9a..cb0ef71 100644 --- a/web/src/canvas/canvas.ts +++ b/web/src/ui/canvas/canvas.ts @@ -1,38 +1,30 @@ import {select, Selection} from "d3"; import {BehaviorSubject, Subject} from "rxjs"; -import {BannerData, CircleData, DatabaseInfo, NetworkInfoboxData} from "../observable/model"; -import {SettingsData} from "../configuration/hatnote_settings"; -import {InfoBox, InfoboxType} from "./info_box"; -import {Header} from "./header"; -import {VisualisationDirector} from "../theme/visualisationDirector"; -import {HatnoteVisService} from "../service_event/model"; +import {BannerData, CircleData, DatabaseInfo, NetworkInfoboxData} from "../../observable/model"; +import {SettingsData} from "../../configuration/hatnote_settings"; +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"; +import {Visualisation} from "../../theme/model"; +import {Transition} from "../transition"; 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 onCarouselTransitionStart: Subject<[HatnoteVisService, Visualisation]> public readonly onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]> public readonly onCarouselTransitionEnd: Subject<[HatnoteVisService, Visualisation]> + public readonly showLegendInfoboxSubject: Subject public readonly updateVersionSubject: Subject<[string, number]> public readonly newCircleSubject: Subject public readonly newBannerSubject: Subject @@ -60,35 +52,34 @@ export class Canvas { } constructor(theme: VisualisationDirector, settings: SettingsData, newCircleSubject: Subject, - showNetworkInfoboxObservable: Subject, updateVersionSubject: Subject<[string, number]>, onCarouselTransitionStart: Subject<[HatnoteVisService, Visualisation]>, onCarouselTransitionMid: Subject<[HatnoteVisService, Visualisation]>, onCarouselTransitionEnd: Subject<[HatnoteVisService, Visualisation]>, updateDatabaseInfoSubject: Subject, newBannerSubject: Subject, - appContainer: Selection) { + appContainer: Selection, + transition: Transition, + showLegendInfoboxSubject: Subject) { this._width = window.innerWidth; - this._height = window.innerHeight; + this._height = window.innerHeight - theme.hatnoteTheme.header_height; this.visDirector = theme; this.settings = settings + this.showLegendInfoboxSubject = showLegendInfoboxSubject 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("id", "canvas") .attr('fill', this.visDirector.hatnoteTheme.svg_background_color) .style('background-color', '#1c2733'); this.geoPopUpContainer = this.appContainer.append('div') @@ -100,25 +91,17 @@ export class Canvas { 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) + if(settings.carousel_mode && !this.visDirector.isMobileScreen){ + this.carousel = new Carousel(this, transition) } // 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) { + if (this.visDirector.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() @@ -127,10 +110,6 @@ export class Canvas { this.renderCurrentTheme(); - if(!settings.kiosk_mode && !settings.audio_mute && !(!settings.carousel_mode && settings.map)){ - this.mute_icon.show() - } - window.onresize = (_) => this.windowUpdate(); } @@ -148,11 +127,6 @@ export class Canvas { // 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 @@ -160,12 +134,9 @@ export class Canvas { public windowUpdate() { // update canvas root dimensions this.width = window.innerWidth; - this.height = window.innerHeight; + this.height = window.innerHeight - this.visDirector.hatnoteTheme.header_height; this._root.attr("width", this.width).attr("height", this.height); - // update canvas header dimensions - this.header.windowUpdate() - // update banner this.banner_layer.windowUpdate() @@ -177,11 +148,5 @@ export class Canvas { // 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/ui/canvas/carousel.ts similarity index 93% rename from web/src/canvas/carousel.ts rename to web/src/ui/canvas/carousel.ts index df8cfa0..aafb532 100644 --- a/web/src/canvas/carousel.ts +++ b/web/src/ui/canvas/carousel.ts @@ -1,13 +1,13 @@ -import {Transition} from "./transition"; +import {Transition} from "../transition"; import {ProgressIndicator} from "./progress_indicator"; -import {DatabaseInfo} from "../observable/model"; +import {DatabaseInfo} from "../../observable/model"; import {BehaviorSubject, Subject} from "rxjs"; -import {HatnoteVisService} from "../service_event/model"; -import {ServiceTheme, Visualisation} from "../theme/model"; +import {HatnoteVisService} from "../../service_event/model"; +import {ServiceTheme, Visualisation} from "../../theme/model"; import {Canvas} from "./canvas"; export class Carousel { - public readonly transition: Transition; + public transition: Transition; public readonly progess_indicator: ProgressIndicator; public readonly updateDatabaseInfoSubject: Subject public databaseInfo: Map @@ -17,15 +17,15 @@ export class Carousel { private readonly canvas: Canvas; private nextTheme: ServiceTheme | undefined; private currentCarouselOrderIndex; - constructor(canvas: Canvas) { + constructor(canvas: Canvas, transition: Transition) { this.canvas = canvas - this.transition = new Transition(this.canvas) + this.transition = transition 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.canvas.visDirector.current_service_theme.id_name,this.canvas.visDirector.current_visualisation]); }) this.transition.onTransitionEnd.subscribe(_ => { this.canvas.onCarouselTransitionEnd.next( diff --git a/web/src/canvas/geo/geoCircle.ts b/web/src/ui/canvas/geo/geoCircle.ts similarity index 90% rename from web/src/canvas/geo/geoCircle.ts rename to web/src/ui/canvas/geo/geoCircle.ts index 5eb59c2..4742a73 100644 --- a/web/src/canvas/geo/geoCircle.ts +++ b/web/src/ui/canvas/geo/geoCircle.ts @@ -1,6 +1,6 @@ -import {HatnoteVisService} from "../../service_event/model"; +import {HatnoteVisService} from "../../../service_event/model"; import {BaseType, select, Selection} from "d3"; -import {CircleData} from "../../observable/model"; +import {CircleData} from "../../../observable/model"; import {GeoCirclesLayer} from "./geoCirclesLayer"; import {Canvas} from "../canvas"; @@ -46,7 +46,7 @@ export class GeoCircle { .duration(40000) .on('interrupt', _ => { highlightedArea.interrupt(); - popUpContainer.remove(); + popUp.remove(); if(this.canvas.settings.debug_mode){ console.log('Circle removed for ' + circleData.type) } @@ -67,10 +67,10 @@ export class GeoCircle { } // add pop up - const popUpContainer = this.canvas.geoPopUpContainer.append("div"); - popUpContainer + const popUp = this.canvas.geoPopUpContainer.append("div"); + popUp .style('position', 'absolute') - .style('top', `${y - this.canvas.visDirector.hatnoteTheme.header_height + 10}px`) + .style('top', `${y + 10}px`) .style('left', `${x + 10}px`) .style('padding', '4px') .style('border-radius', '1px') @@ -78,9 +78,9 @@ export class GeoCircle { .style('box-shadow', '1px 1px 5px #CCC') .style('font-size', '.7em') .style('border', '1px solid #CCC') - popUpContainer.append('span').text(circleData.label_text) + popUp.append('span').text(circleData.label_text) - popUpContainer.transition() + popUp.transition() .style('opacity', 0) .duration(4000) .remove() diff --git a/web/src/canvas/geo/geoCirclesLayer.ts b/web/src/ui/canvas/geo/geoCirclesLayer.ts similarity index 92% rename from web/src/canvas/geo/geoCirclesLayer.ts rename to web/src/ui/canvas/geo/geoCirclesLayer.ts index f409886..a4ae5b7 100644 --- a/web/src/canvas/geo/geoCirclesLayer.ts +++ b/web/src/ui/canvas/geo/geoCirclesLayer.ts @@ -1,8 +1,6 @@ import {GeoProjection, Selection} from "d3"; -import {ServiceTheme} from "../../theme/model"; -import {CircleData} from "../../observable/model"; -import {Canvas} from "../canvas"; -import {ServiceEvent} from "../../service_event/model"; +import {ServiceTheme} from "../../../theme/model"; +import {CircleData} from "../../../observable/model"; import {GeoCircle} from "./geoCircle"; import {GeoVisualisation} from "./geoVisualisation"; diff --git a/web/src/canvas/geo/geoVisualisation.ts b/web/src/ui/canvas/geo/geoVisualisation.ts similarity index 96% rename from web/src/canvas/geo/geoVisualisation.ts rename to web/src/ui/canvas/geo/geoVisualisation.ts index 3a85872..e3da2e2 100644 --- a/web/src/canvas/geo/geoVisualisation.ts +++ b/web/src/ui/canvas/geo/geoVisualisation.ts @@ -1,15 +1,13 @@ import {geoAlbers, geoBounds, geoEqualEarth, geoPath, GeoProjection, Selection} from "d3"; -import '../../style/normalize.css'; -import '../../style/main.css'; import {feature, mesh} from "topojson"; -import countriesJson from '../../../assets/countries-50m.json' -import germanyJson from '../../../assets/germany.json' +import countriesJson from '../../../../assets/countries-50m.json' +import germanyJson from '../../../../assets/germany.json' import {GeometryObject, Topology} from 'topojson-specification'; import {FeatureCollection, GeoJsonProperties} from 'geojson'; import {Canvas} from "../canvas"; -import {HatnoteVisService} from "../../service_event/model"; +import {HatnoteVisService} from "../../../service_event/model"; import {GeoCirclesLayer} from "./geoCirclesLayer"; -import {Visualisation} from "../../theme/model"; +import {Visualisation} from "../../../theme/model"; export class GeoVisualisation { public readonly circles_layer: GeoCirclesLayer diff --git a/web/src/canvas/icon_button.ts b/web/src/ui/canvas/icon_button.ts similarity index 85% rename from web/src/canvas/icon_button.ts rename to web/src/ui/canvas/icon_button.ts index 8ae9608..71e4970 100644 --- a/web/src/canvas/icon_button.ts +++ b/web/src/ui/canvas/icon_button.ts @@ -1,9 +1,9 @@ import {Selection} from "d3"; import {Navigation} from "./navigation"; -import leftIcon from "../../assets/images/navigate_before_FILL0_wght400_GRAD0_opsz24.svg"; -import rightIcon from "../../assets/images/navigate_next_FILL0_wght400_GRAD0_opsz24.svg"; -import infoIcon from "../../assets/images/question_mark_FILL0_wght400_GRAD0_opsz24.svg"; -import closeIcon from "../../assets/images/close_FILL0_wght400_GRAD0_opsz24.svg"; +import leftIcon from "../../../assets/images/navigate_before_FILL0_wght400_GRAD0_opsz24.svg"; +import rightIcon from "../../../assets/images/navigate_next_FILL0_wght400_GRAD0_opsz24.svg"; +import infoIcon from "../../../assets/images/question_mark_FILL0_wght400_GRAD0_opsz24.svg"; +import closeIcon from "../../../assets/images/close_FILL0_wght400_GRAD0_opsz24.svg"; export class IconButton { private readonly navigation: Navigation diff --git a/web/src/canvas/listen/listenToCircle.ts b/web/src/ui/canvas/listen/listenToCircle.ts similarity index 97% rename from web/src/canvas/listen/listenToCircle.ts rename to web/src/ui/canvas/listen/listenToCircle.ts index 8d4142b..fe24e3b 100644 --- a/web/src/canvas/listen/listenToCircle.ts +++ b/web/src/ui/canvas/listen/listenToCircle.ts @@ -1,6 +1,6 @@ import {Selection} from "d3"; -import {ServiceEvent} from "../../service_event/model"; -import {getRandomIntInclusive} from "../../util/random"; +import {ServiceEvent} from "../../../service_event/model"; +import {getRandomIntInclusive} from "../../../util/random"; import {ListenToCirclesLayer} from "./listenToCirclesLayer"; export class ListenToCircle{ diff --git a/web/src/canvas/listen/listenToCirclesLayer.ts b/web/src/ui/canvas/listen/listenToCirclesLayer.ts similarity index 94% rename from web/src/canvas/listen/listenToCirclesLayer.ts rename to web/src/ui/canvas/listen/listenToCirclesLayer.ts index 09d10ed..e94b04a 100644 --- a/web/src/canvas/listen/listenToCirclesLayer.ts +++ b/web/src/ui/canvas/listen/listenToCirclesLayer.ts @@ -1,7 +1,7 @@ import {Selection} from "d3"; -import {ServiceTheme} from "../../theme/model"; -import {CircleData} from "../../observable/model"; -import {HatnoteVisService, ServiceEvent} from "../../service_event/model"; +import {ServiceTheme} from "../../../theme/model"; +import {CircleData} from "../../../observable/model"; +import {HatnoteVisService, ServiceEvent} from "../../../service_event/model"; import {Canvas} from "../canvas"; import {ListenToVisualisation} from "./listenToVisualisation"; import {ListenToCircle} from "./listenToCircle"; diff --git a/web/src/canvas/listen/listenToVisualisation.ts b/web/src/ui/canvas/listen/listenToVisualisation.ts similarity index 91% rename from web/src/canvas/listen/listenToVisualisation.ts rename to web/src/ui/canvas/listen/listenToVisualisation.ts index 418dd07..c498ae2 100644 --- a/web/src/canvas/listen/listenToVisualisation.ts +++ b/web/src/ui/canvas/listen/listenToVisualisation.ts @@ -1,9 +1,7 @@ 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"; +import {Visualisation} from "../../../theme/model"; export class ListenToVisualisation { public readonly circles_layer: ListenToCirclesLayer; diff --git a/web/src/canvas/navigation.ts b/web/src/ui/canvas/navigation.ts similarity index 66% rename from web/src/canvas/navigation.ts rename to web/src/ui/canvas/navigation.ts index 94d3c53..105b36a 100644 --- a/web/src/canvas/navigation.ts +++ b/web/src/ui/canvas/navigation.ts @@ -1,8 +1,5 @@ import {Selection} from "d3"; 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{ @@ -14,13 +11,12 @@ export class Navigation{ private isInfoBoxOpen: boolean; private readonly navigationWidth = 270; private currentServiceIndex = 0; - private readonly legend_items: LegendItem[] = []; constructor(canvas: Canvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'navigation') - .attr('opacity', canvas.isMobileScreen ? 1 : 0); + .attr('opacity', canvas.visDirector.isMobileScreen ? 1 : 0); this.setPosition() this.isInfoBoxOpen = false @@ -34,12 +30,6 @@ export class Navigation{ window.hatnoteLastService = () => this.lastService(this); // @ts-ignore window.hatnoteInfoButton = () => this.clickInfoButton(this); - - if(this.canvas.isMobileScreen){ - for (let i = 0; i < 3; i++) { - this.legend_items.push(new LegendItem(undefined, this.canvas.info_box_legend, this.canvas)) - } - } } private setPosition(){ @@ -61,11 +51,11 @@ export class Navigation{ private clickInfoButton(nav: Navigation){ if(nav.isInfoBoxOpen){ - this.canvas.info_box_legend.show(InfoboxType.legend, false) + this.canvas.showLegendInfoboxSubject.next(false) nav.infoButton.setIcon('info') nav.isInfoBoxOpen = false } else { - this.canvas.info_box_legend.show(InfoboxType.legend, true) + this.canvas.showLegendInfoboxSubject.next(true) nav.infoButton.setIcon('close') nav.isInfoBoxOpen = true } @@ -79,28 +69,7 @@ export class Navigation{ [this.canvas.visDirector.current_service_theme.id_name, this.canvas.visDirector.current_visualisation]) } - private clearLegendItems(){ - this.legend_items.forEach((theme_legend_item, i) => { - this.legend_items[i].hide() - }); - } - - public themeUpdate(currentServiceTheme: ServiceTheme) { - this.clearLegendItems() - currentServiceTheme.legend_items.forEach((theme_legend_item, i) => { - if(i < this.legend_items.length) { - this.legend_items[i].themeUpdate(theme_legend_item) - } - }); - } - public windowUpdate() { this.setPosition() - - 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) - } - }); } } \ No newline at end of file diff --git a/web/src/canvas/progress_indicator.ts b/web/src/ui/canvas/progress_indicator.ts similarity index 98% rename from web/src/canvas/progress_indicator.ts rename to web/src/ui/canvas/progress_indicator.ts index b4eea84..d85b17b 100644 --- a/web/src/canvas/progress_indicator.ts +++ b/web/src/ui/canvas/progress_indicator.ts @@ -1,6 +1,6 @@ import { easeLinear, Selection, transition} from "d3"; -import {ServiceTheme} from "../theme/model"; -import {HatnoteVisService} from "../service_event/model"; +import {ServiceTheme} from "../../theme/model"; +import {HatnoteVisService} from "../../service_event/model"; import {Canvas} from "./canvas"; export class ProgressIndicator{ diff --git a/web/src/canvas/qr_code.ts b/web/src/ui/canvas/qr_code.ts similarity index 92% rename from web/src/canvas/qr_code.ts rename to web/src/ui/canvas/qr_code.ts index a4551b0..c53d15d 100644 --- a/web/src/canvas/qr_code.ts +++ b/web/src/ui/canvas/qr_code.ts @@ -1,6 +1,6 @@ import {Selection} from "d3"; -import QrCodeMinerva from "../../assets/images/qr-code-minerva.png"; -import {ServiceTheme, Visualisation} from "../theme/model"; +import QrCodeMinerva from "../../../assets/images/qr-code-minerva.png"; +import {ServiceTheme, Visualisation} from "../../theme/model"; import {Canvas} from "./canvas"; export class QRCode{ @@ -53,7 +53,7 @@ export class QRCode{ } private setOpacity(){ - if(!this.canvas.isMobileScreen) { + if(!this.canvas.visDirector.isMobileScreen) { this.root.attr("opacity", 1) } else { this.root.attr("opacity", 0) diff --git a/web/src/ui/header.ts b/web/src/ui/header.ts new file mode 100644 index 0000000..8066260 --- /dev/null +++ b/web/src/ui/header.ts @@ -0,0 +1,114 @@ +import {Selection} from "d3"; +import MinervaLogo from "../../assets/images/minervamessenger-banner-kussmund+bulb.png"; +import {ServiceTheme, Visualisation} from "../theme/model"; +import {environmentVariables} from "../configuration/environment"; +import {Canvas} from "./canvas/canvas"; +import {Subject} from "rxjs"; +import {HatnoteVisService} from "../service_event/model"; +import {VisualisationDirector} from "../theme/visualisationDirector"; +import {LegendItemHtml} from "./legend_item"; +import {Legend} from "./legend"; + +export class Header{ + private readonly root: Selection + private readonly title: Selection + private readonly title0: Selection + private readonly title1: Selection + private readonly legend: Selection + private readonly logo: Selection + private readonly updateBox: Selection + private readonly updateText: Selection + private readonly visDirector: VisualisationDirector; + + constructor(appContainer: Selection, visDirector: VisualisationDirector, + onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]>, + updateVersionSubject: Subject<[string,number]> = new Subject()) { + this.visDirector = visDirector; + this.root = appContainer.append("div") + .attr('id', 'header') + .style('width', '100%') + .style('height', `${visDirector.hatnoteTheme.header_height}px`) + .style('background', visDirector.hatnoteTheme.header_bg_color) + .style('display', 'flex') + .style('opacity', 1.0) + .style('align-items', 'center') + + this.logo = this.root.append("img") + .attr('src', MinervaLogo) + .style('width', '44px') + .style('height', 'fit-content') + .style('margin-left', '20px') + + this.title = this.root.append('div') + .style('margin-left', '12px') + this.title0 = this.root.append('span') + .text(visDirector.current_visualisation === Visualisation.listenTo ? 'Listen to\u00A0' : 'Locate\u00A0') + .style('font-family', 'HatnoteVisNormal') + .style('font-size', visDirector.isMobileScreen ? '22px' : '32px') + .style('color', visDirector.hatnoteTheme.header_text_color) + this.title1 = this.root.append('span') + .text(visDirector.current_service_theme.header_title) + .style('font-family', 'HatnoteVisBold') + .style('font-size', visDirector.isMobileScreen ? '22px' : '32px') + .style('color', visDirector.hatnoteTheme.header_text_color) + + this.updateBox = this.root.append('div') + .style('opacity', 0) + .style('width', '190px') + .style('height', '30px') + .style('border-radius', '8px') + .style('background', visDirector.hatnoteTheme.header_version_update_bg) + .style('display', 'flex') + .style('align-items', 'center') + .style('justify-content', 'center') + .style('margin', 'auto') + + this.updateText = this.updateBox.append('span') + .style('opacity', 0) + .style('text-align', 'middle') + .text('New update available') + .style('font-family', 'HatnoteVisBold') + .style('font-size', '16px') + .style('color', '#fff') + + this.legend = this.root.append('div') + .attr('id', 'legend_items') + .style('display', 'flex') + .style('margin-left', 'auto') + .style('gap', '20px') + .style('margin-right', '20px') + + if(!visDirector.isMobileScreen) { + new Legend(this.legend, visDirector, onThemeHasChanged, true) + } + + updateVersionSubject.subscribe({ + next: (versions) => this.updateVersionNumbers(versions) + }) + + onThemeHasChanged.subscribe({ + next: (value) => { + this.themeUpdate(visDirector.current_service_theme) + } + }) + } + + private updateVersionNumbers(versions: [string, number]){ + let expectedFrontendVersion = versions[1] + let browserFrontendVersion: number = Number(environmentVariables.version); + if (!isNaN(browserFrontendVersion) && browserFrontendVersion < expectedFrontendVersion) { + this.updateBox.style('opacity', 1) + this.updateText.style('opacity', 1) + } else { + this.updateBox.style('opacity', 0) + this.updateText.style('opacity', 0) + } + } + + public themeUpdate(currentServiceTheme: ServiceTheme) { + this.logo.attr('src', currentServiceTheme.header_logo) + + this.title0.text(this.visDirector.current_visualisation === Visualisation.listenTo ? 'Listen to\u00A0' : 'Locate\u00A0') + this.title1.text(currentServiceTheme.header_title) + } +} \ No newline at end of file diff --git a/web/src/ui/info_box.ts b/web/src/ui/info_box.ts new file mode 100644 index 0000000..85f0ebd --- /dev/null +++ b/web/src/ui/info_box.ts @@ -0,0 +1,153 @@ +import {Selection} from "d3"; +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 {Subject} from "rxjs"; +import {VisualisationDirector} from "../theme/visualisationDirector"; +import {SettingsData} from "../configuration/hatnote_settings"; +import {Legend} from "./legend"; +import {HatnoteVisService} from "../service_event/model"; +import {Visualisation} from "../theme/model"; +import {Carousel} from "./canvas/carousel"; + +export class InfoBox{ + private readonly root: Selection + private readonly image: Selection | undefined + private readonly title: Selection + private readonly text: Selection + private readonly textContent: Selection + private readonly titleContainer: Selection + private readonly loadingSpinner: Selection | undefined + private readonly infobox_width = 600 + public readonly infobox_height = 190 + private readonly infobox_image_width = 200 + private readonly visDirector: VisualisationDirector + private readonly isMobileScreen : boolean + private readonly settings: SettingsData + private readonly onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]> + private readonly carousel: Carousel | undefined; + private currentType: InfoboxType + + + constructor(appContainer: Selection, visDirector: VisualisationDirector, + showNetworkInfoboxObservable: Subject, type: InfoboxType, settings: SettingsData, + onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]>, + showLegendInfoboxSubject: Subject, carousel: Carousel | undefined) { + this.visDirector = visDirector + this.onThemeHasChanged = onThemeHasChanged + this.isMobileScreen = visDirector.isMobileScreen + this.currentType = type + this.settings = settings + this.carousel = carousel + + this.root = appContainer.append("div") + .attr('id', 'infobox_' + type.toString()) + .style('opacity', 0) + .style('position', 'absolute') + .style('width', `100%`) + .style('max-width', `${this.infobox_width}px`) + .style('min-height', `${this.infobox_height}px`) + .style('left', '50%') + .style('top', '50%') + .style('transform', 'translate(-50%,-50%)') + .style('border', '7px solid rgb(41, 128, 185)') + .style('border-radius', '8px') + .style('background', '#fff') + .style('display', 'flex') + + if(!this.isMobileScreen) { + this.image = this.root.append('img') + .attr('src', InfoboxAudioImg) + .style('width', `${this.infobox_image_width}px`) + .style('padding', `15px`) + } + + this.textContent = this.root.append('div') + .style('padding', '0 10px') + this.titleContainer = this.textContent.append('div') + .style('display', 'flex') + this.loadingSpinner = this.titleContainer.append('img') + .attr('src', LoadingSpinner) + .style('width', 25) + .style('padding-right', '10px') + this.title = this.titleContainer.append('h2') + .text('undefined title') + .style('font-family', 'HatnoteVisBold') + .style('font-size', '26px') + .style('color', visDirector.hatnoteTheme.header_text_color) + this.text = this.textContent.append('p') + .style('font-family', 'HatnoteVisNormal') + .style('font-size', '16px') + .style('fill', visDirector.hatnoteTheme.header_text_color) + + switch (type) { + case InfoboxType.network_websocket_connecting: + case InfoboxType.network_database_can_not_connect: + case InfoboxType.network_database_connecting: + showNetworkInfoboxObservable.subscribe({ + next: (value) => this.show(value.infoboxType, value.show, value) + }) + break; + case InfoboxType.legend: + showLegendInfoboxSubject.subscribe({ + next: (value) => this.show(InfoboxType.legend, value ) + }) + break; + } + } + + public appendSVGElement(type: string): Selection { + return this.root.append(type) + } + + public show(type: InfoboxType, show: boolean, network_infobox_data?: NetworkInfoboxData) { + this.currentType = type + + if(type === InfoboxType.network_websocket_connecting){ + this.root.style('opacity', show ? 1 : 0) + this.image?.attr('src', InfoboxWebsocketConnectingImg) + this.title.text('Connecting to server') + this.text.html(`Your browser is connecting to the server.

` + + `Url: ${network_infobox_data?.target_url}

` + + `Number of reconnects: ${network_infobox_data?.number_of_reconnects}

`) + this.loadingSpinner?.style('display', 'block') + } + + // 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.visDirector.current_service_theme.id_name === network_infobox_data?.service){ + this.root.style('opacity', show ? 1 : 0) + this.image?.attr('src', InfoboxWebsocketConnectingImg) + this.title.text('Connecting to database') + this.text.html(`Connecting to ${this.visDirector.current_service_theme.name} database.`) + this.loadingSpinner?.style('display', 'block') + } + + if(type === InfoboxType.network_database_can_not_connect && this.visDirector.current_service_theme.id_name === network_infobox_data?.service && + (!this.settings.carousel_mode || this.carousel?.allServicesHaveError)){ + this.root.style('opacity', show ? 1 : 0) + this.image?.attr('src', InfoboxDbConnectingImg) + this.title.text('Cannot connect to database') + this.text.html(`The backend can not connect ${this.visDirector.current_service_theme.name} database.

` + + `Next reconnect: ${network_infobox_data?.next_reconnect_date ?? ''}

` + + `Number of reconnects: ${network_infobox_data?.number_of_reconnects}

`) + this.loadingSpinner?.style('display', 'none') + } + + if(type=== InfoboxType.legend) { + this.root.style('opacity', show ? 1 : 0) + this.textContent.selectChildren().remove() + this.textContent.style('padding', '20px') + new Legend(this.textContent, this.visDirector, this.onThemeHasChanged, false) + } + } +} + +export enum InfoboxType { + network_websocket_connecting, + network_database_connecting, + network_database_can_not_connect, + legend +} \ No newline at end of file diff --git a/web/src/ui/legend.ts b/web/src/ui/legend.ts new file mode 100644 index 0000000..5db9bcd --- /dev/null +++ b/web/src/ui/legend.ts @@ -0,0 +1,51 @@ +import {Selection} from "d3"; +import {VisualisationDirector} from "../theme/visualisationDirector"; +import {Subject} from "rxjs"; +import {HatnoteVisService} from "../service_event/model"; +import {ServiceTheme, Visualisation} from "../theme/model"; +import {LegendItemHtml} from "./legend_item"; + +export class Legend { + private readonly root: Selection + private readonly visDirector: VisualisationDirector + constructor(container: Selection, visDirector: VisualisationDirector, + onThemeHasChanged: Subject<[HatnoteVisService, Visualisation]>, + isHeader: boolean) { + this.visDirector = visDirector + this.root = container.append('div') + .attr('id', 'legend_items') + .style('display', 'flex') + .style('margin-left', 'auto') + .style('gap', '20px') + + if(!isHeader){ + this.root.style('flex-direction', 'column') + } else { + this.root.style('margin-right', '20px') + } + + for (let i = 0; i < visDirector.current_service_theme.legend_items.length; i++) { + new LegendItemHtml(this.root, visDirector, visDirector.current_service_theme.legend_items[i]) + } + + onThemeHasChanged.subscribe({ + next: (value) => { + this.themeUpdate() + } + }) + } + + private clearLegendItems(){ + this.root.selectChildren().remove() + } + + public themeUpdate() { + // update legend items + this.clearLegendItems() + if(!this.visDirector.isMobileScreen){ + for (let i = 0; i < this.visDirector.current_service_theme.legend_items.length; i++) { + new LegendItemHtml(this.root, this.visDirector, this.visDirector.current_service_theme.legend_items[i]) + } + } + } +} \ No newline at end of file diff --git a/web/src/ui/legend_item.ts b/web/src/ui/legend_item.ts new file mode 100644 index 0000000..2a4d3ad --- /dev/null +++ b/web/src/ui/legend_item.ts @@ -0,0 +1,34 @@ +import {Selection} from "d3"; +import {ThemeLegendItem} from "../theme/model"; +import {VisualisationDirector} from "../theme/visualisationDirector"; +import {HatnoteVisService} from "../service_event/model"; + +export class LegendItemHtml{ + private readonly root: Selection; + private readonly title: Selection; + private readonly circle: Selection; + private readonly visDirector: VisualisationDirector; + + constructor(legendContainer: Selection, visDirector: VisualisationDirector, + themeLegendItem: ThemeLegendItem) { + this.visDirector = visDirector + this.root = legendContainer.append('div') + .style('display', 'flex') + .style('align-items', 'center') + .style('gap', '12px') + + this.circle = this.root.append('div') + .style('border-radius', '50%') + .style('height', `${this.visDirector.hatnoteTheme.legend_item_circle_r*2}px`) + .style('width', `${this.visDirector.hatnoteTheme.legend_item_circle_r*2}px`) + .style('background', this.visDirector.getThemeColor(themeLegendItem.event)) // default value + + let smallTitle = (themeLegendItem.smallTitle1 ?? '') + ' ' + (themeLegendItem.smallTitle2 ?? '') + this.title = this.root.append('span') + .text(themeLegendItem.title ?? smallTitle) + .style('font-family', 'HatnoteVisNormal') + .style('font-size', this.visDirector.current_service_theme.id_name === HatnoteVisService.Minerva ? '16px' : '26px') + .style('color', this.visDirector.hatnoteTheme.header_text_color) + .style('max-width', this.visDirector.current_service_theme.id_name === HatnoteVisService.Minerva ? '70px' : '100%') + } +} \ No newline at end of file diff --git a/web/src/ui/mute_icon.ts b/web/src/ui/mute_icon.ts new file mode 100644 index 0000000..54e6792 --- /dev/null +++ b/web/src/ui/mute_icon.ts @@ -0,0 +1,57 @@ +import {Selection} from "d3"; +import MuteIconSVG from "../../assets/images/volume_off_FILL1_wght400_GRAD0_opsz24.svg"; + +export class MuteIcon{ + private readonly root: Selection; + private readonly muteIconContainer: Selection; + private readonly image: Selection + private readonly text: Selection; + private readonly image_width = 100; + private readonly text_color = '#fff'; + + // consists not only of the icon but also spans a transparent clickable container above everything + constructor(appContainer: Selection) { + this.root = appContainer.append('div') + .style('opacity', 0) + .style('position', 'absolute') + .style('top', '0') + .style('left', '0') + .style('cursor', 'auto') + .style('width', '100%') + .style('height', '100%') + .attr('id', 'mute_icon') + .style('pointer-events','none') + + this.muteIconContainer = this.root.append('div') + .style('display', 'flex') + .style('flex-direction', 'column') + .style('pointer-events','none') + .style('position','absolute') + .style('left','50%') + .style('top','50%') + .style('transform','translate(-50%,-50%)') + + this.image = this.muteIconContainer.append('img') + .attr('src', MuteIconSVG) + .style('width', `${this.image_width}px`) + + this.text = this.muteIconContainer.append('span') + .text('Click here to unmute') + .style('font-family', 'HatnoteVisNormal') + .style('font-size', '12px') + .style('color', this.text_color) + + document.getElementById('mute_icon')?.addEventListener("click", (_) => { + this.hide(); + }); + } + + public show(){ + this.root.style('opacity', 0.7).style('cursor', 'pointer').style('pointer-events','visiblePainted') + } + + public hide(){ + this.root.style('opacity', 0).style('cursor', 'auto').style('pointer-events','none') + } + +} \ No newline at end of file diff --git a/web/src/canvas/transition.ts b/web/src/ui/transition.ts similarity index 77% rename from web/src/canvas/transition.ts rename to web/src/ui/transition.ts index 98207fc..a5cdbd3 100644 --- a/web/src/canvas/transition.ts +++ b/web/src/ui/transition.ts @@ -1,13 +1,12 @@ -import {easeBackOut, easeCircleOut, easeCubicOut, easeExpOut, easeQuadOut, Selection} from "d3"; +import {easeBackOut, easeCircleOut, easeCubicOut, easeExpOut, easeQuadOut, select, Selection} from "d3"; import MpdlLogo from "../../assets/images/logo-mpdl-twocolor-dark-var1.png"; import {ServiceTheme} from "../theme/model"; import {HatnoteVisService} from "../service_event/model"; import {Subject} from "rxjs"; -import {NetworkInfoboxData} from "../observable/model"; -import {Canvas} from "./canvas"; +import {VisualisationDirector} from "../theme/visualisationDirector"; export class Transition{ - private readonly root: Selection; + private readonly root: Selection; private readonly circle3: Selection; private readonly circle2: Selection; private readonly circle1: Selection; @@ -19,14 +18,21 @@ export class Transition{ private readonly mpdl_logo: Selection; private readonly text: Selection; private readonly service_logo: Selection; - private readonly canvas: Canvas; public readonly onTransitionStart: Subject public readonly onTransitionMid: Subject public readonly onTransitionEnd: Subject - - constructor(canvas: Canvas) { - this.canvas = canvas - this.root = canvas.appendSVGElement('g').attr('id', 'transition_layer').attr('opacity', 0) + private visDirector: VisualisationDirector + + constructor(appContainer: Selection, visDirector: VisualisationDirector) { + this.visDirector = visDirector; + this.root = appContainer.append("svg") + .attr("width", this.visDirector.windowWidth) + .attr("height", this.visDirector.windowHeight) + .attr('id', 'transition_layer') + .attr('opacity', 0) + .style('pointer-events','none') + .style('position', 'absolute') + .style('top', 0) // you could do it also with data points https://gist.github.com/mbostock/1705868 this.circles_path = this.root.append('path').attr('opacity', 0) @@ -69,9 +75,9 @@ export class Transition{ this.onTransitionStart.next() this.root.attr('opacity', 1) - this.circles_path.attr('d', 'M' + this.canvas.width/2 + ' ' + this.canvas.height/2 + ' Q40 ' + ((this.canvas.height/2)+100) +' ,-10 -40') + this.circles_path.attr('d', 'M' + this.visDirector.windowWidth/2 + ' ' + this.visDirector.windowHeight/2 + ' Q40 ' + ((this.visDirector.windowHeight/2)+100) +' ,-10 -40') - let circle_radius = Math.sqrt(Math.pow(this.canvas.width/2, 2) + Math.pow(this.canvas.height/2, 2)) + let circle_radius = Math.sqrt(Math.pow(this.visDirector.windowWidth/2, 2) + Math.pow(this.visDirector.windowHeight/2, 2)) let circle3_color: string = service.color3 switch (service.id_name) { @@ -83,7 +89,7 @@ export class Transition{ } this.circle3.attr('fill', circle3_color) - .attr('transform', 'translate(' + this.canvas.width/2 + ', ' + this.canvas.height/2 + ')') + .attr('transform', 'translate(' + this.visDirector.windowWidth/2 + ', ' + this.visDirector.windowHeight/2 + ')') .attr('r', 40) .attr('opacity', 0) .transition() @@ -109,7 +115,7 @@ export class Transition{ case HatnoteVisService.Keeper: circle2_color = service.color2 } - this.circle2.attr('transform', 'translate(' + this.canvas.width/2 + ', ' + this.canvas.height/2 + ')') + this.circle2.attr('transform', 'translate(' + this.visDirector.windowWidth/2 + ', ' + this.visDirector.windowHeight/2 + ')') .attr('fill', circle2_color) .attr('r', 35) .attr('opacity', 0) @@ -128,7 +134,7 @@ export class Transition{ .ease(easeExpOut) .duration(out_duration) - this.circle1.attr('transform', 'translate(' + this.canvas.width/2 + ', ' + this.canvas.height/2 + ')') + this.circle1.attr('transform', 'translate(' + this.visDirector.windowWidth/2 + ', ' + this.visDirector.windowHeight/2 + ')') .attr('fill', service.color1) .attr('r', 30) .attr('opacity', 0) @@ -147,10 +153,10 @@ export class Transition{ .ease(easeExpOut) .duration(out_duration) - this.mask.attr('height', this.canvas.height) - .attr('width', this.canvas.width) + this.mask.attr('height', this.visDirector.windowHeight) + .attr('width', this.visDirector.windowWidth) - this.mask_circle.attr('transform', 'translate(' + this.canvas.width/2 + ', ' + this.canvas.height/2 + ')') + this.mask_circle.attr('transform', 'translate(' + this.visDirector.windowWidth/2 + ', ' + this.visDirector.windowHeight/2 + ')') .attr('r', 0) .transition() .delay(delay + 70) @@ -167,14 +173,14 @@ export class Transition{ .ease(easeExpOut) .duration(out_duration) - this.background.attr('height', this.canvas.height) - .attr('width', this.canvas.width) + this.background.attr('height', this.visDirector.windowHeight) + .attr('width', this.visDirector.windowWidth) - this.mpdl_logo.attr('x', this.canvas.width*(3/4)).attr('y', 0) + this.mpdl_logo.attr('x', this.visDirector.windowWidth*(3/4)).attr('y', 0) .attr('width', 200) let logo_delay = 1000 - this.text.attr('transform', 'translate(' + this.canvas.width/4 +', ' + this.canvas.height/4 + ')') + this.text.attr('transform', 'translate(' + this.visDirector.windowWidth/4 +', ' + this.visDirector.windowHeight/4 + ')') .attr('font-size', '60px') .attr('opacity', 0) .attr('width', 200) @@ -193,7 +199,7 @@ export class Transition{ let logo_width = 600 this.service_logo.attr('href', service.transition_logo) - .attr('transform', 'translate(' + (this.canvas.width/2-logo_width/2) +', '+ this.canvas.height/2+')') + .attr('transform', 'translate(' + (this.visDirector.windowWidth/2-logo_width/2) +', '+ this.visDirector.windowHeight/2+')') .attr('width', logo_width) .attr('opacity', 0) .transition() diff --git a/web/src/websocket/websocket.ts b/web/src/websocket/websocket.ts index b0980b7..6b5b4ae 100644 --- a/web/src/websocket/websocket.ts +++ b/web/src/websocket/websocket.ts @@ -10,7 +10,7 @@ import { import {SettingsData} from "../configuration/hatnote_settings"; import {Subject} from "rxjs"; import {DatabaseInfo, NetworkInfoboxData} from "../observable/model"; -import {InfoboxType} from "../canvas/info_box"; +import {InfoboxType} from "../ui/info_box"; import {EventBridge} from "../service_event/event_bridge"; import {HatnoteVisService} from "../service_event/model";