forked from MapComplete/MapComplete
		
	Last finishing touches for the opening-hours visualization
This commit is contained in:
		
							parent
							
								
									895ec01213
								
							
						
					
					
						commit
						35bd49e5ba
					
				
					 13 changed files with 487 additions and 97 deletions
				
			
		| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import {UIElement} from "../UI/UIElement";
 | 
			
		||||
import {State} from "../State";
 | 
			
		||||
import State from "../State";
 | 
			
		||||
import Translations from "../UI/i18n/Translations";
 | 
			
		||||
import {UIEventSource} from "./UIEventSource";
 | 
			
		||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
 | 
			
		||||
import Combine from "../UI/Base/Combine";
 | 
			
		||||
import {CheckBox} from "../UI/Input/CheckBox";
 | 
			
		||||
import CheckBox from "../UI/Input/CheckBox";
 | 
			
		||||
import {PersonalLayout} from "./PersonalLayout";
 | 
			
		||||
import {Layout} from "../Customizations/Layout";
 | 
			
		||||
import {SubtleButton} from "../UI/Base/SubtleButton";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								State.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -23,7 +23,7 @@ export default class State {
 | 
			
		|||
    // The singleton of the global state
 | 
			
		||||
    public static state: State;
 | 
			
		||||
    
 | 
			
		||||
    public static vNumber = "0.1.0a";
 | 
			
		||||
    public static vNumber = "0.1.0b";
 | 
			
		||||
    
 | 
			
		||||
    // The user journey states thresholds when a new feature gets unlocked
 | 
			
		||||
    public static userJourney = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,9 +23,9 @@ export default class PublicHolidayInput extends InputElement<string> {
 | 
			
		|||
        const dropdown = new DropDown(
 | 
			
		||||
            Translations.t.general.opening_hours.open_during_ph,
 | 
			
		||||
            [
 | 
			
		||||
                {shown: "unknown", value: ""},
 | 
			
		||||
                {shown: "closed", value: "off"},
 | 
			
		||||
                {shown: "opened", value: " "}
 | 
			
		||||
                {shown: Translations.t.general.opening_hours.ph_not_known, value: ""},
 | 
			
		||||
                {shown: Translations.t.general.opening_hours.ph_closed, value: "off"},
 | 
			
		||||
                {shown:Translations.t.general.opening_hours.ph_open, value: " "}
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        this._dropdown = dropdown.SetStyle("display:inline-block;");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,71 +1,281 @@
 | 
			
		|||
import {UIElement} from "./UIElement";
 | 
			
		||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
			
		||||
import opening_hours from "opening_hours";
 | 
			
		||||
import Combine from "./Base/Combine";
 | 
			
		||||
import Translations from "./i18n/Translations";
 | 
			
		||||
import {FixedUiElement} from "./Base/FixedUiElement";
 | 
			
		||||
import {OH} from "../Logic/OpeningHours";
 | 
			
		||||
 | 
			
		||||
export default class OpeningHoursVisualization extends UIElement {
 | 
			
		||||
    private readonly _key: string;
 | 
			
		||||
 | 
			
		||||
    constructor(tags: UIEventSource<any>) {
 | 
			
		||||
    constructor(tags: UIEventSource<any>, key: string) {
 | 
			
		||||
        super(tags);
 | 
			
		||||
        this._key = key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static GetRanges(tags: any, from: Date, to: Date): {
 | 
			
		||||
    private static GetRanges(oh: any, from: Date, to: Date): ({
 | 
			
		||||
        isOpen: boolean,
 | 
			
		||||
        isUnknown: boolean,
 | 
			
		||||
        isSpecial: boolean,
 | 
			
		||||
        comment: string,
 | 
			
		||||
        startDate: Date
 | 
			
		||||
    }[] {
 | 
			
		||||
        startDate: Date,
 | 
			
		||||
        endDate: Date
 | 
			
		||||
    }[])[] {
 | 
			
		||||
 | 
			
		||||
        const oh = new opening_hours(tags.opening_hours, {
 | 
			
		||||
 | 
			
		||||
        const values = [[], [], [], [], [], [], []];
 | 
			
		||||
 | 
			
		||||
        const iterator = oh.getIterator(from);
 | 
			
		||||
 | 
			
		||||
        let prevValue = undefined;
 | 
			
		||||
        while (iterator.advance(to)) {
 | 
			
		||||
 | 
			
		||||
            if (prevValue) {
 | 
			
		||||
                prevValue.endDate = iterator.getDate() as Date
 | 
			
		||||
            }
 | 
			
		||||
            const endDate = new Date(iterator.getDate()) as Date;
 | 
			
		||||
            endDate.setHours(0, 0, 0, 0)
 | 
			
		||||
            endDate.setDate(endDate.getDate() + 1);
 | 
			
		||||
            const value = {
 | 
			
		||||
                isSpecial: iterator.getUnknown(),
 | 
			
		||||
                isOpen: iterator.getState(),
 | 
			
		||||
                comment: iterator.getComment(),
 | 
			
		||||
                startDate: iterator.getDate() as Date,
 | 
			
		||||
                endDate: endDate // Should be overwritten by the next iteration
 | 
			
		||||
            }
 | 
			
		||||
            prevValue = value;
 | 
			
		||||
 | 
			
		||||
            if (value.comment === undefined && !value.isOpen && !value.isSpecial) {
 | 
			
		||||
                // simply closed, nothing special here
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // Get day: sunday is 0, monday is 1. We move everything so that monday == 0
 | 
			
		||||
            values[(value.startDate.getDay() + 6) % 7].push(value);
 | 
			
		||||
        }
 | 
			
		||||
        return values;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static getMonday(d) {
 | 
			
		||||
        d = new Date(d);
 | 
			
		||||
        const day = d.getDay();
 | 
			
		||||
        const diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
 | 
			
		||||
        return new Date(d.setDate(diff));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private allChangeMoments(ranges: {
 | 
			
		||||
        isOpen: boolean,
 | 
			
		||||
        isSpecial: boolean,
 | 
			
		||||
        comment: string,
 | 
			
		||||
        startDate: Date,
 | 
			
		||||
        endDate: Date
 | 
			
		||||
    }[][]): [number[], string[]] {
 | 
			
		||||
        const changeHours: number[] = []
 | 
			
		||||
        const changeHourText: string[] = [];
 | 
			
		||||
        const extrachangeHours: number[] = []
 | 
			
		||||
        const extrachangeHourText: string[] = [];
 | 
			
		||||
 | 
			
		||||
        for (const weekday of ranges) {
 | 
			
		||||
            for (const range of weekday) {
 | 
			
		||||
                if (!range.isOpen && !range.isSpecial) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                const startOfDay: Date = new Date(range.startDate);
 | 
			
		||||
                startOfDay.setHours(0, 0, 0, 0);
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                const changeMoment: number = (range.startDate - startOfDay) / 1000;
 | 
			
		||||
                if (changeHours.indexOf(changeMoment) < 0) {
 | 
			
		||||
                    changeHours.push(changeMoment);
 | 
			
		||||
                    changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes()))
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                let changeMomentEnd: number = (range.endDate - startOfDay) / 1000;
 | 
			
		||||
                if (changeMomentEnd >= 24 * 60 * 60) {
 | 
			
		||||
                    if (extrachangeHours.indexOf(changeMomentEnd) < 0) {
 | 
			
		||||
                        extrachangeHours.push(changeMomentEnd);
 | 
			
		||||
                        extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes()))
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (changeHours.indexOf(changeMomentEnd) < 0) {
 | 
			
		||||
                    changeHours.push(changeMomentEnd);
 | 
			
		||||
                    changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes()))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        changeHourText.sort();
 | 
			
		||||
        changeHours.sort();
 | 
			
		||||
        extrachangeHourText.sort();
 | 
			
		||||
        extrachangeHours.sort();
 | 
			
		||||
        changeHourText.push(...extrachangeHourText);
 | 
			
		||||
        changeHours.push(...extrachangeHours);
 | 
			
		||||
 | 
			
		||||
        return [changeHours, changeHourText]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly weekdays = [
 | 
			
		||||
        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,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    InnerRender(): string {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const today = new Date();
 | 
			
		||||
        today.setHours(0, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
        const lastMonday = OpeningHoursVisualization.getMonday(today);
 | 
			
		||||
        const nextSunday = new Date(lastMonday);
 | 
			
		||||
        nextSunday.setDate(nextSunday.getDate() + 7);
 | 
			
		||||
 | 
			
		||||
        const tags = this._source.data;
 | 
			
		||||
        const oh = new opening_hours(tags[this._key], {
 | 
			
		||||
            lat: tags._lat,
 | 
			
		||||
            lon: tags._lon,
 | 
			
		||||
            address: {
 | 
			
		||||
                country_code: tags._country
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        }, {tag_key: this._key});
 | 
			
		||||
 | 
			
		||||
        const values = [];
 | 
			
		||||
 | 
			
		||||
        const iterator = oh.getIterator(from);
 | 
			
		||||
 | 
			
		||||
        while (iterator.advance(to)) {
 | 
			
		||||
 | 
			
		||||
            const value = {
 | 
			
		||||
                isUnknown: iterator.getUnknown(),
 | 
			
		||||
                isOpen: iterator.getState(),
 | 
			
		||||
                comment: iterator.getComment(),
 | 
			
		||||
                startDate: iterator.getDate()
 | 
			
		||||
        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 mover further along
 | 
			
		||||
                lastMonday.setDate(lastMonday.getDate() + 7);
 | 
			
		||||
                nextSunday.setDate(nextSunday.getDate() + 7);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (value.comment === undefined && !value.isOpen && !value.isUnknown) {
 | 
			
		||||
                // simply closed, nothing special here
 | 
			
		||||
        // ranges[0] are all ranges for monday
 | 
			
		||||
        const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday);
 | 
			
		||||
        console.log(ranges)
 | 
			
		||||
        if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) {
 | 
			
		||||
            // Closed!
 | 
			
		||||
            const opensAtDate = oh.getNextChange();
 | 
			
		||||
            if(opensAtDate === undefined){
 | 
			
		||||
                return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed").Render()
 | 
			
		||||
            }
 | 
			
		||||
            const moment = `${opensAtDate.getDay()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}`
 | 
			
		||||
            return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed").Render()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isWeekstable = oh.isWeekStable();
 | 
			
		||||
 | 
			
		||||
        let [changeHours, changeHourText] = this.allChangeMoments(ranges);
 | 
			
		||||
 | 
			
		||||
        // 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 rows: UIElement[] = [];
 | 
			
		||||
        const availableArea = latestclose - earliestOpen;
 | 
			
		||||
        // @ts-ignore
 | 
			
		||||
        const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        let header = "";
 | 
			
		||||
 | 
			
		||||
        if (now >= 0 && now <= 100) {
 | 
			
		||||
            header += new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render()
 | 
			
		||||
        }
 | 
			
		||||
        for (const changeMoment of changeHours) {
 | 
			
		||||
            const offset = 100 * (changeMoment - earliestOpen) / availableArea;
 | 
			
		||||
            if (offset < 0 || offset > 100) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            console.log(value)
 | 
			
		||||
            values.push(value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        return values;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    InnerRender(): string {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const from = new Date("2019-12-31");
 | 
			
		||||
        const to = new Date("2020-01-05");
 | 
			
		||||
 | 
			
		||||
        const ranges = OpeningHoursVisualization.GetRanges(this._source.data, from, to);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        let text = "";
 | 
			
		||||
        for (const range of ranges) {
 | 
			
		||||
            text += `From${range.startDate} it is${range.isOpen} ${range.comment?? ""}<br/>`
 | 
			
		||||
            const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render();
 | 
			
		||||
            header += el;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return text;
 | 
			
		||||
        for (let i = 0; i < changeHours.length; i++) {
 | 
			
		||||
            let changeMoment = changeHours[i];
 | 
			
		||||
            const offset = 100 * (changeMoment - earliestOpen) / availableArea;
 | 
			
		||||
            if (offset < 0 || offset > 100) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            const el = new FixedUiElement(
 | 
			
		||||
                `<div style='margin-top: ${i % 2 == 0 ? '1.5em;' : "1%"}'>${changeHourText[i]}</div>`
 | 
			
		||||
            )
 | 
			
		||||
                .SetStyle(`left:${offset}%`)
 | 
			
		||||
                .SetClass("ohviz-time-indication").Render();
 | 
			
		||||
            header += el;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        rows.push(new Combine([`<td width="5%"> </td>`,
 | 
			
		||||
            `<td style="position:relative;height:2.5em;">${header}</td>`]));
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < 7; i++) {
 | 
			
		||||
            const dayRanges = ranges[i];
 | 
			
		||||
            const isToday = (new Date().getDay() + 6) % 7 === i;
 | 
			
		||||
            let weekday = OpeningHoursVisualization.weekdays[i].Render();
 | 
			
		||||
 | 
			
		||||
            if (!isWeekstable) {
 | 
			
		||||
                const day = new Date(lastMonday)
 | 
			
		||||
                day.setDate(day.getDate() + i);
 | 
			
		||||
                weekday = " " + day.getDate() + "/" + (day.getMonth() + 1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let innerContent: string[] = [];
 | 
			
		||||
 | 
			
		||||
            for (const changeMoment of changeHours) {
 | 
			
		||||
                const offset = 100 * (changeMoment - earliestOpen) / availableArea;
 | 
			
		||||
                innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line").Render())
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (const range of dayRanges) {
 | 
			
		||||
                if (!range.isOpen && !range.isSpecial) {
 | 
			
		||||
                    innerContent.push(
 | 
			
		||||
                        new FixedUiElement(range.comment).SetClass("ohviz-day-off").Render())
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const startOfDay: Date = new Date(range.startDate);
 | 
			
		||||
                startOfDay.setHours(0, 0, 0, 0);
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen;
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen);
 | 
			
		||||
                const startPercentage = (100 * startpoint / availableArea);
 | 
			
		||||
                innerContent.push(
 | 
			
		||||
                    new FixedUiElement(range.comment).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range").Render())
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (now >= 0 && now <= 100) {
 | 
			
		||||
                innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now").Render())
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let clss = ""
 | 
			
		||||
            if (isToday) {
 | 
			
		||||
                clss = "ohviz-today"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            rows.push(new Combine(
 | 
			
		||||
                [`<td class="ohviz-weekday ${clss}">${weekday}</td>`,
 | 
			
		||||
                    `<td style="position:relative;" class="${clss}">${innerContent.join("")}</td>`]))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return new Combine([
 | 
			
		||||
            "<table class='ohviz' style='width:100%;'>",
 | 
			
		||||
            rows.map(el => "<tr>" + el.Render() + "</tr>").join(""),
 | 
			
		||||
            "</table>"
 | 
			
		||||
        ]).SetClass("ohviz-container").Render();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								UI/SpecialVisualizations.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								UI/SpecialVisualizations.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import {UIElement} from "./UIElement";
 | 
			
		||||
import OpeningHoursVisualization from "./OhVisualization";
 | 
			
		||||
import {UIEventSource} from "../Logic/UIEventSource";
 | 
			
		||||
 | 
			
		||||
export default class SpecialVisualizations {
 | 
			
		||||
 | 
			
		||||
    public static specialVisualizations: { funcName: string, constr: ((tagSource: UIEventSource<any>, argument: string) => UIElement) }[] =
 | 
			
		||||
 | 
			
		||||
        [{
 | 
			
		||||
            funcName: "opening_hours_table",
 | 
			
		||||
            constr: (tagSource: UIEventSource<any>, keyname) => {
 | 
			
		||||
                return new OpeningHoursVisualization(tagSource, keyname)
 | 
			
		||||
            }
 | 
			
		||||
        }]
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,12 +17,13 @@ import {FixedUiElement} from "./Base/FixedUiElement";
 | 
			
		|||
import ValidatedTextField from "./Input/ValidatedTextField";
 | 
			
		||||
import CheckBoxes from "./Input/Checkboxes";
 | 
			
		||||
import State from "../State";
 | 
			
		||||
import SpecialVisualizations from "./SpecialVisualizations";
 | 
			
		||||
 | 
			
		||||
export class TagRendering extends UIElement implements TagDependantUIElement {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private readonly _question: string | Translation;
 | 
			
		||||
    private readonly _mapping: { k: TagsFilter, txt: string | UIElement, priority?: number }[];
 | 
			
		||||
    private readonly _mapping: { k: TagsFilter, txt: string | Translation, priority?: number }[];
 | 
			
		||||
 | 
			
		||||
    private currentTags: UIEventSource<any>;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -428,37 +429,19 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
 | 
			
		|||
    private RenderAnswer(): UIElement {
 | 
			
		||||
        const tags = TagUtils.proprtiesToKV(this._source.data);
 | 
			
		||||
 | 
			
		||||
        let freeform: UIElement = new FixedUiElement("");
 | 
			
		||||
        let freeformScore = -10;
 | 
			
		||||
        if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) {
 | 
			
		||||
            freeform = this.ApplyTemplate(this._freeform.renderTemplate);
 | 
			
		||||
            freeformScore = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        let highestScore = -100;
 | 
			
		||||
        let highestTemplate = undefined;
 | 
			
		||||
        for (const oneOnOneElement of this._mapping) {
 | 
			
		||||
            if (oneOnOneElement.k == null ||
 | 
			
		||||
                oneOnOneElement.k.matches(tags)) {
 | 
			
		||||
                // We have found a matching key -> we use the template, but only if it scores better
 | 
			
		||||
                let score = oneOnOneElement.priority ??
 | 
			
		||||
                    (oneOnOneElement.k === null ? -1 : 0);
 | 
			
		||||
                if (score > highestScore) {
 | 
			
		||||
                    highestScore = score;
 | 
			
		||||
                    highestTemplate = oneOnOneElement.txt
 | 
			
		||||
                }
 | 
			
		||||
            if (oneOnOneElement.k.matches(tags)) {
 | 
			
		||||
                // We have found a matching key -> we use this template
 | 
			
		||||
                return this.ApplyTemplate(oneOnOneElement.txt);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (freeformScore > highestScore) {
 | 
			
		||||
            return freeform;
 | 
			
		||||
        if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) {
 | 
			
		||||
            return this.ApplyTemplate(this._freeform.renderTemplate);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (highestTemplate !== undefined) {
 | 
			
		||||
            // we render the found template
 | 
			
		||||
            return this.ApplyTemplate(highestTemplate);
 | 
			
		||||
        }
 | 
			
		||||
        return new FixedUiElement("");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    InnerRender(): string {
 | 
			
		||||
| 
						 | 
				
			
			@ -531,13 +514,19 @@ export class TagRendering extends UIElement implements TagDependantUIElement {
 | 
			
		|||
        if (template === undefined || template === null) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const knownSpecials : {funcName: string, constr: ((arg: string) => UIElement)}[]= SpecialVisualizations.specialVisualizations.map(
 | 
			
		||||
            special => ({
 | 
			
		||||
                funcName: special.funcName,
 | 
			
		||||
                constr: arg => special.constr(this.currentTags, arg)
 | 
			
		||||
            })
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        return new VariableUiElement(this.currentTags.map(tags => {
 | 
			
		||||
            const tr = Translations.WT(template);
 | 
			
		||||
            if (tr.Subs === undefined) {
 | 
			
		||||
                // This is a weird edge case
 | 
			
		||||
                return tr.InnerRender();
 | 
			
		||||
            }
 | 
			
		||||
            return tr.Subs(tags).InnerRender()
 | 
			
		||||
            return Translations.WT(template)
 | 
			
		||||
                .Subs(tags)
 | 
			
		||||
                .EvaluateSpecialComponents(knownSpecials)
 | 
			
		||||
                .InnerRender()
 | 
			
		||||
        })).ListenTo(Locale.language);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ export default class Translation extends UIElement {
 | 
			
		|||
 | 
			
		||||
    private static forcedLanguage = undefined;
 | 
			
		||||
 | 
			
		||||
    public Subs(text: any) {
 | 
			
		||||
    public Subs(text: any): Translation {
 | 
			
		||||
        const newTranslations = {};
 | 
			
		||||
        for (const lang in this.translations) {
 | 
			
		||||
            let template: string = this.translations[lang];
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ export default class Translation extends UIElement {
 | 
			
		|||
                const combined = [];
 | 
			
		||||
                const parts = template.split("{" + k + "}");
 | 
			
		||||
                const el: string | UIElement = text[k];
 | 
			
		||||
                if(el === undefined){
 | 
			
		||||
                if (el === undefined) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                let rtext: string = "";
 | 
			
		||||
| 
						 | 
				
			
			@ -40,9 +40,35 @@ export default class Translation extends UIElement {
 | 
			
		|||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public EvaluateSpecialComponents(knownSpecials: { funcName: string, constr: ((call: string) => UIElement) }[]): UIElement {
 | 
			
		||||
        const newTranslations = {};
 | 
			
		||||
        for (const lang in this.translations) {
 | 
			
		||||
            let template: string = this.translations[lang];
 | 
			
		||||
 | 
			
		||||
            for (const knownSpecial of knownSpecials) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                const combined = [];
 | 
			
		||||
                const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*)\\)}(.*)`);
 | 
			
		||||
                if (matched === null) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                const partBefore = matched[1];
 | 
			
		||||
                const argument = matched[2];
 | 
			
		||||
                const partAfter = matched[3];
 | 
			
		||||
 | 
			
		||||
                const element = knownSpecial.constr(argument).Render();
 | 
			
		||||
 | 
			
		||||
                template = partBefore + element + partAfter;
 | 
			
		||||
            }
 | 
			
		||||
            newTranslations[lang] = template;
 | 
			
		||||
        }
 | 
			
		||||
        return new Translation(newTranslations);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    get txt(): string {
 | 
			
		||||
        if(this.translations["*"]){
 | 
			
		||||
        if (this.translations["*"]) {
 | 
			
		||||
            return this.translations["*"];
 | 
			
		||||
        }
 | 
			
		||||
        const txt = this.translations[Translation.forcedLanguage ?? Locale.language.data];
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +86,7 @@ export default class Translation extends UIElement {
 | 
			
		|||
        return undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    InnerRender(): string {
 | 
			
		||||
        return this.txt
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -888,14 +888,34 @@ export default class Translations {
 | 
			
		|||
                    "en":"During a public holiday, this amenity is"
 | 
			
		||||
                }),
 | 
			
		||||
                opensAt: new T({
 | 
			
		||||
                    "en":"from",
 | 
			
		||||
                    "nl":"vanaf"
 | 
			
		||||
                }),openTill: new T({
 | 
			
		||||
                    "en":"till",
 | 
			
		||||
                    "nl":"tot"
 | 
			
		||||
                    "en": "from",
 | 
			
		||||
                    "nl": "vanaf"
 | 
			
		||||
                }), openTill: new T({
 | 
			
		||||
                    "en": "till",
 | 
			
		||||
                    "nl": "tot"
 | 
			
		||||
                }),
 | 
			
		||||
                not_all_rules_parsed: new T({
 | 
			
		||||
                    "en":"The openin hours of this shop are complicated. The following rules are ignored in the input element:"
 | 
			
		||||
                    "en": "The openin hours of this shop are complicated. The following rules are ignored in the input element:"
 | 
			
		||||
                }),
 | 
			
		||||
                closed_until: new T({
 | 
			
		||||
                    "en": "Closed until {date}",
 | 
			
		||||
                    "nl": "Gesloten - open op {date}"
 | 
			
		||||
                }),
 | 
			
		||||
 | 
			
		||||
                closed_permanently: new T({
 | 
			
		||||
                    "en": "Closed - no opening day known",
 | 
			
		||||
                    "nl": "Gesloten"
 | 
			
		||||
                }),
 | 
			
		||||
                ph_not_known: new T({
 | 
			
		||||
                    "en": "unknown",
 | 
			
		||||
                    "nl": "niet gekend"
 | 
			
		||||
                }),
 | 
			
		||||
                ph_closed: new T({
 | 
			
		||||
                    "en": "closed",
 | 
			
		||||
                    "nl": "gesloten"
 | 
			
		||||
                }), ph_open: new T({
 | 
			
		||||
                    "en": "opened",
 | 
			
		||||
                    "nl": "open"
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -952,7 +972,10 @@ export default class Translations {
 | 
			
		|||
        if (typeof (s) === "string") {
 | 
			
		||||
            return new Translation({en: s});
 | 
			
		||||
        }
 | 
			
		||||
        return s;
 | 
			
		||||
        if (s instanceof Translation) {
 | 
			
		||||
            return s;
 | 
			
		||||
        }
 | 
			
		||||
        throw "??? Not a valid translation"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static CountTranslations() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -196,6 +196,17 @@
 | 
			
		|||
        "key": "email",
 | 
			
		||||
        "type": "email"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "question": {
 | 
			
		||||
        "en": "When it this bike café opened?",
 | 
			
		||||
        "nl": "Wanneer is dit fietscafé geopend?"
 | 
			
		||||
      },
 | 
			
		||||
      "render": "{opening_hours_table(opening_hours)}",
 | 
			
		||||
      "freeform": {
 | 
			
		||||
        "key": "opening_hours",
 | 
			
		||||
        "type": "opening_hours"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "hideUnderlayingFeaturesMinPercentage": 0,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -185,7 +185,8 @@
 | 
			
		|||
      },
 | 
			
		||||
      "render": "<a href='{website}' target='_blank'>{website}</a>",
 | 
			
		||||
      "freeform": {
 | 
			
		||||
        "key": "website"
 | 
			
		||||
        "key": "website",
 | 
			
		||||
        "type": "url"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +216,7 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "render": "Shop is open {opening_hours}",
 | 
			
		||||
      "render": "{opening_hours_table(opening_hours)}",
 | 
			
		||||
      "question": "When is this shop opened?",
 | 
			
		||||
      "freeform": {
 | 
			
		||||
        "key": "opening_hours",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,6 +105,119 @@
 | 
			
		|||
    max-width: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.oh-timerange-label{
 | 
			
		||||
.oh-timerange-label {
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**** Opening hours visualization table ****/
 | 
			
		||||
 | 
			
		||||
.ohviz-table {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-range {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background: #99e7ff;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 5%;
 | 
			
		||||
    height: 85%;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-today .ohviz-range {
 | 
			
		||||
    border: 1.5px solid black;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-day-off {
 | 
			
		||||
    display: block;
 | 
			
		||||
    background: repeating-linear-gradient(
 | 
			
		||||
            45deg,
 | 
			
		||||
            rgba(255, 255, 255, 0),
 | 
			
		||||
            rgba(255, 255, 255, 0) 10px,
 | 
			
		||||
            rgba(216, 235, 255, 0.5) 10px,
 | 
			
		||||
            rgba(216, 235, 255, 0.5) 20px
 | 
			
		||||
    );
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    color: black;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.ohviz-now {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    border: 1px solid black;
 | 
			
		||||
    box-sizing: border-box
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-line {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    border-left: 1px solid #ccc;
 | 
			
		||||
    box-sizing: border-box
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.ohviz-time-indication > div {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    left: -50%;
 | 
			
		||||
    padding-left: 0.3em;
 | 
			
		||||
    padding-right: 0.3em;
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-time-indication {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.ohviz-today {
 | 
			
		||||
    background-color: #e5f5ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-weekday {
 | 
			
		||||
    padding-left: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.ohviz {
 | 
			
		||||
    border-collapse: collapse;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-container {
 | 
			
		||||
    border: 0.5em solid #e5f5ff;
 | 
			
		||||
    border-radius: 1em;
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ohviz-closed {
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    background-color: #eee;
 | 
			
		||||
    border-radius: 1em;
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								test.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -3,13 +3,13 @@
 | 
			
		|||
import OpeningHoursVisualization from "./UI/OhVisualization";
 | 
			
		||||
import {UIEventSource} from "./Logic/UIEventSource";
 | 
			
		||||
 | 
			
		||||
new OpeningHoursVisualization(new UIEventSource<any>({
 | 
			
		||||
        opening_hours: "mo-fr 09:00-17:00; Sa 09:00-17:00 'by appointment'; PH off; Th[1] off;",
 | 
			
		||||
new OpeningHoursVisualization( new UIEventSource<any>({
 | 
			
		||||
        opening_hours: "2000 Dec 21 10:00-12:00;",
 | 
			
		||||
        _country: "be",
 | 
			
		||||
        _lat: "51.2",
 | 
			
		||||
        _lon: "3.2"
 | 
			
		||||
    }
 | 
			
		||||
)).AttachTo("maindiv")
 | 
			
		||||
),  'opening_hours').AttachTo("maindiv")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue