UNPKG

opening_hours

Version:

Library to parse and process opening_hours tag from OpenStreetMap data

1,963 lines (1,739 loc) 73 kB
/* * This file is part of YoHours. * * YoHours is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * YoHours is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with YoHours. If not, see <https://www.gnu.org/licenses/>. */ /* * YoHours * Web interface to make opening hours data for OpenStreetMap the easy way * Author: Adrien PAVIE * * Model JS classes */ /* * ========= CONSTANTS ========= */ /** * The days in a week */ DAYS = { MONDAY: 0, TUESDAY: 1, WEDNESDAY: 2, THURSDAY: 3, FRIDAY: 4, SATURDAY: 5, SUNDAY: 6 }; /** * The days in OSM */ OSM_DAYS = [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ]; /** * The days IRL */ IRL_DAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ]; /** * The month in OSM */ OSM_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; /** * The months IRL */ IRL_MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; /** * The last day of month */ MONTH_END_DAY = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; /** * The maximal minute that an interval can have */ MINUTES_MAX = 1440; /** * The maximal value of days */ DAYS_MAX = 6; /** * The weekday ID for PH */ PH_WEEKDAY = -2; /* * ========== CLASSES ========== */ /** * Class Interval, defines an interval in a week where the POI is open. * @param dayStart The start week day (use DAYS constants) * @param dayEnd The end week day (use DAYS constants) * @param minStart The interval start (in minutes since midnight) * @param minEnd The interval end (in minutes since midnight) */ var Interval = function(dayStart, dayEnd, minStart, minEnd) { //ATTRIBUTES /** The start day in the week, see DAYS **/ this._dayStart = dayStart; /** The end day in the week, see DAYS **/ this._dayEnd = dayEnd; /** The interval start, in minutes since midnight (local hour) **/ this._start = minStart; /** The interval end, in minutes since midnight (local hour) **/ this._end = minEnd; //CONSTRUCTOR if(this._dayEnd == 0 && this._end == 0) { this._dayEnd = DAYS_MAX; this._end = MINUTES_MAX; } //console.log("Interval", this._dayStart, this._dayEnd, this._start, this._end); }; //ACCESSORS /** * @return The start day in the week, see DAYS constants */ Interval.prototype.getStartDay = function() { return this._dayStart; }; /** * @return The end day in the week, see DAYS constants */ Interval.prototype.getEndDay = function() { return this._dayEnd; }; /** * @return The interval start, in minutes since midnight */ Interval.prototype.getFrom = function() { return this._start; }; /** * @return The interval end, in minutes since midnight */ Interval.prototype.getTo = function() { return this._end; }; /** * A wide interval is an interval of one or more days, weeks, months, holidays. * Use WideInterval.days/weeks/months/holidays methods to construct one object. */ var WideInterval = function() { //ATTRIBUTES /** The start of the interval **/ this._start = null; /** The end of the interval **/ this._end = null; /** The kind of interval **/ this._type = null; }; //CONSTRUCTORS /** * @return a day-based interval */ WideInterval.prototype.day = function(startDay, startMonth, endDay, endMonth) { if(startDay == null || startMonth == null) { throw Error("Start day and month can't be null"); } this._start = { day: startDay, month: startMonth }; this._end = (endDay != null && endMonth != null && (endDay != startDay || endMonth != startMonth)) ? { day: endDay, month: endMonth } : null; this._type = "day"; return this; }; /** * @return a week-based interval */ WideInterval.prototype.week = function(startWeek, endWeek) { if(startWeek == null) { throw Error("Start week can't be null"); } this._start = { week: startWeek }; this._end = (endWeek != null && endWeek != startWeek) ? { week: endWeek } : null; this._type = "week"; return this; }; /** * @return a month-based interval */ WideInterval.prototype.month = function(startMonth, endMonth) { if(startMonth == null) { throw Error("Start month can't be null"); } this._start = { month: startMonth }; this._end = (endMonth != null && endMonth != startMonth) ? { month: endMonth } : null; this._type = "month"; return this; }; /** * @return a holiday-based interval */ WideInterval.prototype.holiday = function(holiday) { if(holiday == null || (holiday != "PH" && holiday != "SH" && holiday != "easter")) { throw Error("Invalid holiday, must be PH, SH or easter"); } this._start = { holiday: holiday }; this._end = null; this._type = "holiday"; return this; }; /** * @return a holiday-based interval */ WideInterval.prototype.always = function() { this._start = null; this._end = null; this._type = "always"; return this; }; //ACCESSORS /** * @return The kind of wide interval (always, day, month, week, holiday) */ WideInterval.prototype.getType = function() { return this._type; }; /** * @return The start moment */ WideInterval.prototype.getStart = function() { return this._start; }; /** * @return The end moment */ WideInterval.prototype.getEnd = function() { return this._end; }; /** * @return True if the given object concerns the same interval as this one */ WideInterval.prototype.equals = function(o) { if(!o instanceof WideInterval) { return false; } if(this === o) { return true; } if(o._type == "always") { return this._type == "always"; } var result = false; switch(this._type) { case "always": result = o._start == null; break; case "day": result = ( o._type == "day" && o._start.month == this._start.month && o._start.day == this._start.day && ( (o._end == null && this._end == null) || (o._end != null && this._end != null && this._end.month == o._end.month && this._end.day == o._end.day) )) || ( o._type == "month" && o._start.month == this._start.month && (this.isFullMonth() && o.isFullMonth()) || (o._end != null && this._end != null && this._end.month == o._end.month && this.endsMonth() && o.endsMonth()) ); break; case "week": result = o._start.week == this._start.week && (o._end == this._end || (this._end != null && o._end != null && o._end.week == this._end.week)); break; case "month": result = ( o._type == "day" && this._start.month == o._start.month && o.startsMonth() && ( (this._end == null && o._end != null && this._start.month == o._end.month && o.endsMonth()) || (this._end != null && o._end != null && this._end.month == o._end.month && o.endsMonth()) ) ) || ( o._type == "month" && o._start.month == this._start.month && ( (this._end == null && o._end == null) || (this._end != null && o._end != null && this._end.month == o._end.month) ) ); break; case "holiday": result = o._start.holiday == this._start.holiday; break; default: } return result; }; /** * @return The human readable time */ WideInterval.prototype.getTimeForHumans = function() { var result; switch(this._type) { case "day": if(this._end != null) { result = "every week from "+IRL_MONTHS[this._start.month-1]+" "+this._start.day+" to "; if(this._start.month != this._end.month) { result += IRL_MONTHS[this._end.month-1]+" "; } result += this._end.day; } else { result = "day "+IRL_MONTHS[this._start.month-1]+" "+this._start.day; } break; case "week": if(this._end != null) { result = "every week from week "+this._start.week+" to "+this._end.week; } else { result = "week "+this._start.week; } break; case "month": if(this._end != null) { result = "every week from "+IRL_MONTHS[this._start.month-1]+" to "+IRL_MONTHS[this._end.month-1]; } else { result = "every week in "+IRL_MONTHS[this._start.month-1]; } break; case "holiday": if(this._start.holiday == "SH") { result = "every week during school holidays"; } else if(this._start.holiday == "PH") { result = "every public holidays"; } else if(this._start.holiday == "easter") { result = "each easter day"; } else { throw new Error("Invalid holiday type: "+this._start.holiday); } break; case "always": result = "every week of year"; break; default: result = "invalid time"; } return result; }; /** * @return The time selector for OSM opening_hours */ WideInterval.prototype.getTimeSelector = function() { var result; switch(this._type) { case "day": result = OSM_MONTHS[this._start.month-1]+" "+((this._start.day < 10) ? "0" : "")+this._start.day; if(this._end != null) { //Same month as start ? if(this._start.month == this._end.month) { result += "-"+((this._end.day < 10) ? "0" : "")+this._end.day; } else { result += "-"+OSM_MONTHS[this._end.month-1]+" "+((this._end.day < 10) ? "0" : "")+this._end.day; } } break; case "week": result = "week "+((this._start.week < 10) ? "0" : "")+this._start.week; if(this._end != null) { result += "-"+((this._end.week < 10) ? "0" : "")+this._end.week; } break; case "month": result = OSM_MONTHS[this._start.month-1]; if(this._end != null) { result += "-"+OSM_MONTHS[this._end.month-1]; } break; case "holiday": result = this._start.holiday; break; case "always": default: result = ""; } return result; }; /** * Does this interval corresponds to a full month ? */ WideInterval.prototype.isFullMonth = function() { if(this._type == "month" && this._end == null) { return true; } else if(this._type == "day") { return (this._start.day == 1 && this._end != null && this._end.month == this._start.month && this._end.day != undefined && this._end.day == MONTH_END_DAY[this._end.month-1]); } else { return false; } }; /** * Does this interval starts the first day of a month */ WideInterval.prototype.startsMonth = function() { return this._type == "month" || this._type == "always" || (this._type == "day" && this._start.day == 1); }; /** * Does this interval ends the last day of a month */ WideInterval.prototype.endsMonth = function() { return this._type == "month" || this._type == "always" || (this._type == "day" && this._end != null && this._end.day == MONTH_END_DAY[this._end.month-1]); }; /** * Does this interval strictly contains the given one (ie the second is a refinement of the first, and not strictly equal) * @param o The other wide interval * @return True if this date contains the given one (and is not strictly equal to) */ WideInterval.prototype.contains = function(o) { var result = false; /* * Check if it is contained in this one */ if(this.equals(o)) { result = false; } else if(this._type == "always") { result = true; } else if(this._type == "day") { if(o._type == "day") { //Starting after if(o._start.month > this._start.month || (o._start.month == this._start.month && o._start.day >= this._start.day)) { //Ending before if(o._end != null) { if(this._end != null && (o._end.month < this._end.month || (o._end.month == this._end.month && o._end.day <= this._end.day))) { result = true; } } else { if(this._end != null && (o._start.month < this._end.month || (o._start.month == this._end.month && o._start.day <= this._end.day))) { result = true; } } } } else if(o._type == "month"){ //Starting after if(o._start.month > this._start.month || (o._start.month == this._start.month && this._start.day == 1)) { //Ending before if(o._end != null && this._end != null && (o._end.month < this._end.month || (o._end.month == this._end.month && this._end.day == MONTH_END_DAY[end.month-1]))) { result = true; } else if(o._end == null && (this._end != null && o._start.month < this._end.month)) { result = true; } } } } else if(this._type == "week") { if(o._type == "week") { if(o._start.week >= this._start.week) { if(o._end != null && this._end != null && o._end.week <= this._end.week) { result = true; } else if(o._end == null && ((this._end != null && o._start.week <= this._end.week) || o._start.week == this._start.week)) { result = true; } } } } else if(this._type == "month") { if(o._type == "month") { if(o._start.month >= this._start.month) { if(o._end != null && this._end != null && o._end.month <= this._end.month) { result = true; } else if(o._end == null && ((this._end != null && o._start.month <= this._end.month) || o._start.month == this._start.month)) { result = true; } } } else if(o._type == "day") { if(o._end != null) { if(this._end == null) { if( o._start.month == this._start.month && o._end.month == this._start.month && ((o._start.day >= 1 && o._end.day < MONTH_END_DAY[o._start.month-1]) || (o._start.day > 1 && o._end.day <= MONTH_END_DAY[o._start.month-1])) ) { result = true; } } else { if(o._start.month >= this._start.month && o._end.month <= this._end.month) { if( (o._start.month > this._start.month && o._end.month < this._end.month) || (o._start.month == this._start.month && o._end.month < this._end.month && start.day > 1) || (o._start.month > this._start.month && o._end.month == this._end.month && o._end.day < MONTH_END_DAY[o._end.month-1]) || (o._start.day >= 1 && o._end.day < MONTH_END_DAY[o._end.month-1]) || (o._start.day > 1 && o._end.day <= MONTH_END_DAY[o._end.month-1]) ) { result = true; } } } } else { if(this._end == null) { if(this._start.month == o._start.month) { result = true; } } else { if(o._start.month >= this._start.month && o._start.month <= this._end.month) { result = true; } } } } } return result; }; /** * Class Day, represents a typical day */ var Day = function() { //ATTRIBUTES /** The intervals defining this week **/ this._intervals = []; /** The next interval ID **/ this._nextInterval = 0; }; //ACCESSORS /** * @return This day, as a boolean array (minutes since midnight). True if open, false else. */ Day.prototype.getAsMinutesArray = function() { //Create array with all values set to false //For each minute var minuteArray = []; for (var minute = 0; minute <= MINUTES_MAX; minute++) { minuteArray[minute] = false; } //Set to true values where an interval is defined for(var id=0, l=this._intervals.length; id < l; id++) { if(this._intervals[id] != undefined) { var startMinute = null; var endMinute = null; if( this._intervals[id].getStartDay() == this._intervals[id].getEndDay() || (this._intervals[id].getEndDay() == DAYS_MAX && this._intervals[id].getTo() == MINUTES_MAX) ) { //Define start and end minute regarding the current day startMinute = this._intervals[id].getFrom(); endMinute = this._intervals[id].getTo(); } //Set to true the minutes for this day if(startMinute != null && endMinute != null){ for(var minute = startMinute; minute <= endMinute; minute++) { minuteArray[minute] = true; } } else { console.log(this._intervals[id].getFrom()+" "+this._intervals[id].getTo()+" "+this._intervals[id].getStartDay()+" "+this._intervals[id].getEndDay()); throw new Error("Invalid interval"); } } } return minuteArray; }; /** * @param clean Clean intervals ? (default: false) * @return The intervals in this week */ Day.prototype.getIntervals = function(clean) { clean = clean || false; if(clean) { //Create continuous intervals over days var minuteArray = this.getAsMinutesArray(); var intervals = []; var minStart = -1, minEnd; for(var min=0, lm=minuteArray.length; min < lm; min++) { //First minute if(min == 0) { if(minuteArray[min]) { minStart = min; } } //Last minute else if(min == lm-1) { if(minuteArray[min]) { intervals.push(new Interval( 0, 0, minStart, min )); } } //Other minutes else { //New interval if(minuteArray[min] && minStart < 0) { minStart = min; } //Ending interval else if(!minuteArray[min] && minStart >= 0) { intervals.push(new Interval( 0, 0, minStart, min-1 )); minStart = -1; } } } return intervals; } else { return this._intervals; } }; //MODIFIERS /** * Add a new interval to this week * @param interval The new interval * @return The ID of the added interval */ Day.prototype.addInterval = function(interval) { this._intervals[this._nextInterval] = interval; this._nextInterval++; return this._nextInterval-1; }; /** * Edits the given interval * @param id The interval ID * @param interval The new interval */ Day.prototype.editInterval = function(id, interval) { this._intervals[id] = interval; }; /** * Remove the given interval * @param id the interval ID */ Day.prototype.removeInterval = function(id) { this._intervals[id] = undefined; }; /** * Redefines this date range intervals with a copy of the given ones */ Day.prototype.copyIntervals = function(intervals) { this._intervals = []; for(var i=0; i < intervals.length; i++) { if(intervals[i] != undefined && intervals[i].getStartDay() == 0 && intervals[i].getEndDay() == 0) { this._intervals.push($.extend(true, {}, intervals[i])); } } this._intervals = this.getIntervals(true); }; /** * Removes all defined intervals */ Day.prototype.clearIntervals = function() { this._intervals = []; }; //OTHER METHODS /** * Is this day defining the same intervals as the given one ? */ Day.prototype.sameAs = function(d) { return d.getAsMinutesArray().equals(this.getAsMinutesArray()); }; /** * Class Week, represents a typical week of opening hours. */ var Week = function() { //ATTRIBUTES /** The intervals defining this week **/ this._intervals = []; }; //ACCESSORS /** * @return This week, as a two-dimensional boolean array. First dimension is for days (see DAYS), second dimension for minutes since midnight. True if open, false else. */ Week.prototype.getAsMinutesArray = function() { //Create array with all values set to false //For each day var minuteArray = []; for(var day = 0; day <= DAYS_MAX; day++) { //For each minute minuteArray[day] = []; for (var minute = 0; minute <= MINUTES_MAX; minute++) { minuteArray[day][minute] = false; } } //Set to true values where an interval is defined for(var id=0, l=this._intervals.length; id < l; id++) { if(this._intervals[id] != undefined) { for(var day = this._intervals[id].getStartDay(); day <= this._intervals[id].getEndDay(); day++) { //Define start and end minute regarding the current day var startMinute = (day == this._intervals[id].getStartDay()) ? this._intervals[id].getFrom() : 0; var endMinute = (day == this._intervals[id].getEndDay()) ? this._intervals[id].getTo() : MINUTES_MAX; //Set to true the minutes for this day if(startMinute != null && endMinute != null) { for(var minute = startMinute; minute <= endMinute; minute++) { minuteArray[day][minute] = true; } } } } } return minuteArray; }; /** * @param clean Clean intervals ? (default: false) * @return The intervals in this week */ Week.prototype.getIntervals = function(clean) { clean = clean || false; if(clean) { //Create continuous intervals over days var minuteArray = this.getAsMinutesArray(); var intervals = []; var dayStart = -1, minStart = -1, minEnd; for(var day=0, l=minuteArray.length; day < l; day++) { for(var min=0, lm=minuteArray[day].length; min < lm; min++) { //First minute of monday if(day == 0 && min == 0) { if(minuteArray[day][min]) { dayStart = day; minStart = min; } } //Last minute of sunday else if(day == DAYS_MAX && min == lm-1) { if(dayStart >= 0 && minuteArray[day][min]) { intervals.push(new Interval( dayStart, day, minStart, min )); } } //Other days or minutes else { //New interval if(minuteArray[day][min] && dayStart < 0) { dayStart = day; minStart = min; } //Ending interval else if(!minuteArray[day][min] && dayStart >= 0) { if(min == 0) { intervals.push(new Interval( dayStart, day-1, minStart, MINUTES_MAX )); } else { intervals.push(new Interval( dayStart, day, minStart, min-1 )); } dayStart = -1; minStart = -1; } } } } return intervals; } else { return this._intervals; } }; /** * Returns the intervals which are different from those defined in the given week * @param w The general week * @return The intervals which are different, as object { open: [ Intervals ], closed: [ Intervals ] } */ Week.prototype.getIntervalsDiff = function(w) { //Get minutes arrays var myMinArray = this.getAsMinutesArray(); var wMinArray = w.getAsMinutesArray(); //Create diff array var intervals = { open: [], closed: [] }; var dayStart = -1, minStart = -1, minEnd; var diffDay, m, intervalsLength; for(var d=0; d <= DAYS_MAX; d++) { diffDay = false; m = 0; intervalsLength = intervals.open.length; while(m <= MINUTES_MAX) { //Copy entire day if(diffDay) { //First minute of monday if(d == 0 && m == 0) { if(myMinArray[d][m]) { dayStart = d; minStart = m; } } //Last minute of sunday else if(d == DAYS_MAX && m == MINUTES_MAX) { if(dayStart >= 0 && myMinArray[d][m]) { intervals.open.push(new Interval( dayStart, d, minStart, m )); } } //Other days or minutes else { //New interval if(myMinArray[d][m] && dayStart < 0) { dayStart = d; minStart = m; } //Ending interval else if(!myMinArray[d][m] && dayStart >= 0) { if(m == 0) { intervals.open.push(new Interval( dayStart, d-1, minStart, MINUTES_MAX )); } else { intervals.open.push(new Interval( dayStart, d, minStart, m-1 )); } dayStart = -1; minStart = -1; } } m++; } //Check for diff else { diffDay = myMinArray[d][m] ? !wMinArray[d][m] : wMinArray[d][m]; //If there is a difference, start to copy full day if(diffDay) { m = 0; } //Else, continue checking else { m++; } } } //Close intervals if day is identical if(!diffDay && dayStart > -1) { intervals.open.push(new Interval( dayStart, d-1, minStart, MINUTES_MAX )); dayStart = -1; minStart = -1; } //Create closed intervals if closed all day if(diffDay && dayStart == -1 && intervalsLength == intervals.open.length) { //Merge with previous interval if possible if(intervals.closed.length > 0 && intervals.closed[intervals.closed.length-1].getEndDay() == d - 1) { intervals.closed[intervals.closed.length-1] = new Interval( intervals.closed[intervals.closed.length-1].getStartDay(), d, 0, MINUTES_MAX ); } else { intervals.closed.push(new Interval(d, d, 0, MINUTES_MAX)); } } } return intervals; }; //MODIFIERS /** * Add a new interval to this week * @param interval The new interval * @return The ID of the added interval */ Week.prototype.addInterval = function(interval) { this._intervals[this._intervals.length] = interval; return this._intervals.length-1; }; /** * Edits the given interval * @param id The interval ID * @param interval The new interval */ Week.prototype.editInterval = function(id, interval) { this._intervals[id] = interval; }; /** * Remove the given interval * @param id the interval ID */ Week.prototype.removeInterval = function(id) { this._intervals[id] = undefined; }; /** * Removes all intervals during a given day */ Week.prototype.removeIntervalsDuringDay = function(day) { var interval, itLength = this._intervals.length, dayDiff; for(var i=0; i < itLength; i++) { interval = this._intervals[i]; if(interval != undefined) { //If interval over given day if(interval.getStartDay() <= day && interval.getEndDay() >= day) { dayDiff = interval.getEndDay() - interval.getStartDay(); //Avoid deletion if over night interval if(dayDiff > 1 || dayDiff == 0 || interval.getStartDay() == day || interval.getFrom() <= interval.getTo()) { //Create new interval if several day if(interval.getEndDay() - interval.getStartDay() >= 1 && interval.getFrom() <= interval.getTo()) { if(interval.getStartDay() < day) { this.addInterval(new Interval(interval.getStartDay(), day-1, interval.getFrom(), 24*60)); } if(interval.getEndDay() > day) { this.addInterval(new Interval(day+1, interval.getEndDay(), 0, interval.getTo())); } } //Delete this.removeInterval(i); } } } } }; /** * Redefines this date range intervals with a copy of the given ones */ Week.prototype.copyIntervals = function(intervals) { this._intervals = []; for(var i=0; i < intervals.length; i++) { if(intervals[i] != undefined) { this._intervals.push($.extend(true, {}, intervals[i])); } } }; //OTHER METHODS /** * Is this week defining the same intervals as the given one ? */ Week.prototype.sameAs = function(w) { return w.getAsMinutesArray().equals(this.getAsMinutesArray()); }; /** * Class DateRange, defines a range of months, weeks or days. * A typical week or day will be associated. */ var DateRange = function(w) { //ATTRIBUTES /** The wide interval of this date range **/ this._wideInterval = null; /** The typical week or day associated **/ this._typical = undefined; //CONSTRUCTOR this.updateRange(w); }; //ACCESSORS /** * Is this interval defining a typical day ? */ DateRange.prototype.definesTypicalDay = function() { return this._typical instanceof Day; }; /** * Is this interval defining a typical week ? */ DateRange.prototype.definesTypicalWeek = function() { return this._typical instanceof Week; }; /** * @return The typical day or week */ DateRange.prototype.getTypical = function() { return this._typical; }; /** * @return The wide interval this date range concerns */ DateRange.prototype.getInterval = function() { return this._wideInterval; }; //MODIFIERS /** * Changes the date range */ DateRange.prototype.updateRange = function(wide) { this._wideInterval = (wide != null) ? wide : new WideInterval().always(); //Create typical week/day if(this._typical == undefined) { switch(this._wideInterval.getType()) { case "day": if(this._wideInterval.getEnd() == null) { this._typical = new Day(); } else { this._typical = new Week(); } break; case "week": this._typical = new Week(); break; case "month": this._typical = new Week(); break; case "holiday": if(this._wideInterval.getStart().holiday == "SH") { this._typical = new Week(); } else { this._typical = new Day(); } break; case "always": this._typical = new Week(); break; default: throw Error("Invalid interval type: "+this._wideInterval.getType()); } } }; //OTHER METHODS /** * Check if the typical day/week of this date range is the same as in the given date range * @param dr The other DateRange * @return True if same typical day/week */ DateRange.prototype.hasSameTypical = function(dr) { return this.definesTypicalDay() == dr.definesTypicalDay() && this._typical.sameAs(dr.getTypical()); }; /** * Does this date range contains the given date range (ie the second is a refinement of the first) * @param start The start of the date range * @param end The end of the date range * @return True if this date contains the given one (and is not strictly equal to) */ DateRange.prototype.isGeneralFor = function(dr) { return dr.definesTypicalDay() == this.definesTypicalDay() && this._wideInterval.contains(dr.getInterval()); }; /** * An opening_hours time, such as "08:00" or "08:00-10:00" or "off" (if no start and end) * @param start The start minute (from midnight), can be null * @param end The end minute (from midnight), can be null */ var OhTime = function(start, end) { //ATTRIBUTES /** The start minute **/ this._start = (start >= 0) ? start : null; /** The end minute **/ this._end = (end >= 0 && end != start) ? end : null; }; //ACCESSORS /** * @return The time in opening_hours format */ OhTime.prototype.get = function() { if(this._start === null && this._end === null) { return "off"; } else { return this._timeString(this._start) + ((this._end == null) ? "" : "-" + this._timeString(this._end)); } }; /** * @return The start minutes */ OhTime.prototype.getStart = function() { return this._start; }; /** * @return The end minutes */ OhTime.prototype.getEnd = function() { return this._end; }; /** * @return True if same time */ OhTime.prototype.equals = function(t) { return this._start == t.getStart() && this._end == t.getEnd(); }; //OTHER METHODS /** * @return The hour in HH:MM format */ OhTime.prototype._timeString = function(minutes) { var h = Math.floor(minutes / 60); var period = ""; var m = minutes % 60; return (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m + period; }; /** * An opening_hours date, such as "Apr 21", "week 1-15 Mo,Tu", "Apr-Dec Mo-Fr", "SH Su", ... * @param w The wide selector, as string * @param wt The wide selector type (month, week, day, holiday, always) * @param wd The weekdays, as integer array (0 to 6 = Monday to Sunday, -1 = single day date, -2 = PH) */ var OhDate = function(w, wt, wd) { //ATTRIBUTES /** Kind of wide date (month, week, day, holiday, always) **/ this._wideType = wt; /** Wide date **/ this._wide = w; /** Weekdays + PH **/ this._weekdays = wd.sort(); /** Overwritten days (to allow create simpler rules) **/ this._wdOver = []; //CONSTRUCTOR if(w == null || wt == null || wd == null) { throw Error("Missing parameter"); } }; //ACCESSORS /** * @return The wide type */ OhDate.prototype.getWideType = function() { return this._wideType; }; /** * @return The monthday, month, week, SH (depends of type) */ OhDate.prototype.getWideValue = function() { return this._wide; }; /** * @return The weekdays array */ OhDate.prototype.getWd = function() { return this._weekdays; }; /** * @return The overwrittent weekdays array */ OhDate.prototype.getWdOver = function() { return this._wdOver; }; /** * @param a The other weekdays array * @return True if same weekdays as other object */ OhDate.prototype.sameWd = function(a) { return a.equals(this._weekdays); }; /** * @return The weekdays in opening_hours syntax */ OhDate.prototype.getWeekdays = function() { var result = ""; var wd = this._weekdays.concat(this._wdOver).sort(); //PH as weekday if(wd.length > 0 && wd[0] == PH_WEEKDAY) { result = "PH"; wd.shift(); } //Check if we should create a continuous interval for week-end if(wd.length > 0 && wd.contains(6) && wd.contains(0) && (wd.contains(5) || wd.contains(1))) { //Find when the week-end starts var startWE = 6; var i=wd.length-2, stopLooking = false; while(!stopLooking && i >= 0) { if(wd[i] == wd[i+1] - 1) { startWE = wd[i]; i--; } else { stopLooking = true; } } //Find when it stops i=1; stopLooking = false; var endWE = 0; while(!stopLooking && i < wd.length) { if(wd[i-1] == wd[i] - 1) { endWE = wd[i]; i++; } else { stopLooking = true; } } //If long enough, add it as first weekday interval var length = 7 - startWE + endWE + 1; if(length >= 3 && startWE > endWE) { if(result.length > 0) { result += ","; } result += OSM_DAYS[startWE]+"-"+OSM_DAYS[endWE]; //Remove processed days var j=0; while(j < wd.length) { if(wd[j] <= endWE || wd[j] >= startWE) { wd.splice(j, 1); } else { j++; } } } } //Process only if not empty weekday list if(wd.length > 1 || (wd.length == 1 && wd[0] != -1)) { result += (result.length > 0) ? ","+OSM_DAYS[wd[0]] : OSM_DAYS[wd[0]]; var firstInRow = wd[0]; for(var i=1; i < wd.length; i++) { //When days aren't following if(wd[i-1] != wd[i] - 1) { //Previous day range length > 1 if(firstInRow != wd[i-1]) { //Two days if(wd[i-1] - firstInRow == 1) { result += ","+OSM_DAYS[wd[i-1]]; } else { result += "-"+OSM_DAYS[wd[i-1]]; } } //Add the current day result += ","+OSM_DAYS[wd[i]]; firstInRow = wd[i]; } else if(i==wd.length-1) { if(wd[i] - firstInRow == 1) { result += ","+OSM_DAYS[wd[i]]; } else { result += "-"+OSM_DAYS[wd[i]]; } } } } if(result == "Mo-Su") { result = ""; } return result; }; /** * Is the given object of the same kind as this one * @return True if same weekdays and same wide type */ OhDate.prototype.sameKindAs = function(d) { return this._wideType == d.getWideType() && d.sameWd(this._weekdays); }; /** * @return True if this object is equal to the given one */ OhDate.prototype.equals = function(o) { return o instanceof OhDate && this._wideType == o.getWideType() && this._wide == o.getWideValue() && o.sameWd(this._weekdays); }; //MODIFIERS /** * Adds a new weekday in this date */ OhDate.prototype.addWeekday = function(wd) { if(!this._weekdays.contains(wd) && !this._wdOver.contains(wd)) { this._weekdays.push(wd); this._weekdays = this._weekdays.sort(); } }; /** * Adds public holiday as a weekday of this date */ OhDate.prototype.addPhWeekday = function() { this.addWeekday(PH_WEEKDAY); }; /** * Adds an overwritten weekday, which can be included in this date and that will be overwritten in a following rule */ OhDate.prototype.addOverwrittenWeekday = function(wd) { if(!this._wdOver.contains(wd) && !this._weekdays.contains(wd)) { this._wdOver.push(wd); this._wdOver = this._wdOver.sort(); } } /** * An opening_hours rule, such as "Mo,Tu 08:00-18:00" */ var OhRule = function() { //ATTRIBUTES /** The date selectors **/ this._date = []; /** The time selectors **/ this._time = []; }; //ACCESSORS /** * @return The date selectors, as an array */ OhRule.prototype.getDate = function() { return this._date; }; /** * @return The time selectors, as an array */ OhRule.prototype.getTime = function() { return this._time; }; /** * @return The opening_hours value */ OhRule.prototype.get = function() { var result = ""; //Create date part if(this._date.length > 1 || this._date[0].getWideValue() != "") { //Add wide selectors for(var i=0, l=this._date.length; i < l; i++) { if(i > 0) { result += ","; } result += this._date[i].getWideValue(); } } //Add weekdays if(this._date.length > 0) { var wd = this._date[0].getWeekdays(); if(wd.length > 0) { result += " "+wd; } } //Create time part if(this._time.length > 0) { result += " "; for(var i=0, l=this._time.length; i < l; i++) { if(i > 0) { result += ","; } result += this._time[i].get(); } } else { result += " off"; } if(result.trim() == "00:00-24:00") { result = "24/7"; } return result.trim(); }; /** * @return True if the given rule has the same time as this one */ OhRule.prototype.sameTime = function(o) { if(o == undefined || o == null || o.getTime().length != this._time.length) { return false; } else { for(var i=0, l=this._time.length; i < l; i++) { if(!this._time[i].equals(o.getTime()[i])) { return false; } } return true; } }; /** * Is this rule concerning off time ? */ OhRule.prototype.isOff = function() { return this._time.length == 0 || (this._time.length == 1 && this._time[0].getStart() == null); }; /** * Does the rule have any overwritten weekday ? */ OhRule.prototype.hasOverwrittenWeekday = function() { return this._date.length > 0 && this._date[0]._wdOver.length > 0; }; //MODIFIERS /** * Adds a weekday to all the dates */ OhRule.prototype.addWeekday = function(wd) { for(var i=0; i < this._date.length; i++) { this._date[i].addWeekday(wd); } }; /** * Adds public holidays as weekday to all dates */ OhRule.prototype.addPhWeekday = function() { for(var i=0; i < this._date.length; i++) { this._date[i].addPhWeekday(); } }; /** * Adds an overwritten weekday to all the dates */ OhRule.prototype.addOverwrittenWeekday = function(wd) { for(var i=0; i < this._date.length; i++) { this._date[i].addOverwrittenWeekday(wd); } }; /** * @param d A new date selector */ OhRule.prototype.addDate = function(d) { //Check param if(d == null || d == undefined || !d instanceof OhDate) { throw Error("Invalid parameter"); } //Check if date can be added if(this._date.length == 0 || (this._date[0].getWideType() != "always" && this._date[0].sameKindAs(d))) { this._date.push(d); } else { if(this._date.length != 1 || this._date[0].getWideType() != "always" || !this._date[0].sameWd(d.getWd())) { throw Error("This date can't be added to this rule"); } } }; /** * @param t A new time selector */ OhRule.prototype.addTime = function(t) { if((this._time.length == 0 || this._time[0].get() != "off") && !this._time.contains(t)) { this._time.push(t); } else { throw Error("This time can't be added to this rule"); } }; /** * Class OpeningHoursBuilder, creates opening_hours value from date range object */ var OpeningHoursBuilder = function() {}; //OTHER METHODS /** * Parses several date ranges to create an opening_hours string * @param dateRanges The date ranges to parse * @return The opening_hours string */ OpeningHoursBuilder.prototype.build = function(dateRanges) { var rules = []; var dateRange, ohrules, ohrule, ohruleAdded, ruleId, rangeGeneral, rangeGeneralFor; //Read each date range for(var rangeId=0, l=dateRanges.length; rangeId < l; rangeId++) { dateRange = dateRanges[rangeId]; if(dateRange != undefined) { //Check if the defined typical week/day is not strictly equal to a previous wider rule rangeGeneral = null; rangeGeneralFor = null; var rangeGenId=rangeId-1; while(rangeGenId >= 0 && rangeGeneral == null) { if(dateRanges[rangeGenId] != undefined) { generalFor = dateRanges[rangeGenId].isGeneralFor(dateRange); if( dateRanges[rangeGenId].hasSameTypical(dateRange) && ( dateRanges[rangeGenId].getInterval().equals(dateRange.getInterval()) || generalFor ) ) { rangeGeneral = rangeGenId; } else if(generalFor && dateRanges[rangeGenId].definesTypicalWeek() && dateRange.definesTypicalWeek()) { rangeGeneralFor = rangeGenId; //Keep this ID to make differences in order to simplify result } } rangeGenId--; } if(rangeId == 0 || rangeGeneral == null) { //Get rules for this date range if(dateRange.definesTypicalWeek()) { if(rangeGeneralFor != null) { ohrules = this._buildWeekDiff(dateRange, dateRanges[rangeGeneralFor]); } else { ohrules = this._buildWeek(dateRange); } } else { ohrules = this._buildDay(dateRange); } //Process each rule for(var ohruleId=0, orl=ohrules.length; ohruleId < orl; ohruleId++) { ohrule = ohrules[ohruleId]; ohruleAdded = false; ruleId = 0; //Try to add them to previously defined ones while(!ohruleAdded && ruleId < rules.length) { //Identical one if(rules[ruleId].sameTime(ohrule)) { try { for(var dateId=0, dl=ohrule.getDate().length; dateId < dl; dateId++) { rules[ruleId].addDate(ohrule.getDate()[dateId]); } ohruleAdded = true; } //If first date not same kind as in found rule, continue catch(e) { //But before, try to merge PH with always weekdays if( ohrule.getDate()[0].getWideType() == "holiday" && ohrule.getDate()[0].getWideValue() == "PH" && rules[ruleId].getDate()[0].getWideType() == "always" ) { rules[ruleId].addPhWeekday(); ohruleAdded = true; } else if( rules[ruleId].getDate()[0].getWideType() == "holiday" && rules[ruleId].getDate()[0].getWideValue() == "PH" && ohrule.getDate()[0].getWideType() == "always" ) { ohrule.addPhWeekday(); rules[ruleId] = ohrule; ohruleAdded = true; } else { ruleId++; } } } else { ruleId++; } } //If not, add as new rule if(!ohruleAdded) { rules.push(ohrule); } //If some overwritten weekdays are still in last rule if(ohruleId == orl - 1 && ohrule.hasOverwrittenWeekday()) { var ohruleOWD = new OhRule(); for(var ohruleDateId = 0; ohruleDateId < ohrule.getDate().length; ohruleDateId++) { ohruleOWD.addDate( new OhDate( ohrule.getDate()[ohruleDateId].getWideValue(), ohrule.getDate()[ohruleDateId].getWideType(), ohrule.getDate()[ohruleDateId].getWdOver() ) ); } ohruleOWD.addTime(new OhTime()); ohrules.push(ohruleOWD); orl++; } } } } } //Create result string var result = ""; for(var ruleId=0, l=rules.length; ruleId < l; ruleId++) { if(ruleId > 0) { result += "; "; } result += rules[ruleId].get(); } return result; }; /*********************** * Top level functions * ***********************/ /** * Creates rules for a given typical day * @param dateRange The date range defining a typical day * @return An array of OhRules */ OpeningHoursBuilder.prototype._buildDay = function(dateRange) { var intervals = dateRange.getTypical().getIntervals(true); var interval; //Create rule var rule = new OhRule(); var date = new OhDate(dateRange.getInterval().getTimeSelector(), dateRange.getInterval().getType(), [ -1 ]); rule.addDate(date); //Read time for(var i=0, l=intervals.length; i < l; i++) { interval = intervals[i]; if(interval != undefined) { rule.addTime(new OhTime(interval.getFrom(), interval.getTo())); } } return [ rule ]; }; /** * Create rules for a date range defining a typical week * Algorithm inspired by OpeningHoursEdit plugin for JOSM * @param dateRange The date range defining a typical day * @return An array of OhRules */ OpeningHoursBuilder.prototype._buildWeek = function(dateRange) { var result = []; var intervals = dateRange.getTypical().getIntervals(true); var interval, rule, date; /* * Create time intervals per day */ var timeIntervals = this._createTimeIntervals(dateRange.getInterval().getTimeSelector(), dateRange.getInterval().getType(), intervals); var monday0 = timeIntervals[0]; var sunday24 = timeIntervals[1]; var days = timeIntervals[2]; //Create continuous night for monday-sunday days = this._nightMonSun(days, monday0, sunday24); /* * Group rules with same time */ // 0 means nothing done with this day yet // 8 means the day is off // -8 means the day is off and should be shown // 0<x<8 means the day have the openinghours of day x // -8<x<0 means nothing done with this day yet, but it intersects a // range of days with same opening_hours var daysStatus = []; //Init status for(var i=0; i < OSM_DAYS.length; i++) { daysStatus[i] = 0; } //Read status for(var i=0; i < days.length; i++) { if(days[i].isOff() && daysStatus[i] == 0) { daysStatus[i] = 8; } else if(days[i].isOff() && daysStatus[i] < 0 && daysStatus[i] > -8) { daysStatus[i] = -8; //Try to merge with another off day var merged = false, mdOff = 0; while(!merged && mdOff < i) { if(days[mdOff].isOff()) { days[mdOff].addWeekday(i); merged = true; } else { mdOff++; } } //If not merged, add it if(!merged) { result.push(days[i]); } } else if (daysStatus[i] <= 0 && daysStatus[i] > -8) { daysStatus[i] = i + 1; var lastSameDay = i; var sameDayCount = 1; for(var j = i + 1; j < days.length; j++) { if (days[i].sameTime(days[j])) { daysStatus[j] = i + 1; days[i].addWeekday(j); lastSameDay = j; sameDayCount++; } } if (sameDayCount == 1) { // a single Day with this special opening_hours result.push(days[i]); } else if (sameDayCount == 2) { // exactly two Days with this special opening_hours days[i].addWeekday(lastSameDay); result.push(days[i]); } else if (sameDayCount > 2) { // more than two Days with this special opening_hours for (var j = i + 1; j < lastSameDay; j++) { if (daysStatus[j] == 0) { daysStatus[j] = -i - 1; days[i].addOverwrittenWeekday(j); } } days[i].addWeekday(lastSameDay); result.push(days[i]); } } } result = this._mergeDays(result); return result; }; /** * Reads a week to create an opening_hours string for weeks which are overwriting a previous one * @param dateRange The date range defining a typical day * @param generalDateRange The date range which is wider than this one * @return An array of OhRules */ OpeningHoursBuilder.prototype._buildWeekDiff = function(dateRange, generalDateRange) { var intervals = dateRange.getTypical().getIntervalsDiff(generalDateRange.getTypical()); /* * Create time intervals per day */ //Open var timeIntervals = this._createTimeIntervals(dateRange.getInterval().getTimeSelector(), dateRange.getInterval().getType(), intervals.open); var monday0 = timeIntervals[0]; var sunday24 = timeIntervals[1]; var days = timeIntervals[2]; //Closed for(var i=0, l=intervals.closed.length; i < l; i++) { interval = intervals.closed[i]; for(var j=interval.getStartDay(); j <= interval.getEndDay(); j++) { days[j].addTime(new OhTime()); } } //Create continuous night for monday-sunday days = this._nightMonSun(days, monday0, sunday24); /* * Group rules with same time */ // 0 means nothing done with this day yet // 8 means the day is off // -8 means the day is off and should be shown // 0<x<8 means the day have the openinghours of day x // -8<x<0 means nothing done with this day yet, but it intersects a // range of days with same opening_hours var daysStatus = []; //Init status for(var i=0; i < OSM_DAYS.length; i++) { daysStatus[i] = 0; } //Read rules var result = []; for(var i=0; i < days.length; i++) { //Off day which must be shown if(days[i].isOff() && days[i].getTime().length == 1) { daysStatus[i] = -8; //Try to merge with another off day var merged = false, mdOff = 0; while(!merged && mdOff < i) { if(days[mdOff].isOff() && days[mdOff].getTime().length == 1) { days[mdOff].addWeekday(i); merged = true; } else { mdOff++; } } //If not merged, add it if(!merged) { result.push(days[i]); } } //Off day which must be hidden else if(days[i].isOff() && days[i].getTime().length == 0) { daysStatus[i] = 8; } //Non-processed day else if(daysStatus[i] <= 0 && daysStatus[i] > -8) { daysStatus[i] = i+1; var sameDayCount = 1; var lastSameDay = i; result.push(days[i]); for(var j = i + 1; j < days.length; j++) { if (days[i].sameTime(days[j])) { daysStatus[j] = i + 1; days[i].addWeekday(j); lastSameDa