diff --git a/src/UI/Base/Loading.ts b/src/UI/Base/Loading.ts deleted file mode 100644 index 7c76174a1e..0000000000 --- a/src/UI/Base/Loading.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Combine from "./Combine" -import Translations from "../i18n/Translations" -import BaseUIElement from "../BaseUIElement" -import SvelteUIElement from "./SvelteUIElement" -import { default as LoadingSvg } from "../../assets/svg/Loading.svelte" - -/** - * @deprecated - */ -export default class Loading extends Combine { - constructor(msg?: BaseUIElement | string) { - const t = Translations.W(msg) ?? Translations.t.general.loading - t.SetClass("pl-2") - super([ - new SvelteUIElement(LoadingSvg) - .SetClass("animate-spin self-center") - .SetStyle("width: 1.5rem; height: 1.5rem; min-width: 1.5rem;"), - t, - ]) - this.SetClass("flex p-1") - } -} diff --git a/src/UI/Base/MapControlButton.svelte b/src/UI/Base/MapControlButton.svelte index 43fac4cc1f..56f6a51d0b 100644 --- a/src/UI/Base/MapControlButton.svelte +++ b/src/UI/Base/MapControlButton.svelte @@ -2,8 +2,8 @@ import { createEventDispatcher } from "svelte" import { twJoin } from "tailwind-merge" import { Translation } from "../i18n/Translation" - import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel" - import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" + import { ariaLabelStore } from "../../Utils/ariaLabel" + import { ImmutableStore, Store } from "../../Logic/UIEventSource" /** * A round button with an icon and possible a small text, which hovers above the map diff --git a/src/UI/Base/Tr.svelte b/src/UI/Base/Tr.svelte index 82cb74c3d7..cfc4901bf1 100644 --- a/src/UI/Base/Tr.svelte +++ b/src/UI/Base/Tr.svelte @@ -5,7 +5,6 @@ import { Translation } from "../i18n/Translation" import WeblateLink from "./WeblateLink.svelte" import { Store } from "../../Logic/UIEventSource" - import FromHtml from "./FromHtml.svelte" import { Utils } from "../../Utils" export let t: Translation diff --git a/src/UI/Input/Toggle.ts b/src/UI/Input/Toggle.ts index 6bc1394bf3..21bef1c1cc 100644 --- a/src/UI/Input/Toggle.ts +++ b/src/UI/Input/Toggle.ts @@ -1,8 +1,9 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { Store } from "../../Logic/UIEventSource" import BaseUIElement from "../BaseUIElement" import { VariableUiElement } from "../Base/VariableUIElement" /** + * @deprecated * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. * It can be used to implement e.g. checkboxes or collapsible elements */ diff --git a/src/UI/OpeningHours/OpeningHours.ts b/src/UI/OpeningHours/OpeningHours.ts index 9596f8240f..9a8f4b341b 100644 --- a/src/UI/OpeningHours/OpeningHours.ts +++ b/src/UI/OpeningHours/OpeningHours.ts @@ -967,6 +967,52 @@ changes // => [[36000,61200], ["10:00", "17:00"]] } return oh } + + + /** + * + * @param changeHours number of seconds 'till the start of the day, assuming sorted + * @param changeHourText + * @param maxDiff minimum required seconds between two items to be in the same group + * + * OH.partitionOHForDistance([0, 15, 3615], ["start", "15s", "1h15s"]) // => [{changeHours: [0, 3615], changeTexts: ["start", "1h15s"]}, {changeHours: [15], changeTexts: ["15 seconds"]}}] + * + */ + public static partitionOHForDistance(changeHours: number[], changeHourText: string[], maxDiff = 3600): { + changeHours: number[], + changeTexts: string[] + }[] { + const partitionedHours: { changeHours: number[], changeTexts: string[] }[] = [ + { changeHours: [changeHours[0]], changeTexts: [changeHourText[0]] } + ] + for (let i = 1 /*skip the first one, inited ^*/; i < changeHours.length; i++) { + const moment = changeHours[i] + const text = changeHourText[i] + let depth = 0 + while (depth < partitionedHours.length) { + const candidate = partitionedHours[depth] + const lastMoment = candidate.changeHours.at(-1) + const diff = moment - lastMoment + if (diff >= maxDiff) { + candidate.changeHours.push(moment) + candidate.changeTexts.push(text) + break + } + depth++ + } + if (depth == partitionedHours.length) { + // No candidate found - make a new list + partitionedHours.push({ + changeTexts: [text], + changeHours: [moment] + }) + } + + } + + + return partitionedHours + } } export class ToTextualDescription { diff --git a/src/UI/OpeningHours/OpeningHoursVisualization.ts b/src/UI/OpeningHours/OpeningHoursVisualization.ts deleted file mode 100644 index c98e866639..0000000000 --- a/src/UI/OpeningHours/OpeningHoursVisualization.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { UIEventSource } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import { FixedUiElement } from "../Base/FixedUiElement" -import { OH, OpeningRange, ToTextualDescription } from "./OpeningHours" -import Translations from "../i18n/Translations" -import BaseUIElement from "../BaseUIElement" -import Toggle from "../Input/Toggle" -import { VariableUiElement } from "../Base/VariableUIElement" -import Table from "../Base/Table" -import { Translation } from "../i18n/Translation" -import Loading from "../Base/Loading" -import opening_hours from "opening_hours" -import Locale from "../i18n/Locale" -import SpecialCase from "./Visualisation/SpecialCase.svelte" -import SvelteUIElement from "../Base/SvelteUIElement" -import OpeningHoursRangeElement from "./Visualisation/OpeningHoursRangeElement.svelte" - -export default class OpeningHoursVisualization extends Toggle { - private static readonly weekdays: Translation[] = [ - Translations.t.general.weekdays.abbreviations.monday, - Translations.t.general.weekdays.abbreviations.tuesday, - Translations.t.general.weekdays.abbreviations.wednesday, - Translations.t.general.weekdays.abbreviations.thursday, - Translations.t.general.weekdays.abbreviations.friday, - Translations.t.general.weekdays.abbreviations.saturday, - Translations.t.general.weekdays.abbreviations.sunday, - ] - - constructor( - tags: UIEventSource>, - key: string, - prefix = "", - postfix = "" - ) { - const openingHoursStore = OH.CreateOhObjectStore(tags, key, prefix, postfix) - const ohTable = new VariableUiElement( - openingHoursStore.map((opening_hours_obj) => { - if (opening_hours_obj === undefined) { - return new FixedUiElement("No opening hours defined with key " + key).SetClass( - "alert" - ) - } - - if (opening_hours_obj === "error") { - return Translations.t.general.opening_hours.error_loading - } - - const applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj) - const textual = ToTextualDescription.createTextualDescriptionFor( - opening_hours_obj, - applicableWeek.ranges - ) - const vis = OpeningHoursVisualization.CreateFullVisualisation( - opening_hours_obj, - applicableWeek.ranges, - applicableWeek.startingMonday - ) - Locale.language.mapD((lng) => { - console.debug("Setting OH description to", lng, textual) - vis.ConstructElement().ariaLabel = textual?.textFor(lng) - }) - return vis - }) - ) - - super( - ohTable, - new Loading(Translations.t.general.opening_hours.loadingCountry), - tags.map((tgs) => tgs._country !== undefined) - ) - this.SetClass("no-weblate") - } - - private static CreateFullVisualisation( - oh: opening_hours, - ranges: OpeningRange[][], - lastMonday: Date - ): BaseUIElement { - // First, a small sanity check. The business might be permanently closed, 24/7 opened or be another special case - if (ranges.some((range) => range.length > 0)) { - // The normal case: we have items for the coming days - return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday) - } - // The special case that range is completely empty - return new SvelteUIElement(SpecialCase, { oh }) - } - - private static ConstructVizTable( - oh: any, - ranges: { - isOpen: boolean - isSpecial: boolean - comment: string - startDate: Date - endDate: Date - }[][], - rangeStart: Date - ): BaseUIElement { - const isWeekstable: boolean = oh.isWeekStable() - const [changeHours, changeHourText] = OH.allChangeMoments(ranges) - const today = new Date() - today.setHours(0, 0, 0, 0) - - const todayIndex = Math.ceil( - (today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24) - ) - // By default, we always show the range between 8 - 19h, in order to give a stable impression - // Ofc, a bigger range is used if needed - const earliestOpen = Math.min(8 * 60 * 60, ...changeHours) - let latestclose = Math.max(...changeHours) - // We always make sure there is 30m of leeway in order to give enough room for the closing entry - latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) - const availableArea = latestclose - earliestOpen - - /* - * The OH-visualisation is a table, consisting of 8 rows and 2 columns: - * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times - * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars. - * Note that the bars are actually an embedded
spanning the full width, containing multiple sub-elements - * */ - - const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement( - availableArea, - changeHours, - changeHourText, - earliestOpen - ) - - const weekdays = [] - const weekdayStyles = [] - for (let i = 0; i < 7; i++) { - const day = OpeningHoursVisualization.weekdays[i].Clone() - day.SetClass("w-full h-full flex") - - const rangesForDay = ranges[i].map((range) => - new SvelteUIElement(OpeningHoursRangeElement, { - availableArea, - earliestOpen, - latestclose, - range, - isWeekstable - }) - ) - const allRanges = new Combine([ - ...OpeningHoursVisualization.CreateLinesAtChangeHours( - changeHours, - availableArea, - earliestOpen - ), - ...rangesForDay, - ]).SetClass("w-full block") - - let extraStyle = "" - if (todayIndex == i) { - extraStyle = "background-color: var(--subtle-detail-color);" - allRanges.SetClass("ohviz-today") - } else if (i >= 5) { - extraStyle = "background-color: rgba(230, 231, 235, 1);" - } - weekdays.push([day, allRanges]) - weekdayStyles.push([ - "padding-left: 0.5em;" + extraStyle, - `position: relative;` + extraStyle, - ]) - } - return new Table(undefined, [[" ", header], ...weekdays], { - contentStyle: [ - ["width: 5%", `position: relative; height: ${headerHeight}`], - ...weekdayStyles, - ], - }) - .SetClass("w-full") - .SetStyle( - "border-collapse: collapse; word-break; word-break: normal; word-wrap: normal" - ) - } - - private static CreateLinesAtChangeHours( - changeHours: number[], - availableArea: number, - earliestOpen: number - ): BaseUIElement[] { - const allLines: BaseUIElement[] = [] - for (const changeMoment of changeHours) { - const offset = (100 * (changeMoment - earliestOpen)) / availableArea - if (offset < 0 || offset > 100) { - continue - } - const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line") - allLines.push(el) - } - return allLines - } - - /** - * The OH-Visualization header element, a single bar with hours - * @param availableArea - * @param changeHours - * @param changeHourText - * @param earliestOpen - * @constructor - * @private - */ - private static ConstructHeaderElement( - availableArea: number, - changeHours: number[], - changeHourText: string[], - earliestOpen: number - ): [BaseUIElement, string] { - const header: BaseUIElement[] = [] - - header.push( - ...OpeningHoursVisualization.CreateLinesAtChangeHours( - changeHours, - availableArea, - earliestOpen - ) - ) - - let showHigher = false - let showHigherUsed = false - for (let i = 0; i < changeHours.length; i++) { - const changeMoment = changeHours[i] - const offset = (100 * (changeMoment - earliestOpen)) / availableArea - if (offset < 0 || offset > 100) { - continue - } - - if (i > 0 && (changeMoment - changeHours[i - 1]) / (60 * 60) < 2) { - // Quite close to the previous value - // We alternate the heights - showHigherUsed = true - showHigher = !showHigher - } else { - showHigher = false - } - - const el = new Combine([ - new FixedUiElement(changeHourText[i]) - .SetClass( - "relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black border-opacity-50" - ) - .SetStyle("left: -50%; word-break:initial"), - ]) - .SetStyle(`left:${offset}%;margin-top: ${showHigher ? "1.4rem;" : "0.1rem"}`) - .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication") - header.push(el) - } - const headerElem = new Combine(header) - .SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`) - .SetStyle("margin-top: -1rem") - const headerHeight = showHigherUsed ? "4rem" : "2rem" - return [headerElem, headerHeight] - } -} diff --git a/src/UI/OpeningHours/Visualisation/OpeningHours.svelte b/src/UI/OpeningHours/Visualisation/OpeningHours.svelte new file mode 100644 index 0000000000..712fb77676 --- /dev/null +++ b/src/UI/OpeningHours/Visualisation/OpeningHours.svelte @@ -0,0 +1,34 @@ + +
+ + {#if ranges.some((range) => range.length > 0)} + + + {:else} + + + {/if} +
diff --git a/src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte b/src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte new file mode 100644 index 0000000000..d56502c476 --- /dev/null +++ b/src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte @@ -0,0 +1,38 @@ + + +
+ {#each changeHours as changeMoment, i} + {#if calcOffset(changeMoment) >= 0 && calcOffset(changeMoment) <= 100} +
+
+ {changeHourText[i]} +
+
+ {/if} + {/each} +
diff --git a/src/UI/OpeningHours/Visualisation/OpeningHoursRangeElement.svelte b/src/UI/OpeningHours/Visualisation/OpeningHoursRangeElement.svelte index cab67ee298..3f1db45843 100644 --- a/src/UI/OpeningHours/Visualisation/OpeningHoursRangeElement.svelte +++ b/src/UI/OpeningHours/Visualisation/OpeningHoursRangeElement.svelte @@ -19,7 +19,7 @@ startOfDay.setHours(0, 0, 0, 0) let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen // prettier-ignore - let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / (latestclose - earliestOpen) + let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / availableArea let startPercentage = (100 * startpoint) / availableArea diff --git a/src/UI/OpeningHours/Visualisation/OpeningHoursWithError.svelte b/src/UI/OpeningHours/Visualisation/OpeningHoursWithError.svelte new file mode 100644 index 0000000000..e9f9934708 --- /dev/null +++ b/src/UI/OpeningHours/Visualisation/OpeningHoursWithError.svelte @@ -0,0 +1,28 @@ + + + +{#if $tags._country === undefined} + + + +{:else if $opening_hours_obj === undefined} +
No opening hours defined with key {key}
+{:else if $opening_hours_obj === "error"} + +{:else} + +{/if} diff --git a/src/UI/OpeningHours/Visualisation/RegularOpeningHoursTable.svelte b/src/UI/OpeningHours/Visualisation/RegularOpeningHoursTable.svelte new file mode 100644 index 0000000000..9f6c287a3f --- /dev/null +++ b/src/UI/OpeningHours/Visualisation/RegularOpeningHoursTable.svelte @@ -0,0 +1,129 @@ + +
+ {#each allChangeMoments as moment} +
+
+
+
+ +
+
+
+
+ {/each} + + + {#each weekdayHeaders as weekdayHeader} + + + + + {/each} + + {#each weekdays as weekday, i} + = 5}> + + + + + {/each} + + {#each weekendDayHeaders as weekdayHeader} + + + + + {/each} +
+ +
+ + +
+ {#each ranges[i] as range} + + {/each} +
+
+ +
+
diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index db01c4ada9..96f58b5ea0 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -5,12 +5,11 @@ import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState import { HistogramViz } from "./Popup/HistogramViz" import { UploadToOsmViz } from "./Popup/UploadToOsmViz" import { MultiApplyViz } from "./Popup/MultiApplyViz" -import { UIEventSource } from "../Logic/UIEventSource" +import { Store, UIEventSource } from "../Logic/UIEventSource" import AllTagsPanel from "./Popup/AllTagsPanel/AllTagsPanel.svelte" import { VariableUiElement } from "./Base/VariableUIElement" import { Translation } from "./i18n/Translation" import Translations from "./i18n/Translations" -import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" import AutoApplyButtonVis from "./Popup/AutoApplyButtonVis" import { LanguageElement } from "./Popup/LanguageElement/LanguageElement" import SvelteUIElement from "./Base/SvelteUIElement" @@ -43,6 +42,9 @@ import { } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations" import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte" import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.svelte" +import OpeningHoursWithError from "./OpeningHours/Visualisation/OpeningHoursWithError.svelte" +import { OH } from "./OpeningHours/OpeningHours" +import opening_hours from "opening_hours" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() @@ -276,7 +278,12 @@ export default class SpecialVisualizations { "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", constr: (state, tagSource: UIEventSource, args) => { const [key, prefix, postfix] = args - return new OpeningHoursVisualization(tagSource, key, prefix, postfix) + const openingHoursStore: Store = OH.CreateOhObjectStore(tagSource, key, prefix, postfix) + return new SvelteUIElement(OpeningHoursWithError, { + tags: tagSource, + key, + opening_hours_obj: openingHoursStore + }) }, }, {