| /* | 
 | * 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. | 
 | */ | 
 |  | 
 | /** | 
 |  * ECharts option manager | 
 |  */ | 
 |  | 
 |  | 
 | // import ComponentModel, { ComponentModelConstructor } from './Component'; | 
 | import ExtensionAPI from '../core/ExtensionAPI'; | 
 | import { | 
 |     OptionPreprocessor, MediaQuery, ECUnitOption, MediaUnit, ECBasicOption, SeriesOption | 
 | } from '../util/types'; | 
 | import GlobalModel, { InnerSetOptionOpts } from './Global'; | 
 | import { | 
 |     normalizeToArray | 
 |     // , MappingExistingItem, setComponentTypeToKeyInfo, mappingToExists | 
 | } from '../util/model'; | 
 | import { | 
 |     each, clone, map, isTypedArray, setAsPrimitive, isArray, isObject | 
 |     // , HashMap , createHashMap, extend, merge, | 
 | } from 'zrender/src/core/util'; | 
 | import { DatasetOption } from '../component/dataset/install'; | 
 | import { error } from '../util/log'; | 
 |  | 
 | const QUERY_REG = /^(min|max)?(.+)$/; | 
 |  | 
 | interface ParsedRawOption { | 
 |     baseOption: ECUnitOption; | 
 |     timelineOptions: ECUnitOption[]; | 
 |     mediaDefault: MediaUnit; | 
 |     mediaList: MediaUnit[]; | 
 | } | 
 |  | 
 | // Key: mainType | 
 | // type FakeComponentsMap = HashMap<(MappingExistingItem & { subType: string })[]>; | 
 |  | 
 | /** | 
 |  * TERM EXPLANATIONS: | 
 |  * See `ECOption` and `ECUnitOption` in `src/util/types.ts`. | 
 |  */ | 
 | class OptionManager { | 
 |  | 
 |     private _api: ExtensionAPI; | 
 |  | 
 |     private _timelineOptions: ECUnitOption[] = []; | 
 |  | 
 |     private _mediaList: MediaUnit[] = []; | 
 |  | 
 |     private _mediaDefault: MediaUnit; | 
 |  | 
 |     /** | 
 |      * -1, means default. | 
 |      * empty means no media. | 
 |      */ | 
 |     private _currentMediaIndices: number[] = []; | 
 |  | 
 |     private _optionBackup: ParsedRawOption; | 
 |  | 
 |     // private _fakeCmptsMap: FakeComponentsMap; | 
 |  | 
 |     private _newBaseOption: ECUnitOption; | 
 |  | 
 |     // timeline.notMerge is not supported in ec3. Firstly there is rearly | 
 |     // case that notMerge is needed. Secondly supporting 'notMerge' requires | 
 |     // rawOption cloned and backuped when timeline changed, which does no | 
 |     // good to performance. What's more, that both timeline and setOption | 
 |     // method supply 'notMerge' brings complex and some problems. | 
 |     // Consider this case: | 
 |     // (step1) chart.setOption({timeline: {notMerge: false}, ...}, false); | 
 |     // (step2) chart.setOption({timeline: {notMerge: true}, ...}, false); | 
 |  | 
 |     constructor(api: ExtensionAPI) { | 
 |         this._api = api; | 
 |     } | 
 |  | 
 |     setOption( | 
 |         rawOption: ECBasicOption, | 
 |         optionPreprocessorFuncs: OptionPreprocessor[], | 
 |         opt: InnerSetOptionOpts | 
 |     ): void { | 
 |         if (rawOption) { | 
 |             // That set dat primitive is dangerous if user reuse the data when setOption again. | 
 |             each(normalizeToArray((rawOption as ECUnitOption).series), function (series: SeriesOption) { | 
 |                 series && series.data && isTypedArray(series.data) && setAsPrimitive(series.data); | 
 |             }); | 
 |             each(normalizeToArray((rawOption as ECUnitOption).dataset), function (dataset: DatasetOption) { | 
 |                 dataset && dataset.source && isTypedArray(dataset.source) && setAsPrimitive(dataset.source); | 
 |             }); | 
 |         } | 
 |  | 
 |         // Caution: some series modify option data, if do not clone, | 
 |         // it should ensure that the repeat modify correctly | 
 |         // (create a new object when modify itself). | 
 |         rawOption = clone(rawOption); | 
 |  | 
 |         // FIXME | 
 |         // If some property is set in timeline options or media option but | 
 |         // not set in baseOption, a warning should be given. | 
 |  | 
 |         const optionBackup = this._optionBackup; | 
 |         const newParsedOption = parseRawOption( | 
 |             rawOption, optionPreprocessorFuncs, !optionBackup | 
 |         ); | 
 |         this._newBaseOption = newParsedOption.baseOption; | 
 |  | 
 |         // For setOption at second time (using merge mode); | 
 |         if (optionBackup) { | 
 |             // FIXME | 
 |             // the restore merge solution is essentially incorrect. | 
 |             // the mapping can not be 100% consistent with ecModel, which probably brings | 
 |             // potential bug! | 
 |  | 
 |             // The first merge is delayed, because in most cases, users do not call `setOption` twice. | 
 |             // let fakeCmptsMap = this._fakeCmptsMap; | 
 |             // if (!fakeCmptsMap) { | 
 |             //     fakeCmptsMap = this._fakeCmptsMap = createHashMap(); | 
 |             //     mergeToBackupOption(fakeCmptsMap, null, optionBackup.baseOption, null); | 
 |             // } | 
 |  | 
 |             // mergeToBackupOption( | 
 |             //     fakeCmptsMap, optionBackup.baseOption, newParsedOption.baseOption, opt | 
 |             // ); | 
 |  | 
 |             // For simplicity, timeline options and media options do not support merge, | 
 |             // that is, if you `setOption` twice and both has timeline options, the latter | 
 |             // timeline options will not be merged to the former, but just substitute them. | 
 |             if (newParsedOption.timelineOptions.length) { | 
 |                 optionBackup.timelineOptions = newParsedOption.timelineOptions; | 
 |             } | 
 |             if (newParsedOption.mediaList.length) { | 
 |                 optionBackup.mediaList = newParsedOption.mediaList; | 
 |             } | 
 |             if (newParsedOption.mediaDefault) { | 
 |                 optionBackup.mediaDefault = newParsedOption.mediaDefault; | 
 |             } | 
 |         } | 
 |         else { | 
 |             this._optionBackup = newParsedOption; | 
 |         } | 
 |     } | 
 |  | 
 |     mountOption(isRecreate: boolean): ECUnitOption { | 
 |         const optionBackup = this._optionBackup; | 
 |  | 
 |         this._timelineOptions = optionBackup.timelineOptions; | 
 |         this._mediaList = optionBackup.mediaList; | 
 |         this._mediaDefault = optionBackup.mediaDefault; | 
 |         this._currentMediaIndices = []; | 
 |  | 
 |         return clone(isRecreate | 
 |             // this._optionBackup.baseOption, which is created at the first `setOption` | 
 |             // called, and is merged into every new option by inner method `mergeToBackupOption` | 
 |             // each time `setOption` called, can be only used in `isRecreate`, because | 
 |             // its reliability is under suspicion. In other cases option merge is | 
 |             // performed by `model.mergeOption`. | 
 |             ? optionBackup.baseOption : this._newBaseOption | 
 |         ); | 
 |     } | 
 |  | 
 |     getTimelineOption(ecModel: GlobalModel): ECUnitOption { | 
 |         let option; | 
 |         const timelineOptions = this._timelineOptions; | 
 |  | 
 |         if (timelineOptions.length) { | 
 |             // getTimelineOption can only be called after ecModel inited, | 
 |             // so we can get currentIndex from timelineModel. | 
 |             const timelineModel = ecModel.getComponent('timeline'); | 
 |             if (timelineModel) { | 
 |                 option = clone( | 
 |                     // FIXME:TS as TimelineModel or quivlant interface | 
 |                     timelineOptions[(timelineModel as any).getCurrentIndex()] | 
 |                 ); | 
 |             } | 
 |         } | 
 |  | 
 |         return option; | 
 |     } | 
 |  | 
 |     getMediaOption(ecModel: GlobalModel): ECUnitOption[] { | 
 |         const ecWidth = this._api.getWidth(); | 
 |         const ecHeight = this._api.getHeight(); | 
 |         const mediaList = this._mediaList; | 
 |         const mediaDefault = this._mediaDefault; | 
 |         let indices = []; | 
 |         let result: ECUnitOption[] = []; | 
 |  | 
 |         // No media defined. | 
 |         if (!mediaList.length && !mediaDefault) { | 
 |             return result; | 
 |         } | 
 |  | 
 |         // Multi media may be applied, the latter defined media has higher priority. | 
 |         for (let i = 0, len = mediaList.length; i < len; i++) { | 
 |             if (applyMediaQuery(mediaList[i].query, ecWidth, ecHeight)) { | 
 |                 indices.push(i); | 
 |             } | 
 |         } | 
 |  | 
 |         // FIXME | 
 |         // Whether mediaDefault should force users to provide? Otherwise | 
 |         // the change by media query can not be recorvered. | 
 |         if (!indices.length && mediaDefault) { | 
 |             indices = [-1]; | 
 |         } | 
 |  | 
 |         if (indices.length && !indicesEquals(indices, this._currentMediaIndices)) { | 
 |             result = map(indices, function (index) { | 
 |                 return clone( | 
 |                     index === -1 ? mediaDefault.option : mediaList[index].option | 
 |                 ); | 
 |             }); | 
 |         } | 
 |         // Otherwise return nothing. | 
 |  | 
 |         this._currentMediaIndices = indices; | 
 |  | 
 |         return result; | 
 |     } | 
 |  | 
 | } | 
 |  | 
 | /** | 
 |  * [RAW_OPTION_PATTERNS] | 
 |  * (Note: "series: []" represents all other props in `ECUnitOption`) | 
 |  * | 
 |  * (1) No prop "baseOption" declared: | 
 |  * Root option is used as "baseOption" (except prop "options" and "media"). | 
 |  * ```js | 
 |  * option = { | 
 |  *     series: [], | 
 |  *     timeline: {}, | 
 |  *     options: [], | 
 |  * }; | 
 |  * option = { | 
 |  *     series: [], | 
 |  *     media: {}, | 
 |  * }; | 
 |  * option = { | 
 |  *     series: [], | 
 |  *     timeline: {}, | 
 |  *     options: [], | 
 |  *     media: {}, | 
 |  * } | 
 |  * ``` | 
 |  * | 
 |  * (2) Prop "baseOption" declared: | 
 |  * If "baseOption" declared, `ECUnitOption` props can only be declared | 
 |  * inside "baseOption" except prop "timeline" (compat ec2). | 
 |  * ```js | 
 |  * option = { | 
 |  *     baseOption: { | 
 |  *         timeline: {}, | 
 |  *         series: [], | 
 |  *     }, | 
 |  *     options: [] | 
 |  * }; | 
 |  * option = { | 
 |  *     baseOption: { | 
 |  *         series: [], | 
 |  *     }, | 
 |  *     media: [] | 
 |  * }; | 
 |  * option = { | 
 |  *     baseOption: { | 
 |  *         timeline: {}, | 
 |  *         series: [], | 
 |  *     }, | 
 |  *     options: [] | 
 |  *     media: [] | 
 |  * }; | 
 |  * option = { | 
 |  *     // ec3 compat ec2: allow (only) `timeline` declared | 
 |  *     // outside baseOption. Keep this setting for compat. | 
 |  *     timeline: {}, | 
 |  *     baseOption: { | 
 |  *         series: [], | 
 |  *     }, | 
 |  *     options: [], | 
 |  *     media: [] | 
 |  * }; | 
 |  * ``` | 
 |  */ | 
 | function parseRawOption( | 
 |     // `rawOption` May be modified | 
 |     rawOption: ECBasicOption, | 
 |     optionPreprocessorFuncs: OptionPreprocessor[], | 
 |     isNew: boolean | 
 | ): ParsedRawOption { | 
 |     const mediaList: MediaUnit[] = []; | 
 |     let mediaDefault: MediaUnit; | 
 |     let baseOption: ECUnitOption; | 
 |  | 
 |     const declaredBaseOption = rawOption.baseOption; | 
 |     // Compatible with ec2, [RAW_OPTION_PATTERNS] above. | 
 |     const timelineOnRoot = rawOption.timeline; | 
 |     const timelineOptionsOnRoot = rawOption.options; | 
 |     const mediaOnRoot = rawOption.media; | 
 |     const hasMedia = !!rawOption.media; | 
 |     const hasTimeline = !!( | 
 |         timelineOptionsOnRoot || timelineOnRoot || (declaredBaseOption && declaredBaseOption.timeline) | 
 |     ); | 
 |  | 
 |     if (declaredBaseOption) { | 
 |         baseOption = declaredBaseOption; | 
 |         // For merge option. | 
 |         if (!baseOption.timeline) { | 
 |             baseOption.timeline = timelineOnRoot; | 
 |         } | 
 |     } | 
 |     // For convenience, enable to use the root option as the `baseOption`: | 
 |     // `{ ...normalOptionProps, media: [{ ... }, { ... }] }` | 
 |     else { | 
 |         if (hasTimeline || hasMedia) { | 
 |             rawOption.options = rawOption.media = null; | 
 |         } | 
 |         baseOption = rawOption; | 
 |     } | 
 |  | 
 |     if (hasMedia) { | 
 |         if (isArray(mediaOnRoot)) { | 
 |             each(mediaOnRoot, function (singleMedia) { | 
 |                 if (__DEV__) { | 
 |                     // Real case of wrong config. | 
 |                     if (singleMedia | 
 |                         && !singleMedia.option | 
 |                         && isObject(singleMedia.query) | 
 |                         && isObject((singleMedia.query as any).option) | 
 |                     ) { | 
 |                         error('Illegal media option. Must be like { media: [ { query: {}, option: {} } ] }'); | 
 |                     } | 
 |                 } | 
 |                 if (singleMedia && singleMedia.option) { | 
 |                     if (singleMedia.query) { | 
 |                         mediaList.push(singleMedia); | 
 |                     } | 
 |                     else if (!mediaDefault) { | 
 |                         // Use the first media default. | 
 |                         mediaDefault = singleMedia; | 
 |                     } | 
 |                 } | 
 |             }); | 
 |         } | 
 |         else { | 
 |             if (__DEV__) { | 
 |                 // Real case of wrong config. | 
 |                 error('Illegal media option. Must be an array. Like { media: [ {...}, {...} ] }'); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     doPreprocess(baseOption); | 
 |     each(timelineOptionsOnRoot, option => doPreprocess(option)); | 
 |     each(mediaList, media => doPreprocess(media.option)); | 
 |  | 
 |     function doPreprocess(option: ECUnitOption) { | 
 |         each(optionPreprocessorFuncs, function (preProcess) { | 
 |             preProcess(option, isNew); | 
 |         }); | 
 |     } | 
 |  | 
 |     return { | 
 |         baseOption: baseOption, | 
 |         timelineOptions: timelineOptionsOnRoot || [], | 
 |         mediaDefault: mediaDefault, | 
 |         mediaList: mediaList | 
 |     }; | 
 | } | 
 |  | 
 | /** | 
 |  * @see <http://www.w3.org/TR/css3-mediaqueries/#media1> | 
 |  * Support: width, height, aspectRatio | 
 |  * Can use max or min as prefix. | 
 |  */ | 
 | function applyMediaQuery(query: MediaQuery, ecWidth: number, ecHeight: number): boolean { | 
 |     const realMap = { | 
 |         width: ecWidth, | 
 |         height: ecHeight, | 
 |         aspectratio: ecWidth / ecHeight // lower case for convenience. | 
 |     }; | 
 |  | 
 |     let applicable = true; | 
 |  | 
 |     each(query, function (value: number, attr) { | 
 |         const matched = attr.match(QUERY_REG); | 
 |  | 
 |         if (!matched || !matched[1] || !matched[2]) { | 
 |             return; | 
 |         } | 
 |  | 
 |         const operator = matched[1]; | 
 |         const realAttr = matched[2].toLowerCase(); | 
 |  | 
 |         if (!compare(realMap[realAttr as keyof typeof realMap], value, operator)) { | 
 |             applicable = false; | 
 |         } | 
 |     }); | 
 |  | 
 |     return applicable; | 
 | } | 
 |  | 
 | function compare(real: number, expect: number, operator: string): boolean { | 
 |     if (operator === 'min') { | 
 |         return real >= expect; | 
 |     } | 
 |     else if (operator === 'max') { | 
 |         return real <= expect; | 
 |     } | 
 |     else { // Equals | 
 |         return real === expect; | 
 |     } | 
 | } | 
 |  | 
 | function indicesEquals(indices1: number[], indices2: number[]): boolean { | 
 |     // indices is always order by asc and has only finite number. | 
 |     return indices1.join(',') === indices2.join(','); | 
 | } | 
 |  | 
 | /** | 
 |  * Consider case: | 
 |  * `chart.setOption(opt1);` | 
 |  * Then user do some interaction like dataZoom, dataView changing. | 
 |  * `chart.setOption(opt2);` | 
 |  * Then user press 'reset button' in toolbox. | 
 |  * | 
 |  * After doing that all of the interaction effects should be reset, the | 
 |  * chart should be the same as the result of invoke | 
 |  * `chart.setOption(opt1); chart.setOption(opt2);`. | 
 |  * | 
 |  * Although it is not able ensure that | 
 |  * `chart.setOption(opt1); chart.setOption(opt2);` is equivalents to | 
 |  * `chart.setOption(merge(opt1, opt2));` exactly, | 
 |  * this might be the only simple way to implement that feature. | 
 |  * | 
 |  * MEMO: We've considered some other approaches: | 
 |  * 1. Each model handles its self restoration but not uniform treatment. | 
 |  *     (Too complex in logic and error-prone) | 
 |  * 2. Use a shadow ecModel. (Performance expensive) | 
 |  * | 
 |  * FIXME: A possible solution: | 
 |  * Add a extra level of model for each component model. The inheritance chain would be: | 
 |  * ecModel <- componentModel <- componentActionModel <- dataItemModel | 
 |  * And all of the actions can only modify the `componentActionModel` rather than | 
 |  * `componentModel`. `setOption` will only modify the `ecModel` and `componentModel`. | 
 |  * When "resotre" action triggered, model from `componentActionModel` will be discarded | 
 |  * instead of recreating the "ecModel" from the "_optionBackup". | 
 |  */ | 
 | // function mergeToBackupOption( | 
 | //     fakeCmptsMap: FakeComponentsMap, | 
 | //     // `tarOption` Can be null/undefined, means init | 
 | //     tarOption: ECUnitOption, | 
 | //     newOption: ECUnitOption, | 
 | //     // Can be null/undefined | 
 | //     opt: InnerSetOptionOpts | 
 | // ): void { | 
 | //     newOption = newOption || {} as ECUnitOption; | 
 | //     const notInit = !!tarOption; | 
 |  | 
 | //     each(newOption, function (newOptsInMainType, mainType) { | 
 | //         if (newOptsInMainType == null) { | 
 | //             return; | 
 | //         } | 
 |  | 
 | //         if (!ComponentModel.hasClass(mainType)) { | 
 | //             if (tarOption) { | 
 | //                 tarOption[mainType] = merge(tarOption[mainType], newOptsInMainType, true); | 
 | //             } | 
 | //         } | 
 | //         else { | 
 | //             const oldTarOptsInMainType = notInit ? normalizeToArray(tarOption[mainType]) : null; | 
 | //             const oldFakeCmptsInMainType = fakeCmptsMap.get(mainType) || []; | 
 | //             const resultTarOptsInMainType = notInit ? (tarOption[mainType] = [] as ComponentOption[]) : null; | 
 | //             const resultFakeCmptsInMainType = fakeCmptsMap.set(mainType, []); | 
 |  | 
 | //             const mappingResult = mappingToExists( | 
 | //                 oldFakeCmptsInMainType, | 
 | //                 normalizeToArray(newOptsInMainType), | 
 | //                 (opt && opt.replaceMergeMainTypeMap.get(mainType)) ? 'replaceMerge' : 'normalMerge' | 
 | //             ); | 
 | //             setComponentTypeToKeyInfo(mappingResult, mainType, ComponentModel as ComponentModelConstructor); | 
 |  | 
 | //             each(mappingResult, function (resultItem, index) { | 
 | //                 // The same logic as `Global.ts#_mergeOption`. | 
 | //                 let fakeCmpt = resultItem.existing; | 
 | //                 const newOption = resultItem.newOption; | 
 | //                 const keyInfo = resultItem.keyInfo; | 
 | //                 let fakeCmptOpt; | 
 |  | 
 | //                 if (!newOption) { | 
 | //                     fakeCmptOpt = oldTarOptsInMainType[index]; | 
 | //                 } | 
 | //                 else { | 
 | //                     if (fakeCmpt && fakeCmpt.subType === keyInfo.subType) { | 
 | //                         fakeCmpt.name = keyInfo.name; | 
 | //                         if (notInit) { | 
 | //                             fakeCmptOpt = merge(oldTarOptsInMainType[index], newOption, true); | 
 | //                         } | 
 | //                     } | 
 | //                     else { | 
 | //                         fakeCmpt = extend({}, keyInfo); | 
 | //                         if (notInit) { | 
 | //                             fakeCmptOpt = clone(newOption); | 
 | //                         } | 
 | //                     } | 
 | //                 } | 
 |  | 
 | //                 if (fakeCmpt) { | 
 | //                     notInit && resultTarOptsInMainType.push(fakeCmptOpt); | 
 | //                     resultFakeCmptsInMainType.push(fakeCmpt); | 
 | //                 } | 
 | //                 else { | 
 | //                     notInit && resultTarOptsInMainType.push(void 0); | 
 | //                     resultFakeCmptsInMainType.push(void 0); | 
 | //                 } | 
 | //             }); | 
 | //         } | 
 | //     }); | 
 | // } | 
 |  | 
 | export default OptionManager; |