forked from MapComplete/MapComplete
Refactoring: port opening hours visualisation to svelte
This commit is contained in:
parent
3b2c2462c5
commit
cc96df94e9
12 changed files with 290 additions and 285 deletions
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { twJoin } from "tailwind-merge"
|
import { twJoin } from "tailwind-merge"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
|
import { ariaLabelStore } from "../../Utils/ariaLabel"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A round button with an icon and possible a small text, which hovers above the map
|
* A round button with an icon and possible a small text, which hovers above the map
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import WeblateLink from "./WeblateLink.svelte"
|
import WeblateLink from "./WeblateLink.svelte"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import FromHtml from "./FromHtml.svelte"
|
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export let t: Translation
|
export let t: Translation
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../BaseUIElement"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated
|
||||||
* The 'Toggle' is a UIElement showing either one of two elements, depending on the state.
|
* 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
|
* It can be used to implement e.g. checkboxes or collapsible elements
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -967,6 +967,52 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
||||||
}
|
}
|
||||||
return oh
|
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 {
|
export class ToTextualDescription {
|
||||||
|
|
|
@ -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<Record<string, string>>,
|
|
||||||
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 <div> 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]
|
|
||||||
}
|
|
||||||
}
|
|
34
src/UI/OpeningHours/Visualisation/OpeningHours.svelte
Normal file
34
src/UI/OpeningHours/Visualisation/OpeningHours.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Full opening hours visualisations table, dispatches to special cases
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OH, ToTextualDescription } from "../OpeningHours"
|
||||||
|
import opening_hours from "opening_hours"
|
||||||
|
import { ariaLabel } from "../../../Utils/ariaLabel"
|
||||||
|
import RegularOpeningHoursTable from "./RegularOpeningHoursTable.svelte"
|
||||||
|
import SpecialCase from "./SpecialCase.svelte"
|
||||||
|
import { Translation } from "../../i18n/Translation"
|
||||||
|
import type { OpeningRange } from "../OpeningHours"
|
||||||
|
|
||||||
|
export let opening_hours_obj: opening_hours
|
||||||
|
|
||||||
|
let applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj)
|
||||||
|
let oh = opening_hours_obj
|
||||||
|
|
||||||
|
let textual: Translation = ToTextualDescription.createTextualDescriptionFor(oh, applicableWeek.ranges)
|
||||||
|
let applicableWeekRanges: { ranges: OpeningRange[][]; startingMonday: Date } = OH.createRangesForApplicableWeek(oh)
|
||||||
|
let ranges = applicableWeekRanges.ranges
|
||||||
|
let lastMonday = applicableWeekRanges.startingMonday
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div use:ariaLabel={textual} class="no-weblate">
|
||||||
|
<!-- 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 -->
|
||||||
|
<RegularOpeningHoursTable {ranges} rangeStart={lastMonday} oh={opening_hours_obj} />
|
||||||
|
{:else}
|
||||||
|
<!-- The special case that range is completely empty -->
|
||||||
|
<SpecialCase oh={opening_hours_obj} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
38
src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte
Normal file
38
src/UI/OpeningHours/Visualisation/OpeningHoursHeader.svelte
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import BaseUIElement from "../../BaseUIElement"
|
||||||
|
import Combine from "../../Base/Combine"
|
||||||
|
import { FixedUiElement } from "../../Base/FixedUiElement"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element showing an "hour" in a bubble, above or below the opening hours table
|
||||||
|
* Dumbly shows one row of what is given.
|
||||||
|
*
|
||||||
|
* Does not include lines
|
||||||
|
*/
|
||||||
|
export let availableArea: number
|
||||||
|
export let changeHours: number[]
|
||||||
|
export let changeHourText: string[]
|
||||||
|
export let earliestOpen: number
|
||||||
|
export let todayChangeMoments: Set<number>
|
||||||
|
|
||||||
|
function calcOffset(changeMoment: number) {
|
||||||
|
return (100 * (changeMoment - earliestOpen)) / availableArea
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full absolute block h-8" style="margin-top: -1rem">
|
||||||
|
{#each changeHours as changeMoment, i}
|
||||||
|
{#if calcOffset(changeMoment) >= 0 && calcOffset(changeMoment) <= 100}
|
||||||
|
<div style={`left:${calcOffset(changeMoment)}%; margin-top: 0.1rem`}
|
||||||
|
class="block absolute top-0 m-0 h-full box-border ohviz-time-indication">
|
||||||
|
<div
|
||||||
|
style="left: -50%; word-break: initial;"
|
||||||
|
class:border-opacity-50={!todayChangeMoments?.has(changeMoment)}
|
||||||
|
class="relative h-fit bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black">
|
||||||
|
{changeHourText[i]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
|
@ -19,7 +19,7 @@
|
||||||
startOfDay.setHours(0, 0, 0, 0)
|
startOfDay.setHours(0, 0, 0, 0)
|
||||||
let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
|
let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
|
||||||
// prettier-ignore
|
// 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
|
let startPercentage = (100 * startpoint) / availableArea
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* Wrapper around 'OpeningHours' so that the latter can deal with the opening_hours object directly
|
||||||
|
*/
|
||||||
|
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
|
import type opening_hours from "opening_hours"
|
||||||
|
import Translations from "../../i18n/Translations"
|
||||||
|
import Loading from "../../Base/Loading.svelte"
|
||||||
|
import Tr from "../../Base/Tr.svelte"
|
||||||
|
import OpeningHours from "./OpeningHours.svelte"
|
||||||
|
|
||||||
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
|
export let opening_hours_obj: Store<opening_hours | "error">
|
||||||
|
export let key: string
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#if $tags._country === undefined}
|
||||||
|
<Loading>
|
||||||
|
<Tr t={Translations.t.general.opening_hours.loadingCountry} />
|
||||||
|
</Loading>
|
||||||
|
{:else if $opening_hours_obj === undefined}
|
||||||
|
<div class="alert">No opening hours defined with key {key}</div>
|
||||||
|
{:else if $opening_hours_obj === "error"}
|
||||||
|
<Tr cls="alert" t={Translations.t.general.opening_hours.error_loading} />
|
||||||
|
{:else}
|
||||||
|
<OpeningHours opening_hours_obj={$opening_hours_obj} />
|
||||||
|
{/if}
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* The main visualisation which shows ranges, one or more top/bottom headers, ...
|
||||||
|
* Does not handle the special cases
|
||||||
|
*/
|
||||||
|
import opening_hours from "opening_hours"
|
||||||
|
import OpeningHoursHeader from "./OpeningHoursHeader.svelte"
|
||||||
|
import { default as Transl } from "../../Base/Tr.svelte" /* The IDE confuses <tr> (table row) and <Tr> (translation) as they are normally case insensitive -> import under a different name */
|
||||||
|
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
|
||||||
|
import { Translation } from "../../i18n/Translation"
|
||||||
|
import Translations from "../../i18n/Translations"
|
||||||
|
import { OH } from "../OpeningHours"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
|
||||||
|
export let oh: opening_hours
|
||||||
|
export let ranges: {
|
||||||
|
isOpen: boolean
|
||||||
|
isSpecial: boolean
|
||||||
|
comment: string
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date
|
||||||
|
}[][] // Per weekday
|
||||||
|
export let rangeStart: Date
|
||||||
|
let isWeekstable: boolean = oh.isWeekStable()
|
||||||
|
let today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
let todayIndex = Math.ceil((today.getTime() - rangeStart.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
|
||||||
|
let weekdayRanges = ranges.map(ranges => ranges.filter(r => r.startDate.getDay() != 0 && r.startDate.getDay() != 6))
|
||||||
|
let weekendRanges = ranges.map(ranges => ranges.filter(r => r.startDate.getDay() == 0 || r.startDate.getDay() == 6))
|
||||||
|
let todayRanges = ranges.map(((r, i) => r.filter(() => i === todayIndex)))
|
||||||
|
|
||||||
|
|
||||||
|
const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges)
|
||||||
|
const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges)
|
||||||
|
|
||||||
|
const weekdayHeaders: {
|
||||||
|
changeHours: number[];
|
||||||
|
changeTexts: string[]
|
||||||
|
}[] = OH.partitionOHForDistance(changeHours, changeHourText)
|
||||||
|
const weekendDayHeaders: {
|
||||||
|
changeHours: number[];
|
||||||
|
changeTexts: string[]
|
||||||
|
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
|
||||||
|
|
||||||
|
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend])
|
||||||
|
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
|
||||||
|
// 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
|
||||||
|
let earliestOpen = Math.min(8 * 60 * 60, ...changeHours)
|
||||||
|
// We always make sure there is 30m of leeway in order to give enough room for the closing entry
|
||||||
|
let latestclose = Math.max(19 * 60 * 60, Math.max(...changeHours) + 30 * 60)
|
||||||
|
let availableArea = latestclose - earliestOpen
|
||||||
|
|
||||||
|
function calcLineOffset(moment: number) {
|
||||||
|
return 100 * (moment - earliestOpen) / availableArea
|
||||||
|
}
|
||||||
|
|
||||||
|
let 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
|
||||||
|
]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div class="w-full h-fit relative">
|
||||||
|
{#each allChangeMoments as moment}
|
||||||
|
<div class="w-full absolute h-full">
|
||||||
|
<div class="w-full h-full flex">
|
||||||
|
<div style="height: 5rem; width: 5%; min-width: 2.75rem" />
|
||||||
|
<div class="grow">
|
||||||
|
|
||||||
|
<div class="border-x h-full"
|
||||||
|
style={`width: calc( ${calcLineOffset(moment)}% ); border-color: ${todayChangeMoments.has(moment) ? "#000" : "#bbb"}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<table class="w-full" style="border-collapse: collapse; word-break: normal; word-wrap: normal">
|
||||||
|
{#each weekdayHeaders as weekdayHeader}
|
||||||
|
<tr>
|
||||||
|
<td style="width: 5%; min-width: 2.75rem;"></td>
|
||||||
|
<td class="relative h-8">
|
||||||
|
<OpeningHoursHeader {earliestOpen} {availableArea} changeHours={weekdayHeader.changeHours}
|
||||||
|
{todayChangeMoments}
|
||||||
|
changeHourText={weekdayHeader.changeTexts} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each weekdays as weekday, i}
|
||||||
|
<tr class:interactive={i >= 5}>
|
||||||
|
<td style="width: 5%">
|
||||||
|
<Transl t={weekday} />
|
||||||
|
</td>
|
||||||
|
<td class="relative p-0 m-0" class:ohviz-today={i===todayIndex}>
|
||||||
|
<div class="w-full" style="margin-left: -0px">
|
||||||
|
{#each ranges[i] as range}
|
||||||
|
<OpeningHoursRangeElement
|
||||||
|
{availableArea}
|
||||||
|
{earliestOpen}
|
||||||
|
{latestclose}
|
||||||
|
{range}
|
||||||
|
{isWeekstable}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each weekendDayHeaders as weekdayHeader}
|
||||||
|
<tr>
|
||||||
|
<td style="width: 5%"></td>
|
||||||
|
<td class="relative h-8">
|
||||||
|
<OpeningHoursHeader {earliestOpen} {availableArea} changeHours={weekdayHeader.changeHours}
|
||||||
|
{todayChangeMoments}
|
||||||
|
changeHourText={weekdayHeader.changeTexts} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -5,12 +5,11 @@ import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState
|
||||||
import { HistogramViz } from "./Popup/HistogramViz"
|
import { HistogramViz } from "./Popup/HistogramViz"
|
||||||
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
||||||
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
||||||
import { UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import AllTagsPanel from "./Popup/AllTagsPanel/AllTagsPanel.svelte"
|
import AllTagsPanel from "./Popup/AllTagsPanel/AllTagsPanel.svelte"
|
||||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||||
import { Translation } from "./i18n/Translation"
|
import { Translation } from "./i18n/Translation"
|
||||||
import Translations from "./i18n/Translations"
|
import Translations from "./i18n/Translations"
|
||||||
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
|
|
||||||
import AutoApplyButtonVis from "./Popup/AutoApplyButtonVis"
|
import AutoApplyButtonVis from "./Popup/AutoApplyButtonVis"
|
||||||
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
|
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
|
||||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||||
|
@ -43,6 +42,9 @@ import {
|
||||||
} from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
|
} from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
|
||||||
import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
|
import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
|
||||||
import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.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 {
|
export default class SpecialVisualizations {
|
||||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
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)}`",
|
"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<any>, args) => {
|
constr: (state, tagSource: UIEventSource<any>, args) => {
|
||||||
const [key, prefix, postfix] = args
|
const [key, prefix, postfix] = args
|
||||||
return new OpeningHoursVisualization(tagSource, key, prefix, postfix)
|
const openingHoursStore: Store<opening_hours | "error" | undefined> = OH.CreateOhObjectStore(tagSource, key, prefix, postfix)
|
||||||
|
return new SvelteUIElement(OpeningHoursWithError, {
|
||||||
|
tags: tagSource,
|
||||||
|
key,
|
||||||
|
opening_hours_obj: openingHoursStore
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue