<commit_before>/**
 * Copyright 2021 Google Inc.
 *
 * Licensed 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.
 *
 * @fileoverview Shim and trace calls to EME, and log them to the extension's
 * log window.
 */

/**
 * A custom logger to plug into TraceAnything.
 *
 * @param {TraceAnything.Log} log
 */
function emeLogger(log) {
  // Hook into the EME formatter interface for update calls and message events.
  if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeySession' &&
      log.methodName == 'update' &&
      log.args.length >= 1) {
    log.args[0] = formatEmeResponse(log.args[0]);
  } else if (log.type == TraceAnything.LogTypes.Event &&
      log.className == 'MediaKeySession' &&
      log.eventName == 'message') {
    // You can't (and shouldn't, to avoid breaking the app) modify the event
    // object here.  So clone it, and replace the message field of that.
    log.event = cloneEvent(log.event, {
      // This overrides the message field with a new value from the formatter.
      message: formatEmeRequest(log.event.message),
    });
  } else if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeys' &&
      log.methodName == 'setServerCertificate' &&
      log.args.length >= 1) {
    log.args[0] = formatEmeCertificate(log.args[0]);
  } else if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeySession' &&
      log.methodName == 'generateRequest' &&
      log.args.length >= 2) {
    log.args[1] = formatEmeInitData(log.args[0], log.args[1]);
  }

  // Log to the default logger in the JS console.
  TraceAnything.defaultLogger(log);

  // This is not needed and can't be easily serialized.
  delete log.instance;

  window.postMessage({type: 'emeTraceLog', log: prepLogForMessage(log)}, '*');
}

/**
 * Clone an event.  The clone will have the same type and values, except for the
 * values overridden by the overrides parameter.
 *
 * The spread operator and Object.assign can only read "own" properties of an
 * object, so this custom cloning method must be used for Events instead.
 *
 * @param {!Event} original
 * @param {Object=} overrides
 * @return {!Event}
 */
function cloneEvent(original, overrides) {
  const clone = {};

  // When reading the original, we must use a for-in loop to see all fields.
  // Static fields from Event and methods are skipped, but everything else is
  // copied into a plain object.
  for (const key in original) {
    if (!(key in Event) && (typeof original[key] != 'function')) {
      clone[key] = original[key];
    }
  }

  // Overrides are applied all at once using Object.assign.
  if (overrides) {
    Object.assign(clone, overrides);
  }

  // Setting the prototype of the clone gives the clone the same type as the
  // original.
  Object.setPrototypeOf(clone, original.constructor.prototype);

  return clone;
}

/**
 * @param {BufferSource} response
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeResponse(response) {
  if (!response || !document.emeFormatters) {
    return response;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedResponse;

      if (formatter.formatResponse) {  // v3 API
        formattedResponse = formatter.formatResponse(response);
      } else if (formatter.formatUpdateCall) {  // v2 API
        formattedResponse = formatter.formatUpdateCall(response);
      }

      if (formattedResponse) {
        return formattedResponse;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original response, which will be formatted as bytes.
  return response;
}

/**
 * @param {BufferSource} request
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeRequest(request) {
  if (!request || !document.emeFormatters) {
    return request;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedRequest;

      if (formatter.formatRequest) {  // v3 API
        formattedRequest = formatter.formatRequest(request);
      } else if (formatter.formatmessage) {  // v2 API
        formattedRequest = formatter.formatmessage(request);
      }

      if (formattedRequest) {
        return formattedRequest;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original request, which will be formatted as bytes.
  return request;
}

/**
 * @param {BufferSource} certificate
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeCertificate(certificate) {
  if (!certificate || !document.emeFormatters) {
    return certificate;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedCertificate;

      if (formatter.formatServerCertificate) {  // v3 API
        formattedCertificate = formatter.formatServerCertificate(certificate);
      }

      if (formattedCertificate) {
        return formattedCertificate;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original certificate, which will be formatted as bytes.
  return certificate;
}

/**
 * @param {string} initDataType
 * @param {BufferSource} initData
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeInitData(initDataType, initData) {
  if (!initData || !document.emeFormatters) {
    return initData;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedInitData;

      if (formatter.formatInitData) {  // v3 API
        formattedInitData = formatter.formatInitData(initDataType, initData);
      }

      if (formattedInitData) {
        return formattedInitData;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original init data, which will be formatted as bytes.
  return initData;
}

/**
 * @param {Object} log
 * @return {Object}
 */
function prepLogForMessage(log) {
  const clone = {};
  for (const k in log) {
    clone[k] = getSerializable(log[k]);
  }
  return clone;
}

/**
 * @param {*} obj A value that may or may not be serializable.
 * @return {*} A value that can be serialized.
 */
function getSerializable(obj) {
  // Return primitive types directly.
  if (obj == null || typeof obj == 'string' || typeof obj == 'number' ||
      typeof obj == 'boolean') {
    return obj;
  }

  // Events are full of garbage, so only serialize the interesting fields.
  if (obj instanceof Event) {
    const clone = {};
    for (const k in obj) {
      // Skip fields that are in the Event base class, as well as "isTrusted".
      // These are not interesting for logging.
      if (!(k in Event.prototype) && k != 'isTrusted') {
        clone[k] = getSerializable(obj[k]);
      }
    }
    return {
      __type__: obj.type + ' Event',
      __fields__: clone,
    };
  }

  // Elements, Nodes, and Windows are dangerous to serialize because they
  // contain many fields and circular references.
  if (obj instanceof Element) {
    return {
      __type__: '<' + obj.tagName.toLowerCase() + '> element',
    };
  }
  if (obj instanceof Node) {
    return {
      __type__: obj.nodeName.toLowerCase() + ' node',
    };
  }
  if (obj instanceof Window) {
    return {
      __type__: 'Window',
    };
  }

  // Convert array buffers into views.
  // Format views into an object that can be serialized and logged.
  if (obj instanceof ArrayBuffer) {
    obj = new Uint8Array(obj);
  }
  if (ArrayBuffer.isView(obj)) {
    return {
      __type__: obj.constructor.name,
      __data__: Array.from(obj),
    };
  }

  // Get all key statuses and serialize them.
  if (obj instanceof MediaKeyStatusMap) {
    const statuses = {};
    obj.forEach((status, arrayBuffer) => {
      const keyId = uint8ArrayToHexString(new Uint8Array(arrayBuffer));
      statuses[keyId] = status;
    });
    return {
      __type__: obj.constructor.name,
      __fields__: statuses,
    }
  }

  // DOMExceptions don't serialize right if done generically.  None of their
  // properties are their "own".  This follows the same format used below for
  // serializing other typed objects.
  if (obj instanceof DOMException) {
    return {
      __type__: 'DOMException',
      __fields__: {
        name: obj.name,
        code: obj.code,
        message: obj.message,
      },
    };
  }

  // For readability, Date objects are best serialized as strings.
  // This will use the browser's timezone to interpret the the date/time.
  if (obj instanceof Date) {
    return obj.toString();
  }

  if (obj instanceof TimeRanges) {
    const clone = [];
    for (let i = 0; i < obj.length; i++) {
       clone[i] = [obj.start(i), obj.end(i)];
    }
    return clone;
  }

  // Clone the elements of an array into serializable versions.
  if (Array.isArray(obj)) {
    const clone = [];
    for (const k in obj) {
      if (typeof obj[k] == 'function') {
        clone[k] = {__type__: 'function'};
      } else {
        clone[k] = getSerializable(obj[k]);
      }
    }
    return clone;
  }

  // Clone the fields of an object into serializable versions.
  const clone = {};
  for (const k in obj) {
    if (k.startsWith('__TraceAnything') || typeof obj[k] == 'function') {
      continue;
    }
    clone[k] = getSerializable(obj[k]);
  }
  // Make sure generated IDs get logged.  Do this through a synthetic field.
  if ('__TraceAnythingId__' in obj && !('id' in clone)) {
    clone.autoId = obj.__TraceAnythingId__;
  }
  if (obj.constructor != Object) {
    // If it's an object with a type, send that info, too.
    return {
      __type__: obj.constructor.name,
      __fields__: clone,
    };
  }
  return clone;
}

function byteToHexString(byte) {
  return byte.toString(16).padStart(2, '0');
}

function uint8ArrayToHexString(view) {
  return Array.from(view).map(byteToHexString).join('');
}

function combineOptions(baseOptions, overrideOptions) {
  return Object.assign({}, baseOptions, overrideOptions);
}

// General options for TraceAnything.
const options = {
  // When formatting logs and sending them as serialized messages, we need to
  // wait for async results to be resolved before we log them.
  logAsyncResultsImmediately: false,

  // Our custom logger.  Using an arrow function makes it possible to spy on
  // emeLogger in our tests without breaking this connection.
  logger: (log) => emeLogger(log),

  // Don't bother logging event listener methods.  It's not useful.
  // We can still log events, though.
  skipProperties: [
    'addEventListener',
    'removeEventListener',
  ],
};

// These will be shimmed in place.
TraceAnything.traceMember(navigator, 'requestMediaKeySystemAccess', options);
TraceAnything.traceMember(
    navigator.mediaCapabilities, 'decodingInfo', combineOptions(options, {
  // The result from decodingInfo is a plain object.  We need to dive into
  // this field in particular to find MediaKeySystemAccess instances to
  // trace.
  exploreResultFields: ['keySystemAccess'],
}));

// These constructors are not used directly, but this registers them to the
// tracing system so that instances we find later will be shimmed.
TraceAnything.traceClass(MediaKeys, options);
TraceAnything.traceClass(MediaKeySystemAccess, options);
TraceAnything.traceClass(MediaKeySession, combineOptions(options, {
  idProperty: 'sessionId',

  skipProperties: options.skipProperties.concat([
    // Also skip logging certain noisy properties on MediaKeySession.
    // They are logged with keystatuschange event.
    'expiration',
    'keyStatuses',

    // This is still logged in the representation of the session, but we don't
    // need to log access to the property's getter.
    'sessionId',
  ]),

  eventProperties: {
    'keystatuseschange': ['expiration', 'keyStatuses'],
  },
}));

// Trace media element types, and monitor the document for new instances.
const playbackStateProperties = [
  'currentTime',
  'paused',
  'ended',
  'played',
  'buffered',
  'getVideoPlaybackQuality',
];
const elementOptions = combineOptions(options, {
  // Skip all property access on media elements.
  // It's a little noisy and unhelpful (currentTime getter, for example).
  properties: false,

  // And these specific events are VERY noisy.  Skip them.
  skipEvents: [
    'progress',
    'timeupdate',
  ],

  // Methods we don't care about on media elements:
  skipProperties: options.skipProperties.concat([
    'getAttribute',
    'getBoundingClientRect',
    'querySelector',
    'querySelectorAll',
    'getVideoPlaybackQuality',
  ]),

  eventProperties: {
    'ratechange': 'playbackRate',
    'resize': 'getBoundingClientRect',
    'play': playbackStateProperties,
    'playing': playbackStateProperties,
    'pause': playbackStateProperties,
    'ended': playbackStateProperties,
    'durationchange': playbackStateProperties,
    'waiting': playbackStateProperties,
    'loadedmetadata': playbackStateProperties,
    'loadeddata': playbackStateProperties,
    'canplay': playbackStateProperties,
    'canplaythrough': playbackStateProperties,
  },
});
TraceAnything.traceElement('video', elementOptions);
TraceAnything.traceElement('audio', elementOptions);
<commit_msg>fix: Display keystatuseschange.expiration as a Date (#48)<commit_after>/**
 * Copyright 2021 Google Inc.
 *
 * Licensed 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.
 *
 * @fileoverview Shim and trace calls to EME, and log them to the extension's
 * log window.
 */

/**
 * A custom logger to plug into TraceAnything.
 *
 * @param {TraceAnything.Log} log
 */
function emeLogger(log) {
  // Hook into the EME formatter interface for update calls and message events.
  if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeySession' &&
      log.methodName == 'update' &&
      log.args.length >= 1) {
    log.args[0] = formatEmeResponse(log.args[0]);
  } else if (log.type == TraceAnything.LogTypes.Event &&
      log.className == 'MediaKeySession' &&
      log.eventName == 'message') {
    // You can't (and shouldn't, to avoid breaking the app) modify the event
    // object here.  So clone it, and replace the message field of that.
    log.event = cloneEvent(log.event, {
      // This overrides the message field with a new value from the formatter.
      message: formatEmeRequest(log.event.message),
    });
  } else if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeys' &&
      log.methodName == 'setServerCertificate' &&
      log.args.length >= 1) {
    log.args[0] = formatEmeCertificate(log.args[0]);
  } else if (log.type == TraceAnything.LogTypes.Method &&
      log.className == 'MediaKeySession' &&
      log.methodName == 'generateRequest' &&
      log.args.length >= 2) {
    log.args[1] = formatEmeInitData(log.args[0], log.args[1]);
  }

  // Log to the default logger in the JS console.
  TraceAnything.defaultLogger(log);

  // This is not needed and can't be easily serialized.
  delete log.instance;

  window.postMessage({type: 'emeTraceLog', log: prepLogForMessage(log)}, '*');
}

/**
 * Clone an event.  The clone will have the same type and values, except for the
 * values overridden by the overrides parameter.
 *
 * The spread operator and Object.assign can only read "own" properties of an
 * object, so this custom cloning method must be used for Events instead.
 *
 * @param {!Event} original
 * @param {Object=} overrides
 * @return {!Event}
 */
function cloneEvent(original, overrides) {
  const clone = {};

  // When reading the original, we must use a for-in loop to see all fields.
  // Static fields from Event and methods are skipped, but everything else is
  // copied into a plain object.
  for (const key in original) {
    if (!(key in Event) && (typeof original[key] != 'function')) {
      clone[key] = original[key];
    }
  }

  // Overrides are applied all at once using Object.assign.
  if (overrides) {
    Object.assign(clone, overrides);
  }

  // Setting the prototype of the clone gives the clone the same type as the
  // original.
  Object.setPrototypeOf(clone, original.constructor.prototype);

  return clone;
}

/**
 * @param {BufferSource} response
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeResponse(response) {
  if (!response || !document.emeFormatters) {
    return response;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedResponse;

      if (formatter.formatResponse) {  // v3 API
        formattedResponse = formatter.formatResponse(response);
      } else if (formatter.formatUpdateCall) {  // v2 API
        formattedResponse = formatter.formatUpdateCall(response);
      }

      if (formattedResponse) {
        return formattedResponse;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original response, which will be formatted as bytes.
  return response;
}

/**
 * @param {BufferSource} request
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeRequest(request) {
  if (!request || !document.emeFormatters) {
    return request;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedRequest;

      if (formatter.formatRequest) {  // v3 API
        formattedRequest = formatter.formatRequest(request);
      } else if (formatter.formatmessage) {  // v2 API
        formattedRequest = formatter.formatmessage(request);
      }

      if (formattedRequest) {
        return formattedRequest;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original request, which will be formatted as bytes.
  return request;
}

/**
 * @param {BufferSource} certificate
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeCertificate(certificate) {
  if (!certificate || !document.emeFormatters) {
    return certificate;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedCertificate;

      if (formatter.formatServerCertificate) {  // v3 API
        formattedCertificate = formatter.formatServerCertificate(certificate);
      }

      if (formattedCertificate) {
        return formattedCertificate;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original certificate, which will be formatted as bytes.
  return certificate;
}

/**
 * @param {string} initDataType
 * @param {BufferSource} initData
 * @return {(string|!Object|BufferSource)}
 */
function formatEmeInitData(initDataType, initData) {
  if (!initData || !document.emeFormatters) {
    return initData;
  }

  for (const formatter of document.emeFormatters) {
    try {
      let formattedInitData;

      if (formatter.formatInitData) {  // v3 API
        formattedInitData = formatter.formatInitData(initDataType, initData);
      }

      if (formattedInitData) {
        return formattedInitData;
      }
    } catch (exception) {}  // Ignore, move on to the next formatter.
  }

  // Return the original init data, which will be formatted as bytes.
  return initData;
}

/**
 * @param {Object} log
 * @return {Object}
 */
function prepLogForMessage(log) {
  const clone = {};
  for (const k in log) {
    clone[k] = getSerializable(log[k]);
  }
  return clone;
}

/**
 * @param {*} obj A value that may or may not be serializable.
 * @return {*} A value that can be serialized.
 */
function getSerializable(obj) {
  // Return primitive types directly.
  if (obj == null || typeof obj == 'string' || typeof obj == 'number' ||
      typeof obj == 'boolean') {
    return obj;
  }

  // Events are full of garbage, so only serialize the interesting fields.
  if (obj instanceof Event) {
    const clone = {};
    for (const k in obj) {
      // Skip fields that are in the Event base class, as well as "isTrusted".
      // These are not interesting for logging.
      if (!(k in Event.prototype) && k != 'isTrusted') {
        clone[k] = getSerializable(obj[k]);
      }
    }
    return {
      __type__: obj.type + ' Event',
      __fields__: clone,
    };
  }

  // Elements, Nodes, and Windows are dangerous to serialize because they
  // contain many fields and circular references.
  if (obj instanceof Element) {
    return {
      __type__: '<' + obj.tagName.toLowerCase() + '> element',
    };
  }
  if (obj instanceof Node) {
    return {
      __type__: obj.nodeName.toLowerCase() + ' node',
    };
  }
  if (obj instanceof Window) {
    return {
      __type__: 'Window',
    };
  }

  // Convert array buffers into views.
  // Format views into an object that can be serialized and logged.
  if (obj instanceof ArrayBuffer) {
    obj = new Uint8Array(obj);
  }
  if (ArrayBuffer.isView(obj)) {
    return {
      __type__: obj.constructor.name,
      __data__: Array.from(obj),
    };
  }

  // Get all key statuses and serialize them.
  if (obj instanceof MediaKeyStatusMap) {
    const statuses = {};
    obj.forEach((status, arrayBuffer) => {
      const keyId = uint8ArrayToHexString(new Uint8Array(arrayBuffer));
      statuses[keyId] = status;
    });
    return {
      __type__: obj.constructor.name,
      __fields__: statuses,
    }
  }

  // DOMExceptions don't serialize right if done generically.  None of their
  // properties are their "own".  This follows the same format used below for
  // serializing other typed objects.
  if (obj instanceof DOMException) {
    return {
      __type__: 'DOMException',
      __fields__: {
        name: obj.name,
        code: obj.code,
        message: obj.message,
      },
    };
  }

  // For readability, Date objects are best serialized as strings.
  // This will use the browser's timezone to interpret the the date/time.
  if (obj instanceof Date) {
    return obj.toString();
  }

  if (obj instanceof TimeRanges) {
    const clone = [];
    for (let i = 0; i < obj.length; i++) {
       clone[i] = [obj.start(i), obj.end(i)];
    }
    return clone;
  }

  // Clone the elements of an array into serializable versions.
  if (Array.isArray(obj)) {
    const clone = [];
    for (const k in obj) {
      if (typeof obj[k] == 'function') {
        clone[k] = {__type__: 'function'};
      } else {
        clone[k] = getSerializable(obj[k]);
      }
    }
    return clone;
  }

  // Clone the fields of an object into serializable versions.
  const clone = {};
  for (const k in obj) {
    if (k.startsWith('__TraceAnything') || typeof obj[k] == 'function') {
      continue;
    }

    // 'keystatuseschange.expiration' is returned as a ECMAScript Time Value,
    // so convert it into a Date if specified for better readability.
    if (k == 'expiration' && !isNaN(obj[k])) {
      clone[k] = getSerializable(new Date(obj[k]));
      continue;
    }

    clone[k] = getSerializable(obj[k]);
  }
  // Make sure generated IDs get logged.  Do this through a synthetic field.
  if ('__TraceAnythingId__' in obj && !('id' in clone)) {
    clone.autoId = obj.__TraceAnythingId__;
  }
  if (obj.constructor != Object) {
    // If it's an object with a type, send that info, too.
    return {
      __type__: obj.constructor.name,
      __fields__: clone,
    };
  }
  return clone;
}

function byteToHexString(byte) {
  return byte.toString(16).padStart(2, '0');
}

function uint8ArrayToHexString(view) {
  return Array.from(view).map(byteToHexString).join('');
}

function combineOptions(baseOptions, overrideOptions) {
  return Object.assign({}, baseOptions, overrideOptions);
}

// General options for TraceAnything.
const options = {
  // When formatting logs and sending them as serialized messages, we need to
  // wait for async results to be resolved before we log them.
  logAsyncResultsImmediately: false,

  // Our custom logger.  Using an arrow function makes it possible to spy on
  // emeLogger in our tests without breaking this connection.
  logger: (log) => emeLogger(log),

  // Don't bother logging event listener methods.  It's not useful.
  // We can still log events, though.
  skipProperties: [
    'addEventListener',
    'removeEventListener',
  ],
};

// These will be shimmed in place.
TraceAnything.traceMember(navigator, 'requestMediaKeySystemAccess', options);
TraceAnything.traceMember(
    navigator.mediaCapabilities, 'decodingInfo', combineOptions(options, {
  // The result from decodingInfo is a plain object.  We need to dive into
  // this field in particular to find MediaKeySystemAccess instances to
  // trace.
  exploreResultFields: ['keySystemAccess'],
}));

// These constructors are not used directly, but this registers them to the
// tracing system so that instances we find later will be shimmed.
TraceAnything.traceClass(MediaKeys, options);
TraceAnything.traceClass(MediaKeySystemAccess, options);
TraceAnything.traceClass(MediaKeySession, combineOptions(options, {
  idProperty: 'sessionId',

  skipProperties: options.skipProperties.concat([
    // Also skip logging certain noisy properties on MediaKeySession.
    // They are logged with keystatuschange event.
    'expiration',
    'keyStatuses',

    // This is still logged in the representation of the session, but we don't
    // need to log access to the property's getter.
    'sessionId',
  ]),

  eventProperties: {
    'keystatuseschange': ['expiration', 'keyStatuses'],
  },
}));

// Trace media element types, and monitor the document for new instances.
const playbackStateProperties = [
  'currentTime',
  'paused',
  'ended',
  'played',
  'buffered',
  'getVideoPlaybackQuality',
];
const elementOptions = combineOptions(options, {
  // Skip all property access on media elements.
  // It's a little noisy and unhelpful (currentTime getter, for example).
  properties: false,

  // And these specific events are VERY noisy.  Skip them.
  skipEvents: [
    'progress',
    'timeupdate',
  ],

  // Methods we don't care about on media elements:
  skipProperties: options.skipProperties.concat([
    'getAttribute',
    'getBoundingClientRect',
    'querySelector',
    'querySelectorAll',
    'getVideoPlaybackQuality',
  ]),

  eventProperties: {
    'ratechange': 'playbackRate',
    'resize': 'getBoundingClientRect',
    'play': playbackStateProperties,
    'playing': playbackStateProperties,
    'pause': playbackStateProperties,
    'ended': playbackStateProperties,
    'durationchange': playbackStateProperties,
    'waiting': playbackStateProperties,
    'loadedmetadata': playbackStateProperties,
    'loadeddata': playbackStateProperties,
    'canplay': playbackStateProperties,
    'canplaythrough': playbackStateProperties,
  },
});
TraceAnything.traceElement('video', elementOptions);
TraceAnything.traceElement('audio', elementOptions);
<|endoftext|>


<commit_before>(function () {
	"use strict";
	Date.Parsing = {
		Exception: function (s) {
			this.message = "Parse error at '" + s.substring(0, 10) + " ...'";
		}
	};
	var $P = Date.Parsing;
	var dayOffsets = {
		standard: [0,31,59,90,120,151,181,212,243,273,304,334],
		leap: [0,31,60,91,121,152,182,213,244,274,305,335]
	};

	var multiReplace = function (str, hash ) {
		var key;
		for (key in hash) {
			if (Object.prototype.hasOwnProperty.call(hash, key)) {
				var regex = (hash[key] instanceof RegExp) ? hash[key] : new RegExp(hash[key], "g");
				str = str.replace(regex, key);
			}
		}
		return str;
	};

	$P.isLeapYear = function(year) {
		return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
	};

	$P.processTimeObject = function (obj) {
		var d, jan4, date, offset, dayOffset;
		d = new Date();
		dayOffset = ($P.isLeapYear(obj.year)) ? dayOffsets.leap : dayOffsets.standard;
		obj.hours = obj.hours ? obj.hours : 0;
		obj.minutes = obj.minutes ? obj.minutes : 0;
		obj.seconds = obj.seconds ? obj.seconds : 0;
		obj.milliseconds = obj.milliseconds ? obj.milliseconds : 0;
		if (!obj.year) {
			obj.year = d.getFullYear();
		}
		if (!obj.month && (obj.week || obj.dayOfYear)) {
			// work out the day of the year...
			if (!obj.dayOfYear) {
				obj.weekDay = (!obj.weekDay && obj.weekDay !== 0) ? 1 : obj.weekDay;
				d = new Date(obj.year, 0, 4);
				jan4 = d.getDay() === 0 ? 7 : d.getDay(); // JS is 0 indexed on Sunday.
				offset = jan4+3;
				obj.dayOfYear = ((obj.week * 7) + (obj.weekDay === 0 ? 7 : obj.weekDay))-offset;
			}
			for (var i=0;i <= dayOffset.length;i++) {
				if (obj.dayOfYear < dayOffset[i] || i === dayOffset.length) {
					obj.day = obj.day ? obj.day : (obj.dayOfYear - dayOffset[i-1]);
					break;
				} else {
					obj.month = i;
				}
			}
		} else {
			obj.month = obj.month ? obj.month : 0;
			obj.day = obj.day ? obj.day : 1;
			obj.dayOfYear = dayOffset[obj.month] + obj.day;
		}
		date = new Date(obj.year, obj.month, obj.day, obj.hours, obj.minutes, obj.seconds, obj.milliseconds);

		if (obj.zone) {
			// adjust (and calculate) for timezone here
			if (obj.zone.toUpperCase() === "Z" || (obj.zone_hours === 0 && obj.zone_minutes === 0)) {
				// it's UTC/GML so work out the current timeszone offset
				offset = -date.getTimezoneOffset();
			} else {
				offset = (obj.zone_hours*60) + (obj.zone_minutes ? obj.zone_minutes : 0);
				if (obj.zone_sign === "+") {
					offset *= -1;
				}
				offset -= date.getTimezoneOffset();
			}
			date.setMinutes(date.getMinutes()+offset);
		}
		return date;
	};

	$P.ISO = {
		regex : /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-4])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?\s?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/,
		parse : function (s) {
			var data = s.match(this.regex);
			if (!data || !data.length) {
				return null;
			}
			var time = {
				year : data[1] ? Number(data[1]) : data[1],
				month : data[5] ? (Number(data[5])-1) : data[5],
				day : data[7] ? Number(data[7]) : data[7],
				week : data[8] ? Number(data[8]) : data[8],
				weekDay : data[9] ? (Math.abs(Number(data[9])) === 7 ? 0 : Math.abs(Number(data[9]))) : data[9], // 1-7, starts on Monday. Convert to JS's 0-6 index.
				dayOfYear : data[10] ? Number(data[10]) : data[10],
				hours : data[15] ? Number(data[15]) : data[15],
				minutes : data[16] ? Number(data[16].replace(":","")) : data[16],
				seconds : data[19] ? Math.floor(Number(data[19].replace(":","").replace(",","."))) : data[19],
				milliseconds : data[20] ? (Number(data[20].replace(",","."))*1000) : data[20],
				zone : data[21],
				zone_sign : data[22],
				zone_hours : (data[23] && typeof data[23] !== "undefined") ? Number(data[23]) : data[23],
				zone_minutes : (data[24] && typeof data[23] !== "undefined") ? Number(data[24]) : data[24]
			};
			if (data[18]) {
				data[18] = 60 * Number(data[18].replace(",", "."));
				if (!time.minutes) {
					time.minutes = data[18];
				} else if (!time.seconds) {
					time.seconds = data[18];
				}
			}
			if (!time.year || (!time.year && (!time.month && !time.day) && (!time.week && !time.dayOfYear)) ) {
				return null;
			}
			return $P.processTimeObject(time);
		}
	};
	$P.Numeric = {
		isNumeric: function (e){return!isNaN(parseFloat(e))&&isFinite(e);},
		regex: /\b([0-1]?[0-9])([0-3]?[0-9])([0-2]?[0-9]?[0-9][0-9])\b/i,
		parse: function (s) {
			var data, i,
				time = {},
				order = Date.CultureInfo.dateElementOrder.split("");
			if (!(this.isNumeric(s)) || // if it's non-numeric OR
				(s[0] === "+" && s[0] === "-")) {			// It's an arithmatic string (eg +/-1000)
				return null;
			}
			if (s.length < 5) { // assume it's just a year.
				time.year = s;
				return $P.processTimeObject(time);
			}
			data = s.match(this.regex);
			if (!data || !data.length) {
				return null;
			}
			for (i=0; i < order.length; i++) {
				switch(order[i]) {
					case "d":
						time.day = data[i+1];
						break;
					case "m":
						time.month = (data[i+1]-1);
						break;
					case "y":
						time.year = data[i+1];
						break;
				}
			}
			return $P.processTimeObject(time);
		}
	};
	$P.Normalizer = {
		parse: function (s) {
			var $R = Date.CultureInfo.regexPatterns,
				__ = Date.i18n.__,
				tomorrow = Date.today().addDays(1).toString("d"),
				yesterday = Date.today().addDays(-1).toString("d"),
				lastMon = Date.today().last().monday().toString("d"),
				lastTues = Date.today().last().tuesday().toString("d"),
				lastWed = Date.today().last().wednesday().toString("d"),
				lastThurs = Date.today().last().thursday().toString("d"),
				lastFri = Date.today().last().friday().toString("d"),
				lastSat = Date.today().last().saturday().toString("d"),
				lastSun = Date.today().last().sunday().toString("d");

			var replaceHash = {
				"January": $R.jan.source,
				"February": $R.feb,
				"March": $R.mar,
				"April": $R.apr,
				"May": $R.may,
				"June": $R.jun,
				"July": $R.jul,
				"August": $R.aug,
				"September": $R.sep,
				"October": $R.oct,
				"November": $R.nov,
				"December": $R.dec,
				"": /\bat\b/gi,
				" ": /\s{2,}/,
			};
			replaceHash[tomorrow] = $R.tomorrow;
			replaceHash[yesterday] = $R.yesterday;
			replaceHash[lastMon] = new RegExp("(("+$R.past.source+")\\s("+$R.mon.source+"))");
			replaceHash[lastTues] = new RegExp("(("+$R.past.source+")\\s("+$R.tue.source+"))");
			replaceHash[lastWed] = new RegExp("(("+$R.past.source+")\\s("+$R.wed.source+"))");
			replaceHash[lastThurs] = new RegExp("(("+$R.past.source+")\\s("+$R.thu.source+"))");
			replaceHash[lastFri] = new RegExp("(("+$R.past.source+")\\s("+$R.fri.source+"))");
			replaceHash[lastSat] = new RegExp("(("+$R.past.source+")\\s("+$R.sat.source+"))");
			replaceHash[lastSun] = new RegExp("(("+$R.past.source+")\\s("+$R.sun.source+"))");

			var regexStr = "(\\b\\d\\d?("+__("AM")+"|"+__("PM")+")? )("+$R.tomorrow.source.slice(1)+")";
			s = s.replace(new RegExp(regexStr, "i"), function(full, m1) {
				var t = Date.today().addDays(1).toString("d");
				return (t + " " + m1);
			});

			s =	s.replace($R.amThisMorning, function(str, am){return am;})
				.replace($R.inTheMorning, "am")
				.replace($R.thisMorning, "9am")
				.replace($R.amThisEvening, function(str, pm){return pm;})
				.replace($R.inTheEvening, "pm")
				.replace($R.thisEvening, "7pm");

			s = multiReplace(s, replaceHash);

			try {
				var n = s.split(/([\s\-\.\,\/\x27]+)/);
				if (n.length === 3) {
					if ($P.Numeric.isNumeric(n[0]) && $P.Numeric.isNumeric(n[2])) {
						if (n[2].length >= 4) {
							// ok, so we're dealing with x/year. But that's not a full date.
							// This fixes wonky dateElementOrder parsing when set to dmy order.
							if (Date.CultureInfo.dateElementOrder[0] === "d") {
								s = "1/" + n[0] + "/" + n[2]; // set to 1st of month and normalize the seperator
							}
						}
					}
				}
			} catch (e) {
				// continue...
			}

			return s;
		}
	};
}());<commit_msg>minor refactor of replacement normalization code<commit_after>(function () {
	"use strict";
	Date.Parsing = {
		Exception: function (s) {
			this.message = "Parse error at '" + s.substring(0, 10) + " ...'";
		}
	};
	var $P = Date.Parsing;
	var dayOffsets = {
		standard: [0,31,59,90,120,151,181,212,243,273,304,334],
		leap: [0,31,60,91,121,152,182,213,244,274,305,335]
	};

	var multiReplace = function (str, hash ) {
		var key;
		for (key in hash) {
			if (Object.prototype.hasOwnProperty.call(hash, key)) {
				var regex = (hash[key] instanceof RegExp) ? hash[key] : new RegExp(hash[key], "g");
				str = str.replace(regex, key);
			}
		}
		return str;
	};

	$P.isLeapYear = function(year) {
		return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
	};

	$P.processTimeObject = function (obj) {
		var d, jan4, date, offset, dayOffset;
		d = new Date();
		dayOffset = ($P.isLeapYear(obj.year)) ? dayOffsets.leap : dayOffsets.standard;
		obj.hours = obj.hours ? obj.hours : 0;
		obj.minutes = obj.minutes ? obj.minutes : 0;
		obj.seconds = obj.seconds ? obj.seconds : 0;
		obj.milliseconds = obj.milliseconds ? obj.milliseconds : 0;
		if (!obj.year) {
			obj.year = d.getFullYear();
		}
		if (!obj.month && (obj.week || obj.dayOfYear)) {
			// work out the day of the year...
			if (!obj.dayOfYear) {
				obj.weekDay = (!obj.weekDay && obj.weekDay !== 0) ? 1 : obj.weekDay;
				d = new Date(obj.year, 0, 4);
				jan4 = d.getDay() === 0 ? 7 : d.getDay(); // JS is 0 indexed on Sunday.
				offset = jan4+3;
				obj.dayOfYear = ((obj.week * 7) + (obj.weekDay === 0 ? 7 : obj.weekDay))-offset;
			}
			for (var i=0;i <= dayOffset.length;i++) {
				if (obj.dayOfYear < dayOffset[i] || i === dayOffset.length) {
					obj.day = obj.day ? obj.day : (obj.dayOfYear - dayOffset[i-1]);
					break;
				} else {
					obj.month = i;
				}
			}
		} else {
			obj.month = obj.month ? obj.month : 0;
			obj.day = obj.day ? obj.day : 1;
			obj.dayOfYear = dayOffset[obj.month] + obj.day;
		}
		date = new Date(obj.year, obj.month, obj.day, obj.hours, obj.minutes, obj.seconds, obj.milliseconds);

		if (obj.zone) {
			// adjust (and calculate) for timezone here
			if (obj.zone.toUpperCase() === "Z" || (obj.zone_hours === 0 && obj.zone_minutes === 0)) {
				// it's UTC/GML so work out the current timeszone offset
				offset = -date.getTimezoneOffset();
			} else {
				offset = (obj.zone_hours*60) + (obj.zone_minutes ? obj.zone_minutes : 0);
				if (obj.zone_sign === "+") {
					offset *= -1;
				}
				offset -= date.getTimezoneOffset();
			}
			date.setMinutes(date.getMinutes()+offset);
		}
		return date;
	};

	$P.ISO = {
		regex : /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-4])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?\s?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/,
		parse : function (s) {
			var data = s.match(this.regex);
			if (!data || !data.length) {
				return null;
			}
			var time = {
				year : data[1] ? Number(data[1]) : data[1],
				month : data[5] ? (Number(data[5])-1) : data[5],
				day : data[7] ? Number(data[7]) : data[7],
				week : data[8] ? Number(data[8]) : data[8],
				weekDay : data[9] ? (Math.abs(Number(data[9])) === 7 ? 0 : Math.abs(Number(data[9]))) : data[9], // 1-7, starts on Monday. Convert to JS's 0-6 index.
				dayOfYear : data[10] ? Number(data[10]) : data[10],
				hours : data[15] ? Number(data[15]) : data[15],
				minutes : data[16] ? Number(data[16].replace(":","")) : data[16],
				seconds : data[19] ? Math.floor(Number(data[19].replace(":","").replace(",","."))) : data[19],
				milliseconds : data[20] ? (Number(data[20].replace(",","."))*1000) : data[20],
				zone : data[21],
				zone_sign : data[22],
				zone_hours : (data[23] && typeof data[23] !== "undefined") ? Number(data[23]) : data[23],
				zone_minutes : (data[24] && typeof data[23] !== "undefined") ? Number(data[24]) : data[24]
			};
			if (data[18]) {
				data[18] = 60 * Number(data[18].replace(",", "."));
				if (!time.minutes) {
					time.minutes = data[18];
				} else if (!time.seconds) {
					time.seconds = data[18];
				}
			}
			if (!time.year || (!time.year && (!time.month && !time.day) && (!time.week && !time.dayOfYear)) ) {
				return null;
			}
			return $P.processTimeObject(time);
		}
	};
	$P.Numeric = {
		isNumeric: function (e){return!isNaN(parseFloat(e))&&isFinite(e);},
		regex: /\b([0-1]?[0-9])([0-3]?[0-9])([0-2]?[0-9]?[0-9][0-9])\b/i,
		parse: function (s) {
			var data, i,
				time = {},
				order = Date.CultureInfo.dateElementOrder.split("");
			if (!(this.isNumeric(s)) || // if it's non-numeric OR
				(s[0] === "+" && s[0] === "-")) {			// It's an arithmatic string (eg +/-1000)
				return null;
			}
			if (s.length < 5) { // assume it's just a year.
				time.year = s;
				return $P.processTimeObject(time);
			}
			data = s.match(this.regex);
			if (!data || !data.length) {
				return null;
			}
			for (i=0; i < order.length; i++) {
				switch(order[i]) {
					case "d":
						time.day = data[i+1];
						break;
					case "m":
						time.month = (data[i+1]-1);
						break;
					case "y":
						time.year = data[i+1];
						break;
				}
			}
			return $P.processTimeObject(time);
		}
	};
	$P.Normalizer = {
		parse: function (s) {
			var $R = Date.CultureInfo.regexPatterns,
				__ = Date.i18n.__,
				tomorrow = Date.today().addDays(1).toString("d"),
				yesterday = Date.today().addDays(-1).toString("d"),
				lastMon = Date.today().last().monday().toString("d"),
				lastTues = Date.today().last().tuesday().toString("d"),
				lastWed = Date.today().last().wednesday().toString("d"),
				lastThurs = Date.today().last().thursday().toString("d"),
				lastFri = Date.today().last().friday().toString("d"),
				lastSat = Date.today().last().saturday().toString("d"),
				lastSun = Date.today().last().sunday().toString("d");

			var replaceHash = {
				"January": $R.jan.source,
				"February": $R.feb,
				"March": $R.mar,
				"April": $R.apr,
				"May": $R.may,
				"June": $R.jun,
				"July": $R.jul,
				"August": $R.aug,
				"September": $R.sep,
				"October": $R.oct,
				"November": $R.nov,
				"December": $R.dec,
				"": /\bat\b/gi,
				" ": /\s{2,}/,
				"am": $R.inTheMorning,
				"9am": $R.thisMorning,
				"pm": $R.inTheEvening,
				"7pm":$R.thisEvening
			};
			replaceHash[tomorrow] = $R.tomorrow;
			replaceHash[yesterday] = $R.yesterday;
			replaceHash[lastMon] = new RegExp("(("+$R.past.source+")\\s("+$R.mon.source+"))");
			replaceHash[lastTues] = new RegExp("(("+$R.past.source+")\\s("+$R.tue.source+"))");
			replaceHash[lastWed] = new RegExp("(("+$R.past.source+")\\s("+$R.wed.source+"))");
			replaceHash[lastThurs] = new RegExp("(("+$R.past.source+")\\s("+$R.thu.source+"))");
			replaceHash[lastFri] = new RegExp("(("+$R.past.source+")\\s("+$R.fri.source+"))");
			replaceHash[lastSat] = new RegExp("(("+$R.past.source+")\\s("+$R.sat.source+"))");
			replaceHash[lastSun] = new RegExp("(("+$R.past.source+")\\s("+$R.sun.source+"))");

			var regexStr = "(\\b\\d\\d?("+__("AM")+"|"+__("PM")+")? )("+$R.tomorrow.source.slice(1)+")";
			s = s.replace(new RegExp(regexStr, "i"), function(full, m1) {
				var t = Date.today().addDays(1).toString("d");
				return (t + " " + m1);
			});

			s =	s.replace($R.amThisMorning, function(str, am){return am;})
				.replace($R.amThisEvening, function(str, pm){return pm;});

			s = multiReplace(s, replaceHash);

			try {
				var n = s.split(/([\s\-\.\,\/\x27]+)/);
				if (n.length === 3 &&
					$P.Numeric.isNumeric(n[0]) &&
					$P.Numeric.isNumeric(n[2]) &&
					(n[2].length >= 4)) {
						// ok, so we're dealing with x/year. But that's not a full date.
						// This fixes wonky dateElementOrder parsing when set to dmy order.
						if (Date.CultureInfo.dateElementOrder[0] === "d") {
							s = "1/" + n[0] + "/" + n[2]; // set to 1st of month and normalize the seperator
						}
				}
			} catch (e) {}

			return s;
		}
	};
}());<|endoftext|>


<commit_before>'use strict';

var  forEach = require('lodash/collection/forEach');

function UserTaskCreationListener(eventBus,canvas,elementRegistry,overlays) {

// you may hook into any of the following events
    var events = [
        'element.changed'
    ];

    events.forEach(function (event) {

        eventBus.on(event, function (e) {
            // e.element = the model element
            // e.gfx = the graphical element
            if(e.element.businessObject.$type=="bpmn:UserTask"){
                if(!e.element.businessObject['$attrs']['aof:realizedBy']){
                    canvas.addMarker(e.element.id, 'no-app-assigned');
                    canvas.removeMarker(e.element.id, 'color-appensembleapp');
                }
                else{
                    canvas.removeMarker(e.element.id, 'no-app-assigned');
                    canvas.addMarker(e.element.id, 'color-appensembleapp');
                }
            }
            else if(e.element.businessObject.$type=="bpmn:Participant"){
                if(e.element.businessObject['isAppEnsemble']){
                    canvas.addMarker(e.element.id, 'color-appensemble');
                }
                else{
                    canvas.removeMarker(e.element.id, 'color-appensemble');
                }
            }
        });
    });

    eventBus.on('import.success',function(e){
        // Mark AppEnsembleApps grey
        forEach(elementRegistry.filter(
                function (element, gfx) {
                    return (element.type == "bpmn:UserTask" && element.businessObject.isAppEnsembleApp == true )//&& element.businessObject.$attrs['aof:realizedBy'] && element.businessObject.$attrs['aof:realizedBy'] != "")
                }),
            function (element) {
                canvas.addMarker(element.id, 'color-appensembleapp');
            }
        );

        // Mark AppEnsembles
        forEach(elementRegistry.filter(
                function (element, gfx) {
                    return (element.type == "bpmn:Participant" && (element.businessObject.isAppEnsemble && element.businessObject.isAppEnsemble == "true") || (element.businessObject.isAppEnsemble == true))
                }),
            function (element) {
                canvas.addMarker(element.id, 'color-appensemble');
            }
        );

    });

    eventBus.on('element.changed', function(event) {
        var shape = elementRegistry.get(event.element.id);
        if(event.element.businessObject.$type=="bpmn:UserTask") {
            if (!!event.element.businessObject['$attrs']['aof:realizedBy']) {
                var html=buildTooptip('This is a UserTask realized by an App')
            }
            else{
                var html=buildTooptip('This is a UserTask without an App. Please assign an App!')
            }
            overlays.add(event.element,'aofTooltip', {
                id:'overlay-'+shape.id,
                position: {
                    top: shape.height+15,
                    right: 20
                },
                html: html
            });
        }
        else if(event.element.businessObject.$type=="bpmn:Participant"){
            if(event.element.businessObject['isAppEnsemble']){
                overlays.add(event.element,'aofTooltip', {
                    id:'overlay-'+shape.id,
                    position: {
                        top: shape.height+15,
                        right: 20
                    },
                    html: buildTooptip('This is an Participant marked as AppEnsemble!')
                });
            }
        }

    });

    eventBus.on('element.out',removeOverlay);

    function removeOverlay (event) {
        var shape = elementRegistry.get(event.element.id);
        if(event.element.businessObject.$type=="bpmn:UserTask" || (event.element.businessObject.$type=="bpmn:Participant" && event.element.businessObject['isAppEnsemble'])) {
            overlays.remove({type:'aofTooltip'});//element:shape.id});
        }
    }

    function buildTooptip(text){
         return ''+text+'';
    }

    function markAsAppEnsemble(element){
        canvas.addMarker(element.id, 'color-appensemble');
    }
}

UserTaskCreationListener.$inject = [ 'eventBus','canvas','elementRegistry','overlays'];

module.exports = UserTaskCreationListener;
<commit_msg>Set up reload on App-ensemble name change<commit_after>'use strict';

var  forEach = require('lodash/collection/forEach');

function UserTaskCreationListener(eventBus,canvas,elementRegistry,overlays) {

// you may hook into any of the following events
    var events = [
        'element.changed'
    ];

    events.forEach(function (event) {

        eventBus.on(event, function (e) {
            // e.element = the model element
            // e.gfx = the graphical element
            if(e.element.businessObject.$type=="bpmn:UserTask"){
                if(!e.element.businessObject['$attrs']['aof:realizedBy']){
                    canvas.addMarker(e.element.id, 'no-app-assigned');
                    canvas.removeMarker(e.element.id, 'color-appensembleapp');
                }
                else{
                    canvas.removeMarker(e.element.id, 'no-app-assigned');
                    canvas.addMarker(e.element.id, 'color-appensembleapp');
                }
            }
            else if(e.element.businessObject.$type=="bpmn:Participant"){
                if(e.element.businessObject['isAppEnsemble']){
                    canvas.addMarker(e.element.id, 'color-appensemble');
                }
                else{
                    canvas.removeMarker(e.element.id, 'color-appensemble');
                }
            }
        });
    });

    eventBus.on('import.success',function(e){
        // Mark AppEnsembleApps grey
        forEach(elementRegistry.filter(
                function (element, gfx) {
                    return (element.type == "bpmn:UserTask" && element.businessObject.isAppEnsembleApp == true )//&& element.businessObject.$attrs['aof:realizedBy'] && element.businessObject.$attrs['aof:realizedBy'] != "")
                }),
            function (element) {
                canvas.addMarker(element.id, 'color-appensembleapp');
            }
        );

        // Mark AppEnsembles
        forEach(elementRegistry.filter(
                function (element, gfx) {
                    return (element.type == "bpmn:Participant" && (element.businessObject.isAppEnsemble && element.businessObject.isAppEnsemble == "true") || (element.businessObject.isAppEnsemble == true))
                }),
            function (element) {
                canvas.addMarker(element.id, 'color-appensemble');
            }
        );

    });

    eventBus.on('element.changed', function(event) {
        var shape = elementRegistry.get(event.element.id);
        if(event.element.businessObject.$type=="bpmn:UserTask") {
            if (!!event.element.businessObject['$attrs']['aof:realizedBy']) {
                var html=buildTooptip('This is a UserTask realized by an App')
            }
            else{
                var html=buildTooptip('This is a UserTask without an App. Please assign an App!')
            }
            overlays.add(event.element,'aofTooltip', {
                id:'overlay-'+shape.id,
                position: {
                    top: shape.height+15,
                    right: 20
                },
                html: html
            });
        }
        else if(event.element.businessObject.$type=="bpmn:Participant"){
            if(event.element.businessObject['isAppEnsemble']){
                overlays.add(event.element,'aofTooltip', {
                    id:'overlay-'+shape.id,
                    position: {
                        top: shape.height+15,
                        right: 20
                    },
                    html: buildTooptip('This is an Participant marked as AppEnsemble!')
                });
            }
        }

    });

    eventBus.on('commandStack.element.updateProperties.preExecute', function(event) {
        if(event.context.element.type=="bpmn:Participant"){
            var name=event.context.element.businessObject.name;
            eventBus.once('commandStack.element.updateProperties.postExecute', function(event) {
                if(event.context.element.type=="bpmn:Participant") {
                    if(event.context.element.businessObject.name!=name){
                        $('#js-save-appensemble').attr({
                            'reload': event.context.element.businessObject.name.replace(new RegExp(" ", "g"),'-')
                        });
                    }
                }

            });
        }
    });

    eventBus.on('element.out',removeOverlay);

    function removeOverlay (event) {
        var shape = elementRegistry.get(event.element.id);
        if(event.element.businessObject.$type=="bpmn:UserTask" || (event.element.businessObject.$type=="bpmn:Participant" && event.element.businessObject['isAppEnsemble'])) {
            overlays.remove({type:'aofTooltip'});//element:shape.id});
        }
    }

    function buildTooptip(text){
         return ''+text+'';
    }

    function markAsAppEnsemble(element){
        canvas.addMarker(element.id, 'color-appensemble');
    }
}

UserTaskCreationListener.$inject = [ 'eventBus','canvas','elementRegistry','overlays'];

module.exports = UserTaskCreationListener;
<|endoftext|>


<commit_before>'use strict';

var AbstractClientResource = require("./../abstract-client-resource");

/**
 * Process Instance Resource
 *
 * @class
 * @memberof CamSDK.client.resource
 * @augments CamSDK.client.AbstractClientResource
 */
var ProcessInstance = AbstractClientResource.extend(
/** @lends  CamSDK.client.resource.ProcessInstance.prototype */
{

},

/** @lends  CamSDK.client.resource.ProcessInstance */
{
  /**
   * API path for the process instance resource
   */
  path: 'process-instance',

  /**
   * Creates a process instance from a process definition
   *
   * @param  {Object}   params
   * @param  {String}   [params.id]
   * @param  {String}   [params.key]
   * @param  {Object.<String, *>} [params.variables]
   * @param  {requestCallback} [done]
   */
  create: function(params, done) {
    return this.http.post(params, done);
  },

  /**
   * Get a list of process instances
   *
   * @param  {Object}   params
   * @param  {requestCallback} [done]
   */
  list: function(params, done) {
    return this.list(params, done);
  }
});

module.exports = ProcessInstance;
<commit_msg>fix(client): correct the list method of process-instance resource<commit_after>'use strict';

var AbstractClientResource = require("./../abstract-client-resource");

/**
 * Process Instance Resource
 *
 * @class
 * @memberof CamSDK.client.resource
 * @augments CamSDK.client.AbstractClientResource
 */
var ProcessInstance = AbstractClientResource.extend(
/** @lends  CamSDK.client.resource.ProcessInstance.prototype */
{

},

/** @lends  CamSDK.client.resource.ProcessInstance */
{
  /**
   * API path for the process instance resource
   */
  path: 'process-instance',

  /**
   * Creates a process instance from a process definition
   *
   * @param  {Object}   params
   * @param  {String}   [params.id]
   * @param  {String}   [params.key]
   * @param  {Object.<String, *>} [params.variables]
   * @param  {requestCallback} [done]
   */
  create: function(params, done) {
    return this.http.post(params, done);
  },

  /**
   * Get a list of process instances
   *
   * @param  {Object}   params
   * @param {String} [params.processInstanceIds]      Filter by a comma-separated list of process
   *                                                  instance ids.
   * @param {String} [params.businessKey]             Filter by process instance business key.
   * @param {String} [params.caseInstanceId]          Filter by case instance id.
   * @param {String} [params.processDefinitionId]     Filter by the process definition the
   *                                                  instances run on.
   * @param {String} [params.processDefinitionKey]    Filter by the key of the process definition
   *                                                  the instances run on.
   * @param {String} [params.superProcessInstance]    Restrict query to all process instances that
   *                                                  are sub process instances of the given process
   *                                                  instance. Takes a process instance id.
   * @param {String} [params.subProcessInstance]      Restrict query to all process instances that
   *                                                  have the given process instance as a sub
   *                                                  process instance. Takes a process instance id.
   * @param {String} [params.active]                  Only include active process instances.
   *                                                  Values may be true or false.
   * @param {String} [params.suspended]               Only include suspended process instances.
   *                                                  Values may be true or false.
   * @param {String} [params.incidentId]              Filter by the incident id.
   * @param {String} [params.incidentType]            Filter by the incident type.
   * @param {String} [params.incidentMessage]         Filter by the incident message. Exact match.
   * @param {String} [params.incidentMessageLike]     Filter by the incident message that the
   *                                                  parameter is a substring of.
   * @param {String} [params.variables]               Only include process instances that have
   *                                                  variables with certain values.
   *                                                  Variable filtering expressions are
   *                                                  comma-separated and are structured as follows:
   *                                                  A valid parameter value has the form
   *                                                  key_operator_value. key is the variable name,
   *                                                  operator is the comparison operator to be used
   *                                                  and value the variable value.
   *                                                  Note: Values are always treated as String
   *                                                  objects on server side.
   *                                                  Valid operator values are:
   *                                                  - eq - equal to;
   *                                                  - neq - not equal to;
   *                                                  - gt - greater than;
   *                                                  - gteq - greater than or equal to;
   *                                                  - lt - lower than;
   *                                                  - lteq - lower than or equal to;
   *                                                  - like.
   *                                                  key and value may not contain underscore or
   *                                                  comma characters.
   * @param {String} [params.sortBy]                  Sort the results lexicographically by a given
   *                                                  criterion.
   *                                                  Valid values are:
   *                                                  - instanceId
   *                                                  - definitionKey
   *                                                  - definitionId.
   *                                                  Must be used in conjunction with the sortOrder
   *                                                  parameter.
   * @param {String} [params.sortOrder]               Sort the results in a given order.
   *                                                  Values may be asc for ascending order
   *                                                  or desc for descending order.
   *                                                  Must be used in conjunction with sortBy param.
   * @param {String} [params.firstResult]             Pagination of results. Specifies the index of
   *                                                  the first result to return.
   * @param {String} [params.maxResults]              Pagination of results. Specifies the maximum
   *                                                  number of results to return.
   *                                                  Will return less results if there are no more
   *                                                  results left.
   * @param  {requestCallback} [done]
   */
  list: function(params, done) {
    AbstractClientResource.list.apply(this, arguments);
  }
});

module.exports = ProcessInstance;
<|endoftext|>


<commit_before>module.exports = {
  activate: function() {
      atom.commands.add('atom-workspace', {
      'dawid-wave-tools:convert-to-json': () => this.converttojson(),
      'dawid-wave-tools:replace-ids': () => this.replaceids(),
      'dawid-wave-tools:open-config-file': () => this.openconfigfile()
    });
  },

  loadconfig: function() {
    this.replacejson = require("../replace.json");
    },

  replaceids: function() {
    var editor = atom.workspace.getActiveTextEditor();
    var text = editor.getSelectedText();
    this.loadconfig();
    var keys = Object.keys(this.replacejson.replaceids);
    var replacejson = this.replacejson;
    var replaceids = replacejson.replaceids;
    keys.forEach(function(key){
      var value = replaceids[key];
      console.log('key:', key, 'value:',value);
      text = text.replace(key,value);
    });
    editor.insertText(text);
  },

  openconfigfile: function() {
    atom.workspace.open("replace.json")
  },

  converttojson: function() {
    var editor = atom.workspace.getActiveTextEditor();
    var text = editor.getSelectedText();
    text = text.replace(/\n/g, "");
    text = text.replace(/\"/g, "\\\"");
    this.loadconfig();
    var keys = Object.keys(this.replacejson.converttojson);
    var replacejson = this.replacejson;
    var converttojson = replacejson.converttojson;
//    console.log(converttojson["converttojson1"]);

    keys.forEach(function(key){
      var value = converttojson[key];

      console.log('key:', key, 'value:',value);

      text = text.replace(key,value);
    });
    editor.insertText(text);
  }
}

//getSelectedText()
<commit_msg>adds ability to generate a config file if it doesn't exist<commit_after>module.exports = {
  activate: function() {
      atom.commands.add('atom-workspace', {
      'dawid-wave-tools:convert-to-json': () => this.converttojson(),
      'dawid-wave-tools:replace-ids': () => this.replaceids(),
      'dawid-wave-tools:open-config-file': () => this.openconfigfile()
    });
  },

  loadconfig: function() {
    this.replacejson = require("../replace.json");
    },

  replaceids: function() {
    var editor = atom.workspace.getActiveTextEditor();
    var text = editor.getSelectedText();
    this.loadconfig();
    var keys = Object.keys(this.replacejson.replaceids);
    var replacejson = this.replacejson;
    var replaceids = replacejson.replaceids;
    keys.forEach(function(key){
      var value = replaceids[key];
      console.log('key:', key, 'value:',value);
      text = text.replace(key,value);
    });
    editor.insertText(text);
  },

  openconfigfile: function() {
    var editor = atom.workspace.getActiveTextEditor();
    atom.workspace.open("replace.json").then((editor) => {
      var text = editor.getText();
      var templateText = [
        '{',
          '"replaceids": {',
            '"replaceids1": "replaceids1success",',
            '"replaceids2": "replaceids2success"',
          '},',
          '"converttojson": {',
            '"converttojson1": "converttojson1success",',
            '"converttojson2": "converttojson2success"',
          '}',
        '}',
].join("\n");
      if (text == "") {
        editor.insertText(templateText);
        editor.save()
      }

    })},

  converttojson: function() {
    var editor = atom.workspace.getActiveTextEditor();
    var text = editor.getSelectedText();
    text = text.replace(/\n/g, "");
    text = text.replace(/\"/g, "\\\"");
    this.loadconfig();
    var keys = Object.keys(this.replacejson.converttojson);
    var replacejson = this.replacejson;
    var converttojson = replacejson.converttojson;
//    console.log(converttojson["converttojson1"]);

    keys.forEach(function(key){
      var value = converttojson[key];

      console.log('key:', key, 'value:',value);

      text = text.replace(key,value);
    });
    editor.insertText(text);
  }
}

//getSelectedText()
<|endoftext|>


<commit_before>
/*
 * extend infowindow to serialize only the data we need
 */
_.extend(cdb.geo.ui.InfowindowModel.prototype, {
  toJSON: function() {
    return {
      fields: _.clone(this.attributes.fields),
      template_name: this.attributes.template_name
    };
  }
});

/**
 * extend gmaps layer for data serialization
 */
cdb.admin.GMapsBaseLayer = cdb.geo.GMapsBaseLayer.extend({

  clone: function() {
    return new cdb.admin.GMapsBaseLayer(_.clone(this.attributes));
  },

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'GMapsBase'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'gmapsbase',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});
/**
 * extend plain layer for data serialization
 */
cdb.admin.PlainLayer= cdb.geo.PlainLayer.extend({

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'Plain'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'background',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});

/**
 * extend tiled layer to adapt serialization
 */
cdb.admin.TileLayer = cdb.geo.TileLayer.extend({

  clone: function() {
    return new cdb.admin.TileLayer(_.clone(this.attributes));
  },

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'Tiled'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'tiled',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});

var attrHistory = function(model, attr) {

    model.MAX_HISTORY = 5;

    // variables needed to keep history state
    var attrHistory =  attr + '_history';
    var attrHistoryPos = attr + '_history_position'
    model[attrHistoryPos] = 0;

    // save history when attr changes
    var save = model['save' + attrHistory] = function(m, attrVal) {
      // If null, doesn't save anything
      if(attrVal == null) return;

      var h = model.attributes[attrHistory];
      h.push(attrVal);
      if(h.length > model.MAX_HISTORY)  {
        h.shift();
      }
      model.trigger('change:' + attrHistory, model, model.attributes[attrHistory]);
    }

    // to go foward and backward in history
    function moveHistory(d) {
      var h = model[attrHistoryPos] + d;
      // clamp
      model[attrHistoryPos] = Math.min(Math.max(0, h), model.attributes[attrHistory].length - 1);
      model.unbind('change:' + attr, save, model);
      var a = {};
      a[attr] =  model.attributes[attrHistory][model[attrHistoryPos]];
      model.set(a);
      model.bind('change:' + attr, save, model);
    }
    model['move' + attr + 'History'] = moveHistory;

    // bind changes
    model.bind('change:' + attr, save, model);
    model.bind('change:' + attrHistory, function() {
      model[attrHistoryPos] = model.get(attrHistory).length - 1;
    }, model);

    // create history list in the model if it does not exist
    if(!model.has(attrHistory)) {
      var a = {};
      a[attrHistory] = [];
      model.set(a, { silent: true });
    }
};

cdb.admin.CartoDBLayer = cdb.geo.CartoDBLayer.extend({

  MAX_HISTORY: 5,

  initialize: function() {
    this.set('use_server_style', true);
    attrHistory(this, 'tile_style');
    attrHistory(this, 'query');
    //this.bind('change:tile_style', this.saveStyle, this);
  },

  redoStyle: function() {
    this.movetile_styleHistory(1);
  },

  undoStyle: function() {
    this.movetile_styleHistory(-1);
  },

  redoQuery: function() {
    this.movequeryHistory(1);
  },

  undoQuery: function() {
    this.movequeryHistory(-1);
  },

  clone: function() {
    return new cdb.admin.CartoDBLayer(_.clone(this.attributes));
  },

  toJSON: function() {
    var c = _.clone(this.attributes);
    // remove api key
    if(c.extra_params) {
      c.extra_params = _.clone(this.attributes.extra_params);
      if(c.extra_params.api_key) {
        delete c.extra_params.api_key;
      }
      if(c.extra_params.map_key) {
        delete c.extra_params.map_key;
      }
    }

    var infowindow = c.infowindow;
    delete c.infowindow;
    var d = {
      kind: 'carto',
      options: c,
      order: c.order,
      infowindow: infowindow
    };
    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  },

  parse: function(data) {
    var c = {};

    // if api_key exist alread, set in params to not lose it
    if(data.options.extra_params && this.attributes && this.attributes.extra_params) {
      data.options.extra_params.map_key = this.attributes.extra_params.map_key;
    }

    _.extend(c, data.options, {
      id: data.id,
      type: 'CartoDB',
      infowindow: data.infowindow
    });

    return c;
  },

  sync: function(method, model, options) {
    var self = this;
    var s = this.get('extra_params');
    s.cache_buster = new Date().getTime();
    options.data =  JSON.stringify(model.toJSON());
    options.contentType = 'application/json';
    options.url = model.url();
    var st = model.changedAttributes().tile_style;
    options.wait = false;
    this.saveStyle(this, st, function() {
      // raise model change now in order to update the layer
      if(!options.silent) model.change();
      // dont pass the model to sync
      // the model data is stored in options.data because
      // it needs to be saved when sync is called (and not
      // after it)
      Backbone.sync(method, null, options);
    });
  },

  save: function(attrs, opts) {
    opts = opts || {};
    if(attrs && attrs.tile_style) opts.wait = true;
    cdb.geo.CartoDBLayer.prototype.save.call(this, attrs, opts);
  },

  /**
   * save style to tiler end point
   * this method should be removed in thr future
   */
  saveStyle: function(model, st, done) {
    var self = this;

    if(!st) {
      //pass through
      done && done();
      return;
    }

    var protocol = cdb.config.get("tiler_port") == 443 ? 'https': 'http';
    var url = protocol + '://' + this.get('user_name') + "." + cdb.config.get("tiler_domain") + ':' + cdb.config.get("tiler_port") + '/tiles/' + this.get('table_name') + '/style';
    var api_key = this.get('extra_params').map_key;
    var style_version = this.get('style_version');
    if(style_version) {
      style_version = '&style_version=' + style_version;
    } else {
      style_version = '';
    }
    if(api_key) {
      url += '?map_key=' + api_key;
    }
    $.ajax({
      url: url,
      type: 'POST',
      crossOrigin: true,
      data:"style="+  encodeURIComponent(st) + style_version,
      success: function() {
        done && done();
      },
      error: function(res) {
        try {
          var syntaxErrors = JSON.parse(res.responseText);
          self.trigger('parseError', syntaxErrors);
        } catch(e) {
        }
        self.trigger('error');
      }
    });
  }

});

cdb.admin.Layers = cdb.geo.Layers.extend({

  // the model class works here like a factory
  // depending of the kind of layer creates a
  // type of layer or other
  model: function(attrs, options) {
    var typeClass = {
      'Tiled': cdb.admin.TileLayer,
      'CartoDB': cdb.admin.CartoDBLayer,
      'Plain': cdb.admin.PlainLayer,
      'GMapsBase': cdb.admin.GMapsBaseLayer
    };
    var typeMap = {
      'Layer::Tiled': 'Tiled',
      'Layer::Carto': 'CartoDB',
      'Layer::Background': 'Plain',
      'tiled': 'Tiled',
      'carto': 'CartoDB',
      'background': 'Plain',
      'gmapsbase': 'GMapsBase'
    };

    return new typeClass[typeMap[attrs.kind]](attrs, options);
  },

  url: function() {
    return '/api/v1/maps/' +  this.map.id + '/layers';
  },

  parse: function(data) {
    return data.layers;
  },

  saveLayers: function(opts) {
    this.each(function(l) { l.save(null, opts); });
  },

  /**
   * on reset order the layers so tiled layer are under cartodb layers
   */
  reset: function(models, options) {
    var sortOrder = {
      'Tiled': 0,
      'CartoDB': 1,
      'tiled': 0,
      'carto': 1
    };
    var sortModels = function(a, b) {
      return sortOrder[a.get('type')] - sortOrder[b.get('type')];
    };
    var sortRaw = function(a, b) {
      return sortOrder[a.kind] - sortOrder[b.kind];
    };

    if(_.size(models)) {
      // if is not a model use raw sort
      _(models).sort(models[0].get === undefined ? sortRaw : sortModels);
    }

    Backbone.Collection.prototype.reset.call(this, models, options);
  }

});

/**
 * this is a specialization of generic map prepared to hold two layers:
 *  - a base layer
 *  - a data layer which contains the table data
 *
 * cartodb only supports one data layer per map so this will change when
 * that changes
 */

cdb.admin.Map = cdb.geo.Map.extend({

  urlRoot: '/api/v1/maps',

  initialize: function() {
    this.constructor.__super__.initialize.apply(this);
    this.delayedSave = _.debounce(this.save, 1000);
    //this.autoSave();
    this.bind('change:id', this._fetchLayers, this);
    this.layers = new cdb.admin.Layers();
    this.layers.map = this;
    this.layers.bind('reset', this._layersChanged, this);
    this.layers.bind('add', this._layersChanged, this);

  },

  _layersChanged: function() {
    if(this.layers.size() >= 1) {
      this._adjustZoomtoLayer(this.layers.at(0));
      if(this.layers.size() >= 2) {
        this.set({ dataLayer: this.layers.at(1) });
      }
    }
  },

  // fetch related layers
  _fetchLayers: function() {
    this.layers.fetch();
  },

  /**
   * link to a table
   */
  relatedTo: function(table) {
    this.table = table;
    this.table.bind('change:map_id', this._fetchOrCreate, this);
  },

  parse: function(data) {
    data.bounding_box_ne = JSON.parse(data.bounding_box_ne);
    data.bounding_box_sw = JSON.parse(data.bounding_box_sw);
    data.view_bounds_ne = JSON.parse(data.view_bounds_ne);
    data.view_bounds_sw = JSON.parse(data.view_bounds_sw);
    data.center = JSON.parse(data.center);
    return data;
  },

  _fetchOrCreate: function() {
    var self = this;
    var map_id = this.table.get('map_id');
    if(!map_id) {
      this.create();
    } else {
      this.set({ id: map_id });
      this.fetch({
        error: function() {
          cdb.log.info("creating map for table");
          self.create();
        }
      });
    }
  },

  /**
   * change base layer and save all the layers to preserve the order
   */
  setBaseLayer: function(layer) {

    var self = this;

    this.trigger('savingLayers');

    var done = _.after(this.layers.size(), function() {
      self.trigger('savingLayersFinish');
    });

    this.elder('setBaseLayer', layer, {
      success: function() {
        self.layers.saveLayers({
          success: function() {
            done && done();
          },
          error: function() {
            cdb.log.error("error saving layer order");
            self.trigger('savingLayersFinish');
          }
        });
      },
      error: function() {
        self.trigger('savingLayersFinish');
        self.trigger('savingLayersError');
      }
    });
  },

  /**
   * the first version of cartodb contains one single layer
   * per table with information.
   */
  addDataLayer: function(lyr) {
    this.addLayer(lyr);
    this.set({ dataLayer: lyr });
  },

  /**
   * create a new map. this is a helper to use from javascript command line
   */
  create: function() {
    this.unset('id');
    this.set({ table_id: this.table.id });
    this.save();
  },

  /**
   * enable save map each time the viewport changes
   * not working
   */
  autoSave: function() {
    this.bind('change:center', this.delayedSave);
    this.bind('change:zoom', this.delayedSave);
  },

  toJSON: function() {
    var c = _.clone(this.attributes);
    // data layer is a helper to work in local
    delete c.dataLayer;
    return c;
  },

  /**
   * change provider and optionally baselayer
   */
  changeProvider: function(provider, baselayer) {
    var self = this;
    if(baselayer && baselayer.get('id')) {
      cdb.log.error("the baselayer should not be saved in the server");
      return;
    }
    var _changeBaseLayer = function() {
      if(baselayer) {
        self.setBaseLayer(baselayer);
      }
    }
    if(this.get('provider') !== provider) {
      this.save({ provider: provider }, {
        success: function() {
          _changeBaseLayer();
          self.change();
        },
        error: function(e, resp) {
          self.error(_t('error switching base layer'), resp);
        },
        silent: true
      });
    } else {
      _changeBaseLayer();
    }
  },

  notice: function(msg, type, timeout) {
    this.trigger('notice', msg, type, timeout);
  },

  error: function(msg, resp) {
    var err =  resp && JSON.parse(resp.responseText).errors[0];
    this.trigger('notice', msg + " " + err, 'error');
  }

});
<commit_msg>whitespace removed (this was screwing a merge, sorry)<commit_after>
/*
 * extend infowindow to serialize only the data we need
 */
_.extend(cdb.geo.ui.InfowindowModel.prototype, {
  toJSON: function() {
    return {
      fields: _.clone(this.attributes.fields),
      template_name: this.attributes.template_name
    };
  }
});

/**
 * extend gmaps layer for data serialization
 */
cdb.admin.GMapsBaseLayer = cdb.geo.GMapsBaseLayer.extend({

  clone: function() {
    return new cdb.admin.GMapsBaseLayer(_.clone(this.attributes));
  },

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'GMapsBase'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'gmapsbase',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});
/**
 * extend plain layer for data serialization
 */
cdb.admin.PlainLayer= cdb.geo.PlainLayer.extend({

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'Plain'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'background',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});

/**
 * extend tiled layer to adapt serialization
 */
cdb.admin.TileLayer = cdb.geo.TileLayer.extend({

  clone: function() {
    return new cdb.admin.TileLayer(_.clone(this.attributes));
  },

  parse: function(data) {
    var c = {};
    _.extend(c, data.options, {
      id: data.id,
      type: 'Tiled'
    });
    return c;
  },

  toJSON: function() {
    var c = _.clone(this.attributes);

    var d = {
      kind:  'tiled',
      options: c,
      order: c.order
    };

    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  }

});

var attrHistory = function(model, attr) {

    model.MAX_HISTORY = 5;

    // variables needed to keep history state
    var attrHistory =  attr + '_history';
    var attrHistoryPos = attr + '_history_position'
    model[attrHistoryPos] = 0;

    // save history when attr changes
    var save = model['save' + attrHistory] = function(m, attrVal) {
      // If null, doesn't save anything
      if(attrVal == null) return;

      var h = model.attributes[attrHistory];
      h.push(attrVal);
      if(h.length > model.MAX_HISTORY)  {
        h.shift();
      }
      model.trigger('change:' + attrHistory, model, model.attributes[attrHistory]);
    }

    // to go foward and backward in history
    function moveHistory(d) {
      var h = model[attrHistoryPos] + d;
      // clamp
      model[attrHistoryPos] = Math.min(Math.max(0, h), model.attributes[attrHistory].length - 1);
      model.unbind('change:' + attr, save, model);
      var a = {};
      a[attr] =  model.attributes[attrHistory][model[attrHistoryPos]];
      model.set(a);
      model.bind('change:' + attr, save, model);
    }
    model['move' + attr + 'History'] = moveHistory;

    // bind changes
    model.bind('change:' + attr, save, model);
    model.bind('change:' + attrHistory, function() {
      model[attrHistoryPos] = model.get(attrHistory).length - 1;
    }, model);

    // create history list in the model if it does not exist
    if(!model.has(attrHistory)) {
      var a = {};
      a[attrHistory] = [];
      model.set(a, { silent: true });
    }
};

cdb.admin.CartoDBLayer = cdb.geo.CartoDBLayer.extend({

  MAX_HISTORY: 5,

  initialize: function() {
    this.set('use_server_style', true);
    attrHistory(this, 'tile_style');
    attrHistory(this, 'query');
    //this.bind('change:tile_style', this.saveStyle, this);
  },

  redoStyle: function() {
    this.movetile_styleHistory(1);
  },

  undoStyle: function() {
    this.movetile_styleHistory(-1);
  },

  redoQuery: function() {
    this.movequeryHistory(1);
  },

  undoQuery: function() {
    this.movequeryHistory(-1);
  },

  clone: function() {
    return new cdb.admin.CartoDBLayer(_.clone(this.attributes));
  },

  toJSON: function() {
    var c = _.clone(this.attributes);
    // remove api key
    if(c.extra_params) {
      c.extra_params = _.clone(this.attributes.extra_params);
      if(c.extra_params.api_key) {
        delete c.extra_params.api_key;
      }
      if(c.extra_params.map_key) {
        delete c.extra_params.map_key;
      }
    }

    var infowindow = c.infowindow;
    delete c.infowindow;
    var d = {
      kind: 'carto',
      options: c,
      order: c.order,
      infowindow: infowindow
    };
    if(c.id !== undefined) {
      d.id = c.id;
    }
    return d;
  },

  parse: function(data) {
    var c = {};

    // if api_key exist alread, set in params to not lose it
    if(data.options.extra_params && this.attributes && this.attributes.extra_params) {
      data.options.extra_params.map_key = this.attributes.extra_params.map_key;
    }

    _.extend(c, data.options, {
      id: data.id,
      type: 'CartoDB',
      infowindow: data.infowindow
    });

    return c;
  },

  sync: function(method, model, options) {
    var self = this;
    var s = this.get('extra_params');
    s.cache_buster = new Date().getTime();
    options.data =  JSON.stringify(model.toJSON());
    options.contentType = 'application/json';
    options.url = model.url();
    var st = model.changedAttributes().tile_style;
    options.wait = false;
    this.saveStyle(this, st, function() {
      // raise model change now in order to update the layer
      if(!options.silent) model.change();
      // dont pass the model to sync
      // the model data is stored in options.data because
      // it needs to be saved when sync is called (and not
      // after it)
      Backbone.sync(method, null, options);
    });
  },

  save: function(attrs, opts) {
    opts = opts || {};
    if(attrs && attrs.tile_style) opts.wait = true;
    cdb.geo.CartoDBLayer.prototype.save.call(this, attrs, opts);
  },

  /**
   * save style to tiler end point
   * this method should be removed in thr future
   */
  saveStyle: function(model, st, done) {
    var self = this;

    if(!st) {
      //pass through
      done && done();
      return;
    }

    var protocol = cdb.config.get("tiler_port") == 443 ? 'https': 'http';
    var url = protocol + '://' + this.get('user_name') + "." + cdb.config.get("tiler_domain") + ':' + cdb.config.get("tiler_port") + '/tiles/' + this.get('table_name') + '/style';
    var api_key = this.get('extra_params').map_key;
    var style_version = this.get('style_version');
    if(style_version) {
      style_version = '&style_version=' + style_version;
    } else {
      style_version = '';
    }
    if(api_key) {
      url += '?map_key=' + api_key;
    }
    $.ajax({
      url: url,
      type: 'POST',
      crossOrigin: true,
      data:"style="+  encodeURIComponent(st) + style_version,
      success: function() {
        done && done();
      },
      error: function(res) {
        try {
          var syntaxErrors = JSON.parse(res.responseText);
          self.trigger('parseError', syntaxErrors);
        } catch(e) {
        }
        self.trigger('error');
      }
    });
  }

});

cdb.admin.Layers = cdb.geo.Layers.extend({

  // the model class works here like a factory
  // depending of the kind of layer creates a
  // type of layer or other
  model: function(attrs, options) {
    var typeClass = {
      'Tiled': cdb.admin.TileLayer,
      'CartoDB': cdb.admin.CartoDBLayer,
      'Plain': cdb.admin.PlainLayer,
      'GMapsBase': cdb.admin.GMapsBaseLayer
    };
    var typeMap = {
      'Layer::Tiled': 'Tiled',
      'Layer::Carto': 'CartoDB',
      'Layer::Background': 'Plain',
      'tiled': 'Tiled',
      'carto': 'CartoDB',
      'background': 'Plain',
      'gmapsbase': 'GMapsBase'
    };

    return new typeClass[typeMap[attrs.kind]](attrs, options);
  },

  url: function() {
    return '/api/v1/maps/' +  this.map.id + '/layers';
  },

  parse: function(data) {
    return data.layers;
  },

  saveLayers: function(opts) {
    this.each(function(l) { l.save(null, opts); });
  },

  /**
   * on reset order the layers so tiled layer are under cartodb layers
   */
  reset: function(models, options) {
    var sortOrder = {
      'Tiled': 0,
      'CartoDB': 1,
      'tiled': 0,
      'carto': 1
    };
    var sortModels = function(a, b) {
      return sortOrder[a.get('type')] - sortOrder[b.get('type')];
    };
    var sortRaw = function(a, b) {
      return sortOrder[a.kind] - sortOrder[b.kind];
    };

    if(_.size(models)) {
      // if is not a model use raw sort
      _(models).sort(models[0].get === undefined ? sortRaw : sortModels);
    }

    Backbone.Collection.prototype.reset.call(this, models, options);
  }

});

/**
 * this is a specialization of generic map prepared to hold two layers:
 *  - a base layer
 *  - a data layer which contains the table data
 *
 * cartodb only supports one data layer per map so this will change when
 * that changes
 */

cdb.admin.Map = cdb.geo.Map.extend({

  urlRoot: '/api/v1/maps',

  initialize: function() {
    this.constructor.__super__.initialize.apply(this);
    this.delayedSave = _.debounce(this.save, 1000);
    //this.autoSave();
    this.bind('change:id', this._fetchLayers, this);
    this.layers = new cdb.admin.Layers();
    this.layers.map = this;
    this.layers.bind('reset', this._layersChanged, this);
    this.layers.bind('add', this._layersChanged, this);

  },

  _layersChanged: function() {
    if(this.layers.size() >= 1) {
      this._adjustZoomtoLayer(this.layers.at(0));
      if(this.layers.size() >= 2) {
        this.set({ dataLayer: this.layers.at(1) });
      }
    }
  },

  // fetch related layers
  _fetchLayers: function() {
    this.layers.fetch();
  },

  /**
   * link to a table
   */
  relatedTo: function(table) {
    this.table = table;
    this.table.bind('change:map_id', this._fetchOrCreate, this);
  },

  parse: function(data) {
    data.bounding_box_ne = JSON.parse(data.bounding_box_ne);
    data.bounding_box_sw = JSON.parse(data.bounding_box_sw);
    data.view_bounds_ne = JSON.parse(data.view_bounds_ne);
    data.view_bounds_sw = JSON.parse(data.view_bounds_sw);
    data.center = JSON.parse(data.center);
    return data;
  },

  _fetchOrCreate: function() {
    var self = this;
    var map_id = this.table.get('map_id');
    if(!map_id) {
      this.create();
    } else {
      this.set({ id: map_id });
      this.fetch({
        error: function() {
          cdb.log.info("creating map for table");
          self.create();
        }
      });
    }
  },

  /**
   * change base layer and save all the layers to preserve the order
   */
  setBaseLayer: function(layer) {

    var self = this;

    this.trigger('savingLayers');

    var done = _.after(this.layers.size(), function() {
      self.trigger('savingLayersFinish');
    });

    this.elder('setBaseLayer', layer, {
      success: function() {
        self.layers.saveLayers({
          success: function() {
            done && done();
          },
          error: function() {
            cdb.log.error("error saving layer order");
            self.trigger('savingLayersFinish');
          }
        });
      },
      error: function() {
        self.trigger('savingLayersFinish');
        self.trigger('savingLayersError');
      }
    });
  },

  /**
   * the first version of cartodb contains one single layer
   * per table with information.
   */
  addDataLayer: function(lyr) {
    this.addLayer(lyr);
    this.set({ dataLayer: lyr });
  },

  /**
   * create a new map. this is a helper to use from javascript command line
   */
  create: function() {
    this.unset('id');
    this.set({ table_id: this.table.id });
    this.save();
  },

  /**
   * enable save map each time the viewport changes
   * not working
   */
  autoSave: function() {
    this.bind('change:center', this.delayedSave);
    this.bind('change:zoom', this.delayedSave);
  },

  toJSON: function() {
    var c = _.clone(this.attributes);
    // data layer is a helper to work in local
    delete c.dataLayer;
    return c;
  },

  /**
   * change provider and optionally baselayer
   */
  changeProvider: function(provider, baselayer) {
    var self = this;
    if(baselayer && baselayer.get('id')) {
      cdb.log.error("the baselayer should not be saved in the server");
      return;
    }
    var _changeBaseLayer = function() {
      if(baselayer) {
        self.setBaseLayer(baselayer);
      }
    }
    if(this.get('provider') !== provider) {
      this.save({ provider: provider }, {
        success: function() {
          _changeBaseLayer();
          self.change();
        },
        error: function(e, resp) {
          self.error(_t('error switching base layer'), resp);
        },
        silent: true
      });
    } else {
      _changeBaseLayer();
    }
  },

  notice: function(msg, type, timeout) {
    this.trigger('notice', msg, type, timeout);
  },

  error: function(msg, resp) {
    var err =  resp && JSON.parse(resp.responseText).errors[0];
    this.trigger('notice', msg + " " + err, 'error');
  }

});
<|endoftext|>


<commit_before>'use strict';

var RawQuery = require('../query/raw');
var CreateTableQuery = require('./table/create');
var DropTableQuery = require('./table/drop');

/**
 * A schema is the building block of Azul's schema migration layer. It exposes
 * methods that returns {@link RawQuery} objects.
 *
 * Generally, you will not create a schema object directly. Instead, you will
 * receive a schema object via the {@link Database}.
 *
 * @since 1.0
 * @protected
 * @constructor
 * @param {Adapter} adapter The adapter to use when using the query.
 */
var Schema = RawQuery.extend(/** @lends Schema# */{
  init: function(adapter) {
    this._super();
    this._adapter = adapter;
  },

  /**
   * Documentation forthcoming.
   */
  createTable: function(name, cb) {
    return CreateTableQuery.create(this._adapter, name, cb)
      .transaction(this.transaction());
  },

  /**
   * Documentation forthcoming.
   */
  dropTable: function(name) {
    return DropTableQuery.create(this._adapter, name)
      .transaction(this.transaction());
  },

  sql: function() {
    var result = this._super();
    if (!result) {
      throw new Error('Schema cannot be executed. ' +
        'You must first call one of the schema methods.');
    }
    return result;
  }
});

module.exports = Schema.reopenClass({ __name__: 'Schema' });
<commit_msg>Added a few TODOs.<commit_after>'use strict';

var RawQuery = require('../query/raw');
var CreateTableQuery = require('./table/create');
var DropTableQuery = require('./table/drop');

/**
 * A schema is the building block of Azul's schema migration layer. It exposes
 * methods that returns {@link RawQuery} objects.
 *
 * Generally, you will not create a schema object directly. Instead, you will
 * receive a schema object via the {@link Database}.
 *
 * @since 1.0
 * @protected
 * @constructor
 * @param {Adapter} adapter The adapter to use when using the query.
 */
var Schema = RawQuery.extend(/** @lends Schema# */{
  init: function(adapter) {
    this._super();
    this._adapter = adapter;
  },

  /**
   * Documentation forthcoming.
   */
  createTable: function(name, cb) {
    // TODO: shouldn't set the transaction manually here, instead we should use
    // whatever methodology that we come up with for the EntryQuery to "spawn"
    // queries of a different class. also apply this to the code below.
    return CreateTableQuery.create(this._adapter, name, cb)
      .transaction(this.transaction());
  },

  /**
   * Documentation forthcoming.
   */
  dropTable: function(name) {
    return DropTableQuery.create(this._adapter, name)
      .transaction(this.transaction()); // TODO: remove (see above)
  },

  sql: function() {
    var result = this._super();
    if (!result) {
      throw new Error('Schema cannot be executed. ' +
        'You must first call one of the schema methods.');
    }
    return result;
  }
});

module.exports = Schema.reopenClass({ __name__: 'Schema' });
<|endoftext|>