From 9442ff5199be8170e31c4d11c97b86b49d2a6f33 Mon Sep 17 00:00:00 2001 From: Felix Riehm <mail@felixriehm.de> Date: Thu, 14 Mar 2024 14:07:18 +0100 Subject: [PATCH] add carousel for map --- web/src/canvas/canvas.ts | 10 ++++- web/src/canvas/carousel.ts | 16 +++---- web/src/canvas/geo/circle.ts | 11 +++-- web/src/canvas/geo/circles_layer.ts | 14 +++--- web/src/canvas/geo/geoCanvas.ts | 60 ++++++++++++++++++------- web/src/canvas/listen/listenToCanvas.ts | 11 +---- web/src/canvas/progress_indicator.ts | 9 ++-- web/src/canvas/transition.ts | 22 ++++++--- web/src/main.ts | 2 +- 9 files changed, 98 insertions(+), 57 deletions(-) diff --git a/web/src/canvas/canvas.ts b/web/src/canvas/canvas.ts index 7376eea..e519271 100644 --- a/web/src/canvas/canvas.ts +++ b/web/src/canvas/canvas.ts @@ -1,20 +1,23 @@ import {select, Selection} from "d3"; -import {Subject} from "rxjs"; +import {BehaviorSubject, Subject} from "rxjs"; import {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 {HatnoteVisService} from "../service_event/model"; export abstract class Canvas { abstract header: Header; public readonly theme: Theme; abstract info_box_websocket: InfoBox; abstract info_box_legend: InfoBox; + public readonly isMobileScreen: boolean = false; public readonly settings: SettingsData; public readonly showNetworkInfoboxObservable: Subject<NetworkInfoboxData> public readonly updateDatabaseInfoSubject: Subject<DatabaseInfo> + public readonly hatnoteVisServiceChangedSubject: BehaviorSubject<HatnoteVisService> public readonly updateVersionSubject: Subject<[string, number]> public readonly newCircleSubject: Subject<CircleData> public readonly appContainer: Selection<HTMLDivElement, unknown, null, undefined>; @@ -40,17 +43,22 @@ export abstract class Canvas { protected constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject<CircleData>, showNetworkInfoboxObservable: Subject<NetworkInfoboxData>, updateVersionSubject: Subject<[string, number]>, + hatnoteVisServiceChangedSubject: BehaviorSubject<HatnoteVisService>, updateDatabaseInfoSubject: Subject<DatabaseInfo>, appContainer: Selection<HTMLDivElement, unknown, null, undefined>) { this._width = window.innerWidth; this._height = window.innerHeight; this.theme = theme; this.settings = settings + this.hatnoteVisServiceChangedSubject = hatnoteVisServiceChangedSubject this.newCircleSubject = newCircleSubject this.showNetworkInfoboxObservable = showNetworkInfoboxObservable this.updateDatabaseInfoSubject = updateDatabaseInfoSubject this.updateVersionSubject = updateVersionSubject this.appContainer = appContainer; + if (this.width <= 430 || this.height <= 430) { // iPhone 12 Pro Max 430px viewport width + this.isMobileScreen = true; + } } public appendSVGElement(type: string): Selection<SVGGElement, unknown, null, any> { diff --git a/web/src/canvas/carousel.ts b/web/src/canvas/carousel.ts index 9b140bc..273d74e 100644 --- a/web/src/canvas/carousel.ts +++ b/web/src/canvas/carousel.ts @@ -5,6 +5,7 @@ import {DatabaseInfo} from "../observable/model"; import {Subject} from "rxjs"; import {HatnoteVisService} from "../service_event/model"; import {ServiceTheme} from "../theme/model"; +import {GeoCanvas} from "./geo/geoCanvas"; export class Carousel { public readonly transition: Transition; @@ -14,11 +15,13 @@ export class Carousel { public serviceError: Map<HatnoteVisService, boolean> public allServicesHaveError: boolean private startCarouselService: HatnoteVisService | null - private readonly canvas: ListenToCanvas + private readonly canvas: ListenToCanvas | GeoCanvas private currentCarouselOrderIndex; - constructor(canvas: ListenToCanvas) { + constructor(canvas: ListenToCanvas | GeoCanvas) { 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.progess_indicator = new ProgressIndicator(this.canvas) this.updateDatabaseInfoSubject = this.canvas.updateDatabaseInfoSubject this.serviceError = new Map<HatnoteVisService, boolean>() @@ -75,9 +78,7 @@ export class Carousel { if(dbInfo.service === this.canvas.theme.current_service_theme.id_name && !this.allServicesHaveError) { this.initNextTheme() - this.transition.startTransition(this.canvas.theme.current_service_theme, - (_) => this.canvas.renderCurrentTheme(), - (_) => this.continueCarousel()) + this.transition.startTransition(this.canvas.theme.current_service_theme) } return; } @@ -110,7 +111,6 @@ export class Carousel { if(nextTheme){ this.canvas.theme.set_current_theme(nextTheme); this.progess_indicator.setCurrentServiceIndicator(nextTheme) - this.canvas.hatnoteVisServiceChangedSubject.next(this.canvas.theme.current_service_theme.id_name) } } @@ -134,9 +134,7 @@ export class Carousel { indicator?.start(() => { if (!this.allServicesHaveError) { this.initNextTheme() - this.transition.startTransition(this.canvas.theme.current_service_theme, - (_) => this.canvas.renderCurrentTheme(), - (_) => this.continueCarousel()) + this.transition.startTransition(this.canvas.theme.current_service_theme) } }) } diff --git a/web/src/canvas/geo/circle.ts b/web/src/canvas/geo/circle.ts index ec03b87..d4f6eab 100644 --- a/web/src/canvas/geo/circle.ts +++ b/web/src/canvas/geo/circle.ts @@ -1,6 +1,6 @@ import {CirclesLayer} from "./circles_layer"; import {HatnoteVisService} from "../../service_event/model"; -import {BaseType, GeoProjection, select, Selection} from "d3"; +import {BaseType, select, Selection} from "d3"; import {CircleData} from "../../observable/model"; export class Circle{ @@ -8,11 +8,16 @@ export class Circle{ private readonly root: Selection<SVGCircleElement, unknown, null, undefined>; constructor(circlesLayer: CirclesLayer, circleData: CircleData, - svgCircle: SVGCircleElement, projection: GeoProjection, service: HatnoteVisService) { + svgCircle: SVGCircleElement, service: HatnoteVisService) { this.circlesLayer = circlesLayer // init circle values this.root = select(svgCircle) - const point = projection([circleData.location?.coordinate.long ?? 0, circleData.location?.coordinate.lat?? 0]) + let point; + if(this.circlesLayer.canvas.theme.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]) + } if(point === null || circleData.location === undefined){ return } diff --git a/web/src/canvas/geo/circles_layer.ts b/web/src/canvas/geo/circles_layer.ts index ff665df..9397efa 100644 --- a/web/src/canvas/geo/circles_layer.ts +++ b/web/src/canvas/geo/circles_layer.ts @@ -8,18 +8,20 @@ import {ServiceEvent} from "../../service_event/model"; export class CirclesLayer{ private readonly root: Selection<SVGGElement, unknown, null, any>; public readonly canvas: Canvas - public readonly projection: GeoProjection + public readonly germanyProjection: GeoProjection + public readonly worldProjection: GeoProjection - constructor(canvas: Canvas, projection: GeoProjection) { + constructor(canvas: Canvas, germanyProjection: GeoProjection, worldProjection: GeoProjection) { this.canvas = canvas - this.projection = projection + this.germanyProjection = germanyProjection; + this.worldProjection = worldProjection; this.root = canvas.appendSVGElement('g').attr('id', 'circle_layer') canvas.newCircleSubject.subscribe({ - next: (value) => this.addCircle(value, this.projection) + next: (value) => this.addCircle(value) }) } - private addCircle(circle: CircleData, projection: GeoProjection){ + private addCircle(circle: CircleData){ let that = this; // make sure that circle that already exits a removed so that the animation can start from start @@ -34,7 +36,7 @@ export class CirclesLayer{ } new Circle(that,circleData, - this, that.projection, service) + this, service) }) } diff --git a/web/src/canvas/geo/geoCanvas.ts b/web/src/canvas/geo/geoCanvas.ts index 3f60f76..a198528 100644 --- a/web/src/canvas/geo/geoCanvas.ts +++ b/web/src/canvas/geo/geoCanvas.ts @@ -5,7 +5,7 @@ import {CirclesLayer} from "./circles_layer"; import {Header} from "../header"; import {InfoBox, InfoboxType} from "../info_box"; import {Theme} from "../../theme/theme"; -import {Subject} from "rxjs"; +import {BehaviorSubject, Subject} from "rxjs"; import {CircleData, DatabaseInfo, NetworkInfoboxData} from "../../observable/model"; import {SettingsData} from "../../configuration/hatnote_settings"; import {feature, mesh} from "topojson"; @@ -15,6 +15,7 @@ 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 @@ -22,35 +23,48 @@ export class GeoCanvas extends Canvas{ protected readonly _root: Selection<SVGSVGElement, unknown, null, any>; public readonly info_box_websocket: InfoBox; public readonly info_box_legend: InfoBox; - public world_map: any; + public readonly worldMap: Selection<SVGGElement, unknown, null, any>; + public readonly worldMapProjection: GeoProjection; + public readonly germanyMap: Selection<SVGGElement, unknown, null, any>; + public readonly germanyMapProjection: GeoProjection; + public readonly carousel: Carousel | undefined constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject<CircleData>, showNetworkInfoboxObservable: Subject<NetworkInfoboxData>, updateVersionSubject: Subject<[string, number]>, + hatnoteVisServiceChangedSubject: BehaviorSubject<HatnoteVisService>, updateDatabaseInfoSubject: Subject<DatabaseInfo>, appContainer: Selection<HTMLDivElement, unknown, null, undefined>) { - super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,updateDatabaseInfoSubject, appContainer) + super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject, appContainer) - // draw order matters in this function. Do not change without checking the result. + // 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) .attr("style", "max-width: 100%; height: auto;"); - let projection; - if(this.theme.current_service_theme.id_name === HatnoteVisService.Bloxberg){ - projection = this.initWorldMapSvg() - } else { - projection = this.initGermanyMapSvg() - } - this.circles_layer = new CirclesLayer(this, projection) + 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.renderCurrentTheme(); window.onresize = (_) => this.windowUpdate(); @@ -95,7 +109,8 @@ export class GeoCanvas extends Canvas{ const path = geoPath(projection); // Add a path for each country and color it according te this data. - this._root.append("g") + this.germanyMap.append("g") + .attr("id", "countries-mesh") .selectAll("path") .data((states as FeatureCollection).features) .join("path") @@ -106,7 +121,8 @@ export class GeoCanvas extends Canvas{ let countrymesh = mesh(germany, statesGeometry as GeometryObject, (a: GeometryObject, b: GeometryObject) => a !== b) // Add a white mesh. - this._root.append("path") + this.germanyMap.append("path") + .attr("id", "countries-border-mesh") .datum(countrymesh) .attr("fill", "none") .attr("stroke", "white") @@ -128,7 +144,8 @@ export class GeoCanvas extends Canvas{ // draw order matters here, check before changing something // Add a white sphere with a black border. - this._root.append("path") + this.worldMap.append("path") + .attr("id", "black-world-boundary") .datum({type: "Sphere"}) .attr("fill", "white") .attr("stroke", "currentColor") @@ -139,7 +156,8 @@ export class GeoCanvas extends Canvas{ let countriesGeometry: GeometryObject<GeoJsonProperties> = world.objects.countries; let countries = feature(world, countriesGeometry) // Add a path for each country and color it according te this data. - this._root.append("g") + this.worldMap.append("g") + .attr("id", "countries-mesh") .selectAll("path") .data((countries as FeatureCollection).features) .join("path") @@ -150,7 +168,8 @@ export class GeoCanvas extends Canvas{ let countrymesh = mesh(world, countriesGeometry as GeometryObject, (a: GeometryObject, b: GeometryObject) => a !== b) // Add a white mesh. - this._root.append("path") + this.worldMap.append("path") + .attr("id", "countries-border-mesh") .datum(countrymesh) .attr("fill", "none") .attr("stroke", "white") @@ -160,11 +179,18 @@ export class GeoCanvas extends Canvas{ } public renderCurrentTheme(){ + if (this.theme.current_service_theme.id_name == HatnoteVisService.Bloxberg) { + this.worldMap.attr("opacity", 1) + this.germanyMap.attr("opacity", 0) + } else { + this.worldMap.attr("opacity", 0) + this.germanyMap.attr("opacity", 1) + } + // remove circles from other services this.circles_layer.removeOtherServiceCircles(this.theme.current_service_theme) // update header logo - console.log(this.theme.current_service_theme.name) this.header.themeUpdate(this.theme.current_service_theme) } diff --git a/web/src/canvas/listen/listenToCanvas.ts b/web/src/canvas/listen/listenToCanvas.ts index 3be1385..4a86464 100644 --- a/web/src/canvas/listen/listenToCanvas.ts +++ b/web/src/canvas/listen/listenToCanvas.ts @@ -23,15 +23,12 @@ export class ListenToCanvas extends Canvas { public readonly header: Header; protected readonly _root: Selection<SVGSVGElement, unknown, null, any>; public readonly navigation: Navigation | undefined; - public readonly isMobileScreen: boolean = false; 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<BannerData> - public readonly hatnoteVisServiceChangedSubject: BehaviorSubject<HatnoteVisService> public readonly showAudioInfoboxObservable: Subject<boolean> - public readonly showNetworkInfoboxObservable: Subject<NetworkInfoboxData> public readonly carousel: Carousel | undefined constructor(theme: Theme, settings: SettingsData, newCircleSubject: Subject<CircleData>, @@ -42,16 +39,10 @@ export class ListenToCanvas extends Canvas { hatnoteVisServiceChangedSubject: BehaviorSubject<HatnoteVisService>, updateDatabaseInfoSubject: Subject<DatabaseInfo>, appContainer: Selection<HTMLDivElement, unknown, null, undefined>){ - super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,updateDatabaseInfoSubject, appContainer) + super(theme, settings, newCircleSubject, showNetworkInfoboxObservable, updateVersionSubject,hatnoteVisServiceChangedSubject,updateDatabaseInfoSubject, appContainer) this.newBannerSubject = newBannerSubject this.showAudioInfoboxObservable = showAudioInfoboxObservable - this.showNetworkInfoboxObservable = showNetworkInfoboxObservable - this.hatnoteVisServiceChangedSubject = hatnoteVisServiceChangedSubject - - 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") diff --git a/web/src/canvas/progress_indicator.ts b/web/src/canvas/progress_indicator.ts index f6719a5..0b595ba 100644 --- a/web/src/canvas/progress_indicator.ts +++ b/web/src/canvas/progress_indicator.ts @@ -2,6 +2,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"; export class ProgressIndicator{ public readonly service_indicators: Map<HatnoteVisService, ServiceProgressIndicator>; @@ -11,9 +12,9 @@ export class ProgressIndicator{ return this._currentServiceIndicator; } - private readonly canvas: ListenToCanvas + private readonly canvas: ListenToCanvas | GeoCanvas - constructor(canvas: ListenToCanvas) { + constructor(canvas: ListenToCanvas | GeoCanvas) { this.canvas = canvas this.service_indicators = new Map<HatnoteVisService, ServiceProgressIndicator>() canvas.theme.service_themes.forEach(service => { @@ -41,9 +42,9 @@ class ServiceProgressIndicator{ private readonly textBox: Selection<SVGRectElement, unknown, null, any> private readonly text: Selection<SVGTextElement, unknown, null, any> public readonly service_id: HatnoteVisService - private readonly canvas: ListenToCanvas + private readonly canvas: ListenToCanvas | GeoCanvas - constructor(canvas: ListenToCanvas, service_theme: ServiceTheme) { + constructor(canvas: ListenToCanvas | GeoCanvas, 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); diff --git a/web/src/canvas/transition.ts b/web/src/canvas/transition.ts index 563d816..a00c8b7 100644 --- a/web/src/canvas/transition.ts +++ b/web/src/canvas/transition.ts @@ -3,6 +3,9 @@ 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"; export class Transition{ private readonly root: Selection<SVGGElement, unknown, null, any>; @@ -17,9 +20,12 @@ export class Transition{ private readonly mpdl_logo: Selection<SVGImageElement, unknown, null, any>; private readonly text: Selection<SVGTextElement, unknown, null, any>; private readonly service_logo: Selection<SVGImageElement, unknown, null, any>; - private readonly canvas: ListenToCanvas; + private readonly canvas: ListenToCanvas | GeoCanvas; + public readonly onTransitionStart: Subject<void> + public readonly onTransitionMid: Subject<void> + public readonly onTransitionEnd: Subject<void> - constructor(canvas: ListenToCanvas) { + constructor(canvas: ListenToCanvas | GeoCanvas) { this.canvas = canvas this.root = canvas.appendSVGElement('g').attr('id', 'transition_layer').attr('opacity', 0) @@ -53,11 +59,15 @@ export class Transition{ .text('Next service:') this.service_logo = this.transition_screen.append('image') + + this.onTransitionStart = new Subject() + this.onTransitionMid = new Subject() + this.onTransitionEnd= new Subject() } - startTransition(service: ServiceTheme, renderCurrentTheme: (currentServiceTheme: ServiceTheme) => void, - continueCarousel: (currentServiceTheme: ServiceTheme) => void, delay:number = 0, + startTransition(service: ServiceTheme, delay:number = 0, in_duration: number = 2500, active_duration: number = 4000, out_duration: number = 1500){ + 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') @@ -193,7 +203,7 @@ export class Transition{ .ease(Math.sqrt) .duration(in_duration - logo_delay) .on('end', () => { - renderCurrentTheme(service) + this.onTransitionMid.next() }) .transition() .delay(active_duration) @@ -203,7 +213,7 @@ export class Transition{ .ease(easeExpOut) .duration(out_duration) .on('start', () => { - continueCarousel(service) + this.onTransitionEnd.next() }) } diff --git a/web/src/main.ts b/web/src/main.ts index 79c3ed0..dc93133 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -41,7 +41,7 @@ function main(){ // build canvas if (settings_data.map) { new GeoCanvas(theme, settings_data, newCircleSubject, - showWebsocketInfoboxSubject, updateVersionSubject, updateDatabaseInfoSubject, select(appContainer)) + showWebsocketInfoboxSubject, updateVersionSubject, hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject, select(appContainer)) } else { new ListenToCanvas(theme, settings_data, newCircleSubject, newBannerSubject, showAudioInfoboxSubject, showWebsocketInfoboxSubject, updateVersionSubject, hatnoteVisServiceChangedSubject, updateDatabaseInfoSubject,select(appContainer)) -- GitLab