|  | /* | 
|  | * 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; |