forked from MapComplete/MapComplete
Themes(postboxes): add 'points_in_time' input element, add 'collection_times' to posbox theme, fix #757
This commit is contained in:
parent
4c0a42d5b6
commit
602c51bcb9
15 changed files with 425 additions and 68 deletions
|
@ -139,10 +139,24 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
"images",
|
|
||||||
{
|
{
|
||||||
"id": "minimap",
|
"id": "collection_times",
|
||||||
"render": "{minimap(18): height: 5rem; overflow: hidden; border-radius:3rem; }"
|
"question": {
|
||||||
|
"en": "When is the mail collected?"
|
||||||
|
},
|
||||||
|
"render": {
|
||||||
|
"before": {
|
||||||
|
"en": "<h3>Collection times</h3>"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"key": "collection_times",
|
||||||
|
"type": "points_in_time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"freeform": {
|
||||||
|
"key": "collection_times",
|
||||||
|
"type": "points_in_time"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"builtin": "operator",
|
"builtin": "operator",
|
||||||
|
|
|
@ -353,6 +353,7 @@
|
||||||
"open_during_ph": "During a public holiday, it is",
|
"open_during_ph": "During a public holiday, it is",
|
||||||
"open_until": "Closes at {date}",
|
"open_until": "Closes at {date}",
|
||||||
"opensAt": "from",
|
"opensAt": "from",
|
||||||
|
"ph": "Public holidays",
|
||||||
"ph_closed": "closed",
|
"ph_closed": "closed",
|
||||||
"ph_not_known": " ",
|
"ph_not_known": " ",
|
||||||
"ph_open": "open",
|
"ph_open": "open",
|
||||||
|
@ -373,6 +374,12 @@
|
||||||
"versionInfo": "v{version} - generated on {date}"
|
"versionInfo": "v{version} - generated on {date}"
|
||||||
},
|
},
|
||||||
"pickLanguage": "Select language",
|
"pickLanguage": "Select language",
|
||||||
|
"points_in_time": {
|
||||||
|
"closed": "Closed",
|
||||||
|
"daily": "Every day",
|
||||||
|
"weekdays": "On weekdays",
|
||||||
|
"weekends": "On weekends"
|
||||||
|
},
|
||||||
"poweredByMapComplete": "Powered by MapComplete - crowdsourced, thematic maps with OpenStreetMap",
|
"poweredByMapComplete": "Powered by MapComplete - crowdsourced, thematic maps with OpenStreetMap",
|
||||||
"poweredByOsm": "Powered by OpenStreetMap",
|
"poweredByOsm": "Powered by OpenStreetMap",
|
||||||
"questionBox": {
|
"questionBox": {
|
||||||
|
|
|
@ -9157,6 +9157,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
|
"collection_times": {
|
||||||
|
"question": "When is the mail collected?",
|
||||||
|
"render": {
|
||||||
|
"before": "<h3>Collection times</h3>"
|
||||||
|
}
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"override": {
|
"override": {
|
||||||
"question": "Who operates this postbox?",
|
"question": "Who operates this postbox?",
|
||||||
|
|
|
@ -142,6 +142,12 @@ export class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJso
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
json.freeform["type"] === "points_in_time" &&
|
||||||
|
txt.indexOf("{points_in_time(") >= 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
|
||||||
if (
|
if (
|
||||||
keyFirstArg.some(
|
keyFirstArg.some(
|
||||||
|
|
53
src/UI/CollectionTimes/CollectionTimeRange.svelte
Normal file
53
src/UI/CollectionTimes/CollectionTimeRange.svelte
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/*
|
||||||
|
* Visualises collection times for a single collection range
|
||||||
|
*/
|
||||||
|
import { OH } from "../OpeningHours/OpeningHours"
|
||||||
|
import type { OpeningRange } from "../OpeningHours/OpeningHours"
|
||||||
|
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
|
||||||
|
export let range: OpeningRange[][]
|
||||||
|
const wt = Translations.t.general.weekdays
|
||||||
|
const weekdays: Translation[] = [wt.sunday, wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday]
|
||||||
|
let allTheSame = OH.weekdaysIdentical(range, 0, range.length - 1)
|
||||||
|
let today = new Date().getDay()
|
||||||
|
|
||||||
|
let dayZero = -1
|
||||||
|
for (let i = 0; i < range.length; i++) {
|
||||||
|
const moments = range[i]
|
||||||
|
if (moments.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const moment = moments[0]
|
||||||
|
const day = moment.startDate.getDay()
|
||||||
|
dayZero = day - i
|
||||||
|
}
|
||||||
|
function isToday(i:number ){
|
||||||
|
i = (i+dayZero) % 7
|
||||||
|
return i === today
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if allTheSame && range[0]?.length > 0}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<slot />
|
||||||
|
{#each range[0] as moment (moment)}
|
||||||
|
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if dayZero >= 0 } <!-- /*If dayZero == -1, then we got no valid values at all*/ -->
|
||||||
|
{#each range as moments, i (moments)}
|
||||||
|
<div class="flex gap-x-4 justify-between w-full px-2" class:interactive={isToday(i)} class:text-bold={isToday(i)} >
|
||||||
|
<Tr t={weekdays[(i + dayZero) % 7]} />
|
||||||
|
{#if range[i].length > 0}
|
||||||
|
{#each moments as moment (moment)}
|
||||||
|
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Tr cls="italic subtle" t={Translations.t.general.points_in_time.closed}/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
55
src/UI/CollectionTimes/CollectionTimes.svelte
Normal file
55
src/UI/CollectionTimes/CollectionTimes.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import CollectionTimeRange from "./CollectionTimeRange.svelte"
|
||||||
|
import opening_hours from "opening_hours"
|
||||||
|
import { OH } from "../OpeningHours/OpeningHours"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
|
||||||
|
export let times: opening_hours
|
||||||
|
let monday = OH.getMondayBefore(new Date())
|
||||||
|
let sunday = new Date()
|
||||||
|
sunday.setTime(monday.getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||||
|
let ranges = OH.getRanges(times, monday, sunday)
|
||||||
|
let weekdays = ranges.slice(0, 5)
|
||||||
|
let weekend = ranges.slice(5, 7)
|
||||||
|
let everyDaySame = OH.weekdaysIdentical(ranges, 0, ranges.length - 1)
|
||||||
|
let weekdaysAndWeekendsSame = OH.weekdaysIdentical(weekdays, 0, 4) && OH.weekdaysIdentical(weekend, 0, 1)
|
||||||
|
const t = Translations.t.general.points_in_time
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="m-4 border">
|
||||||
|
|
||||||
|
{#if everyDaySame || !weekdaysAndWeekendsSame}
|
||||||
|
<CollectionTimeRange range={ranges}>
|
||||||
|
<Tr t={t.daily} />
|
||||||
|
</CollectionTimeRange>
|
||||||
|
{:else if times.isWeekStable()}
|
||||||
|
<div class="flex flex-col w-fit">
|
||||||
|
<CollectionTimeRange range={weekdays}>
|
||||||
|
<Tr t={t.weekdays} />
|
||||||
|
|
||||||
|
</CollectionTimeRange>
|
||||||
|
<CollectionTimeRange range={weekend}>
|
||||||
|
<Tr t={t.weekends} />
|
||||||
|
|
||||||
|
</CollectionTimeRange>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each ranges as range (range)}
|
||||||
|
{#if range.length > 0}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
{range[0].startDate.toLocaleDateString()}
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
{#each range as moment}
|
||||||
|
<div>
|
||||||
|
{moment.startDate.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* Multiple 'SingleCollectionTime'-rules togehter
|
||||||
|
*/
|
||||||
|
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
|
||||||
|
import SingleCollectionTime from "./SingleCollectionTime.svelte"
|
||||||
|
import { Utils } from "../../../../Utils"
|
||||||
|
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
|
|
||||||
|
export let value: UIEventSource<string>
|
||||||
|
|
||||||
|
let initialRules: string[] = Utils.NoEmpty(value.data?.split(";")?.map(v => v.trim()))
|
||||||
|
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
|
||||||
|
initialRules?.map(v => new UIEventSource(v)) ?? []
|
||||||
|
)
|
||||||
|
if(singleRules.data.length === 0){
|
||||||
|
singleRules.data.push(new UIEventSource(undefined))
|
||||||
|
}
|
||||||
|
singleRules.bindD(stores => Stores.concat(stores)).addCallbackAndRunD(subrules => {
|
||||||
|
console.log("Setting subrules", subrules)
|
||||||
|
value.set(Utils.NoEmpty(subrules).join("; "))
|
||||||
|
})
|
||||||
|
|
||||||
|
function rm(rule: UIEventSource){
|
||||||
|
const index = singleRules.data.indexOf(rule)
|
||||||
|
singleRules.data.splice(index, 1)
|
||||||
|
singleRules.ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="interactive">
|
||||||
|
|
||||||
|
{#each $singleRules as rule}
|
||||||
|
<SingleCollectionTime value={rule}>
|
||||||
|
<svelte:fragment slot="right">
|
||||||
|
{#if $singleRules.length > 1}
|
||||||
|
|
||||||
|
<button on:click={() => { rm(rule) }} class="as-link">
|
||||||
|
<TrashIcon class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</SingleCollectionTime>
|
||||||
|
{/each}
|
||||||
|
<button on:click={() => {singleRules.data.push(new UIEventSource(undefined)); singleRules.ping()}}>Add schedule
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import PlusCircle from "@rgossiaux/svelte-heroicons/solid/PlusCircle"
|
||||||
|
import TimeInput from "../TimeInput.svelte"
|
||||||
|
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
|
import Checkbox from "../../../Base/Checkbox.svelte"
|
||||||
|
import Tr from "../../../Base/Tr.svelte"
|
||||||
|
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
|
||||||
|
import Translations from "../../../i18n/Translations"
|
||||||
|
import { OH } from "../../../OpeningHours/OpeningHours"
|
||||||
|
import { Utils } from "../../../../Utils"
|
||||||
|
|
||||||
|
export let value: UIEventSource<string>
|
||||||
|
|
||||||
|
const wt = Translations.t.general.weekdays.abbreviations
|
||||||
|
/*
|
||||||
|
Single rule for collection times, e.g. "Mo-Fr 10:00, 17:00"
|
||||||
|
*/
|
||||||
|
let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph]
|
||||||
|
|
||||||
|
let initialTimes= Utils.NoEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim())??[])
|
||||||
|
let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t)))
|
||||||
|
if(values.data.length === 0){
|
||||||
|
values.data.push(new UIEventSource(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let daysOfTheWeek = [...OH.days, "PH"]
|
||||||
|
let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false))
|
||||||
|
|
||||||
|
let initialDays = Utils.NoEmpty(value.data?.split(" ")?.[0]?.split(",") ?? [])
|
||||||
|
for (const initialDay of initialDays) {
|
||||||
|
if (initialDay.indexOf("-") > 0) {
|
||||||
|
let [start, end] = initialDay.split("-")
|
||||||
|
let startindex = daysOfTheWeek.indexOf(start)
|
||||||
|
let stopindex = daysOfTheWeek.indexOf(end)
|
||||||
|
for (let i = startindex; i <= stopindex; i++) {
|
||||||
|
selectedDays[i]?.set(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let index = daysOfTheWeek.indexOf(initialDay)
|
||||||
|
if (index >= 0) {
|
||||||
|
selectedDays[index]?.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedDaysBound = Stores.concat(selectedDays)
|
||||||
|
.mapD(days => Utils.NoNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined)))
|
||||||
|
let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values))
|
||||||
|
.mapD(values => Utils.NoEmpty(values))
|
||||||
|
valuesConcat.mapD(times => {
|
||||||
|
console.log(times)
|
||||||
|
times = Utils.NoNull(times)
|
||||||
|
if (!times || times?.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
times?.sort(/*concatted, new array*/)
|
||||||
|
return (Utils.NoEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
|
||||||
|
}, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v))
|
||||||
|
|
||||||
|
function selectWeekdays() {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
selectedDays[i].set(true)
|
||||||
|
}
|
||||||
|
for (let i = 5; i < selectedDays.length; i++) {
|
||||||
|
selectedDays[i].set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDays() {
|
||||||
|
for (let i = 0; i < selectedDays.length; i++) {
|
||||||
|
selectedDays[i].set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-xl my-2 p-2 low-interaction flex w-full justify-between flex-wrap">
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
{#each $values as value, i}
|
||||||
|
<div class="flex mx-4 gap-x-1 items-center">
|
||||||
|
<TimeInput {value} />
|
||||||
|
{#if $values.length > 1}
|
||||||
|
<button class="as-link">
|
||||||
|
<TrashIcon class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<button on:click={() => {values.data.push(new UIEventSource(undefined)); values.ping()}}>
|
||||||
|
<PlusCircle class="w-6 h-6" />
|
||||||
|
Add time
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-fit flex-wrap">
|
||||||
|
{#each daysOfTheWeek as day, i}
|
||||||
|
<div class="w-fit">
|
||||||
|
<Checkbox selected={selectedDays[i]}>
|
||||||
|
<Tr t={weekdays[i]} />
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap justify-between w-full">
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-x-4">
|
||||||
|
<button class="as-link text-sm" on:click={() => selectWeekdays()}>Select weekdays</button>
|
||||||
|
<button class="as-link text-sm" on:click={() => clearDays()}>Clear days</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<slot name="right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -21,6 +21,7 @@
|
||||||
import WikidataInputHelper from "./Helpers/WikidataInputHelper.svelte"
|
import WikidataInputHelper from "./Helpers/WikidataInputHelper.svelte"
|
||||||
import DistanceInput from "./Helpers/DistanceInput.svelte"
|
import DistanceInput from "./Helpers/DistanceInput.svelte"
|
||||||
import TimeInput from "./Helpers/TimeInput.svelte"
|
import TimeInput from "./Helpers/TimeInput.svelte"
|
||||||
|
import CollectionTimes from "./Helpers/CollectionTimes/CollectionTimes.svelte"
|
||||||
|
|
||||||
export let type: ValidatorType
|
export let type: ValidatorType
|
||||||
export let value: UIEventSource<string | object>
|
export let value: UIEventSource<string | object>
|
||||||
|
@ -42,6 +43,8 @@
|
||||||
<DateInput {value} />
|
<DateInput {value} />
|
||||||
{:else if type === "time"}
|
{:else if type === "time"}
|
||||||
<TimeInput {value} />
|
<TimeInput {value} />
|
||||||
|
{:else if type === "points_in_time"}
|
||||||
|
<CollectionTimes {value} />
|
||||||
{:else if type === "color"}
|
{:else if type === "color"}
|
||||||
<ColorInput {value} />
|
<ColorInput {value} />
|
||||||
{:else if type === "image"}
|
{:else if type === "image"}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
import { MapProperties } from "../../Models/MapProperties"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import { ValidatorType } from "./Validators"
|
||||||
|
|
||||||
export interface InputHelperProperties {
|
export interface InputHelperProperties {
|
||||||
/**
|
/**
|
||||||
|
@ -25,11 +26,12 @@ export interface InputHelperProperties {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class InputHelpers {
|
export default class InputHelpers {
|
||||||
public static hideInputField: string[] = ["translation", "simple_tag", "tag","time"]
|
public static hideInputField: ValidatorType[] = ["translation", "simple_tag", "tag","time"]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a mapProperties-object for the given properties.
|
* Constructs a mapProperties-object for the given properties.
|
||||||
* Assumes that the first helper-args contains the desired zoom-level
|
* Assumes that the first helper-args contains the desired zoom-level
|
||||||
|
* Used for the 'direction' input helper
|
||||||
* @param properties
|
* @param properties
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,6 +29,7 @@ import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValida
|
||||||
import CurrencyValidator from "./Validators/CurrencyValidator"
|
import CurrencyValidator from "./Validators/CurrencyValidator"
|
||||||
import RegexValidator from "./Validators/RegexValidator"
|
import RegexValidator from "./Validators/RegexValidator"
|
||||||
import { TimeValidator } from "./Validators/TimeValidator"
|
import { TimeValidator } from "./Validators/TimeValidator"
|
||||||
|
import CollectionTimesValidator from "./Validators/CollectionTimesValidator"
|
||||||
|
|
||||||
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ export default class Validators {
|
||||||
"pfloat",
|
"pfloat",
|
||||||
"phone",
|
"phone",
|
||||||
"pnat",
|
"pnat",
|
||||||
|
"points_in_time",
|
||||||
"regex",
|
"regex",
|
||||||
"simple_tag",
|
"simple_tag",
|
||||||
"slope",
|
"slope",
|
||||||
|
@ -97,6 +99,7 @@ export default class Validators {
|
||||||
new NameSuggestionIndexValidator(),
|
new NameSuggestionIndexValidator(),
|
||||||
new CurrencyValidator(),
|
new CurrencyValidator(),
|
||||||
new RegexValidator(),
|
new RegexValidator(),
|
||||||
|
new CollectionTimesValidator()
|
||||||
]
|
]
|
||||||
|
|
||||||
private static _byType = Validators._byTypeConstructor()
|
private static _byType = Validators._byTypeConstructor()
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import StringValidator from "./StringValidator"
|
||||||
|
|
||||||
|
export default class CollectionTimesValidator extends StringValidator{
|
||||||
|
constructor() {
|
||||||
|
super("points_in_time", "'Points in time' are points according to a fixed schedule, e.g. 'every monday at 10:00'. They are typically used for postbox collection times or times of mass at a place of worship")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import opening_hours from "opening_hours"
|
import opening_hours, { mode, optional_conf } from "opening_hours"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import { Translation, TypedTranslation } from "../i18n/Translation"
|
import { Translation, TypedTranslation } from "../i18n/Translation"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
|
@ -25,7 +25,7 @@ export interface OpeningRange {
|
||||||
* Various utilities manipulating opening hours
|
* Various utilities manipulating opening hours
|
||||||
*/
|
*/
|
||||||
export class OH {
|
export class OH {
|
||||||
private static readonly days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
public static readonly days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
||||||
private static readonly daysIndexed = {
|
private static readonly daysIndexed = {
|
||||||
mo: 0,
|
mo: 0,
|
||||||
tu: 1,
|
tu: 1,
|
||||||
|
@ -570,7 +570,8 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
||||||
public static createOhObject(
|
public static createOhObject(
|
||||||
tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
|
tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
|
||||||
textToParse: string,
|
textToParse: string,
|
||||||
country: string
|
country: string,
|
||||||
|
mode?: mode,
|
||||||
) {
|
) {
|
||||||
return new opening_hours(
|
return new opening_hours(
|
||||||
textToParse,
|
textToParse,
|
||||||
|
@ -582,11 +583,61 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
||||||
state: undefined,
|
state: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
<any>{ tag_key: "opening_hours" }
|
<optional_conf> {
|
||||||
|
tag_key: "opening_hours",
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week.
|
||||||
|
* Note: 'today' is mostly used for testing
|
||||||
|
*
|
||||||
|
* const oh = new opening_hours("mar 15 - oct 15")
|
||||||
|
* const ranges = OH.createRangesForApplicableWeek(oh, new Date(2025,4,20,10,0,0))
|
||||||
|
* ranges // => {ranges: [[],[],[],[],[],[],[]], startingMonday: new Date(2025,4,18,24,0,0)}
|
||||||
|
*/
|
||||||
|
public static createRangesForApplicableWeek(
|
||||||
|
oh: opening_hours,
|
||||||
|
today?: Date,
|
||||||
|
): {
|
||||||
|
ranges: OpeningRange[][]
|
||||||
|
startingMonday: Date
|
||||||
|
} {
|
||||||
|
today ??= new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const lastMonday = OH.getMondayBefore(today)
|
||||||
|
const nextSunday = new Date(lastMonday)
|
||||||
|
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||||
|
|
||||||
|
if (!oh.getState() && !oh.getUnknown()) {
|
||||||
|
// POI is currently closed
|
||||||
|
const nextChange: Date = oh.getNextChange()
|
||||||
|
if (
|
||||||
|
// Shop isn't gonna open anymore in this timerange
|
||||||
|
nextSunday < nextChange &&
|
||||||
|
// And we are already in the weekend to show next week
|
||||||
|
(today.getDay() == 0 || today.getDay() == 6)
|
||||||
|
) {
|
||||||
|
// We move the range to next week!
|
||||||
|
lastMonday.setDate(lastMonday.getDate() + 7)
|
||||||
|
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We calculate the ranges when it is opened! */
|
||||||
|
return { startingMonday: lastMonday, ranges: OH.getRanges(oh, lastMonday, nextSunday) }
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Checks that the days are identical
|
||||||
|
*
|
||||||
|
* @param startday start index, inclusive
|
||||||
|
* @param endday end index, INCLUSIVE (!)
|
||||||
|
*
|
||||||
* let ranges = <any> [
|
* let ranges = <any> [
|
||||||
* [
|
* [
|
||||||
* {
|
* {
|
||||||
|
@ -666,47 +717,6 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
|
||||||
* OH.weekdaysIdentical(ranges, 4, 5) // => false
|
* OH.weekdaysIdentical(ranges, 4, 5) // => false
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week.
|
|
||||||
* Note: 'today' is mostly used for testing
|
|
||||||
*
|
|
||||||
* const oh = new opening_hours("mar 15 - oct 15")
|
|
||||||
* const ranges = OH.createRangesForApplicableWeek(oh, new Date(2025,4,20,10,0,0))
|
|
||||||
* ranges // => {ranges: [[],[],[],[],[],[],[]], startingMonday: new Date(2025,4,18,24,0,0)}
|
|
||||||
*/
|
|
||||||
public static createRangesForApplicableWeek(
|
|
||||||
oh: opening_hours,
|
|
||||||
today?: Date
|
|
||||||
): {
|
|
||||||
ranges: OpeningRange[][]
|
|
||||||
startingMonday: Date
|
|
||||||
} {
|
|
||||||
today ??= new Date()
|
|
||||||
today.setHours(0, 0, 0, 0)
|
|
||||||
const lastMonday = OH.getMondayBefore(today)
|
|
||||||
const nextSunday = new Date(lastMonday)
|
|
||||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
|
||||||
|
|
||||||
if (!oh.getState() && !oh.getUnknown()) {
|
|
||||||
// POI is currently closed
|
|
||||||
const nextChange: Date = oh.getNextChange()
|
|
||||||
if (
|
|
||||||
// Shop isn't gonna open anymore in this timerange
|
|
||||||
nextSunday < nextChange &&
|
|
||||||
// And we are already in the weekend to show next week
|
|
||||||
(today.getDay() == 0 || today.getDay() == 6)
|
|
||||||
) {
|
|
||||||
// We move the range to next week!
|
|
||||||
lastMonday.setDate(lastMonday.getDate() + 7)
|
|
||||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We calculate the ranges when it is opened! */
|
|
||||||
return { startingMonday: lastMonday, ranges: OH.getRanges(oh, lastMonday, nextSunday) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
|
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
|
||||||
const monday = openingRanges[startday]
|
const monday = openingRanges[startday]
|
||||||
for (let i = startday + 1; i <= endday; i++) {
|
for (let i = startday + 1; i <= endday; i++) {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { OpeningRange } from "../OpeningHours"
|
import type { OpeningRange } from "../OpeningHours"
|
||||||
import { OH, ToTextualDescription } from "../OpeningHours"
|
import { ToTextualDescription } from "../OpeningHours"
|
||||||
import opening_hours from "opening_hours"
|
import opening_hours from "opening_hours"
|
||||||
import { ariaLabel } from "../../../Utils/ariaLabel"
|
import { ariaLabel } from "../../../Utils/ariaLabel"
|
||||||
import RegularOpeningHoursTable from "./RegularOpeningHoursTable.svelte"
|
import RegularOpeningHoursTable from "./RegularOpeningHoursTable.svelte"
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
|
||||||
SpecialVisualization,
|
|
||||||
SpecialVisualizationState,
|
|
||||||
SpecialVisualizationSvelte,
|
|
||||||
} from "../SpecialVisualization"
|
|
||||||
import { HistogramViz } from "./HistogramViz"
|
import { HistogramViz } from "./HistogramViz"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
@ -28,6 +24,7 @@ import TagRenderingEditable from "./TagRendering/TagRenderingEditable.svelte"
|
||||||
import AllTagsPanel from "./AllTagsPanel/AllTagsPanel.svelte"
|
import AllTagsPanel from "./AllTagsPanel/AllTagsPanel.svelte"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
|
import CollectionTimes from "../CollectionTimes/CollectionTimes.svelte"
|
||||||
|
|
||||||
class DirectionIndicatorVis extends SpecialVisualization {
|
class DirectionIndicatorVis extends SpecialVisualization {
|
||||||
funcName = "direction_indicator"
|
funcName = "direction_indicator"
|
||||||
|
@ -41,7 +38,7 @@ class DirectionIndicatorVis extends SpecialVisualization {
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature
|
feature: Feature,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||||
}
|
}
|
||||||
|
@ -69,7 +66,7 @@ class DirectionAbsolute extends SpecialVisualization {
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
args: string[]
|
args: string[],
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
||||||
const offset = args[1] === "" ? 0 : Number(args[1])
|
const offset = args[1] === "" ? 0 : Number(args[1])
|
||||||
|
@ -82,11 +79,11 @@ class DirectionAbsolute extends SpecialVisualization {
|
||||||
})
|
})
|
||||||
.mapD((value) => {
|
.mapD((value) => {
|
||||||
const dir = GeoOperations.bearingToHuman(
|
const dir = GeoOperations.bearingToHuman(
|
||||||
GeoOperations.parseBearing(value) + offset
|
GeoOperations.parseBearing(value) + offset,
|
||||||
)
|
)
|
||||||
console.log("Human dir", dir)
|
console.log("Human dir", dir)
|
||||||
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,7 +153,7 @@ class OpeningHoursState extends SpecialVisualizationSvelte {
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
args: string[]
|
args: string[],
|
||||||
): SvelteUIElement {
|
): SvelteUIElement {
|
||||||
const keyToUse = args[0]
|
const keyToUse = args[0]
|
||||||
const prefix = args[1]
|
const prefix = args[1]
|
||||||
|
@ -198,7 +195,7 @@ class Canonical extends SpecialVisualization {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const allUnits: Unit[] = [].concat(
|
const allUnits: Unit[] = [].concat(
|
||||||
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
|
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? []),
|
||||||
)
|
)
|
||||||
const unit = allUnits.filter((unit) => unit.isApplicableToKey(key))[0]
|
const unit = allUnits.filter((unit) => unit.isApplicableToKey(key))[0]
|
||||||
if (unit === undefined) {
|
if (unit === undefined) {
|
||||||
|
@ -206,7 +203,7 @@ class Canonical extends SpecialVisualization {
|
||||||
}
|
}
|
||||||
const getCountry = () => tagSource.data._country
|
const getCountry = () => tagSource.data._country
|
||||||
return unit.asHumanLongValue(value, getCountry)
|
return unit.asHumanLongValue(value, getCountry)
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,7 +228,7 @@ class PresetDescription extends SpecialVisualization {
|
||||||
|
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const translation = tagSource.map((tags) => {
|
const translation = tagSource.map((tags) => {
|
||||||
const layer = state.theme.getMatchingLayer(tags)
|
const layer = state.theme.getMatchingLayer(tags)
|
||||||
|
@ -251,7 +248,7 @@ class PresetTypeSelect extends SpecialVisualizationSvelte {
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
selectedElement: Feature,
|
selectedElement: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): SvelteUIElement {
|
): SvelteUIElement {
|
||||||
const t = Translations.t.preset_type
|
const t = Translations.t.preset_type
|
||||||
if (layer._basedOn !== layer.id) {
|
if (layer._basedOn !== layer.id) {
|
||||||
|
@ -312,7 +309,7 @@ class TagsVis extends SpecialVisualization {
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[]
|
argument: string[],
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const key = argument[0] ?? "value"
|
const key = argument[0] ?? "value"
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
@ -329,14 +326,37 @@ class TagsVis extends SpecialVisualization {
|
||||||
return parsed.asHumanString(true, false, {})
|
return parsed.asHumanString(true, false, {})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return new FixedUiElement(
|
return new FixedUiElement(
|
||||||
"Could not parse this tag: " + JSON.stringify(value) + " due to " + e
|
"Could not parse this tag: " + JSON.stringify(value) + " due to " + e,
|
||||||
).SetClass("alert")
|
).SetClass("alert")
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PointsInTimeVis extends SpecialVisualization {
|
||||||
|
docs = "Creates a visualisation for 'points in time', e.g. collection times of a postbox"
|
||||||
|
group = "data"
|
||||||
|
funcName = "points_in_time"
|
||||||
|
args = [
|
||||||
|
{
|
||||||
|
name: "key",
|
||||||
|
required: true,
|
||||||
|
doc: "The key out of which the points_in_time will be parsed",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||||
|
const key = args[0]
|
||||||
|
const points_in_time = tagSource.map(tags => tags[key])
|
||||||
|
const times = points_in_time.map(times =>
|
||||||
|
OH.createOhObject(<any>tagSource.data, times, tagSource.data["_country"], 1), [tagSource])
|
||||||
|
return new VariableUiElement(times.map(times =>
|
||||||
|
new SvelteUIElement(CollectionTimes, { times }),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DataVisualisations {
|
export class DataVisualisations {
|
||||||
public static initList(): SpecialVisualization[] {
|
public static initList(): SpecialVisualization[] {
|
||||||
return [
|
return [
|
||||||
|
@ -346,6 +366,7 @@ export class DataVisualisations {
|
||||||
new DirectionIndicatorVis(),
|
new DirectionIndicatorVis(),
|
||||||
new OpeningHoursTableVis(),
|
new OpeningHoursTableVis(),
|
||||||
new OpeningHoursState(),
|
new OpeningHoursState(),
|
||||||
|
new PointsInTimeVis(),
|
||||||
new Canonical(),
|
new Canonical(),
|
||||||
new LanguageElement(),
|
new LanguageElement(),
|
||||||
new PresetDescription(),
|
new PresetDescription(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue