<commit_before>import algoliasearchHelper from 'algoliasearch-helper';
import version from '../lib/version.js';
import urlHelper from 'algoliasearch-helper/src/url';
import isEqual from 'lodash/isEqual';
import assign from 'lodash/assign';

const AlgoliaSearchHelper = algoliasearchHelper.AlgoliaSearchHelper;
const majorVersionNumber = version.split('.')[0];
let firstRender = true;

function timerMaker(t0) {
  let t = t0;
  return function timer() {
    const now = Date.now();
    const delta = now - t;
    t = now;
    return delta;
  };
}

/**
 * @typedef {object} UrlUtil
 * @property {string} character the character used in the url
 * @property {function} onpopstate add an event listener for the URL change
 * @property {function} pushState creates a new entry in the browser history
 * @property {function} replaceState update the current entry of the browser history
 * @property {function} readUrl reads the query string of the parameters
 */

/**
 * Handles the legacy browsers
 * @type {UrlUtil}
 */
const hashUrlUtils = {
  character: '#',
  onpopstate(cb) {
    window.addEventListener('hashchange', cb);
  },
  pushState(qs) {
    window.location.assign(getFullURL(this.createURL(qs)));
  },
  replaceState(qs) {
    window.location.replace(getFullURL(this.createURL(qs)));
  },
  createURL(qs) {
    return window.location.search + this.character + qs;
  },
  readUrl() {
    return window.location.hash.slice(1);
  },
};

/**
 * Handles the modern API
 * @type {UrlUtil}
 */
const modernUrlUtils = {
  character: '?',
  onpopstate(cb) {
    window.addEventListener('popstate', cb);
  },
  pushState(qs, {getHistoryState}) {
    window.history.pushState(getHistoryState(), '', getFullURL(this.createURL(qs)));
  },
  replaceState(qs, {getHistoryState}) {
    window.history.replaceState(getHistoryState(), '', getFullURL(this.createURL(qs)));
  },
  createURL(qs) {
    return this.character + qs + document.location.hash;
  },
  readUrl() {
    return window.location.search.slice(1);
  },
};

// we always push the full url to the url bar. Not a relative one.
// So that we handle cases like using a <base href>, see
// https://github.com/algolia/instantsearch.js/issues/790 for the original issue
function getFullURL(relative) {
  return getLocationOrigin() + window.location.pathname + relative;
}

// IE <= 11 has no location.origin or buggy
function getLocationOrigin() {
  // eslint-disable-next-line max-len
  return `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
}

// see InstantSearch.js file for urlSync options
class URLSync {
  constructor(urlUtils, options) {
    this.urlUtils = urlUtils;
    this.originalConfig = null;
    this.timer = timerMaker(Date.now());
    this.mapping = options.mapping || {};
    this.getHistoryState = options.getHistoryState || (() => null);
    this.threshold = options.threshold || 700;
    this.updateOnEveryKeyStroke = options.updateOnEveryKeyStroke !== undefined ?
      options.updateOnEveryKeyStroke :
      true;
    this.trackedParameters = options.trackedParameters || ['query', 'attribute:*', 'index', 'page', 'hitsPerPage'];

    this.searchParametersFromUrl = AlgoliaSearchHelper
      .getConfigurationFromQueryString(
        this.urlUtils.readUrl(),
        {mapping: this.mapping}
      );
  }

  getConfiguration(currentConfiguration) {
    // we need to create a REAL helper to then get its state. Because some parameters
    // like hierarchicalFacet.rootPath are then triggering a default refinement that would
    // be not present if it was not going trough the SearchParameters constructor
    this.originalConfig = algoliasearchHelper(
      {addAlgoliaAgent() {}},
      currentConfiguration.index,
      currentConfiguration
    ).state;
    return this.searchParametersFromUrl;
  }

  render({helper}) {
    if (firstRender) {
      firstRender = false;
      this.onHistoryChange(this.onPopState.bind(this, helper));
      helper.on('change', state => this.renderURLFromState(state));
    }
  }

  onPopState(helper, fullState) {
    // compare with helper.state
    const partialHelperState = helper.getState(this.trackedParameters);
    const fullHelperState = assign({}, this.originalConfig, partialHelperState);

    if (isEqual(fullHelperState, fullState)) return;

    helper.overrideStateWithoutTriggeringChangeEvent(fullState).search();
  }

  renderURLFromState(state) {
    const currentQueryString = this.urlUtils.readUrl();
    const foreignConfig = AlgoliaSearchHelper
      .getForeignConfigurationInQueryString(currentQueryString, {mapping: this.mapping});
    // eslint-disable-next-line camelcase
    foreignConfig.is_v = majorVersionNumber;

    const qs = urlHelper.getQueryStringFromState(
      state.filter(this.trackedParameters),
      {
        moreAttributes: foreignConfig,
        mapping: this.mapping,
        safe: true,
      }
    );

    if (this.updateOnEveryKeyStroke === true) {
      if (this.timer() < this.threshold) {
        this.urlUtils.replaceState(qs, {getHistoryState: this.getHistoryState});
      } else {
        this.urlUtils.pushState(qs, {getHistoryState: this.getHistoryState});
      }
      return;
    }

    clearTimeout(this.urlUpdateTimeout);
    this.urlUpdateTimeout = setTimeout(() => {
      this.urlUtils.pushState(qs, {getHistoryState: this.getHistoryState});
    }, this.threshold);
  }

  // External API's

  createURL(state, {absolute}) {
    const currentQueryString = this.urlUtils.readUrl();
    const filteredState = state.filter(this.trackedParameters);
    const foreignConfig = algoliasearchHelper
      .url
      .getUnrecognizedParametersInQueryString(currentQueryString, {mapping: this.mapping});
    // Add instantsearch version to reconciliate old url with newer versions
    // eslint-disable-next-line camelcase
    foreignConfig.is_v = majorVersionNumber;
    const relative = this
      .urlUtils
      .createURL(algoliasearchHelper.url.getQueryStringFromState(filteredState, {mapping: this.mapping}));

    return absolute ? getFullURL(relative) : relative;
  }

  onHistoryChange(fn) {
    this.urlUtils.onpopstate(() => {
      const qs = this.urlUtils.readUrl();
      const partialState = AlgoliaSearchHelper.getConfigurationFromQueryString(qs, {mapping: this.mapping});
      const fullState = assign({}, this.originalConfig, partialState);
      fn(fullState);
    });
  }
}

/**
 * Instanciate a url sync widget. This widget let you synchronize the search
 * parameters with the URL. It can operate with legacy API and hash or it can use
 * the modern history API. By default, it will use the modern API, but if you are
 * looking for compatibility with IE8 and IE9, then you should set 'useHash' to
 * true.
 * @param {object} options all the parameters to configure the URL synchronization. It
 * may contain the following keys :
 *  - threshold:number time in ms after which a new state is created in the browser
 * history. The default value is 700.
 *  - trackedParameters:string[] parameters that will be synchronized in the
 * URL. By default, it will track the query, all the refinable attribute (facets and numeric
 * filters), the index and the page.
 *  - useHash:boolean if set to true, the url will be hash based. Otherwise,
 * it'll use the query parameters using the modern history API.
 * @return {object} the widget instance
 */
function urlSync(options = {}) {
  const useHash = options.useHash || false;

  const urlUtils = useHash ? hashUrlUtils : modernUrlUtils;

  return new URLSync(urlUtils, options);
}

export default urlSync;
<commit_msg>fix(url): clear timeout on pop<commit_after>import algoliasearchHelper from 'algoliasearch-helper';
import version from '../lib/version.js';
import urlHelper from 'algoliasearch-helper/src/url';
import isEqual from 'lodash/isEqual';
import assign from 'lodash/assign';

const AlgoliaSearchHelper = algoliasearchHelper.AlgoliaSearchHelper;
const majorVersionNumber = version.split('.')[0];
let firstRender = true;

function timerMaker(t0) {
  let t = t0;
  return function timer() {
    const now = Date.now();
    const delta = now - t;
    t = now;
    return delta;
  };
}

/**
 * @typedef {object} UrlUtil
 * @property {string} character the character used in the url
 * @property {function} onpopstate add an event listener for the URL change
 * @property {function} pushState creates a new entry in the browser history
 * @property {function} replaceState update the current entry of the browser history
 * @property {function} readUrl reads the query string of the parameters
 */

/**
 * Handles the legacy browsers
 * @type {UrlUtil}
 */
const hashUrlUtils = {
  character: '#',
  onpopstate(cb) {
    window.addEventListener('hashchange', cb);
  },
  pushState(qs) {
    window.location.assign(getFullURL(this.createURL(qs)));
  },
  replaceState(qs) {
    window.location.replace(getFullURL(this.createURL(qs)));
  },
  createURL(qs) {
    return window.location.search + this.character + qs;
  },
  readUrl() {
    return window.location.hash.slice(1);
  },
};

/**
 * Handles the modern API
 * @type {UrlUtil}
 */
const modernUrlUtils = {
  character: '?',
  onpopstate(cb) {
    window.addEventListener('popstate', cb);
  },
  pushState(qs, {getHistoryState}) {
    window.history.pushState(getHistoryState(), '', getFullURL(this.createURL(qs)));
  },
  replaceState(qs, {getHistoryState}) {
    window.history.replaceState(getHistoryState(), '', getFullURL(this.createURL(qs)));
  },
  createURL(qs) {
    return this.character + qs + document.location.hash;
  },
  readUrl() {
    return window.location.search.slice(1);
  },
};

// we always push the full url to the url bar. Not a relative one.
// So that we handle cases like using a <base href>, see
// https://github.com/algolia/instantsearch.js/issues/790 for the original issue
function getFullURL(relative) {
  return getLocationOrigin() + window.location.pathname + relative;
}

// IE <= 11 has no location.origin or buggy
function getLocationOrigin() {
  // eslint-disable-next-line max-len
  return `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
}

// see InstantSearch.js file for urlSync options
class URLSync {
  constructor(urlUtils, options) {
    this.urlUtils = urlUtils;
    this.originalConfig = null;
    this.timer = timerMaker(Date.now());
    this.mapping = options.mapping || {};
    this.getHistoryState = options.getHistoryState || (() => null);
    this.threshold = options.threshold || 700;
    this.updateOnEveryKeyStroke = options.updateOnEveryKeyStroke !== undefined ?
      options.updateOnEveryKeyStroke :
      true;
    this.trackedParameters = options.trackedParameters || ['query', 'attribute:*', 'index', 'page', 'hitsPerPage'];

    this.searchParametersFromUrl = AlgoliaSearchHelper
      .getConfigurationFromQueryString(
        this.urlUtils.readUrl(),
        {mapping: this.mapping}
      );
  }

  getConfiguration(currentConfiguration) {
    // we need to create a REAL helper to then get its state. Because some parameters
    // like hierarchicalFacet.rootPath are then triggering a default refinement that would
    // be not present if it was not going trough the SearchParameters constructor
    this.originalConfig = algoliasearchHelper(
      {addAlgoliaAgent() {}},
      currentConfiguration.index,
      currentConfiguration
    ).state;
    return this.searchParametersFromUrl;
  }

  render({helper}) {
    if (firstRender) {
      firstRender = false;
      this.onHistoryChange(this.onPopState.bind(this, helper));
      helper.on('change', state => this.renderURLFromState(state));
    }
  }

  onPopState(helper, fullState) {
    clearTimeout(this.urlUpdateTimeout);
    // compare with helper.state
    const partialHelperState = helper.getState(this.trackedParameters);
    const fullHelperState = assign({}, this.originalConfig, partialHelperState);

    if (isEqual(fullHelperState, fullState)) return;

    helper.overrideStateWithoutTriggeringChangeEvent(fullState).search();
  }

  renderURLFromState(state) {
    const currentQueryString = this.urlUtils.readUrl();
    const foreignConfig = AlgoliaSearchHelper
      .getForeignConfigurationInQueryString(currentQueryString, {mapping: this.mapping});
    // eslint-disable-next-line camelcase
    foreignConfig.is_v = majorVersionNumber;

    const qs = urlHelper.getQueryStringFromState(
      state.filter(this.trackedParameters),
      {
        moreAttributes: foreignConfig,
        mapping: this.mapping,
        safe: true,
      }
    );

    if (this.updateOnEveryKeyStroke === true) {
      if (this.timer() < this.threshold) {
        this.urlUtils.replaceState(qs, {getHistoryState: this.getHistoryState});
      } else {
        this.urlUtils.pushState(qs, {getHistoryState: this.getHistoryState});
      }
      return;
    }

    clearTimeout(this.urlUpdateTimeout);
    this.urlUpdateTimeout = setTimeout(() => {
      this.urlUtils.pushState(qs, {getHistoryState: this.getHistoryState});
    }, this.threshold);
  }

  // External API's

  createURL(state, {absolute}) {
    const currentQueryString = this.urlUtils.readUrl();
    const filteredState = state.filter(this.trackedParameters);
    const foreignConfig = algoliasearchHelper
      .url
      .getUnrecognizedParametersInQueryString(currentQueryString, {mapping: this.mapping});
    // Add instantsearch version to reconciliate old url with newer versions
    // eslint-disable-next-line camelcase
    foreignConfig.is_v = majorVersionNumber;
    const relative = this
      .urlUtils
      .createURL(algoliasearchHelper.url.getQueryStringFromState(filteredState, {mapping: this.mapping}));

    return absolute ? getFullURL(relative) : relative;
  }

  onHistoryChange(fn) {
    this.urlUtils.onpopstate(() => {
      const qs = this.urlUtils.readUrl();
      const partialState = AlgoliaSearchHelper.getConfigurationFromQueryString(qs, {mapping: this.mapping});
      const fullState = assign({}, this.originalConfig, partialState);
      fn(fullState);
    });
  }
}

/**
 * Instanciate a url sync widget. This widget let you synchronize the search
 * parameters with the URL. It can operate with legacy API and hash or it can use
 * the modern history API. By default, it will use the modern API, but if you are
 * looking for compatibility with IE8 and IE9, then you should set 'useHash' to
 * true.
 * @param {object} options all the parameters to configure the URL synchronization. It
 * may contain the following keys :
 *  - threshold:number time in ms after which a new state is created in the browser
 * history. The default value is 700.
 *  - trackedParameters:string[] parameters that will be synchronized in the
 * URL. By default, it will track the query, all the refinable attribute (facets and numeric
 * filters), the index and the page.
 *  - useHash:boolean if set to true, the url will be hash based. Otherwise,
 * it'll use the query parameters using the modern history API.
 * @return {object} the widget instance
 */
function urlSync(options = {}) {
  const useHash = options.useHash || false;

  const urlUtils = useHash ? hashUrlUtils : modernUrlUtils;

  return new URLSync(urlUtils, options);
}

export default urlSync;
<|endoftext|>


<commit_before>export { decode_Generator };

const base64String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/** Mapping table for base64 (as Uint8) to index. */
let table_base64_Uint8_to_index = new Uint8Array( new ArrayBuffer(256) );
{
  for (let i = 0; i < table_base64_Uint8_to_index.length; ++i)
    table_base64_Uint8_to_index[ i ] = 255; // For identifying any non-base64 bytes.

  for (let i = 0; i < base64String.length; ++i)
    table_base64_Uint8_to_index[ base64String.codePointAt(i) ] = i;

  // Support decoding URL-safe base64 strings, as Node.js does.
  // See: https://en.wikipedia.org/wiki/Base64#URL_applications
  table_base64_Uint8_to_index['-'.charCodeAt(0)] = 62
  table_base64_Uint8_to_index['_'.charCodeAt(0)] = 63
}

/**
 *
 * @param  {ArrayBuffer} sourceBase64ArrayBuffer
 *   The input base64 data as ArrayBuffer. If the last bytes not enough 4 bytes, they will be discarded (will
 * not be decoded). If an input byte is not a legal base64 code (i.e. not A..Z, a..z, 0..0, +, /), the byte
 * will be skipped (as if it does not exist). So the input bytes can be separated by new line.
 *
 * @param  {Uint32} skipLineCount
 *   Skip how many lines in the source before decoding.
 *
 * @param  {ValueMax.Percentage.Aggregate} progressToYield
 *   Return this when every time yield.
 *
 * @param  {ValueMax.Percentage.Concrete}  progressToAdvance
 *   Increase this when every time advanced.
 *
 * @param  {Uint32} suspendByteCount
 *   Everytime so many bytes decoded, yield for releasing CPU time. Default is 1024 bytes.
 *
 * @return {ValueMax.Percentage.Aggregate or Uint8Array}
 *   Return progressYield when ( done = false ). Return decoded data as Uint8Array when ( done = true ).
 */
function* decode_Generator(
  sourceBase64ArrayBuffer, skipLineCount, progressToYield, progressToAdvance, suspendByteCount = 1024) {

  let byteCountAfterYield = 0;

  function progress_accumulateOne_isNeedYield() {
    progressToAdvance.accumulation++;
    byteCountAfterYield++;

    if (byteCountAfterYield >= suspendByteCount) { // Every suspendByteCount, release CPU time.
      byteCountAfterYield = 0;
      return true;
    }
    return false;
  }

  let sourceByteLength = sourceBase64ArrayBuffer.byteLength;

  // Initialize progress.
  progressToAdvance.accumulation = 0;
  progressToAdvance.total = sourceByteLength;

  // Decoding base64 will result a shorten data (75% in size). However, the source may not be pure base64 codes.
  // So it is safer to prepare same size bufer as source.
  let targetByteLength = sourceByteLength;
  let targetArrayBuffer = new ArrayBuffer( targetByteLength );

  let sourceBytes = new Uint8Array( sourceBase64ArrayBuffer );
  let targetBytes = new Uint8Array( targetArrayBuffer );

  let resultByteCount = 0; // Accumulate the real result byte count.
  let sourceIndex = 0;

  // Skip several lines.
  {
    let skippedLineCount = 0;
    let rawByte;

    while (sourceIndex < sourceByteLength) {
      if (skippedLineCount >= skipLineCount)
        break;                  // Already skip enough lines.

      rawByte = sourceBytes[ sourceIndex++ ];

      if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
        yield progressToYield;

      if (13 == rawByte) {      // "\r" (carriage return; CR)
        ++skippedLineCount;     // One line is skipped.

        // If a LF follows a CR, it is considered as CRLF sequence and viewed as the same one line.
        if ((sourceIndex < sourceByteLength) && (10 == sourceBytes[ sourceIndex ])) {
          ++sourceIndex;       // Skip it.
          if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
            yield progressToYield;
        }

      } else {
        if (10 == rawByte)      // "\n" (new line; LF)
          ++skippedLineCount; // One line is skipped.
      }
    }
  }

  {
    const BYTES_PER_DECODE_UNIT = 4; // A decode unit consists of 4 base64 encoded source bytes.
    let encodedBytes = new Uint8Array( new ArrayBuffer( BYTES_PER_DECODE_UNIT ) );

    let j, encodedByte;
    while (sourceIndex < sourceByteLength) {

      // Extract 4 source bytes.
      j = 0;
      while (j < BYTES_PER_DECODE_UNIT) {
        if (sourceIndex >= sourceByteLength)
          break; // Decoding is done. (Ignore last non-4-bytes.)

        let encodedByte = table_base64_Uint8_to_index[ sourceBytes[ sourceIndex++ ] ];

        if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
          yield progressToYield;

        if (255 === encodedByte)
          continue; // Skip any non-base64 bytes.

        encodedBytes[ j++ ] = encodedByte;
      }

      if (j != BYTES_PER_DECODE_UNIT)
        break; // Decoding is done. (Ignore last non-4-bytes.)

      targetBytes[resultByteCount++] =  (encodedBytes[ 0 ]       << 2) | (encodedBytes[ 1 ] >> 4);
      targetBytes[resultByteCount++] = ((encodedBytes[ 1 ] & 15) << 4) | (encodedBytes[ 2 ] >> 2);
      targetBytes[resultByteCount++] = ((encodedBytes[ 2 ] &  3) << 6) | (encodedBytes[ 3 ] & 63);
    }
  }

  // The resultBytes is a sub-range of target buffer.
  let resultBytes = new Uint8Array( targetArrayBuffer, 0, resultByteCount );
  return resultBytes;
}
<commit_msg>Update Base64ArrayBufferToUint8Array.js<commit_after>export { decode_Generator };

const base64String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/** Mapping table for base64 (as Uint8) to index. */
let table_base64_Uint8_to_index = new Uint8Array( new ArrayBuffer(256) );
{
  for (let i = 0; i < table_base64_Uint8_to_index.length; ++i)
    table_base64_Uint8_to_index[ i ] = 255; // For identifying any non-base64 bytes.

  for (let i = 0; i < base64String.length; ++i)
    table_base64_Uint8_to_index[ base64String.codePointAt(i) ] = i;

  // Support decoding URL-safe base64 strings, as Node.js does.
  // See: https://en.wikipedia.org/wiki/Base64#URL_applications
  table_base64_Uint8_to_index['-'.charCodeAt(0)] = 62
  table_base64_Uint8_to_index['_'.charCodeAt(0)] = 63
}

/**
 *
 * @param  {ArrayBuffer} sourceBase64ArrayBuffer
 *   The input base64 data as ArrayBuffer. If the last bytes not enough 4 bytes, they will be discarded (will
 * not be decoded). If an input byte is not a legal base64 code (i.e. not A..Z, a..z, 0..0, +, /), the byte
 * will be skipped (as if it does not exist). So the input bytes can be separated by new line.
 *
 * @param  {Uint32} skipLineCount
 *   Skip how many lines in the source before decoding.
 *
 * @param  {ValueMax.Percentage.Aggregate} progressToYield
 *   Return this when every time yield.
 *
 * @param  {ValueMax.Percentage.Concrete}  progressToAdvance
 *   Increase this when every time advanced.
 *
 * @param  {Uint32} suspendByteCount
 *   Everytime so many bytes decoded, yield for releasing CPU time. Default is 1024 bytes.
 *
 * @return {ValueMax.Percentage.Aggregate or Uint8Array}
 *   Return progressYield when ( done = false ). Return decoded data as Uint8Array when ( done = true ).
 */
function* decode_Generator(
  sourceBase64ArrayBuffer, skipLineCount, progressToYield, progressToAdvance, suspendByteCount = 1024) {

  let byteCountAfterYield = 0;
  let hasEverYielded = false;  // True, if yielded at least once.

  function progress_accumulateOne_isNeedYield() {
    progressToAdvance.accumulation++;
    byteCountAfterYield++;

    if (byteCountAfterYield >= suspendByteCount) { // Every suspendByteCount, release CPU time.
      byteCountAfterYield = 0;
      hasEverYielded = true;
      return true;
    }
    return false;
  }

  let sourceByteLength = sourceBase64ArrayBuffer.byteLength;

  // Initialize progress.
  progressToAdvance.accumulation = 0;
  progressToAdvance.total = sourceByteLength;

  // Decoding base64 will result a shorten data (75% in size). However, the source may not be pure base64 codes.
  // So it is safer to prepare same size bufer as source.
  let targetByteLength = sourceByteLength;
  let targetArrayBuffer = new ArrayBuffer( targetByteLength );

  let sourceBytes = new Uint8Array( sourceBase64ArrayBuffer );
  let targetBytes = new Uint8Array( targetArrayBuffer );

  let resultByteCount = 0; // Accumulate the real result byte count.
  let sourceIndex = 0;

  // Skip several lines.
  {
    let skippedLineCount = 0;
    let rawByte;

    while (sourceIndex < sourceByteLength) {
      if (skippedLineCount >= skipLineCount)
        break;                  // Already skip enough lines.

      rawByte = sourceBytes[ sourceIndex++ ];

      if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
        yield progressToYield;

      if (13 == rawByte) {      // "\r" (carriage return; CR)
        ++skippedLineCount;     // One line is skipped.

        // If a LF follows a CR, it is considered as CRLF sequence and viewed as the same one line.
        if ((sourceIndex < sourceByteLength) && (10 == sourceBytes[ sourceIndex ])) {
          ++sourceIndex;       // Skip it.
          if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
            yield progressToYield;
        }

      } else {
        if (10 == rawByte)      // "\n" (new line; LF)
          ++skippedLineCount; // One line is skipped.
      }
    }
  }

  {
    const BYTES_PER_DECODE_UNIT = 4; // A decode unit consists of 4 base64 encoded source bytes.
    let encodedBytes = new Uint8Array( new ArrayBuffer( BYTES_PER_DECODE_UNIT ) );

    let j, encodedByte;
    while (sourceIndex < sourceByteLength) {

      // Extract 4 source bytes.
      j = 0;
      while (j < BYTES_PER_DECODE_UNIT) {
        if (sourceIndex >= sourceByteLength)
          break; // Decoding is done. (Ignore last non-4-bytes.)

        let encodedByte = table_base64_Uint8_to_index[ sourceBytes[ sourceIndex++ ] ];

        if (progress_accumulateOne_isNeedYield()) // Every suspendByteCount, release CPU time.
          yield progressToYield;

        if (255 === encodedByte)
          continue; // Skip any non-base64 bytes.

        encodedBytes[ j++ ] = encodedByte;
      }

      if (j != BYTES_PER_DECODE_UNIT)
        break; // Decoding is done. (Ignore last non-4-bytes.)

      targetBytes[resultByteCount++] =  (encodedBytes[ 0 ]       << 2) | (encodedBytes[ 1 ] >> 4);
      targetBytes[resultByteCount++] = ((encodedBytes[ 1 ] & 15) << 4) | (encodedBytes[ 2 ] >> 2);
      targetBytes[resultByteCount++] = ((encodedBytes[ 2 ] &  3) << 6) | (encodedBytes[ 3 ] & 63);
    }
  }

  // The resultBytes is a sub-range of target buffer.
  let resultBytes = new Uint8Array( targetArrayBuffer, 0, resultByteCount );

  if ((byteCountAfterYield > 0) || (false == hasEverYielded)) {
    yield progressToYield; // Report the progress has been 100%
  else
    ; // The last progress report is (just luckily) 100%. No need to report the progress again.

  return resultBytes;
}
<|endoftext|>


<commit_before>
function ScriptLoader_createPromise( url, isModule, htmlElementId ) {
  if ( htmlElementId ) {
    let scriptElement = document.getElementById( htmlElementId );
    if ( scriptElement )
      return Promise.resolve();
  }

  console.log( "Loading \"" + url + "\"" );
  return new Promise( ( resolve, reject ) => {
    let attributes = { src: url, onload: e => resolve(e), onerror: e => reject(e) };

    if ( isModule )
      attributes.type = "module";

    if ( htmlElementId )
      attributes.id = htmlElementId;

    document.head.appendChild( Object.assign( document.createElement("script"), attributes ) );
  });
}

/**
 * A name and a binded function.
 */
class NameFunc {
  constructor( name, f ) {
    this.name = name;
    this.f = f;
  }
}

async function testByBackend( backendName ) {
  console.log( `backend = ${backendName}` );
  await tf.setBackend( backendName );

  let c_more = 5; // Define the more channel count.
  let testTimes = 10; //10 * 1000; // Run how many times for a function.

  const x_c4 = tf.randomNormal([1000, 1000, 4]);
  const x_cN = tf.randomNormal([1000, 1000, c_more]);

  const c_broadcast_none = tf.randomNormal([1000, 1000, 4]);
  const c_broadcast_channel = tf.randomNormal([1000, 1000, 1]);
  const c_broadcast_height_width_channel = tf.randomNormal([1, 1, 4]);

  let pointwiseFilter_c4m1 = tf.randomNormal( [ 1, 1, 4, 4 ] );
  let pointwiseFilter_c4m2 = tf.randomNormal( [ 1, 1, 4, 8 ] );

  let pointwiseFilter_cNm1 = tf.randomNormal( [ 1, 1, c_more, c_more ] );
  let pointwiseFilter_cNm2 = tf.randomNormal( [ 1, 1, c_more, ( c_more * 2 ) ] );

  let pointwiseFilter_c4cN = tf.randomNormal( [ 1, 1, 4, c_more ] );
  let pointwiseFilter_cNc4 = tf.randomNormal( [ 1, 1, c_more, 4 ] );

  let depthwiseFilter_c4m1_3x3 = tf.randomNormal( [ 3, 3, 4, 4 ] );
  let depthwiseFilter_c4m2_3x3 = tf.randomNormal( [ 3, 3, 4, 8 ] );

  let depthwiseFilter_cNm1_3x3 = tf.randomNormal( [ 3, 3, c_more, c_more ] );
  let depthwiseFilter_cNm2_3x3 = tf.randomNormal( [ 3, 3, c_more, ( c_more * 2 ) ] );

  let depthwiseFilter_c4m1_1x1 = tf.randomNormal( [ 1, 1, 4, 4 ] );
  let depthwiseFilter_c4m2_1x1 = tf.randomNormal( [ 1, 1, 4, 8 ] );

  let depthwiseFilter_cNm1_1x1 = tf.randomNormal( [ 1, 1, c_more, c_more ] );
  let depthwiseFilter_cNm2_1x1 = tf.randomNormal( [ 1, 1, c_more, ( c_more * 2 ) ] );

  function timesFunc( f ) {
    let y;
    for ( let i = 0; i < testTimes; ++i ) {
      y = f();
      y.dispose();
    }
  }

  /** Try pointwise-SIGMOID to achieve implicit bias. */
  function pointwise_c4cN_SIGMOID_pointwise_cNc4_SIGMOID() {
    let t0 = tf.conv2d( x_c4, pointwiseFilter_c4cN, 1, "valid" );

    let t1 = tf.sigmoid( t0 );
    t0.dispose();

    t0 = tf.conv2d( t1, pointwiseFilter_cNc4, 1, "valid" );
    t1.dispose();

    t1 = tf.sigmoid( t0 );
    t0.dispose();

    return t1;
  }

  /** Try explicit bias. */
  function add_pointwise_c4m1_SIGMOID() {
    let t0 = tf.add( x_c4, c_broadcast_height_width_channel );

    let t1 = tf.conv2d( x_c4, pointwiseFilter_c4m1, 1, "valid" );
    t0.dispose();

    t0 = tf.sigmoid( t1 );
    t1.dispose();

    return t0;
  }

  let testFuncArray = [
//     new NameFunc( "sin", tf.sin.bind( null, x_c4 ) ),
//    new NameFunc( "asin", tf.asin.bind( null, x_c4 ) ),
//     new NameFunc( "cos", tf.cos.bind( null, x_c4 ) ),
//    new NameFunc( "acos", tf.acos.bind( null, x_c4 ) ),
//    new NameFunc( "tan", tf.tan.bind( null, x_c4 ) ),
    new NameFunc( "tanh", tf.tanh.bind( null, x_c4 ) ),
    new NameFunc( "sigmoid", tf.sigmoid.bind( null, x_c4 ) ),
    new NameFunc( `sigmoid_c${c_more}`, tf.sigmoid.bind( null, x_cN ) ),
    new NameFunc( "relu", tf.relu.bind( null, x_c4 ) ),
    new NameFunc( "relu6", tf.relu6.bind( null, x_c4 ) ),
//     new NameFunc( "log", tf.log.bind( null, x_c4 ) ),
//     new NameFunc( "log1p", tf.log1p.bind( null, x_c4 ) ),
//     new NameFunc( "exp", tf.exp.bind( null, x_c4 ) ),
//     new NameFunc( "expm1", tf.expm1.bind( null, x_c4 ) ),
//     new NameFunc( "softplus", tf.softplus.bind( null, x_c4 ) ),
//     new NameFunc( "reciprocal", tf.reciprocal.bind( null, x_c4 ) ),

    new NameFunc( "add_broadcast_none", tf.add.bind( null, x_c4, c_broadcast_none ) ),
    new NameFunc( "add_broadcast_channel", tf.add.bind( null, x_c4, c_broadcast_channel ) ),
    new NameFunc( "add_broadcast_height_width_channel", tf.add.bind( null, x_c4, c_broadcast_height_width_channel ) ),

    new NameFunc( `pointwise_c4c${c_more}_SIGMOID_pointwise_c${c_more}c4_SIGMOID`, pointwise_c4cN_SIGMOID_pointwise_cNc4_SIGMOID ),
    new NameFunc( `add_pointwise_c4m1_SIGMOID`, add_pointwise_c4m1_SIGMOID ),

    new NameFunc( "pointwise_1x1x4_cm1_strides1_padValid", tf.conv2d.bind( null, x_c4, pointwiseFilter_c4m1, 1, "valid" ) ),
    new NameFunc( `pointwise_1x1x${c_more}_cm1_strides1_padValid`, tf.conv2d.bind( null, x_cN, pointwiseFilter_cNm1, 1, "valid" ) ),

//     new NameFunc( "pointwise_1x1x4_cm2_strides1_padValid", tf.conv2d.bind( null, x_c4, pointwiseFilter_c4m2, 1, "valid" ) ),
//     new NameFunc( `pointwise_1x1x${c_more}_cm2_strides1_padValid`, tf.conv2d.bind( null, x_cN, pointwiseFilter_cNm2, 1, "valid" ) ),

    new NameFunc( "depthwise_3x3x4_cm1_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_3x3, 1, "same" ) ),
    new NameFunc( `depthwise_3x3x${c_more}_cm1_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_3x3, 1, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm2_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_3x3, 1, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm2_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_3x3, 1, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm1_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_3x3, 2, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm1_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_3x3, 2, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm2_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_3x3, 2, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm2_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_3x3, 2, "same" ) ),

    new NameFunc( "depthwise_1x1x4_cm1_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_1x1, 1, "same" ) ),
    new NameFunc( `depthwise_1x1x${c_more}_cm1_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_1x1, 1, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm2_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_1x1, 1, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm2_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_1x1, 1, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm1_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_1x1, 2, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm1_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_1x1, 2, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm2_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_1x1, 2, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm2_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_1x1, 2, "same" ) ),
  ];

  for ( let i = 0; i < testFuncArray.length; ++i ) {
    let testNameFunc = testFuncArray[ i ];
    let testFuncTimes = timesFunc.bind( null, testNameFunc.f );

    const time = await tf.time( testFuncTimes );

    let kernelMs = time.kernelMs / testTimes;
    let wallMs = time.wallMs / testTimes;

    console.log( `${testNameFunc.name}, `
      + `kernelMs: ${kernelMs.toFixed( 2 )}, `
      + `wallTimeMs: ${wallMs.toFixed( 2 )}`);
  }
}

let wasmUrl = "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm/dist/tf-backend-wasm.js";
await ScriptLoader_createPromise( wasmUrl, false, "wasmBackend" );

//await testByBackend( "wasm" );
//await testByBackend( "cpu" );
await testByBackend( "webgl" );
<commit_msg>Update console_test_func_perf.js<commit_after>
function ScriptLoader_createPromise( url, isModule, htmlElementId ) {
  if ( htmlElementId ) {
    let scriptElement = document.getElementById( htmlElementId );
    if ( scriptElement )
      return Promise.resolve();
  }

  console.log( "Loading \"" + url + "\"" );
  return new Promise( ( resolve, reject ) => {
    let attributes = { src: url, onload: e => resolve(e), onerror: e => reject(e) };

    if ( isModule )
      attributes.type = "module";

    if ( htmlElementId )
      attributes.id = htmlElementId;

    document.head.appendChild( Object.assign( document.createElement("script"), attributes ) );
  });
}

/**
 * A name and a binded function.
 */
class NameFunc {
  constructor( name, f ) {
    this.name = name;
    this.f = f;
  }
}

async function testByBackend( backendName ) {
  console.log( `backend = ${backendName}` );
  await tf.setBackend( backendName );

  let c_more = 5; // Define the more channel count.
  let testTimes = 10; //10 * 1000; // Run how many times for a function.

  const x_c4 = tf.randomNormal([1000, 1000, 4]);
  const x_cN = tf.randomNormal([1000, 1000, c_more]);

  const c_broadcast_none = tf.randomNormal([1000, 1000, 4]);
  const c_broadcast_channel = tf.randomNormal([1000, 1000, 1]);
  const c_broadcast_height_width_channel = tf.randomNormal([1, 1, 4]);

  let pointwiseFilter_c4m1 = tf.randomNormal( [ 1, 1, 4, 4 ] );
  let pointwiseFilter_c4m2 = tf.randomNormal( [ 1, 1, 4, 8 ] );

  let pointwiseFilter_cNm1 = tf.randomNormal( [ 1, 1, c_more, c_more ] );
  let pointwiseFilter_cNm2 = tf.randomNormal( [ 1, 1, c_more, ( c_more * 2 ) ] );

  let pointwiseFilter_c4cN = tf.randomNormal( [ 1, 1, 4, c_more ] );
  let pointwiseFilter_cNc4 = tf.randomNormal( [ 1, 1, c_more, 4 ] );

  let depthwiseFilter_c4m1_3x3 = tf.randomNormal( [ 3, 3, 4, 4 ] );
  let depthwiseFilter_c4m2_3x3 = tf.randomNormal( [ 3, 3, 4, 8 ] );

  let depthwiseFilter_cNm1_3x3 = tf.randomNormal( [ 3, 3, c_more, c_more ] );
  let depthwiseFilter_cNm2_3x3 = tf.randomNormal( [ 3, 3, c_more, ( c_more * 2 ) ] );

  let depthwiseFilter_c4m1_1x1 = tf.randomNormal( [ 1, 1, 4, 4 ] );
  let depthwiseFilter_c4m2_1x1 = tf.randomNormal( [ 1, 1, 4, 8 ] );

  let depthwiseFilter_cNm1_1x1 = tf.randomNormal( [ 1, 1, c_more, c_more ] );
  let depthwiseFilter_cNm2_1x1 = tf.randomNormal( [ 1, 1, c_more, ( c_more * 2 ) ] );

  function timesFunc( f ) {
    let y;
    for ( let i = 0; i < testTimes; ++i ) {
      y = f();
      y.dispose();
    }
  }

  /** Try pointwise-SIGMOID to achieve implicit bias. */
  function pointwise_c4cN_SIGMOID_pointwise_cNc4_SIGMOID() {
    let t0 = tf.conv2d( x_c4, pointwiseFilter_c4cN, 1, "valid" );

    let t1 = tf.sigmoid( t0 );
    t0.dispose();

    t0 = tf.conv2d( t1, pointwiseFilter_cNc4, 1, "valid" );
    t1.dispose();

    t1 = tf.sigmoid( t0 );
    t0.dispose();

    return t1;
  }

  /** Try explicit bias. */
  function pointwise_c4m1_add_SIGMOID() {
    let t0 = tf.conv2d( x_c4, pointwiseFilter_c4m1, 1, "valid" );

    let t1 = tf.add( t0, c_broadcast_height_width_channel );
    t0.dispose();

    t0 = tf.sigmoid( t1 );
    t1.dispose();

    return t0;
  }

  let testFuncArray = [
//     new NameFunc( "sin", tf.sin.bind( null, x_c4 ) ),
//    new NameFunc( "asin", tf.asin.bind( null, x_c4 ) ),
//     new NameFunc( "cos", tf.cos.bind( null, x_c4 ) ),
//    new NameFunc( "acos", tf.acos.bind( null, x_c4 ) ),
//    new NameFunc( "tan", tf.tan.bind( null, x_c4 ) ),
    new NameFunc( "tanh", tf.tanh.bind( null, x_c4 ) ),
    new NameFunc( "sigmoid", tf.sigmoid.bind( null, x_c4 ) ),
    new NameFunc( `sigmoid_c${c_more}`, tf.sigmoid.bind( null, x_cN ) ),
    new NameFunc( "relu", tf.relu.bind( null, x_c4 ) ),
    new NameFunc( "relu6", tf.relu6.bind( null, x_c4 ) ),
//     new NameFunc( "log", tf.log.bind( null, x_c4 ) ),
//     new NameFunc( "log1p", tf.log1p.bind( null, x_c4 ) ),
//     new NameFunc( "exp", tf.exp.bind( null, x_c4 ) ),
//     new NameFunc( "expm1", tf.expm1.bind( null, x_c4 ) ),
//     new NameFunc( "softplus", tf.softplus.bind( null, x_c4 ) ),
//     new NameFunc( "reciprocal", tf.reciprocal.bind( null, x_c4 ) ),

    new NameFunc( "add_broadcast_none", tf.add.bind( null, x_c4, c_broadcast_none ) ),
    new NameFunc( "add_broadcast_channel", tf.add.bind( null, x_c4, c_broadcast_channel ) ),
    new NameFunc( "add_broadcast_height_width_channel", tf.add.bind( null, x_c4, c_broadcast_height_width_channel ) ),

    new NameFunc( `pointwise_c4c${c_more}_SIGMOID_pointwise_c${c_more}c4_SIGMOID`, pointwise_c4cN_SIGMOID_pointwise_cNc4_SIGMOID ),
    new NameFunc( `add_pointwise_c4m1_SIGMOID`, add_pointwise_c4m1_SIGMOID ),

    new NameFunc( "pointwise_1x1x4_cm1_strides1_padValid", tf.conv2d.bind( null, x_c4, pointwiseFilter_c4m1, 1, "valid" ) ),
    new NameFunc( `pointwise_1x1x${c_more}_cm1_strides1_padValid`, tf.conv2d.bind( null, x_cN, pointwiseFilter_cNm1, 1, "valid" ) ),

//     new NameFunc( "pointwise_1x1x4_cm2_strides1_padValid", tf.conv2d.bind( null, x_c4, pointwiseFilter_c4m2, 1, "valid" ) ),
//     new NameFunc( `pointwise_1x1x${c_more}_cm2_strides1_padValid`, tf.conv2d.bind( null, x_cN, pointwiseFilter_cNm2, 1, "valid" ) ),

    new NameFunc( "depthwise_3x3x4_cm1_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_3x3, 1, "same" ) ),
    new NameFunc( `depthwise_3x3x${c_more}_cm1_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_3x3, 1, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm2_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_3x3, 1, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm2_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_3x3, 1, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm1_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_3x3, 2, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm1_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_3x3, 2, "same" ) ),

//     new NameFunc( "depthwise_3x3x4_cm2_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_3x3, 2, "same" ) ),
//     new NameFunc( `depthwise_3x3x${c_more}_cm2_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_3x3, 2, "same" ) ),

    new NameFunc( "depthwise_1x1x4_cm1_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_1x1, 1, "same" ) ),
    new NameFunc( `depthwise_1x1x${c_more}_cm1_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_1x1, 1, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm2_strides1_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_1x1, 1, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm2_strides1_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_1x1, 1, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm1_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m1_1x1, 2, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm1_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm1_1x1, 2, "same" ) ),

//     new NameFunc( "depthwise_1x1x4_cm2_strides2_padSame", tf.depthwiseConv2d.bind( null, x_c4, depthwiseFilter_c4m2_1x1, 2, "same" ) ),
//     new NameFunc( `depthwise_1x1x${c_more}_cm2_strides2_padSame`, tf.depthwiseConv2d.bind( null, x_cN, depthwiseFilter_cNm2_1x1, 2, "same" ) ),
  ];

  for ( let i = 0; i < testFuncArray.length; ++i ) {
    let testNameFunc = testFuncArray[ i ];
    let testFuncTimes = timesFunc.bind( null, testNameFunc.f );

    const time = await tf.time( testFuncTimes );

    let kernelMs = time.kernelMs / testTimes;
    let wallMs = time.wallMs / testTimes;

    console.log( `${testNameFunc.name}, `
      + `kernelMs: ${kernelMs.toFixed( 2 )}, `
      + `wallTimeMs: ${wallMs.toFixed( 2 )}`);
  }
}

let wasmUrl = "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm/dist/tf-backend-wasm.js";
await ScriptLoader_createPromise( wasmUrl, false, "wasmBackend" );

//await testByBackend( "wasm" );
//await testByBackend( "cpu" );
await testByBackend( "webgl" );
<|endoftext|>


<commit_before>// The MIT License (MIT)
//
// Copyright (c) 2019-2020 Camptocamp SA
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import angular from 'angular';

import {unlistenByKeys as ngeoEventsUnlistenByKeys} from 'ngeo/events.js';
import ngeoQueryMode from 'ngeo/query/Mode.js';
import ngeoQueryModeSelector from 'ngeo/query/ModeSelector.js';
import ngeoQueryMapQuerent from 'ngeo/query/MapQuerent.js';

import {listen as olEventsListen} from 'ol/events.js';
import {always as olEventsConditionAlways} from 'ol/events/condition.js';
import olInteractionDraw, {createBox as olInteractionDrawCreateBox} from 'ol/interaction/Draw.js';
import {Vector as olLayerVector} from 'ol/layer.js';
import MapBrowserEvent from 'ol/MapBrowserEvent.js';
import {Vector as olSourceVector} from 'ol/source.js';

/**
 * @type {angular.IModule}
 * @hidden
 */
const module = angular.module('ngeoQuery', [ngeoQueryModeSelector.name, ngeoQueryMapQuerent.name]);

/**
 * @private
 * @hidden
 */
class QueryController {
  /**
   * @param {import("ngeo/query/MapQuerent.js").MapQuerent}
   *     ngeoMapQuerent The ngeo map querent service.
   * @param {import("ngeo/query/ModeSelector.js").QueryModeSelector}
   *     ngeoQueryModeSelector The ngeo query modeSelector service.
   * @param {angular.auto.IInjectorService} $injector Main injector.
   * @param {angular.IScope} $scope Scope.
   * @private
   * @ngInject
   * @ngdoc controller
   * @ngname NgeoQueryController
   */
  constructor(ngeoMapQuerent, ngeoQueryModeSelector, $injector, $scope) {
    // === Binding properties ===

    /**
     * @type {boolean}
     */
    this.active;

    /**
     * @type {boolean}
     */
    this.autoclear;

    /**
     * @type {!import("ol/Map.js").default}
     */
    this.map;

    // === Injected properties ===

    /**
     * @type {import("ngeo/query/MapQuerent.js").MapQuerent}
     * @private
     */
    this.ngeoMapQuerent_ = ngeoMapQuerent;

    /**
     * @type {import("ngeo/query/ModeSelector.js").QueryModeSelector}
     * @private
     */
    this.ngeoQueryModeSelector_ = ngeoQueryModeSelector;

    const ngeoQueryOptions = /** @type {import('ngeo/query/MapQuerent.js').QueryOptions} */ ($injector.has(
      'ngeoQueryOptions'
    )
      ? $injector.get('ngeoQueryOptions')
      : {});

    /**
     * @type {import('ngeo/query/MapQuerent.js').QueryOptions}
     * @private
     */
    this.ngeoQueryOptions_ = ngeoQueryOptions;

    /**
     * @type {angular.IScope}
     * @private
     */
    this.scope_ = $scope;

    // === Inner properties ===

    /**
     * @type {olSourceVector<import("ol/geom/Polygon.js").default>}
     * @private
     */
    this.vectorSource_ = new olSourceVector({
      wrapX: false,
    });

    /**
     * @type {olLayerVector}
     * @private
     */
    this.vectorLayer_ = new olLayerVector({
      source: this.vectorSource_,
    });

    /**
     * @type {olInteractionDraw}
     * @private
     */
    this.drawBoxInteraction_ = new olInteractionDraw({
      condition: olEventsConditionAlways,
      geometryFunction: olInteractionDrawCreateBox(),
      source: this.vectorSource_,
      type: 'Circle',
    });

    /**
     * @type {olInteractionDraw}
     * @private
     */
    this.drawPolygonInteraction_ = new olInteractionDraw({
      condition: olEventsConditionAlways,
      source: this.vectorSource_,
      type: 'Polygon',
    });

    /**
     * The event keys of the currently active "mode".
     *
     * @type {Array<import('ol/events.js').EventsKey>}
     * @private
     */
    this.listenerKeys_ = [];

    /**
     * @type {?string}
     * @private
     */
    this.mode_ = null;

    // === Event listeners that uses angular $scope

    $scope.$watch(() => this.active, this.handleActiveChange_.bind(this));

    $scope.$watch(() => {
      let value = null;
      if (this.active) {
        value = this.ngeoQueryModeSelector_.mode;
      }
      return value;
    }, this.enableMode_.bind(this));
  }

  /**
   * Called on initialization of the controller.
   */
  $onInit() {
    // Set default value of optional binding properties
    this.autoclear = !!this.autoclear;
  }

  /**
   * Called on destruction of the controller.
   */
  $onDestroy() {
    this.enableMode_(null);
  }

  // === Mode enabling/disabling ===

  /**
   * Disable the current mode, then enable a new mode, i.e. add any
   * interaction of that mode to the map.
   *
   * @param {?string} mode Mode to enable
   * @private
   */
  enableMode_(mode) {
    this.disableMode_();

    // No need to do anything if there's no mode to enable
    if (!mode) {
      return;
    }

    // For debug purpose
    // console.log(`mode enable: ${mode}`);

    switch (mode) {
      case ngeoQueryMode.CLICK:
        this.listenerKeys_.push(olEventsListen(this.map, 'singleclick', this.handleMapClick_, this));
        if (this.ngeoQueryOptions_.cursorHover) {
          this.listenerKeys_.push(olEventsListen(this.map, 'pointermove', this.handleMapPointerMove_, this));
        }
        break;

      case ngeoQueryMode.DRAW_BOX:
        this.map.addLayer(this.vectorLayer_);
        this.map.addInteraction(this.drawBoxInteraction_);
        this.listenerKeys_.push(
          olEventsListen(this.drawBoxInteraction_, 'drawend', this.handleDrawBoxInteractionDrawEnd_, this)
        );
        break;

      case ngeoQueryMode.DRAW_POLYGON:
        this.map.addLayer(this.vectorLayer_);
        this.map.addInteraction(this.drawPolygonInteraction_);
        this.listenerKeys_.push(
          olEventsListen(
            this.drawPolygonInteraction_,
            'drawend',
            this.handleDrawPolygonInteractionDrawEnd_,
            this
          )
        );
        break;

      default:
        break;
    }

    this.mode_ = mode;
  }

  /**
   * Disable the current mode, i.e. remove any interaction of that
   * mode from the map.
   * @private
   */
  disableMode_() {
    // No need to do anything if there's no mode currently active
    if (!this.mode_) {
      return;
    }

    // For debug purpose
    // console.log(`mode disable: ${this.mode_}`);

    switch (this.mode_) {
      case ngeoQueryMode.CLICK:
        // Reset cursor, if required
        if (this.ngeoQueryOptions_.cursorHover) {
          this.map.getTargetElement().style.cursor = '';
        }
        break;

      case ngeoQueryMode.DRAW_BOX:
        this.vectorSource_.clear();
        this.map.removeLayer(this.vectorLayer_);
        this.map.removeInteraction(this.drawBoxInteraction_);
        // FIXME: remove next lines after updating OpenLayers > 6.1.1
        this.drawBoxInteraction_.sketchPoint_ = null;
        this.drawBoxInteraction_.overlay_.getSource().clear(true);
        break;

      case ngeoQueryMode.DRAW_POLYGON:
        this.vectorSource_.clear();
        this.map.removeLayer(this.vectorLayer_);
        this.map.removeInteraction(this.drawPolygonInteraction_);
        // FIXME: remove next lines after updating OpenLayers > 6.1.1
        this.drawPolygonInteraction_.sketchPoint_ = null;
        this.drawPolygonInteraction_.overlay_.getSource().clear(true);
        break;

      default:
        break;
    }

    ngeoEventsUnlistenByKeys(this.listenerKeys_);

    this.mode_ = null;
  }

  // === Utilities ===

  /**
   * The maximum number of features a query should return. Obtained
   * from the options.
   * @return {number|undefined}
   * @private
   */
  getLimitOption_() {
    return this.ngeoQueryOptions_ && this.ngeoQueryOptions_.limit ? this.ngeoQueryOptions_.limit : undefined;
  }

  // === Handlers ===

  /**
   * Called when active property changes
   * @param {boolean} active Whether this component is active or not.
   * @private
   */
  handleActiveChange_(active) {
    if (!active) {
      if (this.autoclear) {
        this.ngeoMapQuerent_.clear();
      }
    }
  }

  /**
   * Called when a box is drawn on the map. Use it to issue a query.
   * @param {Event|import("ol/events/Event.js").default} evt The draw
   *     interaction drawend event being fired.
   * @private
   */
  handleDrawBoxInteractionDrawEnd_(evt) {
    // @ts-ignore: evt should be of type {import('ol/interaction/Draw.js').DrawEvent but it is private
    const feature = evt.feature;

    const action = this.ngeoQueryModeSelector_.action;
    const extent = feature.getGeometry().getExtent();
    const limit = this.getLimitOption_();
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        extent,
        limit,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.vectorSource_.clear();
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when a polygon is drawn on the map. Use it to issue a query.
   * @param {Event|import("ol/events/Event.js").default} evt The draw
   *     interaction drawend event being fired.
   * @private
   */
  handleDrawPolygonInteractionDrawEnd_(evt) {
    // @ts-ignore: evt should be of type {import('ol/interaction/Draw.js').DrawEvent but it is private
    const feature = evt.feature;

    const action = this.ngeoQueryModeSelector_.action;
    const geometry = feature.getGeometry();
    const limit = this.getLimitOption_();
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        geometry,
        limit,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.vectorSource_.clear();
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when the map is clicked while this component is active and
   * the mode is "click". Issue a request to the query service using
   * the coordinate that was clicked.
   * @param {Event|import("ol/events/Event.js").default} evt The map
   *     browser event being fired.
   * @private
   */
  handleMapClick_(evt) {
    if (!(evt instanceof MapBrowserEvent)) {
      return;
    }

    const action = this.ngeoQueryModeSelector_.action;
    const coordinate = evt.coordinate;
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        coordinate,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when the pointer is moved over the map while this
   * component is active and the mode is "click".  Change the
   * mouse pointer when hovering a non-transparent pixel on the
   * map.
   * @param {Event|import("ol/events/Event.js").default} evt
   *     The map browser event being fired.
   */
  handleMapPointerMove_(evt) {
    // No need to do anything if user is dragging the map
    if (!(evt instanceof MapBrowserEvent) || evt.dragging) {
      return;
    }

    const pixel = this.map.getEventPixel(evt.originalEvent);

    /**
     * @param {import('ol/layer/Base').default} layer
     */
    const queryable = function (layer) {
      const visible = layer.get('visible');
      const sourceids = layer.get('querySourceIds');
      return visible && !!sourceids;
    };
    const hit = this.map.forEachLayerAtPixel(pixel, () => true, {
      layerFilter: queryable,
    });
    this.map.getTargetElement().style.cursor = hit ? 'pointer' : '';
  }
}

module.component('ngeoQuery', {
  bindings: {
    'active': '=',
    'autoclear': '=?',
    'map': '<',
  },
  controller: QueryController,
});

export default module;
<commit_msg>Remove stuff for ol pre 6.1.1<commit_after>// The MIT License (MIT)
//
// Copyright (c) 2019-2020 Camptocamp SA
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import angular from 'angular';

import {unlistenByKeys as ngeoEventsUnlistenByKeys} from 'ngeo/events.js';
import ngeoQueryMode from 'ngeo/query/Mode.js';
import ngeoQueryModeSelector from 'ngeo/query/ModeSelector.js';
import ngeoQueryMapQuerent from 'ngeo/query/MapQuerent.js';

import {listen as olEventsListen} from 'ol/events.js';
import {always as olEventsConditionAlways} from 'ol/events/condition.js';
import olInteractionDraw, {createBox as olInteractionDrawCreateBox} from 'ol/interaction/Draw.js';
import {Vector as olLayerVector} from 'ol/layer.js';
import MapBrowserEvent from 'ol/MapBrowserEvent.js';
import {Vector as olSourceVector} from 'ol/source.js';

/**
 * @type {angular.IModule}
 * @hidden
 */
const module = angular.module('ngeoQuery', [ngeoQueryModeSelector.name, ngeoQueryMapQuerent.name]);

/**
 * @private
 * @hidden
 */
class QueryController {
  /**
   * @param {import("ngeo/query/MapQuerent.js").MapQuerent}
   *     ngeoMapQuerent The ngeo map querent service.
   * @param {import("ngeo/query/ModeSelector.js").QueryModeSelector}
   *     ngeoQueryModeSelector The ngeo query modeSelector service.
   * @param {angular.auto.IInjectorService} $injector Main injector.
   * @param {angular.IScope} $scope Scope.
   * @private
   * @ngInject
   * @ngdoc controller
   * @ngname NgeoQueryController
   */
  constructor(ngeoMapQuerent, ngeoQueryModeSelector, $injector, $scope) {
    // === Binding properties ===

    /**
     * @type {boolean}
     */
    this.active;

    /**
     * @type {boolean}
     */
    this.autoclear;

    /**
     * @type {!import("ol/Map.js").default}
     */
    this.map;

    // === Injected properties ===

    /**
     * @type {import("ngeo/query/MapQuerent.js").MapQuerent}
     * @private
     */
    this.ngeoMapQuerent_ = ngeoMapQuerent;

    /**
     * @type {import("ngeo/query/ModeSelector.js").QueryModeSelector}
     * @private
     */
    this.ngeoQueryModeSelector_ = ngeoQueryModeSelector;

    const ngeoQueryOptions = /** @type {import('ngeo/query/MapQuerent.js').QueryOptions} */ ($injector.has(
      'ngeoQueryOptions'
    )
      ? $injector.get('ngeoQueryOptions')
      : {});

    /**
     * @type {import('ngeo/query/MapQuerent.js').QueryOptions}
     * @private
     */
    this.ngeoQueryOptions_ = ngeoQueryOptions;

    /**
     * @type {angular.IScope}
     * @private
     */
    this.scope_ = $scope;

    // === Inner properties ===

    /**
     * @type {olSourceVector<import("ol/geom/Polygon.js").default>}
     * @private
     */
    this.vectorSource_ = new olSourceVector({
      wrapX: false,
    });

    /**
     * @type {olLayerVector}
     * @private
     */
    this.vectorLayer_ = new olLayerVector({
      source: this.vectorSource_,
    });

    /**
     * @type {olInteractionDraw}
     * @private
     */
    this.drawBoxInteraction_ = new olInteractionDraw({
      condition: olEventsConditionAlways,
      geometryFunction: olInteractionDrawCreateBox(),
      source: this.vectorSource_,
      type: 'Circle',
    });

    /**
     * @type {olInteractionDraw}
     * @private
     */
    this.drawPolygonInteraction_ = new olInteractionDraw({
      condition: olEventsConditionAlways,
      source: this.vectorSource_,
      type: 'Polygon',
    });

    /**
     * The event keys of the currently active "mode".
     *
     * @type {Array<import('ol/events.js').EventsKey>}
     * @private
     */
    this.listenerKeys_ = [];

    /**
     * @type {?string}
     * @private
     */
    this.mode_ = null;

    // === Event listeners that uses angular $scope

    $scope.$watch(() => this.active, this.handleActiveChange_.bind(this));

    $scope.$watch(() => {
      let value = null;
      if (this.active) {
        value = this.ngeoQueryModeSelector_.mode;
      }
      return value;
    }, this.enableMode_.bind(this));
  }

  /**
   * Called on initialization of the controller.
   */
  $onInit() {
    // Set default value of optional binding properties
    this.autoclear = !!this.autoclear;
  }

  /**
   * Called on destruction of the controller.
   */
  $onDestroy() {
    this.enableMode_(null);
  }

  // === Mode enabling/disabling ===

  /**
   * Disable the current mode, then enable a new mode, i.e. add any
   * interaction of that mode to the map.
   *
   * @param {?string} mode Mode to enable
   * @private
   */
  enableMode_(mode) {
    this.disableMode_();

    // No need to do anything if there's no mode to enable
    if (!mode) {
      return;
    }

    // For debug purpose
    // console.log(`mode enable: ${mode}`);

    switch (mode) {
      case ngeoQueryMode.CLICK:
        this.listenerKeys_.push(olEventsListen(this.map, 'singleclick', this.handleMapClick_, this));
        if (this.ngeoQueryOptions_.cursorHover) {
          this.listenerKeys_.push(olEventsListen(this.map, 'pointermove', this.handleMapPointerMove_, this));
        }
        break;

      case ngeoQueryMode.DRAW_BOX:
        this.map.addLayer(this.vectorLayer_);
        this.map.addInteraction(this.drawBoxInteraction_);
        this.listenerKeys_.push(
          olEventsListen(this.drawBoxInteraction_, 'drawend', this.handleDrawBoxInteractionDrawEnd_, this)
        );
        break;

      case ngeoQueryMode.DRAW_POLYGON:
        this.map.addLayer(this.vectorLayer_);
        this.map.addInteraction(this.drawPolygonInteraction_);
        this.listenerKeys_.push(
          olEventsListen(
            this.drawPolygonInteraction_,
            'drawend',
            this.handleDrawPolygonInteractionDrawEnd_,
            this
          )
        );
        break;

      default:
        break;
    }

    this.mode_ = mode;
  }

  /**
   * Disable the current mode, i.e. remove any interaction of that
   * mode from the map.
   * @private
   */
  disableMode_() {
    // No need to do anything if there's no mode currently active
    if (!this.mode_) {
      return;
    }

    // For debug purpose
    // console.log(`mode disable: ${this.mode_}`);

    switch (this.mode_) {
      case ngeoQueryMode.CLICK:
        // Reset cursor, if required
        if (this.ngeoQueryOptions_.cursorHover) {
          this.map.getTargetElement().style.cursor = '';
        }
        break;

      case ngeoQueryMode.DRAW_BOX:
        this.vectorSource_.clear();
        this.map.removeLayer(this.vectorLayer_);
        this.map.removeInteraction(this.drawBoxInteraction_);
        break;

      case ngeoQueryMode.DRAW_POLYGON:
        this.vectorSource_.clear();
        this.map.removeLayer(this.vectorLayer_);
        this.map.removeInteraction(this.drawPolygonInteraction_);
        break;

      default:
        break;
    }

    ngeoEventsUnlistenByKeys(this.listenerKeys_);

    this.mode_ = null;
  }

  // === Utilities ===

  /**
   * The maximum number of features a query should return. Obtained
   * from the options.
   * @return {number|undefined}
   * @private
   */
  getLimitOption_() {
    return this.ngeoQueryOptions_ && this.ngeoQueryOptions_.limit ? this.ngeoQueryOptions_.limit : undefined;
  }

  // === Handlers ===

  /**
   * Called when active property changes
   * @param {boolean} active Whether this component is active or not.
   * @private
   */
  handleActiveChange_(active) {
    if (!active) {
      if (this.autoclear) {
        this.ngeoMapQuerent_.clear();
      }
    }
  }

  /**
   * Called when a box is drawn on the map. Use it to issue a query.
   * @param {Event|import("ol/events/Event.js").default} evt The draw
   *     interaction drawend event being fired.
   * @private
   */
  handleDrawBoxInteractionDrawEnd_(evt) {
    // @ts-ignore: evt should be of type {import('ol/interaction/Draw.js').DrawEvent but it is private
    const feature = evt.feature;

    const action = this.ngeoQueryModeSelector_.action;
    const extent = feature.getGeometry().getExtent();
    const limit = this.getLimitOption_();
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        extent,
        limit,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.vectorSource_.clear();
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when a polygon is drawn on the map. Use it to issue a query.
   * @param {Event|import("ol/events/Event.js").default} evt The draw
   *     interaction drawend event being fired.
   * @private
   */
  handleDrawPolygonInteractionDrawEnd_(evt) {
    // @ts-ignore: evt should be of type {import('ol/interaction/Draw.js').DrawEvent but it is private
    const feature = evt.feature;

    const action = this.ngeoQueryModeSelector_.action;
    const geometry = feature.getGeometry();
    const limit = this.getLimitOption_();
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        geometry,
        limit,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.vectorSource_.clear();
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when the map is clicked while this component is active and
   * the mode is "click". Issue a request to the query service using
   * the coordinate that was clicked.
   * @param {Event|import("ol/events/Event.js").default} evt The map
   *     browser event being fired.
   * @private
   */
  handleMapClick_(evt) {
    if (!(evt instanceof MapBrowserEvent)) {
      return;
    }

    const action = this.ngeoQueryModeSelector_.action;
    const coordinate = evt.coordinate;
    const map = this.map;

    this.ngeoQueryModeSelector_.pending = true;

    this.ngeoMapQuerent_
      .issue({
        action,
        coordinate,
        map,
      })
      .then(() => {})
      .catch(() => {})
      .then(() => {
        // "finally"
        this.ngeoQueryModeSelector_.pending = false;
      });
  }

  /**
   * Called when the pointer is moved over the map while this
   * component is active and the mode is "click".  Change the
   * mouse pointer when hovering a non-transparent pixel on the
   * map.
   * @param {Event|import("ol/events/Event.js").default} evt
   *     The map browser event being fired.
   */
  handleMapPointerMove_(evt) {
    // No need to do anything if user is dragging the map
    if (!(evt instanceof MapBrowserEvent) || evt.dragging) {
      return;
    }

    const pixel = this.map.getEventPixel(evt.originalEvent);

    /**
     * @param {import('ol/layer/Base').default} layer
     */
    const queryable = function (layer) {
      const visible = layer.get('visible');
      const sourceids = layer.get('querySourceIds');
      return visible && !!sourceids;
    };
    const hit = this.map.forEachLayerAtPixel(pixel, () => true, {
      layerFilter: queryable,
    });
    this.map.getTargetElement().style.cursor = hit ? 'pointer' : '';
  }
}

module.component('ngeoQuery', {
  bindings: {
    'active': '=',
    'autoclear': '=?',
    'map': '<',
  },
  controller: QueryController,
});

export default module;
<|endoftext|>


<commit_before>
'use strict';

import React, {
    AppRegistry,
    Component,
    StyleSheet,
    Text,
    Image,
    View,
    TouchableHighlight,
    TouchableNativeFeedback,
    Platform,
} from 'react-native';

import SimpleStore from 'react-native-simple-store';

import AnswerScreen from './answer-screen';
import CommonStyles from './common-styles.js';
import TweetBox from './tweet-box.js';
import Toolbar from './toolbar.js';

var TouchableElement = TouchableHighlight;
if (Platform.OS === 'android') {
    TouchableElement = TouchableNativeFeedback;
}

class QuestionScreen extends Component {
    constructor(props) {
        super(props);
        this.state = { question: null, currentStreak: null };
    }

    componentWillMount() {
        SimpleStore.get('questions').then((questions) => {
            var question = this.pickRandomQuestion(questions);
            this.setState(Object.assign(this.state, { question: question }));
        });

        SimpleStore.get('currentStreak').then((currentStreak) => {
            this.setState(Object.assign(this.state, { currentStreak: currentStreak }));
        });
    }

    render() {
        if (!this.state.question) {
            return (
                <View style={CommonStyles.screenBackground}>
                </View>
            );
        }

        return (
            <View style={[CommonStyles.screenBackground, styles.gameScreen]}>
                <Toolbar title={'Who tweeted?'} currentStreak={this.state.currentStreak} />
                <Question question={this.state.question} />

                <View style={{width: 256, borderTopWidth: 1, borderColor: '#FFFFFD', marginTop: 20, marginBottom: 5, alignSelf: 'center', height: 0}}/>

                <Answers
                    question={this.state.question}
                    navigator={this.props.navigator}
                    selectCallback={this.onSelectedAnswer.bind(this)}/>
            </View>
        )
    }

    onSelectedAnswer(selectedOption) {
        var questionUpdate = {};
        questionUpdate[this.state.question.uuid] = Object.assign({}, this.state.question, { answered: true });

        SimpleStore.update('questions', questionUpdate).then(() => {
            this.props.navigator.push({
                name: 'AnswerScreen',
                component: (props) => {
                    return new AnswerScreen(props, this.state.question, selectedOption);
                }
            });
        });
    }

    pickRandomQuestion(questions) {
        var newQuestions = [],
            oldQuestions = [];

        for (var id in questions) {
            var questionGroup = questions[id].answered ? oldQuestions : newQuestions;
            questionGroup.push(questions[id]);
        }

        var questionGroup = newQuestions.length ? newQuestions : oldQuestions;
        return questionGroup[Math.floor(Math.random() * questionGroup.length)];
    }
}

class Question extends Component {
    render() {
        return (
            <View style={styles.questionBox}>
                <TweetBox
                    image={require('../assets/egg-profile.jpeg')}
                    name={'???'}
                    text={this.props.question.text}
                />
            </View>
        );
    }
}

class Answers extends Component {
    render() {
        return (
            <View style={styles.answerBox}>
                {this.props.question.options.map((option) => {
                    return this.renderOption(option);
                })}
            </View>
        )
    }

    renderOption(option) {
        return (
            <Option
                option={option}
                question={this.props.question}
                navigator={this.props.navigator}
                selectCallback={this.props.selectCallback}

                key={option.handle}
            />
        );
    }
}

class Option extends Component {
    render() {
        return (
            <View style={styles.optionBuffer}>
                <TouchableElement onPress={() => this.props.selectCallback(this.props.option)}>
                    <View style={styles.answerOption}>
                        <Image
                            style={CommonStyles.profilePic}
                            source={this.props.option.image}
                        />
                        <View style={styles.optionContent}>
                            <Text style={[CommonStyles.baseText, styles.optionName]}>
                                {this.props.option.name}
                            </Text>
                            <Text style={[CommonStyles.baseText, styles.optionHandle]}>
                                @{this.props.option.handle}
                            </Text>
                        </View>
                    </View>
                </TouchableElement>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    gameScreen: {
        flex: 1,
    },
    questionBox: {
        flex: 1,
        flexDirection: 'row',
        alignItems: 'center',
        padding: 20,
    },
    answerBox: {
        flex: 2,
        margin: 20,
        flexDirection: 'column',
        justifyContent: 'flex-start',
    },
    answerOption: {
        padding: 20,
        flexDirection: 'row',
        backgroundColor: '#FFFFFD',
        padding: 8,
        flex: 1,
        borderRadius: 6,
        alignItems: 'center',
    },
    optionBuffer: {
        marginBottom: 20,
    },
    optionContent: {
        flex: 1,
        margin: 8,
        flexDirection: 'column',
    },
    optionName: {
        fontSize: 16,
        textAlign: 'left',
    },
    optionHandle: {
        fontSize: 12,
        textAlign: 'left',
        color: '#8899a6',
    },
});

export default QuestionScreen;
<commit_msg>increase space around divider<commit_after>
'use strict';

import React, {
    AppRegistry,
    Component,
    StyleSheet,
    Text,
    Image,
    View,
    TouchableHighlight,
    TouchableNativeFeedback,
    Platform,
} from 'react-native';

import SimpleStore from 'react-native-simple-store';

import AnswerScreen from './answer-screen';
import CommonStyles from './common-styles.js';
import TweetBox from './tweet-box.js';
import Toolbar from './toolbar.js';

var TouchableElement = TouchableHighlight;
if (Platform.OS === 'android') {
    TouchableElement = TouchableNativeFeedback;
}

class QuestionScreen extends Component {
    constructor(props) {
        super(props);
        this.state = { question: null, currentStreak: null };
    }

    componentWillMount() {
        SimpleStore.get('questions').then((questions) => {
            var question = this.pickRandomQuestion(questions);
            this.setState(Object.assign(this.state, { question: question }));
        });

        SimpleStore.get('currentStreak').then((currentStreak) => {
            this.setState(Object.assign(this.state, { currentStreak: currentStreak }));
        });
    }

    render() {
        if (!this.state.question) {
            return (
                <View style={CommonStyles.screenBackground}>
                </View>
            );
        }

        return (
            <View style={[CommonStyles.screenBackground, styles.gameScreen]}>
                <Toolbar title={'Who tweeted?'} currentStreak={this.state.currentStreak} />
                <Question question={this.state.question} />

                <View style={{width: 256, borderTopWidth: 1, borderColor: '#FFFFFD', marginTop: 40, marginBottom: 25, alignSelf: 'center', height: 0}}/>

                <Answers
                    question={this.state.question}
                    navigator={this.props.navigator}
                    selectCallback={this.onSelectedAnswer.bind(this)}/>
            </View>
        )
    }

    onSelectedAnswer(selectedOption) {
        var questionUpdate = {};
        questionUpdate[this.state.question.uuid] = Object.assign({}, this.state.question, { answered: true });

        SimpleStore.update('questions', questionUpdate).then(() => {
            this.props.navigator.push({
                name: 'AnswerScreen',
                component: (props) => {
                    return new AnswerScreen(props, this.state.question, selectedOption);
                }
            });
        });
    }

    pickRandomQuestion(questions) {
        var newQuestions = [],
            oldQuestions = [];

        for (var id in questions) {
            var questionGroup = questions[id].answered ? oldQuestions : newQuestions;
            questionGroup.push(questions[id]);
        }

        var questionGroup = newQuestions.length ? newQuestions : oldQuestions;
        return questionGroup[Math.floor(Math.random() * questionGroup.length)];
    }
}

class Question extends Component {
    render() {
        return (
            <View style={styles.questionBox}>
                <TweetBox
                    image={require('../assets/egg-profile.jpeg')}
                    name={'???'}
                    text={this.props.question.text}
                />
            </View>
        );
    }
}

class Answers extends Component {
    render() {
        return (
            <View style={styles.answerBox}>
                {this.props.question.options.map((option) => {
                    return this.renderOption(option);
                })}
            </View>
        )
    }

    renderOption(option) {
        return (
            <Option
                option={option}
                question={this.props.question}
                navigator={this.props.navigator}
                selectCallback={this.props.selectCallback}

                key={option.handle}
            />
        );
    }
}

class Option extends Component {
    render() {
        return (
            <View style={styles.optionBuffer}>
                <TouchableElement onPress={() => this.props.selectCallback(this.props.option)}>
                    <View style={styles.answerOption}>
                        <Image
                            style={CommonStyles.profilePic}
                            source={this.props.option.image}
                        />
                        <View style={styles.optionContent}>
                            <Text style={[CommonStyles.baseText, styles.optionName]}>
                                {this.props.option.name}
                            </Text>
                            <Text style={[CommonStyles.baseText, styles.optionHandle]}>
                                @{this.props.option.handle}
                            </Text>
                        </View>
                    </View>
                </TouchableElement>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    gameScreen: {
        flex: 1,
    },
    questionBox: {
        flex: 1,
        flexDirection: 'row',
        alignItems: 'center',
        padding: 20,
    },
    answerBox: {
        flex: 2,
        margin: 20,
        flexDirection: 'column',
        justifyContent: 'flex-start',
    },
    answerOption: {
        padding: 20,
        flexDirection: 'row',
        backgroundColor: '#FFFFFD',
        padding: 8,
        flex: 1,
        borderRadius: 6,
        alignItems: 'center',
    },
    optionBuffer: {
        marginBottom: 20,
    },
    optionContent: {
        flex: 1,
        margin: 8,
        flexDirection: 'column',
    },
    optionName: {
        fontSize: 16,
        textAlign: 'left',
    },
    optionHandle: {
        fontSize: 12,
        textAlign: 'left',
        color: '#8899a6',
    },
});

export default QuestionScreen;
<|endoftext|>


<commit_before>'use strict'
const Bluebird = require('bluebird')
const Buffer = require('safe-buffer').Buffer
const crypto = require('crypto')
const mkdirpCB = require('mkdirp')
const os = require('os')
const path = require('path')
const url = require('url')
const inflight = require('promise-inflight')

const fs = use('fs-promises')
const mkdirp = use('mkdirp')
const promisify = use('promisify')
const zlib = use('zlib-promises')

const pathDirname = promisify.args(path.dirname)

exports.readFile = readFile
exports.clearFile = clearFile
exports.readUrl = readUrl
exports.clearUrl = clearUrl
exports.invalidateUrl = invalidateUrl

const invalidated = {}

async function resolveCall (...promisedArgs) {
  const [fn, ...args] = await Promise.all(promisedArgs)
  return fn.apply(null, args)
}

async function cacheFilename (filename) {
  return path.join(os.homedir(), '.fetch-fic', await filename)
}

function readFile (filename, onMiss) {
  const cacheFile = cacheFilename(filename)
  return inflight(['read:', filename], thenReadFile)

  async function thenReadFile () {
    try {
      return await fs.readFile(cacheFile)
    } catch (_) {
      const content = await resolveCall(onMiss)
      return writeFile(filename, Buffer.from(content))
    }
  }
}

async function writeFile (filename, content) {
  const cacheFile = cacheFilename(filename)
  await inflight(['write:', filename], thenWriteFile)
  return content

  async function thenWriteFile () {
    await mkdirp(pathDirname(cacheFile))
    return fs.writeFile(cacheFile, content)
  }
}

function clearFile (filename) {
  const cacheFile = cacheFilename(filename)
  return ignoreHarmlessErrors(fs.unlink(cacheFile))
}

async function readJSON (filename, onMiss) {
  let didMiss = false
  const result = await readFile(filename, stringifyOnMiss)
  try {
    return JSON.parse(result)
  } catch (ex) {
    if (didMiss) throw ex
    await clearFile(filename)
    return JSON.parse(await readFile(filename, stringifyOnMiss))
  }

  async function stringifyOnMiss () {
    didMiss = true
    const result = await resolveCall(onMiss)
    return JSON.stringify(result, null, 2)
  }
}

/*
function writeJSON (filename, content) {
  return writeFile(filename, JSON.stringify(content, null, 2))
}
*/

async function readGzipFile (filename, onMiss) {
  const buf = await readFile(filename, gzipOnMiss)
  try {
    return await zlib.gunzip(buf)
  } catch (_) {
    await clearFile(filename)
    return readGzipFile(filename, onMiss)
  }

  async function gzipOnMiss () {
    const result = await resolveCall(onMiss)
    return zlib.gzip(result)
  }
}

async function writeGzipFile (filename, content) {
  await writeFile(filename, zlib.gzip(content))
  return content
}

async function getUrlHash (toFetch) {
  const parsed = url.parse(await toFetch)
  parsed.hash = null
  const normalized = url.format(parsed)
  return crypto.createHash('sha256').update(normalized).digest('hex')
}

async function cacheUrlBase (promisedFetchUrl) {
  const [fetchUrl, urlHash] = await Promise.all([promisedFetchUrl, getUrlHash(promisedFetchUrl)])
  const fetchP = url.parse(fetchUrl)
  return path.join('urls', fetchP.hostname, urlHash.slice(0, 1), urlHash.slice(1, 2), urlHash)
}
async function cacheUrlMetaName (fetchUrl) {
  const cacheUrl = await cacheUrlBase(fetchUrl)
  return cacheUrl + '.json'
}
async function cacheUrlContentName (promisedFetchUrl) {
  const fetchUrl = await promisedFetchUrl
  const fetchP = url.parse(fetchUrl)
  const ext = path.parse(fetchP.pathname).ext || '.data'
  return cacheUrlBase(fetchUrl).then(cacheUrl => cacheUrl + ext + '.gz')
}

const noMetadata = new Error('NOMETADATA')
noMetadata.code = 'NOMETADATA'

function readUrl (fetchUrl, onMiss) {
  const metafile = cacheUrlMetaName(fetchUrl)
  const content = cacheUrlContentName(fetchUrl)
  const fetchedAt = Date.now()
  const meta = {
    startUrl: fetchUrl,
    finalUrl: null
  }
  let existingMeta = {}
  return inflight(['readUrl:', fetchUrl], thenReadExistingMetadata)

  function thenReadExistingMetadata () {
    return readJSON(metafile, () => Bluebird.reject(noMetadata)).then(meta => {
      // corrupt JSON, clear the entry
      if (!meta || typeof meta !== 'object' || !meta.finalUrl) {
        return clearUrl(fetchUrl)
      } else {
        existingMeta = meta
        return null
      }
    }).catch(err => err.code !== 'NOMETADATA' && Promise.reject(err))
      .then(() => thenReadContent())
  }

  function thenReadContent () {
    let result
    let allow304 = false
    if (invalidated[fetchUrl]) {
      delete invalidated[fetchUrl]
      allow304 = true
      return writeGzipFile(content, orFetchUrl()).then(thenReadMetadata).catch(err => {
        if (err.code !== 304) throw err
        return thenReadContent()
      })
    } else {
      return readGzipFile(content, orFetchUrl).catch(err => {
        // corrupted gzips we retry, anything else explode
        if (err.code !== 'Z_DATA_ERROR') throw err
        return clearUrl(fetchUrl).then(() => {
          return readGzipFile(content, orFetchUrl)
        })
      }).then(thenReadMetadata)
    }
  }

  function orFetchUrl () {
    return resolveCall(onMiss, fetchUrl, existingMeta).then(([res, content]) => {
      meta.finalUrl = res.url || meta.startUrl
      meta.status = res.status
      meta.statusText = res.statusText
      meta.headers = res.headers.raw()
      meta.fetchedAt = fetchedAt
      if (meta.status && meta.status === 304) {
        return thenReadContent().then(([_, data]) => data)
      } else if (meta.status && meta.status === 403) {
        const err403 = new Error('Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl)
        err403.code = meta.status
        err403.url = fetchUrl
        err403.meta = meta
        return Bluebird.reject(err403)
      } else if (meta.status && meta.status === 429) {
        const err429 = new Error('Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl)
        err429.code = meta.status
        err429.url = fetchUrl
        err429.meta = meta
        err429.retryAfter = res.headers['retry-after']
        return Bluebird.reject(err429)
      } else if (meta.status && meta.status === 404) {
        const non200 = new Error('Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl)
        non200.code = meta.status
        non200.url = fetchUrl
        non200.meta = meta
        return JSON.stringify(non200)
      } else if (meta.status && meta.status !== 200) {
        const non200 = new Error('Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl)
        non200.code = meta.status
        non200.url = fetchUrl
        non200.meta = meta
        return Bluebird.reject(non200)
      }
      return content
    })
  }

  function thenReadMetadata (result) {
    return Bluebird.all([metafile, readJSON(metafile, () => meta)]).then(([metafile, meta]) => {
      meta.fromCache = meta.fetchedAt !== fetchedAt ? metafile : null
      if (meta.startURL) {
        meta.startUrl = meta.startURL
        delete meta.startURL
      }
      if (meta.finalURL) {
        meta.finalUrl = meta.finalURL
        delete meta.finalURL
      }
      if (!meta.finalUrl) meta.finalUrl = meta.startUrl
      if (meta.status && meta.status !== 200) {
        const non200 = new Error('Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl)
        non200.code = meta.status
        non200.url = fetchUrl
        non200.meta = meta
        return Bluebird.reject(non200)
      }
      return [meta, result]
    })
  }
}

async function ignoreHarmlessErrors (p) {
  try {
    return await p
  } catch (er) {
    if (er.code === 'ENOENT' || er.code === 'EINVAL') return
    throw er
  }
}

function clearUrl (fetchUrl) {
  const metafile = cacheUrlMetaName(fetchUrl)
  const content = cacheUrlContentName(fetchUrl)
  return Promise.all([clearFile(metafile), clearFile(content)])
}

async function invalidateUrl (fetchUrl) {
  invalidated[await fetchUrl] = true
}
<commit_msg>cache: finish async/awaitifying<commit_after>'use strict'
const Buffer = require('safe-buffer').Buffer
const crypto = require('crypto')
const mkdirpCB = require('mkdirp')
const os = require('os')
const path = require('path')
const url = require('url')
const inflight = require('promise-inflight')

const fs = use('fs-promises')
const mkdirp = use('mkdirp')
const promisify = use('promisify')
const zlib = use('zlib-promises')

const pathDirname = promisify.args(path.dirname)

exports.readFile = readFile
exports.clearFile = clearFile
exports.readUrl = readUrl
exports.clearUrl = clearUrl
exports.invalidateUrl = invalidateUrl

const invalidated = {}

async function resolveCall (...promisedArgs) {
  const [fn, ...args] = await Promise.all(promisedArgs)
  return fn.apply(null, args)
}

async function cacheFilename (filename) {
  return path.join(os.homedir(), '.fetch-fic', await filename)
}

function readFile (filename, onMiss) {
  const cacheFile = cacheFilename(filename)
  return inflight(['read:', filename], thenReadFile)

  async function thenReadFile () {
    try {
      return await fs.readFile(cacheFile)
    } catch (_) {
      const content = await resolveCall(onMiss)
      return writeFile(filename, Buffer.from(content))
    }
  }
}

async function writeFile (filename, content) {
  const cacheFile = cacheFilename(filename)
  await inflight(['write:', filename], thenWriteFile)
  return content

  async function thenWriteFile () {
    await mkdirp(pathDirname(cacheFile))
    return fs.writeFile(cacheFile, content)
  }
}

function clearFile (filename) {
  const cacheFile = cacheFilename(filename)
  return ignoreHarmlessErrors(fs.unlink(cacheFile))
}

async function readJSON (filename, onMiss) {
  let didMiss = false
  const result = await readFile(filename, stringifyOnMiss)
  try {
    return JSON.parse(result)
  } catch (ex) {
    if (didMiss) throw ex
    await clearFile(filename)
    return JSON.parse(await readFile(filename, stringifyOnMiss))
  }

  async function stringifyOnMiss () {
    didMiss = true
    const result = await resolveCall(onMiss)
    return JSON.stringify(result, null, 2)
  }
}

/*
function writeJSON (filename, content) {
  return writeFile(filename, JSON.stringify(content, null, 2))
}
*/

async function readGzipFile (filename, onMiss) {
  const buf = await readFile(filename, gzipOnMiss)
  try {
    return await zlib.gunzip(buf)
  } catch (_) {
    await clearFile(filename)
    return readGzipFile(filename, onMiss)
  }

  async function gzipOnMiss () {
    const result = await resolveCall(onMiss)
    return zlib.gzip(result)
  }
}

async function writeGzipFile (filename, content) {
  await writeFile(filename, zlib.gzip(content))
  return content
}

async function getUrlHash (toFetch) {
  const parsed = url.parse(await toFetch)
  parsed.hash = null
  const normalized = url.format(parsed)
  return crypto.createHash('sha256').update(normalized).digest('hex')
}

async function cacheUrlBase (promisedFetchUrl) {
  const [fetchUrl, urlHash] = await Promise.all([promisedFetchUrl, getUrlHash(promisedFetchUrl)])
  const fetchP = url.parse(fetchUrl)
  return path.join('urls', fetchP.hostname, urlHash.slice(0, 1), urlHash.slice(1, 2), urlHash)
}
async function cacheUrlMetaName (fetchUrl) {
  const cacheUrl = await cacheUrlBase(fetchUrl)
  return cacheUrl + '.json'
}
async function cacheUrlContentName (promisedFetchUrl) {
  const fetchUrl = await promisedFetchUrl
  const fetchP = url.parse(fetchUrl)
  const ext = path.parse(fetchP.pathname).ext || '.data'
  const cacheUrl = await cacheUrlBase(fetchUrl)
  return cacheUrl + ext + '.gz'
}

const noMetadata = new Error('NOMETADATA')
noMetadata.code = 'NOMETADATA'

function readUrl (fetchUrl, onMiss) {
  const metafile = cacheUrlMetaName(fetchUrl)
  const content = cacheUrlContentName(fetchUrl)
  const fetchedAt = Date.now()
  const meta = {
    startUrl: fetchUrl,
    finalUrl: null
  }
  const err = new Error()
  let existingMeta = {}
  return inflight(['readUrl:', fetchUrl], thenReadExistingMetadata)

  async function thenReadExistingMetadata () {
    try {
      const meta = await readJSON(metafile, () => Promise.reject(noMetadata))
      // corrupt JSON, clear the entry
      if (!meta || typeof meta !== 'object' || !meta.finalUrl) {
        await clearUrl(fetchUrl)
      } else {
        existingMeta = meta
      }
    } catch (err) {
      if (err.code !== 'NOMETADATA') { throw err }
    }
    return thenReadContent()
  }

  async function thenReadContent () {
    let result
    let allow304 = false
    if (invalidated[fetchUrl]) {
      delete invalidated[fetchUrl]
      allow304 = true
      const [res, data] = await resolveCall(onMiss, fetchUrl, existingMeta)
      meta.finalUrl = res.url || meta.startUrl
      meta.status = res.status
      meta.statusText = res.statusText
      meta.headers = res.headers.raw()
      meta.fetchedAt = fetchedAt
      await writeJSON(metafile, meta)
      if (meta.status && meta.status === 304) {
        return thenReadContent()
      } else {
        await writeGzipFile(content, data)
        return rejectIfHTTPError(fetchUrl, meta, [meta, data], err)
      }
    } else {
      let data
      try {
        data = await readGzipFile(content, orFetchUrl)
      } catch (err) {
        // corrupted gzips we retry, anything else explode
        if (err.code !== 'Z_DATA_ERROR') throw err
        await clearUrl(fetchUrl)
        data = await readGzipFile(content, orFetchUrl)
      }
      return thenReadMetadata(data)
    }
  }

  async function orFetchUrl () {
    const [res, content] = await resolveCall(onMiss, fetchUrl, existingMeta)
    meta.finalUrl = res.url || meta.startUrl
    meta.status = res.status
    meta.statusText = res.statusText
    meta.headers = res.headers.raw()
    meta.fetchedAt = fetchedAt
    if (meta.status && meta.status === 304) {
      const [, data] = await thenReadContent()
      return data
    } else {
      return rejectIfHTTPError(fetchUrl, meta, content, err)
    }
  }

  async function thenReadMetadata (result) {
    const [newMetafile, newMeta] = await Promise.all([metafile, readJSON(metafile, () => meta)])
    newMeta.fromCache = newMeta.fetchedAt !== fetchedAt ? newMetafile : null
    if (newMeta.startURL) {
      newMeta.startUrl = newMeta.startURL
      delete newMeta.startURL
    }
    if (newMeta.finalURL) {
      newMeta.finalUrl = newMeta.finalURL
      delete newMeta.finalURL
    }
    if (!newMeta.finalUrl) newMeta.finalUrl = newMeta.startUrl
    return rejectIfHTTPError(newMeta.startUrl, newMeta, [newMeta, result], err)
  }
}

function rejectIfHTTPError (fetchUrl, meta, payload, err) {
  if (meta.status && meta.status === 403) {
    err.message = 'Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl
    err.code = meta.status
    err.url = fetchUrl
    err.meta = meta
    return Promise.reject(err)
  } else if (meta.status && meta.status === 429) {
    err.message = 'Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl
    err.code = meta.status
    err.url = fetchUrl
    err.meta = meta
    err.retryAfter = meta.headers['retry-after']
    return Promise.reject(err)
  } else if (meta.status && meta.status === 404) {
    err.message = 'Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl
    err.code = meta.status
    err.url = fetchUrl
    err.meta = meta
    return Promise.reject(err)
  } else if (meta.status && (meta.status < 200 || meta.status >= 400) ) {
    err.message = 'Got status: ' + meta.status + ' ' + meta.statusText + ' for ' + fetchUrl
    err.code = meta.status
    err.url = fetchUrl
    err.meta = meta
    return Promise.reject(err)
  }
  return payload
}

async function ignoreHarmlessErrors (p) {
  try {
    return await p
  } catch (er) {
    if (er.code === 'ENOENT' || er.code === 'EINVAL') return
    throw er
  }
}

async function clearUrl (fetchUrl) {
  const metafile = cacheUrlMetaName(fetchUrl)
  const content = cacheUrlContentName(fetchUrl)
  return Promise.all([clearFile(metafile), clearFile(content)])
}

async function invalidateUrl (fetchUrl) {
  const furl = await fetchUrl
  invalidated[furl] = true
}
<|endoftext|>


<commit_before>/*
  QuoJS 1.0
  (c) 2011, 2012 Javi Jiménez Villar (@soyjavi)
  http://quojs.tapquo.com
*/

(function($$) {

    var IS_WEBKIT = /WebKit\/([\d.]+)/;
    var SUPPORTED_OS = {
        android: /(Android)\s+([\d.]+)/,
        ipad: /(iPad).*OS\s([\d_]+)/,
        iphone: /(iPhone\sOS)\s([\d_]+)/,
        blackberry: /(BlackBerry).*Version\/([\d.]+)/,
        webos: /(webOS|hpwOS)[\s\/]([\d.]+)/
    };
    var CURRENT_ENVIRONMENT = null;

    /**
     * ?
     */
    $$.isMobile = function() {
        CURRENT_ENVIRONMENT = CURRENT_ENVIRONMENT || _detectEnvironment();

        return CURRENT_ENVIRONMENT.isMobile;
    };

    /**
     * ?
     */
    $$.environment = function() {
        CURRENT_ENVIRONMENT = CURRENT_ENVIRONMENT || _detectEnvironment();

        return CURRENT_ENVIRONMENT;
    };

    /**
     * ?
     */
    $$.isOnline = isOnline = function(selector) {
        return (navigator.onLine) ? true : false;
    }

    var _detectEnvironment = function() {
        var user_agent = navigator.userAgent;
        var environment = {};

        environment.browser = _detectBrowser(user_agent);
        environment.os = _detectOS(user_agent);
        environment.isMobile = (environment.os.name) ? true : false;

        return environment;
    };

    var _detectBrowser = function(user_agent) {
        var is_webkit = user_agent.match(IS_WEBKIT);

        return (is_webkit) ? is_webkit[0]: user_agent;
    };

    var _detectOS = function(user_agent) {
        var detected_os = {};

        for (os in SUPPORTED_OS) {
            var supported = user_agent.match(SUPPORTED_OS[os]);

            if (supported) {
                detected_os.name = os;
                detected_os.version = supported[2];
                break;
            }
        }

        return detected_os;
    }

})(Quo);<commit_msg>If device is iPhone/iPad de OS is iOS<commit_after>/*
  QuoJS 1.0
  (c) 2011, 2012 Javi Jiménez Villar (@soyjavi)
  http://quojs.tapquo.com
*/

(function($$) {

    var IS_WEBKIT = /WebKit\/([\d.]+)/;
    var SUPPORTED_OS = {
        android: /(Android)\s+([\d.]+)/,
        ipad: /(iPad).*OS\s([\d_]+)/,
        iphone: /(iPhone\sOS)\s([\d_]+)/,
        blackberry: /(BlackBerry).*Version\/([\d.]+)/,
        webos: /(webOS|hpwOS)[\s\/]([\d.]+)/
    };
    var CURRENT_ENVIRONMENT = null;

    /**
     * ?
     */
    $$.isMobile = function() {
        CURRENT_ENVIRONMENT = CURRENT_ENVIRONMENT || _detectEnvironment();

        return CURRENT_ENVIRONMENT.isMobile;
    };

    /**
     * ?
     */
    $$.environment = function() {
        CURRENT_ENVIRONMENT = CURRENT_ENVIRONMENT || _detectEnvironment();

        return CURRENT_ENVIRONMENT;
    };

    /**
     * ?
     */
    $$.isOnline = isOnline = function(selector) {
        return (navigator.onLine) ? true : false;
    }

    var _detectEnvironment = function() {
        var user_agent = navigator.userAgent;
        var environment = {};

        environment.browser = _detectBrowser(user_agent);
        environment.os = _detectOS(user_agent);
        environment.isMobile = (environment.os) ? true : false;

        return environment;
    };

    var _detectBrowser = function(user_agent) {
        var is_webkit = user_agent.match(IS_WEBKIT);

        return (is_webkit) ? is_webkit[0]: user_agent;
    };

    var _detectOS = function(user_agent) {
        var detected_os;

        for (os in SUPPORTED_OS) {
            var supported = user_agent.match(SUPPORTED_OS[os]);

            if (supported) {
                detected_os = {
                    name: (os === 'iphone' || os === 'ipad') ? 'ios' : os,
                    version: supported[2]
                }
                break;
            }
        }

        return detected_os;
    }

})(Quo);<|endoftext|>