forked from MapComplete/MapComplete
Feature(opening_hours): correctly display "open ended" opening hours, see #2438
This commit is contained in:
parent
a01be66592
commit
9689cdfb65
7 changed files with 125 additions and 45 deletions
|
@ -1530,10 +1530,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.mr-0\.5 {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
|
|
@ -145,6 +145,12 @@
|
|||
font-size: smaller;
|
||||
}
|
||||
|
||||
.open-end {
|
||||
border-right: none !important;
|
||||
border-radius: 0;
|
||||
background: linear-gradient(to right, #99e7ffff, #99e7ff00 );
|
||||
}
|
||||
|
||||
.ohviz-today .ohviz-range {
|
||||
border: 1.5px solid black;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* The interactive table to select opening hours
|
||||
*/
|
||||
import { UIEventSource } from "../../../../Logic/UIEventSource"
|
||||
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
|
||||
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface OpeningHour {
|
|||
export interface OpeningRange {
|
||||
isOpen: boolean
|
||||
isSpecial: boolean
|
||||
openEnd: boolean
|
||||
comment: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
|
@ -464,14 +465,10 @@ const changes = OH.allChangeMoments([[{isOpen: true, isSpecial: false, comment:
|
|||
changes // => [[36000,61200], ["10:00", "17:00"]]
|
||||
*/
|
||||
public static allChangeMoments(
|
||||
ranges: {
|
||||
isOpen: boolean
|
||||
isSpecial: boolean
|
||||
comment: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}[][]
|
||||
ranges: OpeningRange[][],
|
||||
includeOpenEnds = false,
|
||||
): [number[], string[]] {
|
||||
|
||||
const changeHours: number[] = []
|
||||
const changeHourText: string[] = []
|
||||
|
||||
|
@ -480,7 +477,8 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
|
||||
for (const weekday of ranges) {
|
||||
for (const range of weekday) {
|
||||
if (!range.isOpen && !range.isSpecial) {
|
||||
|
||||
if (!(range.openEnd || range.isOpen || range.isSpecial)) {
|
||||
continue
|
||||
}
|
||||
const startOfDay: Date = new Date(range.startDate)
|
||||
|
@ -496,6 +494,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
)
|
||||
}
|
||||
|
||||
if(range.openEnd && !includeOpenEnds){
|
||||
continue
|
||||
}
|
||||
|
||||
// The number of seconds till between the start of the day and closing
|
||||
const changeMomentEnd: number =
|
||||
(range.endDate.getTime() - startOfDay.getTime()) / 1000
|
||||
|
@ -558,7 +560,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
.mapD(
|
||||
(ohtext) => {
|
||||
try {
|
||||
return OH.CreateOhObject(<any>tags.data, ohtext, country.data)
|
||||
return OH.createOhObject(<any>tags.data, ohtext, country.data)
|
||||
} catch (e) {
|
||||
return "error"
|
||||
}
|
||||
|
@ -567,19 +569,18 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
)
|
||||
}
|
||||
|
||||
public static CreateOhObject(
|
||||
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
|
||||
public static createOhObject(
|
||||
tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
|
||||
textToParse: string,
|
||||
country?: string
|
||||
country: string,
|
||||
) {
|
||||
// noinspection JSPotentiallyInvalidConstructorUsage
|
||||
return new opening_hours(
|
||||
textToParse,
|
||||
{
|
||||
lat: tags._lat,
|
||||
lon: tags._lon,
|
||||
address: {
|
||||
country_code: country.toLowerCase(),
|
||||
country_code: country?.toLowerCase(),
|
||||
state: undefined,
|
||||
},
|
||||
},
|
||||
|
@ -705,7 +706,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
}
|
||||
|
||||
/* We calculate the ranges when it is opened! */
|
||||
return { startingMonday: lastMonday, ranges: OH.GetRanges(oh, lastMonday, nextSunday) }
|
||||
return { startingMonday: lastMonday, ranges: OH.getRanges(oh, lastMonday, nextSunday) }
|
||||
}
|
||||
|
||||
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
|
||||
|
@ -742,8 +743,9 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
/**
|
||||
* Calculates when the business is opened (or on holiday) between two dates.
|
||||
* Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
|
||||
*
|
||||
*/
|
||||
public static GetRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
|
||||
public static getRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
|
||||
const values = [[], [], [], [], [], [], []]
|
||||
|
||||
const start = new Date(from)
|
||||
|
@ -752,25 +754,41 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
|
||||
const iterator = oh.getIterator(start)
|
||||
|
||||
let prevValue = undefined
|
||||
let prevValue: OpeningRange = undefined
|
||||
while (iterator.advance(to)) {
|
||||
if (prevValue) {
|
||||
prevValue.endDate = iterator.getDate() as Date
|
||||
if (prevValue.openEnd) {
|
||||
prevValue.endDate = new Date(prevValue.startDate.getTime())
|
||||
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
|
||||
}
|
||||
}
|
||||
const endDate = new Date(iterator.getDate()) as Date
|
||||
endDate.setHours(0, 0, 0, 0)
|
||||
endDate.setDate(endDate.getDate() + 1)
|
||||
const value = {
|
||||
let comment = iterator.getComment()
|
||||
// See https://github.com/opening-hours/opening_hours.js/
|
||||
const openEnd = comment === "Specified as open end. Closing time was guessed."
|
||||
if (openEnd) {
|
||||
comment = undefined
|
||||
}
|
||||
const value: OpeningRange = {
|
||||
isSpecial: iterator.getUnknown(),
|
||||
isOpen: iterator.getState(),
|
||||
comment: iterator.getComment(),
|
||||
startDate: iterator.getDate() as Date,
|
||||
endDate: endDate, // Should be overwritten by the next iteration
|
||||
comment,
|
||||
openEnd,
|
||||
startDate: iterator.getDate(),
|
||||
endDate, // The end date gets overwritten in the next iteration, see the first lines of this loop
|
||||
}
|
||||
prevValue = value
|
||||
|
||||
if (value.comment === undefined && !value.isOpen && !value.isSpecial) {
|
||||
// simply closed, nothing special here
|
||||
// siif (prevValue) {
|
||||
prevValue.endDate = iterator.getDate() as Date
|
||||
if (prevValue.openEnd) {
|
||||
prevValue.endDate = new Date(prevValue.startDate.getTime())
|
||||
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -780,6 +798,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
|||
// Get day: sunday is 0, monday is 1. We move everything so that monday == 0
|
||||
values[(value.startDate.getDay() + 6) % 7].push(value)
|
||||
}
|
||||
if (prevValue && prevValue.openEnd) {
|
||||
prevValue.endDate = new Date(prevValue.startDate.getTime())
|
||||
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,26 @@
|
|||
<script lang="ts">
|
||||
import type { OpeningRange } from "../OpeningHours"
|
||||
|
||||
/**
|
||||
* A single bar in the Opening-Hours visualisations table, eventually with a text
|
||||
*/
|
||||
export let availableArea: number
|
||||
export let earliestOpen: number
|
||||
export let latestclose: number
|
||||
export let range: {
|
||||
isOpen: boolean
|
||||
isSpecial: boolean
|
||||
comment: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}
|
||||
export let range: OpeningRange
|
||||
export let isWeekstable: boolean
|
||||
|
||||
let textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString())
|
||||
let startOfDay: Date = new Date(range.startDate)
|
||||
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) / availableArea
|
||||
let startPercentage = (100 * startpoint) / availableArea
|
||||
console.log("Available area is", availableArea, "for", range.endDate.toISOString())
|
||||
</script>
|
||||
|
||||
{#if !range.isOpen && !range.isSpecial}
|
||||
{#if range.openEnd}
|
||||
<div class="ohviz-range open-end" style={`left:${startPercentage}%; width:${width}%`}/>
|
||||
{:else if !range.isOpen && !range.isSpecial}
|
||||
<div class="ohviz-day-off">{textToShow}</div>
|
||||
{:else}
|
||||
<div class="ohviz-range" style={`left:${startPercentage}%; width:${width}%`}>{textToShow}</div>
|
||||
|
|
|
@ -10,16 +10,12 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { OH } from "../OpeningHours"
|
||||
import type { OpeningRange } 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 ranges: OpeningRange[][] // Per weekday
|
||||
export let rangeStart: Date
|
||||
let isWeekstable: boolean = oh.isWeekStable()
|
||||
let today = new Date()
|
||||
|
@ -34,8 +30,12 @@
|
|||
)
|
||||
let todayRanges = ranges.map((r, i) => r.filter(() => i === todayIndex))
|
||||
|
||||
// For the header
|
||||
const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges)
|
||||
// For the header
|
||||
const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges)
|
||||
// To calculate the range to display
|
||||
const [changeHoursIncludingOpenEnd] = OH.allChangeMoments(weekdayRanges, true)
|
||||
|
||||
const weekdayHeaders: {
|
||||
changeHours: number[]
|
||||
|
@ -51,9 +51,9 @@
|
|||
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)
|
||||
let earliestOpen = Math.min(8 * 60 * 60, ...changeHoursIncludingOpenEnd)
|
||||
// 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 latestclose = Math.max(19 * 60 * 60, Math.max(...changeHoursIncludingOpenEnd) + 30 * 60)
|
||||
let availableArea = latestclose - earliestOpen
|
||||
|
||||
function calcLineOffset(moment: number) {
|
||||
|
|
56
test/UI/OpeningHours.spec.ts
Normal file
56
test/UI/OpeningHours.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { describe, it } from "vitest"
|
||||
import { REPORT_REASONS } from "panoramax-js"
|
||||
import Translations from "../../src/UI/i18n/Translations"
|
||||
import { OH, OpeningRange } from "../../src/UI/OpeningHours/OpeningHours"
|
||||
import { expect } from "chai"
|
||||
|
||||
describe("OH", () => {
|
||||
describe("getRanges", () => {
|
||||
it("standard opening hours", () => {
|
||||
const oh_obj = OH.createOhObject({
|
||||
"opening_hours": "10:00-18:00",
|
||||
_lat: 0, _lon: 0, _country: "be",
|
||||
}, "10:00-18:00", "be")
|
||||
const ranges = OH.getRanges(oh_obj, new Date("2025-06-10T00:00:00Z"), new Date("2025-06-11T00:00:00Z"))
|
||||
// Deep equal compares the dates correctly
|
||||
expect(ranges[1]).to.deep.equal([
|
||||
{
|
||||
"comment": undefined,
|
||||
"endDate": new Date("2025-06-10T16:00:00.000Z"),
|
||||
"isOpen": true,
|
||||
"isSpecial": false,
|
||||
"openEnd": false,
|
||||
"startDate": new Date("2025-06-10T08:00:00.000Z"),
|
||||
},
|
||||
])
|
||||
})
|
||||
it("open ended opening hours", () => {
|
||||
const oh_obj = OH.createOhObject({
|
||||
"opening_hours": "10:00-18:00+",
|
||||
_lat: 0, _lon: 0, _country: "be",
|
||||
}, "10:00+", "be")
|
||||
const ranges = OH.getRanges(oh_obj, new Date("2025-06-09T00:00:00Z"), new Date("2025-06-16T00:00:00Z"))
|
||||
// Deep equal compares the dates correctly
|
||||
expect(ranges[1]).to.deep.equal([
|
||||
{
|
||||
"comment": undefined,
|
||||
"endDate": new Date("2025-06-10T11:00:00.000Z"),
|
||||
"isOpen": false,
|
||||
"isSpecial": true,
|
||||
"openEnd": true,
|
||||
"startDate": new Date("2025-06-10T08:00:00.000Z"),
|
||||
},
|
||||
])
|
||||
expect(ranges.at(-1)).to.deep.equal([
|
||||
{
|
||||
"comment": undefined,
|
||||
"endDate": new Date("2025-06-15T11:00:00.000Z"),
|
||||
"isOpen": false,
|
||||
"isSpecial": true,
|
||||
"openEnd": true,
|
||||
"startDate": new Date("2025-06-15T08:00:00.000Z"),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue