| /* | 
 | * 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 {ArrayLike} from 'zrender/src/core/types'; | 
 |  | 
 | // return key. | 
 | type DiffKeyGetter<CTX = unknown> = | 
 |     (this: DataDiffer<CTX>, value: unknown, index: number) => string; | 
 |  | 
 | type DiffCallbackAdd = (newIndex: number) => void; | 
 | type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void; | 
 | type DiffCallbackRemove = (oldIndex: number) => void; | 
 | type DiffCallbackUpdateManyToOne = (newIndex: number, oldIndex: number[]) => void; | 
 | type DiffCallbackUpdateOneToMany = (newIndex: number[], oldIndex: number) => void; | 
 | type DiffCallbackUpdateManyToMany = (newIndex: number[], oldIndex: number[]) => void; | 
 |  | 
 | /** | 
 |  * The value of `DataIndexMap` can only be: | 
 |  * + a number | 
 |  * + a number[] that length >= 2. | 
 |  * + null/undefined | 
 |  */ | 
 | type DataIndexMap = {[key: string]: number | number[]}; | 
 |  | 
 | function dataIndexMapValueLength( | 
 |     valNumOrArrLengthMoreThan2: number | number[] | 
 | ): number { | 
 |     return valNumOrArrLengthMoreThan2 == null | 
 |         ? 0 | 
 |         : ((valNumOrArrLengthMoreThan2 as number[]).length || 1); | 
 | } | 
 |  | 
 | function defaultKeyGetter(item: string): string { | 
 |     return item; | 
 | } | 
 |  | 
 | export type DataDiffMode = 'oneToOne' | 'multiple'; | 
 |  | 
 | class DataDiffer<CTX = unknown> { | 
 |  | 
 |     private _old: ArrayLike<unknown>; | 
 |     private _new: ArrayLike<unknown>; | 
 |     private _oldKeyGetter: DiffKeyGetter<CTX>; | 
 |     private _newKeyGetter: DiffKeyGetter<CTX>; | 
 |     private _add: DiffCallbackAdd; | 
 |     private _update: DiffCallbackUpdate; | 
 |     private _updateManyToOne: DiffCallbackUpdateManyToOne; | 
 |     private _updateOneToMany: DiffCallbackUpdateOneToMany; | 
 |     private _updateManyToMany: DiffCallbackUpdateManyToMany; | 
 |     private _remove: DiffCallbackRemove; | 
 |     private _diffModeMultiple: boolean; | 
 |  | 
 |     readonly context: CTX; | 
 |  | 
 |     /** | 
 |      * @param context Can be visited by this.context in callback. | 
 |      */ | 
 |     constructor( | 
 |         oldArr: ArrayLike<unknown>, | 
 |         newArr: ArrayLike<unknown>, | 
 |         oldKeyGetter?: DiffKeyGetter<CTX>, | 
 |         newKeyGetter?: DiffKeyGetter<CTX>, | 
 |         context?: CTX, | 
 |         // By default: 'oneToOne'. | 
 |         diffMode?: DataDiffMode | 
 |     ) { | 
 |         this._old = oldArr; | 
 |         this._new = newArr; | 
 |  | 
 |         this._oldKeyGetter = oldKeyGetter || defaultKeyGetter; | 
 |         this._newKeyGetter = newKeyGetter || defaultKeyGetter; | 
 |  | 
 |         // Visible in callback via `this.context`; | 
 |         this.context = context; | 
 |  | 
 |         this._diffModeMultiple = diffMode === 'multiple'; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Callback function when add a data | 
 |      */ | 
 |     add(func: DiffCallbackAdd): this { | 
 |         this._add = func; | 
 |         return this; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Callback function when update a data | 
 |      */ | 
 |     update(func: DiffCallbackUpdate): this { | 
 |         this._update = func; | 
 |         return this; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Callback function when update a data and only work in `cbMode: 'byKey'`. | 
 |      */ | 
 |     updateManyToOne(func: DiffCallbackUpdateManyToOne): this { | 
 |         this._updateManyToOne = func; | 
 |         return this; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Callback function when update a data and only work in `cbMode: 'byKey'`. | 
 |      */ | 
 |     updateOneToMany(func: DiffCallbackUpdateOneToMany): this { | 
 |         this._updateOneToMany = func; | 
 |         return this; | 
 |     } | 
 |     /** | 
 |      * Callback function when update a data and only work in `cbMode: 'byKey'`. | 
 |      */ | 
 |      updateManyToMany(func: DiffCallbackUpdateManyToMany): this { | 
 |         this._updateManyToMany = func; | 
 |         return this; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Callback function when remove a data | 
 |      */ | 
 |     remove(func: DiffCallbackRemove): this { | 
 |         this._remove = func; | 
 |         return this; | 
 |     } | 
 |  | 
 |     execute(): void { | 
 |         this[this._diffModeMultiple ? '_executeMultiple' : '_executeOneToOne'](); | 
 |     } | 
 |  | 
 |     private _executeOneToOne(): void { | 
 |         const oldArr = this._old; | 
 |         const newArr = this._new; | 
 |         const newDataIndexMap: DataIndexMap = {}; | 
 |         const oldDataKeyArr: string[] = new Array(oldArr.length); | 
 |         const newDataKeyArr: string[] = new Array(newArr.length); | 
 |  | 
 |         this._initIndexMap(oldArr, null, oldDataKeyArr, '_oldKeyGetter'); | 
 |         this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter'); | 
 |  | 
 |         for (let i = 0; i < oldArr.length; i++) { | 
 |             const oldKey = oldDataKeyArr[i]; | 
 |             const newIdxMapVal = newDataIndexMap[oldKey]; | 
 |             const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal); | 
 |  | 
 |             // idx can never be empty array here. see 'set null' logic below. | 
 |             if (newIdxMapValLen > 1) { | 
 |                 // Consider there is duplicate key (for example, use dataItem.name as key). | 
 |                 // We should make sure every item in newArr and oldArr can be visited. | 
 |                 const newIdx = (newIdxMapVal as number[]).shift(); | 
 |                 if ((newIdxMapVal as number[]).length === 1) { | 
 |                     newDataIndexMap[oldKey] = (newIdxMapVal as number[])[0]; | 
 |                 } | 
 |                 this._update && this._update(newIdx as number, i); | 
 |             } | 
 |             else if (newIdxMapValLen === 1) { | 
 |                 newDataIndexMap[oldKey] = null; | 
 |                 this._update && this._update(newIdxMapVal as number, i); | 
 |             } | 
 |             else { | 
 |                 this._remove && this._remove(i); | 
 |             } | 
 |         } | 
 |  | 
 |         this._performRestAdd(newDataKeyArr, newDataIndexMap); | 
 |     } | 
 |  | 
 |     /** | 
 |      * For example, consider the case: | 
 |      * oldData: [o0, o1, o2, o3, o4, o5, o6, o7], | 
 |      * newData: [n0, n1, n2, n3, n4, n5, n6, n7, n8], | 
 |      * Where: | 
 |      *     o0, o1, n0 has key 'a' (many to one) | 
 |      *     o5, n4, n5, n6 has key 'b' (one to many) | 
 |      *     o2, n1 has key 'c' (one to one) | 
 |      *     n2, n3 has key 'd' (add) | 
 |      *     o3, o4 has key 'e' (remove) | 
 |      *     o6, o7, n7, n8 has key 'f' (many to many, treated as add and remove) | 
 |      * Then: | 
 |      *     (The order of the following directives are not ensured.) | 
 |      *     this._updateManyToOne(n0, [o0, o1]); | 
 |      *     this._updateOneToMany([n4, n5, n6], o5); | 
 |      *     this._update(n1, o2); | 
 |      *     this._remove(o3); | 
 |      *     this._remove(o4); | 
 |      *     this._remove(o6); | 
 |      *     this._remove(o7); | 
 |      *     this._add(n2); | 
 |      *     this._add(n3); | 
 |      *     this._add(n7); | 
 |      *     this._add(n8); | 
 |      */ | 
 |     private _executeMultiple(): void { | 
 |         const oldArr = this._old; | 
 |         const newArr = this._new; | 
 |         const oldDataIndexMap: DataIndexMap = {}; | 
 |         const newDataIndexMap: DataIndexMap = {}; | 
 |         const oldDataKeyArr: string[] = []; | 
 |         const newDataKeyArr: string[] = []; | 
 |  | 
 |         this._initIndexMap(oldArr, oldDataIndexMap, oldDataKeyArr, '_oldKeyGetter'); | 
 |         this._initIndexMap(newArr, newDataIndexMap, newDataKeyArr, '_newKeyGetter'); | 
 |  | 
 |         for (let i = 0; i < oldDataKeyArr.length; i++) { | 
 |             const oldKey = oldDataKeyArr[i]; | 
 |             const oldIdxMapVal = oldDataIndexMap[oldKey]; | 
 |             const newIdxMapVal = newDataIndexMap[oldKey]; | 
 |             const oldIdxMapValLen = dataIndexMapValueLength(oldIdxMapVal); | 
 |             const newIdxMapValLen = dataIndexMapValueLength(newIdxMapVal); | 
 |  | 
 |             if (oldIdxMapValLen > 1 && newIdxMapValLen === 1) { | 
 |                 this._updateManyToOne && this._updateManyToOne(newIdxMapVal as number, oldIdxMapVal as number[]); | 
 |                 newDataIndexMap[oldKey] = null; | 
 |             } | 
 |             else if (oldIdxMapValLen === 1 && newIdxMapValLen > 1) { | 
 |                 this._updateOneToMany && this._updateOneToMany(newIdxMapVal as number[], oldIdxMapVal as number); | 
 |                 newDataIndexMap[oldKey] = null; | 
 |             } | 
 |             else if (oldIdxMapValLen === 1 && newIdxMapValLen === 1) { | 
 |                 this._update && this._update(newIdxMapVal as number, oldIdxMapVal as number); | 
 |                 newDataIndexMap[oldKey] = null; | 
 |             } | 
 |             else if (oldIdxMapValLen > 1 && newIdxMapValLen > 1) { | 
 |                 this._updateManyToMany && this._updateManyToMany(newIdxMapVal as number[], oldIdxMapVal as number[]); | 
 |                 newDataIndexMap[oldKey] = null; | 
 |             } | 
 |             else if (oldIdxMapValLen > 1) { | 
 |                 for (let i = 0; i < oldIdxMapValLen; i++) { | 
 |                     this._remove && this._remove((oldIdxMapVal as number[])[i]); | 
 |                 } | 
 |             } | 
 |             else { | 
 |                 this._remove && this._remove(oldIdxMapVal as number); | 
 |             } | 
 |         } | 
 |  | 
 |         this._performRestAdd(newDataKeyArr, newDataIndexMap); | 
 |     } | 
 |  | 
 |     private _performRestAdd(newDataKeyArr: string[], newDataIndexMap: DataIndexMap) { | 
 |         for (let i = 0; i < newDataKeyArr.length; i++) { | 
 |             const newKey = newDataKeyArr[i]; | 
 |             const newIdxMapVal = newDataIndexMap[newKey]; | 
 |             const idxMapValLen = dataIndexMapValueLength(newIdxMapVal); | 
 |             if (idxMapValLen > 1) { | 
 |                 for (let j = 0; j < idxMapValLen; j++) { | 
 |                     this._add && this._add((newIdxMapVal as number[])[j]); | 
 |                 } | 
 |             } | 
 |             else if (idxMapValLen === 1) { | 
 |                 this._add && this._add(newIdxMapVal as number); | 
 |             } | 
 |             // Support both `newDataKeyArr` are duplication removed or not removed. | 
 |             newDataIndexMap[newKey] = null; | 
 |         } | 
 |     } | 
 |  | 
 |     private _initIndexMap( | 
 |         arr: ArrayLike<unknown>, | 
 |         // Can be null. | 
 |         map: DataIndexMap, | 
 |         // In 'byKey', the output `keyArr` is duplication removed. | 
 |         // In 'byIndex', the output `keyArr` is not duplication removed and | 
 |         //     its indices are accurately corresponding to `arr`. | 
 |         keyArr: string[], | 
 |         keyGetterName: '_oldKeyGetter' | '_newKeyGetter' | 
 |     ): void { | 
 |         const cbModeMultiple = this._diffModeMultiple; | 
 |  | 
 |         for (let i = 0; i < arr.length; i++) { | 
 |             // Add prefix to avoid conflict with Object.prototype. | 
 |             const key = '_ec_' + this[keyGetterName](arr[i], i); | 
 |             if (!cbModeMultiple) { | 
 |                 keyArr[i] = key; | 
 |             } | 
 |             if (!map) { | 
 |                 continue; | 
 |             } | 
 |  | 
 |             const idxMapVal = map[key]; | 
 |             const idxMapValLen = dataIndexMapValueLength(idxMapVal); | 
 |  | 
 |             if (idxMapValLen === 0) { | 
 |                 // Simple optimize: in most cases, one index has one key, | 
 |                 // do not need array. | 
 |                 map[key] = i; | 
 |                 if (cbModeMultiple) { | 
 |                     keyArr.push(key); | 
 |                 } | 
 |             } | 
 |             else if (idxMapValLen === 1) { | 
 |                 map[key] = [idxMapVal as number, i]; | 
 |             } | 
 |             else { | 
 |                 (idxMapVal as number[]).push(i); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 | } | 
 |  | 
 | export default DataDiffer; |