| /* | 
 | * Licensed to the Apache Software Foundation (ASF) under one | 
 | * or more contributor license agreements.  See the NOTICE file | 
 | * distributed with this work for additional information | 
 | * regarding copyright ownership.  The ASF licenses this file | 
 | * to you under the Apache License, Version 2.0 (the | 
 | * "License"); you may not use this file except in compliance | 
 | * with the License.  You may obtain a copy of the License at | 
 | * | 
 | *   http://www.apache.org/licenses/LICENSE-2.0 | 
 | * | 
 | * Unless required by applicable law or agreed to in writing, | 
 | * software distributed under the License is distributed on an | 
 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
 | * KIND, either express or implied.  See the License for the | 
 | * specific language governing permissions and limitations | 
 | * under the License. | 
 | */ | 
 |  | 
 | import * as zrUtil from 'zrender/src/core/util'; | 
 | import * as layout from '../../util/layout'; | 
 | import * as numberUtil from '../../util/number'; | 
 | import BoundingRect, {RectLike} from 'zrender/src/core/BoundingRect'; | 
 | import CalendarModel from './CalendarModel'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import { | 
 |     LayoutOrient, | 
 |     ScaleDataValue, | 
 |     OptionDataValueDate, | 
 |     SeriesOption, | 
 |     SeriesOnCalendarOptionMixin | 
 | } from '../../util/types'; | 
 | import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; | 
 | import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; | 
 | import SeriesModel from '../../model/Series'; | 
 |  | 
 | // (24*60*60*1000) | 
 | const PROXIMATE_ONE_DAY = 86400000; | 
 |  | 
 |  | 
 | export interface CalendarParsedDateRangeInfo { | 
 |     range: [string, string], | 
 |     start: CalendarParsedDateInfo | 
 |     end: CalendarParsedDateInfo | 
 |     allDay: number | 
 |     weeks: number | 
 |     nthWeek: number | 
 |     fweek: number | 
 |     lweek: number | 
 | } | 
 |  | 
 | export interface CalendarParsedDateInfo { | 
 |     /** | 
 |      * local full year, eg., '1940' | 
 |      */ | 
 |     y: string | 
 |     /** | 
 |      * local month, from '01' ot '12', | 
 |      */ | 
 |     m: string | 
 |     /** | 
 |      * local date, from '01' to '31' (if exists), | 
 |      */ | 
 |     d: string | 
 |     /** | 
 |      * It is not date.getDay(). It is the location of the cell in a week, from 0 to 6, | 
 |      */ | 
 |     day: number | 
 |     /** | 
 |      * Timestamp | 
 |      */ | 
 |     time: number | 
 |     /** | 
 |      * yyyy-MM-dd | 
 |      */ | 
 |     formatedDate: string | 
 |     /** | 
 |      * The original date object | 
 |      */ | 
 |     date: Date | 
 | } | 
 |  | 
 | export interface CalendarCellRect { | 
 |     contentShape: RectLike | 
 |     center: number[] | 
 |     tl: number[] | 
 |     tr: number[] | 
 |     br: number[] | 
 |     bl: number[] | 
 | } | 
 |  | 
 | class Calendar implements CoordinateSystem, CoordinateSystemMaster { | 
 |  | 
 |     static readonly dimensions = ['time', 'value']; | 
 |     static getDimensionsInfo() { | 
 |         return [{ | 
 |             name: 'time', type: 'time' as const | 
 |         }, 'value']; | 
 |     } | 
 |  | 
 |     readonly type = 'calendar'; | 
 |  | 
 |     readonly dimensions = Calendar.dimensions; | 
 |  | 
 |     private _model: CalendarModel; | 
 |  | 
 |     private _rect: BoundingRect; | 
 |  | 
 |     private _sw: number; | 
 |     private _sh: number; | 
 |     private _orient: LayoutOrient; | 
 |  | 
 |     private _firstDayOfWeek: number; | 
 |  | 
 |     private _rangeInfo: CalendarParsedDateRangeInfo; | 
 |  | 
 |     private _lineWidth: number; | 
 |  | 
 |     constructor(calendarModel: CalendarModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         this._model = calendarModel; | 
 |     } | 
 |     // Required in createListFromData | 
 |     getDimensionsInfo = Calendar.getDimensionsInfo; | 
 |  | 
 |     getRangeInfo() { | 
 |         return this._rangeInfo; | 
 |     } | 
 |  | 
 |     getModel() { | 
 |         return this._model; | 
 |     } | 
 |  | 
 |     getRect() { | 
 |         return this._rect; | 
 |     } | 
 |  | 
 |     getCellWidth() { | 
 |         return this._sw; | 
 |     } | 
 |  | 
 |     getCellHeight() { | 
 |         return this._sh; | 
 |     } | 
 |  | 
 |     getOrient() { | 
 |         return this._orient; | 
 |     } | 
 |  | 
 |     /** | 
 |      * getFirstDayOfWeek | 
 |      * | 
 |      * @example | 
 |      *     0 : start at Sunday | 
 |      *     1 : start at Monday | 
 |      * | 
 |      * @return {number} | 
 |      */ | 
 |     getFirstDayOfWeek() { | 
 |         return this._firstDayOfWeek; | 
 |     } | 
 |  | 
 |     /** | 
 |      * get date info | 
 |      * } | 
 |      */ | 
 |     getDateInfo(date: OptionDataValueDate): CalendarParsedDateInfo { | 
 |  | 
 |         date = numberUtil.parseDate(date); | 
 |  | 
 |         const y = date.getFullYear(); | 
 |  | 
 |         const m = date.getMonth() + 1; | 
 |         const mStr = m < 10 ? '0' + m : '' + m; | 
 |  | 
 |         const d = date.getDate(); | 
 |         const dStr = d < 10 ? '0' + d : '' + d; | 
 |  | 
 |         let day = date.getDay(); | 
 |  | 
 |         day = Math.abs((day + 7 - this.getFirstDayOfWeek()) % 7); | 
 |  | 
 |         return { | 
 |             y: y + '', | 
 |             m: mStr, | 
 |             d: dStr, | 
 |             day: day, | 
 |             time: date.getTime(), | 
 |             formatedDate: y + '-' + mStr + '-' + dStr, | 
 |             date: date | 
 |         }; | 
 |     } | 
 |  | 
 |     getNextNDay(date: OptionDataValueDate, n: number) { | 
 |         n = n || 0; | 
 |         if (n === 0) { | 
 |             return this.getDateInfo(date); | 
 |         } | 
 |  | 
 |         date = new Date(this.getDateInfo(date).time); | 
 |         date.setDate(date.getDate() + n); | 
 |  | 
 |         return this.getDateInfo(date); | 
 |     } | 
 |  | 
 |     update(ecModel: GlobalModel, api: ExtensionAPI) { | 
 |  | 
 |         this._firstDayOfWeek = +this._model.getModel('dayLabel').get('firstDay'); | 
 |         this._orient = this._model.get('orient'); | 
 |         this._lineWidth = this._model.getModel('itemStyle').getItemStyle().lineWidth || 0; | 
 |  | 
 |  | 
 |         this._rangeInfo = this._getRangeInfo(this._initRangeOption()); | 
 |         const weeks = this._rangeInfo.weeks || 1; | 
 |         const whNames = ['width', 'height'] as const; | 
 |         const cellSize = this._model.getCellSize().slice(); | 
 |         const layoutParams = this._model.getBoxLayoutParams(); | 
 |         const cellNumbers = this._orient === 'horizontal' ? [weeks, 7] : [7, weeks]; | 
 |  | 
 |         zrUtil.each([0, 1] as const, function (idx) { | 
 |             if (cellSizeSpecified(cellSize, idx)) { | 
 |                 layoutParams[whNames[idx]] = cellSize[idx] * cellNumbers[idx]; | 
 |             } | 
 |         }); | 
 |  | 
 |         const whGlobal = { | 
 |             width: api.getWidth(), | 
 |             height: api.getHeight() | 
 |         }; | 
 |         const calendarRect = this._rect = layout.getLayoutRect(layoutParams, whGlobal); | 
 |  | 
 |         zrUtil.each([0, 1], function (idx) { | 
 |             if (!cellSizeSpecified(cellSize, idx)) { | 
 |                 cellSize[idx] = calendarRect[whNames[idx]] / cellNumbers[idx]; | 
 |             } | 
 |         }); | 
 |  | 
 |         function cellSizeSpecified(cellSize: (number | 'auto')[], idx: number): cellSize is number[] { | 
 |             return cellSize[idx] != null && cellSize[idx] !== 'auto'; | 
 |         } | 
 |  | 
 |         // Has been calculated out number. | 
 |         this._sw = cellSize[0] as number; | 
 |         this._sh = cellSize[1] as number; | 
 |     } | 
 |  | 
 |  | 
 |     /** | 
 |      * Convert a time data(time, value) item to (x, y) point. | 
 |      */ | 
 |     // TODO Clamp of calendar is not same with cartesian coordinate systems. | 
 |     // It will return NaN if data exceeds. | 
 |     dataToPoint(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean) { | 
 |         zrUtil.isArray(data) && (data = data[0]); | 
 |         clamp == null && (clamp = true); | 
 |  | 
 |         const dayInfo = this.getDateInfo(data); | 
 |         const range = this._rangeInfo; | 
 |         const date = dayInfo.formatedDate; | 
 |  | 
 |         // if not in range return [NaN, NaN] | 
 |         if (clamp && !( | 
 |             dayInfo.time >= range.start.time | 
 |             && dayInfo.time < range.end.time + PROXIMATE_ONE_DAY | 
 |         )) { | 
 |             return [NaN, NaN]; | 
 |         } | 
 |  | 
 |         const week = dayInfo.day; | 
 |         const nthWeek = this._getRangeInfo([range.start.time, date]).nthWeek; | 
 |  | 
 |         if (this._orient === 'vertical') { | 
 |             return [ | 
 |                 this._rect.x + week * this._sw + this._sw / 2, | 
 |                 this._rect.y + nthWeek * this._sh + this._sh / 2 | 
 |             ]; | 
 |  | 
 |         } | 
 |  | 
 |         return [ | 
 |             this._rect.x + nthWeek * this._sw + this._sw / 2, | 
 |             this._rect.y + week * this._sh + this._sh / 2 | 
 |         ]; | 
 |  | 
 |     } | 
 |  | 
 |     /** | 
 |      * Convert a (x, y) point to time data | 
 |      */ | 
 |     pointToData(point: number[]): number { | 
 |  | 
 |         const date = this.pointToDate(point); | 
 |  | 
 |         return date && date.time; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Convert a time date item to (x, y) four point. | 
 |      */ | 
 |     dataToRect(data: OptionDataValueDate | OptionDataValueDate[], clamp?: boolean): CalendarCellRect { | 
 |         const point = this.dataToPoint(data, clamp); | 
 |  | 
 |         return { | 
 |             contentShape: { | 
 |                 x: point[0] - (this._sw - this._lineWidth) / 2, | 
 |                 y: point[1] - (this._sh - this._lineWidth) / 2, | 
 |                 width: this._sw - this._lineWidth, | 
 |                 height: this._sh - this._lineWidth | 
 |             }, | 
 |  | 
 |             center: point, | 
 |  | 
 |             tl: [ | 
 |                 point[0] - this._sw / 2, | 
 |                 point[1] - this._sh / 2 | 
 |             ], | 
 |  | 
 |             tr: [ | 
 |                 point[0] + this._sw / 2, | 
 |                 point[1] - this._sh / 2 | 
 |             ], | 
 |  | 
 |             br: [ | 
 |                 point[0] + this._sw / 2, | 
 |                 point[1] + this._sh / 2 | 
 |             ], | 
 |  | 
 |             bl: [ | 
 |                 point[0] - this._sw / 2, | 
 |                 point[1] + this._sh / 2 | 
 |             ] | 
 |  | 
 |         }; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Convert a (x, y) point to time date | 
 |      * | 
 |      * @param  {Array} point point | 
 |      * @return {Object}       date | 
 |      */ | 
 |     pointToDate(point: number[]): CalendarParsedDateInfo { | 
 |         const nthX = Math.floor((point[0] - this._rect.x) / this._sw) + 1; | 
 |         const nthY = Math.floor((point[1] - this._rect.y) / this._sh) + 1; | 
 |         const range = this._rangeInfo.range; | 
 |  | 
 |         if (this._orient === 'vertical') { | 
 |             return this._getDateByWeeksAndDay(nthY, nthX - 1, range); | 
 |         } | 
 |  | 
 |         return this._getDateByWeeksAndDay(nthX, nthY - 1, range); | 
 |     } | 
 |  | 
 |     convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[]) { | 
 |         const coordSys = getCoordSys(finder); | 
 |         return coordSys === this ? coordSys.dataToPoint(value) : null; | 
 |     } | 
 |  | 
 |     convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { | 
 |         const coordSys = getCoordSys(finder); | 
 |         return coordSys === this ? coordSys.pointToData(pixel) : null; | 
 |     } | 
 |  | 
 |     containPoint(point: number[]): boolean { | 
 |         console.warn('Not implemented.'); | 
 |         return false; | 
 |     } | 
 |  | 
 |     /** | 
 |      * initRange | 
 |      * Normalize to an [start, end] array | 
 |      */ | 
 |     private _initRangeOption(): OptionDataValueDate[] { | 
 |         let range = this._model.get('range'); | 
 |         let normalizedRange: OptionDataValueDate[]; | 
 |  | 
 |         // Convert [1990] to 1990 | 
 |         if (zrUtil.isArray(range) && range.length === 1) { | 
 |             range = range[0]; | 
 |         } | 
 |  | 
 |         if (!zrUtil.isArray(range)) { | 
 |             const rangeStr = range.toString(); | 
 |             // One year. | 
 |             if (/^\d{4}$/.test(rangeStr)) { | 
 |                 normalizedRange = [rangeStr + '-01-01', rangeStr + '-12-31']; | 
 |             } | 
 |             // One month | 
 |             if (/^\d{4}[\/|-]\d{1,2}$/.test(rangeStr)) { | 
 |  | 
 |                 const start = this.getDateInfo(rangeStr); | 
 |                 const firstDay = start.date; | 
 |                 firstDay.setMonth(firstDay.getMonth() + 1); | 
 |  | 
 |                 const end = this.getNextNDay(firstDay, -1); | 
 |                 normalizedRange = [start.formatedDate, end.formatedDate]; | 
 |             } | 
 |             // One day | 
 |             if (/^\d{4}[\/|-]\d{1,2}[\/|-]\d{1,2}$/.test(rangeStr)) { | 
 |                 normalizedRange = [rangeStr, rangeStr]; | 
 |             } | 
 |         } | 
 |         else { | 
 |             normalizedRange = range; | 
 |         } | 
 |  | 
 |         if (!normalizedRange) { | 
 |             if (__DEV__) { | 
 |                 zrUtil.logError('Invalid date range.'); | 
 |             } | 
 |             // Not handling it. | 
 |             return range as OptionDataValueDate[]; | 
 |         } | 
 |  | 
 |         const tmp = this._getRangeInfo(normalizedRange); | 
 |  | 
 |         if (tmp.start.time > tmp.end.time) { | 
 |             normalizedRange.reverse(); | 
 |         } | 
 |  | 
 |         return normalizedRange; | 
 |     } | 
 |  | 
 |     /** | 
 |      * range info | 
 |      * | 
 |      * @private | 
 |      * @param  {Array} range range ['2017-01-01', '2017-07-08'] | 
 |      *  If range[0] > range[1], they will not be reversed. | 
 |      * @return {Object}       obj | 
 |      */ | 
 |     _getRangeInfo(range: OptionDataValueDate[]): CalendarParsedDateRangeInfo { | 
 |         const parsedRange = [ | 
 |             this.getDateInfo(range[0]), | 
 |             this.getDateInfo(range[1]) | 
 |         ]; | 
 |  | 
 |         let reversed; | 
 |         if (parsedRange[0].time > parsedRange[1].time) { | 
 |             reversed = true; | 
 |             parsedRange.reverse(); | 
 |         } | 
 |  | 
 |         let allDay = Math.floor(parsedRange[1].time / PROXIMATE_ONE_DAY) | 
 |             - Math.floor(parsedRange[0].time / PROXIMATE_ONE_DAY) + 1; | 
 |  | 
 |         // Consider case1 (#11677 #10430): | 
 |         // Set the system timezone as "UK", set the range to `['2016-07-01', '2016-12-31']` | 
 |  | 
 |         // Consider case2: | 
 |         // Firstly set system timezone as "Time Zone: America/Toronto", | 
 |         // ``` | 
 |         // let first = new Date(1478412000000 - 3600 * 1000 * 2.5); | 
 |         // let second = new Date(1478412000000); | 
 |         // let allDays = Math.floor(second / ONE_DAY) - Math.floor(first / ONE_DAY) + 1; | 
 |         // ``` | 
 |         // will get wrong result because of DST. So we should fix it. | 
 |         const date = new Date(parsedRange[0].time); | 
 |         const startDateNum = date.getDate(); | 
 |         const endDateNum = parsedRange[1].date.getDate(); | 
 |         date.setDate(startDateNum + allDay - 1); | 
 |         // The bias can not over a month, so just compare date. | 
 |         let dateNum = date.getDate(); | 
 |         if (dateNum !== endDateNum) { | 
 |             const sign = date.getTime() - parsedRange[1].time > 0 ? 1 : -1; | 
 |             while ( | 
 |                 (dateNum = date.getDate()) !== endDateNum | 
 |                 && (date.getTime() - parsedRange[1].time) * sign > 0 | 
 |             ) { | 
 |                 allDay -= sign; | 
 |                 date.setDate(dateNum - sign); | 
 |             } | 
 |         } | 
 |  | 
 |         const weeks = Math.floor((allDay + parsedRange[0].day + 6) / 7); | 
 |         const nthWeek = reversed ? -weeks + 1 : weeks - 1; | 
 |  | 
 |         reversed && parsedRange.reverse(); | 
 |  | 
 |         return { | 
 |             range: [parsedRange[0].formatedDate, parsedRange[1].formatedDate], | 
 |             start: parsedRange[0], | 
 |             end: parsedRange[1], | 
 |             allDay: allDay, | 
 |             weeks: weeks, | 
 |             // From 0. | 
 |             nthWeek: nthWeek, | 
 |             fweek: parsedRange[0].day, | 
 |             lweek: parsedRange[1].day | 
 |         }; | 
 |     } | 
 |  | 
 |     /** | 
 |      * get date by nthWeeks and week day in range | 
 |      * | 
 |      * @private | 
 |      * @param  {number} nthWeek the week | 
 |      * @param  {number} day   the week day | 
 |      * @param  {Array} range [d1, d2] | 
 |      * @return {Object} | 
 |      */ | 
 |     private _getDateByWeeksAndDay(nthWeek: number, day: number, range: OptionDataValueDate[]): CalendarParsedDateInfo { | 
 |         const rangeInfo = this._getRangeInfo(range); | 
 |  | 
 |         if (nthWeek > rangeInfo.weeks | 
 |             || (nthWeek === 0 && day < rangeInfo.fweek) | 
 |             || (nthWeek === rangeInfo.weeks && day > rangeInfo.lweek) | 
 |         ) { | 
 |             return null; | 
 |         } | 
 |  | 
 |         const nthDay = (nthWeek - 1) * 7 - rangeInfo.fweek + day; | 
 |         const date = new Date(rangeInfo.start.time); | 
 |         date.setDate(+rangeInfo.start.d + nthDay); | 
 |  | 
 |         return this.getDateInfo(date); | 
 |     } | 
 |  | 
 |     static create(ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         const calendarList: Calendar[] = []; | 
 |  | 
 |         ecModel.eachComponent('calendar', function (calendarModel: CalendarModel) { | 
 |             const calendar = new Calendar(calendarModel, ecModel, api); | 
 |             calendarList.push(calendar); | 
 |             calendarModel.coordinateSystem = calendar; | 
 |         }); | 
 |  | 
 |         ecModel.eachSeries(function (calendarSeries: SeriesModel<SeriesOption & SeriesOnCalendarOptionMixin>) { | 
 |             if (calendarSeries.get('coordinateSystem') === 'calendar') { | 
 |                 // Inject coordinate system | 
 |                 calendarSeries.coordinateSystem = calendarList[calendarSeries.get('calendarIndex') || 0]; | 
 |             } | 
 |         }); | 
 |         return calendarList; | 
 |     } | 
 | } | 
 |  | 
 | function getCoordSys(finder: ParsedModelFinderKnown): Calendar { | 
 |     const calendarModel = finder.calendarModel as CalendarModel; | 
 |     const seriesModel = finder.seriesModel; | 
 |  | 
 |     const coordSys = calendarModel | 
 |         ? calendarModel.coordinateSystem | 
 |         : seriesModel | 
 |         ? seriesModel.coordinateSystem | 
 |         : null; | 
 |  | 
 |     return coordSys as Calendar; | 
 | } | 
 |  | 
 | export default Calendar; |