<commit_before>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const os = require('os');
const fs = require('fs');
const path = require('path');
const extract = require('extract-zip');
const util = require('util');
const URL = require('url');
const {helper, assert} = require('./helper');
const removeRecursive = require('rimraf');
// @ts-ignore
const ProxyAgent = require('https-proxy-agent');
// @ts-ignore
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;

const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
const downloadURLs = {
  linux: '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
  mac: '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
  win32: '%s/chromium-browser-snapshots/Win/%d/chrome-win32.zip',
  win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
};

const readdirAsync = helper.promisify(fs.readdir.bind(fs));
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
const chmodAsync = helper.promisify(fs.chmod.bind(fs));

function existsAsync(filePath) {
  let fulfill = null;
  const promise = new Promise(x => fulfill = x);
  fs.access(filePath, err => fulfill(!err));
  return promise;
}

class BrowserFetcher {
  /**
   * @param {string} projectRoot
   * @param {!BrowserFetcher.Options=} options
   */
  constructor(projectRoot, options = {}) {
    this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium');
    this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
    this._platform = options.platform || '';
    if (!this._platform) {
      const platform = os.platform();
      if (platform === 'darwin')
        this._platform = 'mac';
      else if (platform === 'linux')
        this._platform = 'linux';
      else if (platform === 'win32')
        this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
      assert(this._platform, 'Unsupported platform: ' + os.platform());
    }
    const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
    assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
  }

  /**
   * @return {string}
   */
  platform() {
    return this._platform;
  }

  /**
   * @param {string} revision
   * @return {!Promise<boolean>}
   */
  canDownload(revision) {
    const url = util.format(downloadURLs[this._platform], this._downloadHost, revision);

    let resolve;
    const promise = new Promise(x => resolve = x);
    const request = httpRequest(url, 'HEAD', response => {
      resolve(response.statusCode === 200);
    });
    request.on('error', error => {
      console.error(error);
      resolve(false);
    });
    return promise;
  }

  /**
   * @param {string} revision
   * @param {?function(number, number)} progressCallback
   * @return {!Promise<!BrowserFetcher.RevisionInfo>}
   */
  async download(revision, progressCallback) {
    let url = downloadURLs[this._platform];
    url = util.format(url, this._downloadHost, revision);
    const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
    const folderPath = this._getFolderPath(revision);
    if (await existsAsync(folderPath))
      return this.revisionInfo(revision);
    if (!(await existsAsync(this._downloadsFolder)))
      await mkdirAsync(this._downloadsFolder);
    try {
      await downloadFile(url, zipPath, progressCallback);
      await extractZip(zipPath, folderPath);
    } finally {
      if (await existsAsync(zipPath))
        await unlinkAsync(zipPath);
    }
    const revisionInfo = this.revisionInfo(revision);
    if (revisionInfo)
      await chmodAsync(revisionInfo.executablePath, 0o755);
    return revisionInfo;
  }

  /**
   * @return {!Promise<!Array<string>>}
   */
  async localRevisions() {
    if (!await existsAsync(this._downloadsFolder))
      return [];
    const fileNames = await readdirAsync(this._downloadsFolder);
    return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
  }

  /**
   * @param {string} revision
   * @return {!Promise}
   */
  async remove(revision) {
    const folderPath = this._getFolderPath(revision);
    assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
    await new Promise(fulfill => removeRecursive(folderPath, fulfill));
  }

  /**
   * @param {string} revision
   * @return {!BrowserFetcher.RevisionInfo}
   */
  revisionInfo(revision) {
    const folderPath = this._getFolderPath(revision);
    let executablePath = '';
    if (this._platform === 'mac')
      executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
    else if (this._platform === 'linux')
      executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
    else if (this._platform === 'win32' || this._platform === 'win64')
      executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
    else
      throw new Error('Unsupported platform: ' + this._platform);
    let url = downloadURLs[this._platform];
    url = util.format(url, this._downloadHost, revision);
    const local = fs.existsSync(folderPath);
    return {revision, executablePath, folderPath, local, url};
  }

  /**
   * @param {string} revision
   * @return {string}
   */
  _getFolderPath(revision) {
    return path.join(this._downloadsFolder, this._platform + '-' + revision);
  }
}

module.exports = BrowserFetcher;

/**
 * @param {string} folderPath
 * @return {?{platform: string, revision: string}}
 */
function parseFolderPath(folderPath) {
  const name = path.basename(folderPath);
  const splits = name.split('-');
  if (splits.length !== 2)
    return null;
  const [platform, revision] = splits;
  if (!downloadURLs[platform])
    return null;
  return {platform, revision};
}

/**
 * @param {string} url
 * @param {string} destinationPath
 * @param {?function(number, number)} progressCallback
 * @return {!Promise}
 */
function downloadFile(url, destinationPath, progressCallback) {
  let fulfill, reject;
  let downloadedBytes = 0;
  let totalBytes = 0;

  const promise = new Promise((x, y) => { fulfill = x; reject = y; });

  const request = httpRequest(url, 'GET', response => {
    if (response.statusCode !== 200) {
      const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
      // consume response data to free up memory
      response.resume();
      reject(error);
      return;
    }
    const file = fs.createWriteStream(destinationPath);
    file.on('finish', () => fulfill());
    file.on('error', error => reject(error));
    response.pipe(file);
    totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
    if (progressCallback)
      response.on('data', onData);
  });
  request.on('error', error => reject(error));
  return promise;

  function onData(chunk) {
    downloadedBytes += chunk.length;
    progressCallback(downloadedBytes, totalBytes);
  }
}

/**
 * @param {string} zipPath
 * @param {string} folderPath
 * @return {!Promise<?Error>}
 */
function extractZip(zipPath, folderPath) {
  return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
    if (err)
      reject(err);
    else
      fulfill();
  }));
}

function httpRequest(url, method, response) {
  /** @type {Object} */
  const options = URL.parse(url);
  options.method = method;

  const proxyURL = getProxyForUrl(url);
  if (proxyURL) {
    /** @type {Object} */
    const parsedProxyURL = URL.parse(proxyURL);
    parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';

    options.agent = new ProxyAgent(parsedProxyURL);
    options.rejectUnauthorized = false;
  }

  const requestCallback = res => {
    if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
      httpRequest(res.headers.location, method, response);
    else
      response(res);
  };
  const request = options.protocol === 'https:' ?
    require('https').request(options, requestCallback) :
    require('http').request(options, requestCallback);
  request.end();
  return request;
}

/**
 * @typedef {Object} BrowserFetcher.Options
 * @property {string=} platform
 * @property {string=} path
 * @property {string=} host
 */

/**
 * @typedef {Object} BrowserFetcher.RevisionInfo
 * @property {string} folderPath
 * @property {string} executablePath
 * @property {string} url
 * @property {boolean} local
 * @property {string} revision
 */
<commit_msg>fix(browserfetcher): Fix windows fetching (#3256)<commit_after>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const os = require('os');
const fs = require('fs');
const path = require('path');
const extract = require('extract-zip');
const util = require('util');
const URL = require('url');
const {helper, assert} = require('./helper');
const removeRecursive = require('rimraf');
// @ts-ignore
const ProxyAgent = require('https-proxy-agent');
// @ts-ignore
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;

const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';

const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
const downloadURLs = {
  linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
  mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
  win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip',
  win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip',
};

/**
 * @param {string} platform
 * @param {string} revision
 * @return {string}
 */
function archiveName(platform, revision) {
  if (platform === 'linux')
    return 'chrome-linux';
  if (platform === 'mac')
    return 'chrome-mac';
  if (platform === 'win32' || platform === 'win64') {
    // Windows archive name changed at r591479.
    return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
  }
  return null;
}

/**
 * @param {string} platform
 * @param {string} host
 * @param {string} revision
 * @return {string}
 */
function downloadURL(platform, host, revision) {
  return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision));
}

const readdirAsync = helper.promisify(fs.readdir.bind(fs));
const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
const chmodAsync = helper.promisify(fs.chmod.bind(fs));

function existsAsync(filePath) {
  let fulfill = null;
  const promise = new Promise(x => fulfill = x);
  fs.access(filePath, err => fulfill(!err));
  return promise;
}

class BrowserFetcher {
  /**
   * @param {string} projectRoot
   * @param {!BrowserFetcher.Options=} options
   */
  constructor(projectRoot, options = {}) {
    this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium');
    this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
    this._platform = options.platform || '';
    if (!this._platform) {
      const platform = os.platform();
      if (platform === 'darwin')
        this._platform = 'mac';
      else if (platform === 'linux')
        this._platform = 'linux';
      else if (platform === 'win32')
        this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
      assert(this._platform, 'Unsupported platform: ' + os.platform());
    }
    assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
  }

  /**
   * @return {string}
   */
  platform() {
    return this._platform;
  }

  /**
   * @param {string} revision
   * @return {!Promise<boolean>}
   */
  canDownload(revision) {
    const url = downloadURL(this._platform, this._downloadHost, revision);
    let resolve;
    const promise = new Promise(x => resolve = x);
    const request = httpRequest(url, 'HEAD', response => {
      resolve(response.statusCode === 200);
    });
    request.on('error', error => {
      console.error(error);
      resolve(false);
    });
    return promise;
  }

  /**
   * @param {string} revision
   * @param {?function(number, number)} progressCallback
   * @return {!Promise<!BrowserFetcher.RevisionInfo>}
   */
  async download(revision, progressCallback) {
    const url = downloadURL(this._platform, this._downloadHost, revision);
    const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
    const folderPath = this._getFolderPath(revision);
    if (await existsAsync(folderPath))
      return this.revisionInfo(revision);
    if (!(await existsAsync(this._downloadsFolder)))
      await mkdirAsync(this._downloadsFolder);
    try {
      await downloadFile(url, zipPath, progressCallback);
      await extractZip(zipPath, folderPath);
    } finally {
      if (await existsAsync(zipPath))
        await unlinkAsync(zipPath);
    }
    const revisionInfo = this.revisionInfo(revision);
    if (revisionInfo)
      await chmodAsync(revisionInfo.executablePath, 0o755);
    return revisionInfo;
  }

  /**
   * @return {!Promise<!Array<string>>}
   */
  async localRevisions() {
    if (!await existsAsync(this._downloadsFolder))
      return [];
    const fileNames = await readdirAsync(this._downloadsFolder);
    return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
  }

  /**
   * @param {string} revision
   * @return {!Promise}
   */
  async remove(revision) {
    const folderPath = this._getFolderPath(revision);
    assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
    await new Promise(fulfill => removeRecursive(folderPath, fulfill));
  }

  /**
   * @param {string} revision
   * @return {!BrowserFetcher.RevisionInfo}
   */
  revisionInfo(revision) {
    const folderPath = this._getFolderPath(revision);
    let executablePath = '';
    if (this._platform === 'mac')
      executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
    else if (this._platform === 'linux')
      executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome');
    else if (this._platform === 'win32' || this._platform === 'win64')
      executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe');
    else
      throw new Error('Unsupported platform: ' + this._platform);
    const url = downloadURL(this._platform, this._downloadHost, revision);
    const local = fs.existsSync(folderPath);
    return {revision, executablePath, folderPath, local, url};
  }

  /**
   * @param {string} revision
   * @return {string}
   */
  _getFolderPath(revision) {
    return path.join(this._downloadsFolder, this._platform + '-' + revision);
  }
}

module.exports = BrowserFetcher;

/**
 * @param {string} folderPath
 * @return {?{platform: string, revision: string}}
 */
function parseFolderPath(folderPath) {
  const name = path.basename(folderPath);
  const splits = name.split('-');
  if (splits.length !== 2)
    return null;
  const [platform, revision] = splits;
  if (!supportedPlatforms.includes(platform))
    return null;
  return {platform, revision};
}

/**
 * @param {string} url
 * @param {string} destinationPath
 * @param {?function(number, number)} progressCallback
 * @return {!Promise}
 */
function downloadFile(url, destinationPath, progressCallback) {
  let fulfill, reject;
  let downloadedBytes = 0;
  let totalBytes = 0;

  const promise = new Promise((x, y) => { fulfill = x; reject = y; });

  const request = httpRequest(url, 'GET', response => {
    if (response.statusCode !== 200) {
      const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
      // consume response data to free up memory
      response.resume();
      reject(error);
      return;
    }
    const file = fs.createWriteStream(destinationPath);
    file.on('finish', () => fulfill());
    file.on('error', error => reject(error));
    response.pipe(file);
    totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
    if (progressCallback)
      response.on('data', onData);
  });
  request.on('error', error => reject(error));
  return promise;

  function onData(chunk) {
    downloadedBytes += chunk.length;
    progressCallback(downloadedBytes, totalBytes);
  }
}

/**
 * @param {string} zipPath
 * @param {string} folderPath
 * @return {!Promise<?Error>}
 */
function extractZip(zipPath, folderPath) {
  return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
    if (err)
      reject(err);
    else
      fulfill();
  }));
}

function httpRequest(url, method, response) {
  /** @type {Object} */
  const options = URL.parse(url);
  options.method = method;

  const proxyURL = getProxyForUrl(url);
  if (proxyURL) {
    /** @type {Object} */
    const parsedProxyURL = URL.parse(proxyURL);
    parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';

    options.agent = new ProxyAgent(parsedProxyURL);
    options.rejectUnauthorized = false;
  }

  const requestCallback = res => {
    if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
      httpRequest(res.headers.location, method, response);
    else
      response(res);
  };
  const request = options.protocol === 'https:' ?
    require('https').request(options, requestCallback) :
    require('http').request(options, requestCallback);
  request.end();
  return request;
}

/**
 * @typedef {Object} BrowserFetcher.Options
 * @property {string=} platform
 * @property {string=} path
 * @property {string=} host
 */

/**
 * @typedef {Object} BrowserFetcher.RevisionInfo
 * @property {string} folderPath
 * @property {string} executablePath
 * @property {string} url
 * @property {boolean} local
 * @property {string} revision
 */
<|endoftext|>


<commit_before>export default function typographyFixer (lang) {
  const rulesLibrary = {}

  if (lang) {
    return {
      rules (block) { rules(lang, block) },
      hasRule (rule) { return hasRule(lang, rule) },
      check (string, options={}) { return check(string, merge(options, {lang})) },
      fix (string, options={}) { return fix(string, merge(options, {lang})) }
    }
  } else {
    return {rules, hasRule, check, fix}
  }

  function check (string, options = {}) {
    let {lang} = options
    if (!lang) { lang = 'en' }

    const rules = rulesLibrary[lang]
    let results = []

    if (!rules) { return undefined }

    for (let i = 0, len = rules.length; i < len; i++) {
      const rule = rules[i]
      results = results.concat(rule.check(string))
    }

    return results.length > 0 ? results : undefined
  }

  function fix (string, options = {}) {
    let {lang} = options
    if (!lang) { lang = 'en' }

    const rules = rulesLibrary[lang]

    if (!rules) { return string }

    for (let i = 0, len = rules.length; i < len; i++) {
      const rule = rules[i]
      string = rule.fix(string)
    }

    return string
  }

  function rules (lang, block) {
    if (typeof lang !== 'string') {
      throw new Error("Can't call rules without a lang")
    }

    if (typeof block !== 'function') {
      throw new Error("Can't call rules without a function")
    }

    const definedRules = evaluateBlock(block)

    rulesLibrary[lang] = (rulesLibrary[lang] || []).concat(definedRules)
  }

  function hasRule (lang, ruleName) {
    const langRules = rulesLibrary[lang]
    if (!langRules) { return false }

    return langRules.some((rule) => { return rule.name === ruleName })
  }
}

function evaluateBlock (block) {
  const rules = []

  block(getRulesContext(rules, []))

  return rules
}

function getRulesContext (rules, context) {
  function define (name, expression, replacement) {
    let source
    if (expression instanceof RegExp) {
      source = expression.source
    } else {
      source = expression
    }

    const ruleName = context.concat(name).join('.')

    rules.push({
      name: ruleName,
      check (string) {
        const re = new RegExp(source, 'g')
        const matches = []
        let match
        do {
          match = re.exec(string)
          if (match) {
            matches.push({
              rule: ruleName,
              range: [match.index, re.lastIndex]
            })
          }
        } while (match)

        return matches
      },
      fix (string) {
        const re = new RegExp(source, 'g')
        return string.replace(re, replacement)
      }
    })
  }

  function group (name, block) {
    block(getRulesContext(rules, context.concat(name)))
  }

  return {define, group}
}

function merge (a, b) {
  const o = {}

  for (const key in a) { o[key] = a[key] }
  for (const key in b) { o[key] = b[key] }

  return o
}
<commit_msg>:art: Reorganise getRulesContext function<commit_after>export default function typographyFixer (lang) {
  const rulesLibrary = {}

  if (lang) {
    return {
      rules (block) { rules(lang, block) },
      hasRule (rule) { return hasRule(lang, rule) },
      check (string, options={}) { return check(string, merge(options, {lang})) },
      fix (string, options={}) { return fix(string, merge(options, {lang})) }
    }
  } else {
    return {rules, hasRule, check, fix}
  }

  function check (string, options = {}) {
    let {lang} = options
    if (!lang) { lang = 'en' }

    const rules = rulesLibrary[lang]
    let results = []

    if (!rules) { return undefined }

    for (let i = 0, len = rules.length; i < len; i++) {
      const rule = rules[i]
      results = results.concat(rule.check(string))
    }

    return results.length > 0 ? results : undefined
  }

  function fix (string, options = {}) {
    let {lang} = options
    if (!lang) { lang = 'en' }

    const rules = rulesLibrary[lang]

    if (!rules) { return string }

    for (let i = 0, len = rules.length; i < len; i++) {
      const rule = rules[i]
      string = rule.fix(string)
    }

    return string
  }

  function rules (lang, block) {
    if (typeof lang !== 'string') {
      throw new Error("Can't call rules without a lang")
    }

    if (typeof block !== 'function') {
      throw new Error("Can't call rules without a function")
    }

    const definedRules = evaluateBlock(block)

    rulesLibrary[lang] = (rulesLibrary[lang] || []).concat(definedRules)
  }

  function hasRule (lang, ruleName) {
    const langRules = rulesLibrary[lang]
    if (!langRules) { return false }

    return langRules.some((rule) => { return rule.name === ruleName })
  }
}

function evaluateBlock (block) {
  const rules = []

  block(getRulesContext(rules, []))

  return rules
}

function getRulesContext (rules, context) {
  return {group, define}

  function group (name, block) {
    block(getRulesContext(rules, context.concat(name)))
  }

  function define (name, expression, replacement) {
    let source
    if (expression instanceof RegExp) {
      source = expression.source
    } else {
      source = expression
    }

    const ruleName = context.concat(name).join('.')

    rules.push({
      name: ruleName,
      check (string) {
        const re = new RegExp(source, 'g')
        const matches = []
        let match
        do {
          match = re.exec(string)
          if (match) {
            matches.push({
              rule: ruleName,
              range: [match.index, re.lastIndex]
            })
          }
        } while (match)

        return matches
      },
      fix (string) {
        const re = new RegExp(source, 'g')
        return string.replace(re, replacement)
      }
    })
  }
}

function merge (a, b) {
  const o = {}

  for (const key in a) { o[key] = a[key] }
  for (const key in b) { o[key] = b[key] }

  return o
}
<|endoftext|>


<commit_before>if(typeof environment == 'undefined') environment = 'default'; // Override me!

(function() {

  var results;

  var currentPackage;
  var currentMethod;
  var currentGroup;
  var currentArgs;

  var syncTestsRunning;
  var asyncTestsRunning;

  var startTime;
  var runtime;

  // The global context
  var globalContext = typeof global !== 'undefined' ? global : this;

  function package(name, fn) {
    // Set up results if there are none.
    if(!results) {
      results = [];
      syncTestsRunning = true;
      asyncTestsRunning = 0;
      testsStarted();
    }
    var split = name.split(' | ');
    currentPackage = {
      name: split[0],
      subname: split[1],
      assertions: 0,
      failures: []
    };
    // Run the tests...
    try {
      fn.call();
    } catch(e) {
      console.info(e.stack);
    }
    results.push(currentPackage);
    currentPackage = null;
  }

  function async(fn) {
    asyncTestsRunning++;
    setTimeout(wrapTestingScope(function() {
      fn(wrapTestingScope, asyncFinish);
    }), 1);
  }

  function asyncFinish() {
    asyncTestsRunning--;
    checkCanFinish();
  }

  function checkCanFinish() {
    if(!syncTestsRunning && asyncTestsRunning === 0) {
      testsFinished();
    }
  }

  function syncTestsFinished() {
    syncTestsRunning = false;
    checkCanFinish();
  }

  function wrapTestingScope(fn) {
    var cachedPackage = currentPackage;
    var cachedMethod = currentMethod;
    var cachedArgs = currentArgs;
    return function () {
      currentPackage = cachedPackage;
      currentMethod = cachedMethod;
      currentArgs = cachedArgs;
      fn.apply(this, arguments);
    }
  }

  function method(name) {
    var fn = arguments[arguments.length - 1];
    if(arguments.length === 3) {
      currentArgs = arguments[1];
    } else {
      currentArgs = [];
    }
    currentMethod = name;
    fn(async);
    currentMethod = null;
    currentArgs   = [];
  }

  function group(name, fn) {
    currentGroup = name;
    fn();
    currentGroup = null;
  }

  function test(subject) {
    var args, expected, message;
    switch(arguments.length) {
      case 2:
        expected = arguments[1];
        message  = subject;
        break;
      case 3:
        expected = arguments[1];
        message  = arguments[2];
        break;
      case 4:
        args     = arguments[1];
        expected = arguments[2];
        message  = arguments[3];
        break;
    }
    equal(run(subject, null, args), expected, message);
  }

  function run(subject, method, args) {
    method = method || currentMethod;
    args = args || currentArgs || [];
    if(Sugar.noConflict) {
      if(!subjectIsClass(subject)) {
        args = [subject].concat(Array.prototype.slice.call(args));
      }
      return Sugar[currentPackage.name][method].apply(null, args);
    } else {
      var globalObject = globalContext[currentPackage.name], fn;
      if(subject && subject[method]) {
        // If the method exists on the subject, then it is the target
        // to be called. This is true in normal prototype testing as well
        // as for Sugar defined objects such as Ranges and Extended Objects.
        fn = subject[method];
      } else if(globalObject.prototype[method]) {
        // If the method is defined on the prototype of the global object,
        // then use it instead. This is the case when testing methods not
        // originally defined on the prototype of the subject, such as string
        // methods on numbers, undefined/null, etc.
        fn = globalObject.prototype[method];
      } else {
        // Otherwise assume a class method of the global object.
        fn = globalObject[method];
      }
      return fn.apply(subject, args);
    }
  }

  function getProperty(subject, prop) {
    if(Sugar.noConflict) {
      return Sugar[currentPackage.name][prop];
    } else {
      return subject[prop];
    }
  }

  function subjectIsClass(subject) {
    switch(subject) {
      case Boolean:
      case Number:
      case String:
      case Array:
      case Object:
      case Date:
      case RegExp:
      case Function:
        return true;
    }
    return false;
  }

  function raisesError(fn, message, errorType) {
    var err;
    try {
      fn.call();
    } catch(e) {
      err = e;
    }
    if(!errorType) {
      equal(!!err, true, getFullMessage(message), 1);
    } else {
      equal(err instanceof errorType, true, getFullMessage(message), 1);
    }
  }

  function getFullMessage(tail) {
    var msg = '';
    var title = currentMethod || currentGroup;
    if(title) {
      msg += title;
      if(currentArgs && currentArgs.length > 0) {
        msg += '('+ currentArgs.join(',') +')';
      }
      msg += ' | ';
    }
    msg += tail;
    return msg;
  }

  // Runner methods

  function equal(actual, expected, message, stack) {
    currentPackage.assertions++;
    if(!isEqual(actual, expected)) {
      addFailure(actual, expected, getFullMessage(message), stack);
    }
  }

  function notEqual(actual, expected, message) {
    equal(actual !== expected, true, message + ' | strict equality', 1);
  }

  function testsStarted() {
    if(environment == 'node') {
      console.info('\n----------------------- STARTING TESTS ----------------------------\n');
    }
    startTime = new Date();
  }

  function testsFinished() {
    runtime = new Date() - startTime;
    if(environment == 'node') {
      // will exit now setting the status to the number of failed tests
      process.exit(logResults());
    } else if(this.testsFinishedCallback) {
      this.testsFinishedCallback(results, runtime);
    }
    results = [];
  }

  function logResults() {
    var i, j, failure, totalAssertions = 0, totalFailures = 0;
    for (i = 0; i < results.length; i += 1) {
      totalAssertions += results[i].assertions;
      totalFailures += results[i].failures.length;
      for(j = 0; j < results[i].failures.length; j++) {
        failure = results[i].failures[j];
        console.info('\n'+ (j + 1) + ') Failure:');
        console.info(failure.message);
        console.info('Expected: ' + JSON.stringify(failure.expected) + ' but was: ' + JSON.stringify(failure.actual));
        console.info('File: ' + failure.file + ', Line: ' + failure.line, ' Col: ' + failure.col + '\n');
      }
    };
    var time = (runtime / 1000);
    console.info(results.length + ' tests, ' + totalAssertions + ' assertions, ' + totalFailures + ' failures, ' + time + 's\n');
    return totalFailures;
  }

  function addFailure(actual, expected, message, stack, warning) {
    var meta = getMeta(stack);
    currentPackage.failures.push({
      actual: actual,
      expected: expected,
      message: message,
      file: meta.file,
      line: meta.line,
      col: meta.col,
      warning: !!warning
    });
  }

  function getMeta(stack) {
    var level = 4;
    if(stack !== undefined) {
      level += stack;
    }
    var e = new Error();
    if(!e.stack) {
      return {};
    }
    var s = e.stack.split(/@|^\s+at/m);
    var match = s[level].match(/(.+\.js):(\d+)(?::(\d+))?/);
    if(!match) match = [];
    return { file: match[1], line: match[2], col: match[3] };
  }

  // Equality test methods.

  function isEqual(one, two) {
    var type, klass;

    type = typeof one;

    if(type === 'string' || type === 'boolean' || one == null) {
      return one === two;
    } else if(type === 'number') {
      return typeof two === 'number' && ((isNaN(one) && isNaN(two)) || one === two);
    }

    klass = Object.prototype.toString.call(one);

    if(klass === '[object Date]') {
      return one.getTime() === two.getTime();
    } else if(klass === '[object RegExp]') {
      return String(one) === String(two);
    } else if(klass === '[object Array]') {
      return arrayIsEqual(one, two);
    } else if((klass === '[object Object]' || klass === '[object Arguments]') && ('hasOwnProperty' in one) && type === 'object') {
      return objectIsEqual(one, two);
    } else if(klass === '[object Number]' && isNaN(one) && isNaN(two)) {
      return true;
    }

    return one === two;
  }

  // Arrays and objects must be treated separately here because in IE arrays with undefined
  // elements will not pass the .hasOwnProperty check. For example [undefined].hasOwnProperty('0')
  // will report false.
  function arrayIsEqual(one, two) {
    var i, result = true;
    if(!one || !two) {
      return false;
    }
    arrayEach(one, function(a, i) {
      if(!isEqual(one[i], two[i])) {
        result = false;
      }
    });
    return result && one.length === two.length;
  }

  function objectIsEqual(one, two) {
    var onep = 0, twop = 0, key;
    if(one && two) {
      for(key in one) {
        if(!one.hasOwnProperty(key)) continue;
        onep++;
        if(!isEqual(one[key], two[key])) {
          return false;
        }
      }
      for(key in two) {
        if(!two.hasOwnProperty(key)) continue;
        twop++;
      }
    }
    return onep === twop && String(one) === String(two);
  }

  function equalWithMargin(actual, expected, margin, message) {
    equal((actual > expected - margin) && (actual < expected + margin), true, message, null, 1);
  }

  // Sets are equal, but order may differ
  function setIsEqual(a, b, message) {
    equal(sortOnStringValue(a), sortOnStringValue(b), message);
  }

  function sortOnStringValue(arr) {
    return arr.sort(function(a, b) {
      var aType = typeof a;
      var bType = typeof b;
      var aVal = String(a);
      var bVal = String(b);
      if(aType != bType) {
        return aType < bType;
      }
      if(aVal === bVal) return 0;
      return a < b ? -1 : 1;
    });
  }

  // Iteration methods

  // Tests may include sparse arrays, so need a way to iterate
  // over them safely here.
  function arrayEach(arr, fn, sparse) {
    var length = arr.length, i = 0;
    while(i < length) {
      if(!(i in arr)) {
        return iterateOverArray(arr, fn, i);
      } else if(fn.call(arr, arr[i], i, arr) === false) {
        break;
      }
      i++;
    }
  }

  function iterateOverArray(arr, fn, fromIndex) {
    var indexes = [], i;
    for(i in arr) {
      if(isArrayIndex(arr, i) && i >= fromIndex) {
        indexes.push(parseInt(i));
      }
    }
    arrayEach(indexes.sort(), function(index) {
      return fn.call(arr, arr[index], index, arr);
    });
    return arr;
  }

  function isArrayIndex(arr, i) {
    return i in arr && (i >>> 0) == i && i != 0xffffffff;
  }

  if(typeof console === 'undefined') {
    var consoleFn = function() {
      var messages = Array.prototype.slice.call(arguments);
      messages = messages.map(function(arg) {
        return String(arg);
      })
      $('<p/>').text(messages.join(',')).appendTo(document.body);
    }
    console = {
      log: consoleFn,
      info: consoleFn
    }
  }

  this.syncTestsFinished = syncTestsFinished;
  this.equalWithMargin = equalWithMargin;
  this.raisesError = raisesError;
  this.getProperty = getProperty;
  this.setIsEqual = setIsEqual;
  this.notEqual = notEqual;
  this.package = package;
  this.method = method;
  this.group = group;
  this.equal = equal;
  this.test = test;
  this.run = run;

})();
<commit_msg>adding "raises and error" to raisesError message<commit_after>if(typeof environment == 'undefined') environment = 'default'; // Override me!

(function() {

  var results;

  var currentPackage;
  var currentMethod;
  var currentGroup;
  var currentArgs;

  var syncTestsRunning;
  var asyncTestsRunning;

  var startTime;
  var runtime;

  // The global context
  var globalContext = typeof global !== 'undefined' ? global : this;

  function package(name, fn) {
    // Set up results if there are none.
    if(!results) {
      results = [];
      syncTestsRunning = true;
      asyncTestsRunning = 0;
      testsStarted();
    }
    var split = name.split(' | ');
    currentPackage = {
      name: split[0],
      subname: split[1],
      assertions: 0,
      failures: []
    };
    // Run the tests...
    try {
      fn.call();
    } catch(e) {
      console.info(e.stack);
    }
    results.push(currentPackage);
    currentPackage = null;
  }

  function async(fn) {
    asyncTestsRunning++;
    setTimeout(wrapTestingScope(function() {
      fn(wrapTestingScope, asyncFinish);
    }), 1);
  }

  function asyncFinish() {
    asyncTestsRunning--;
    checkCanFinish();
  }

  function checkCanFinish() {
    if(!syncTestsRunning && asyncTestsRunning === 0) {
      testsFinished();
    }
  }

  function syncTestsFinished() {
    syncTestsRunning = false;
    checkCanFinish();
  }

  function wrapTestingScope(fn) {
    var cachedPackage = currentPackage;
    var cachedMethod = currentMethod;
    var cachedArgs = currentArgs;
    return function () {
      currentPackage = cachedPackage;
      currentMethod = cachedMethod;
      currentArgs = cachedArgs;
      fn.apply(this, arguments);
    }
  }

  function method(name) {
    var fn = arguments[arguments.length - 1];
    if(arguments.length === 3) {
      currentArgs = arguments[1];
    } else {
      currentArgs = [];
    }
    currentMethod = name;
    fn(async);
    currentMethod = null;
    currentArgs   = [];
  }

  function group(name, fn) {
    currentGroup = name;
    fn();
    currentGroup = null;
  }

  function test(subject) {
    var args, expected, message;
    switch(arguments.length) {
      case 2:
        expected = arguments[1];
        message  = subject;
        break;
      case 3:
        expected = arguments[1];
        message  = arguments[2];
        break;
      case 4:
        args     = arguments[1];
        expected = arguments[2];
        message  = arguments[3];
        break;
    }
    equal(run(subject, null, args), expected, message);
  }

  function run(subject, method, args) {
    method = method || currentMethod;
    args = args || currentArgs || [];
    if(Sugar.noConflict) {
      if(!subjectIsClass(subject)) {
        args = [subject].concat(Array.prototype.slice.call(args));
      }
      return Sugar[currentPackage.name][method].apply(null, args);
    } else {
      var globalObject = globalContext[currentPackage.name], fn;
      if(subject && subject[method]) {
        // If the method exists on the subject, then it is the target
        // to be called. This is true in normal prototype testing as well
        // as for Sugar defined objects such as Ranges and Extended Objects.
        fn = subject[method];
      } else if(globalObject.prototype[method]) {
        // If the method is defined on the prototype of the global object,
        // then use it instead. This is the case when testing methods not
        // originally defined on the prototype of the subject, such as string
        // methods on numbers, undefined/null, etc.
        fn = globalObject.prototype[method];
      } else {
        // Otherwise assume a class method of the global object.
        fn = globalObject[method];
      }
      return fn.apply(subject, args);
    }
  }

  function getProperty(subject, prop) {
    if(Sugar.noConflict) {
      return Sugar[currentPackage.name][prop];
    } else {
      return subject[prop];
    }
  }

  function subjectIsClass(subject) {
    switch(subject) {
      case Boolean:
      case Number:
      case String:
      case Array:
      case Object:
      case Date:
      case RegExp:
      case Function:
        return true;
    }
    return false;
  }

  function raisesError(fn, message, errorType) {
    var err, message = getFullMessage(message) + ' should raise an error';
    try {
      fn.call();
    } catch(e) {
      err = e;
    }
    if(!errorType) {
      equal(!!err, true, message, 1);
    } else {
      equal(err instanceof errorType, true, message, 1);
    }
  }

  function getFullMessage(tail) {
    var msg = '';
    var title = currentMethod || currentGroup;
    if(title) {
      msg += title;
      if(currentArgs && currentArgs.length > 0) {
        msg += '('+ currentArgs.join(',') +')';
      }
      msg += ' | ';
    }
    msg += tail;
    return msg;
  }

  // Runner methods

  function equal(actual, expected, message, stack) {
    currentPackage.assertions++;
    if(!isEqual(actual, expected)) {
      addFailure(actual, expected, getFullMessage(message), stack);
    }
  }

  function notEqual(actual, expected, message) {
    equal(actual !== expected, true, message + ' | strict equality', 1);
  }

  function testsStarted() {
    if(environment == 'node') {
      console.info('\n----------------------- STARTING TESTS ----------------------------\n');
    }
    startTime = new Date();
  }

  function testsFinished() {
    runtime = new Date() - startTime;
    if(environment == 'node') {
      // will exit now setting the status to the number of failed tests
      process.exit(logResults());
    } else if(this.testsFinishedCallback) {
      this.testsFinishedCallback(results, runtime);
    }
    results = [];
  }

  function logResults() {
    var i, j, failure, totalAssertions = 0, totalFailures = 0;
    for (i = 0; i < results.length; i += 1) {
      totalAssertions += results[i].assertions;
      totalFailures += results[i].failures.length;
      for(j = 0; j < results[i].failures.length; j++) {
        failure = results[i].failures[j];
        console.info('\n'+ (j + 1) + ') Failure:');
        console.info(failure.message);
        console.info('Expected: ' + JSON.stringify(failure.expected) + ' but was: ' + JSON.stringify(failure.actual));
        console.info('File: ' + failure.file + ', Line: ' + failure.line, ' Col: ' + failure.col + '\n');
      }
    };
    var time = (runtime / 1000);
    console.info(results.length + ' tests, ' + totalAssertions + ' assertions, ' + totalFailures + ' failures, ' + time + 's\n');
    return totalFailures;
  }

  function addFailure(actual, expected, message, stack, warning) {
    var meta = getMeta(stack);
    currentPackage.failures.push({
      actual: actual,
      expected: expected,
      message: message,
      file: meta.file,
      line: meta.line,
      col: meta.col,
      warning: !!warning
    });
  }

  function getMeta(stack) {
    var level = 4;
    if(stack !== undefined) {
      level += stack;
    }
    var e = new Error();
    if(!e.stack) {
      return {};
    }
    var s = e.stack.split(/@|^\s+at/m);
    var match = s[level].match(/(.+\.js):(\d+)(?::(\d+))?/);
    if(!match) match = [];
    return { file: match[1], line: match[2], col: match[3] };
  }

  // Equality test methods.

  function isEqual(one, two) {
    var type, klass;

    type = typeof one;

    if(type === 'string' || type === 'boolean' || one == null) {
      return one === two;
    } else if(type === 'number') {
      return typeof two === 'number' && ((isNaN(one) && isNaN(two)) || one === two);
    }

    klass = Object.prototype.toString.call(one);

    if(klass === '[object Date]') {
      return one.getTime() === two.getTime();
    } else if(klass === '[object RegExp]') {
      return String(one) === String(two);
    } else if(klass === '[object Array]') {
      return arrayIsEqual(one, two);
    } else if((klass === '[object Object]' || klass === '[object Arguments]') && ('hasOwnProperty' in one) && type === 'object') {
      return objectIsEqual(one, two);
    } else if(klass === '[object Number]' && isNaN(one) && isNaN(two)) {
      return true;
    }

    return one === two;
  }

  // Arrays and objects must be treated separately here because in IE arrays with undefined
  // elements will not pass the .hasOwnProperty check. For example [undefined].hasOwnProperty('0')
  // will report false.
  function arrayIsEqual(one, two) {
    var i, result = true;
    if(!one || !two) {
      return false;
    }
    arrayEach(one, function(a, i) {
      if(!isEqual(one[i], two[i])) {
        result = false;
      }
    });
    return result && one.length === two.length;
  }

  function objectIsEqual(one, two) {
    var onep = 0, twop = 0, key;
    if(one && two) {
      for(key in one) {
        if(!one.hasOwnProperty(key)) continue;
        onep++;
        if(!isEqual(one[key], two[key])) {
          return false;
        }
      }
      for(key in two) {
        if(!two.hasOwnProperty(key)) continue;
        twop++;
      }
    }
    return onep === twop && String(one) === String(two);
  }

  function equalWithMargin(actual, expected, margin, message) {
    equal((actual > expected - margin) && (actual < expected + margin), true, message, null, 1);
  }

  // Sets are equal, but order may differ
  function setIsEqual(a, b, message) {
    equal(sortOnStringValue(a), sortOnStringValue(b), message);
  }

  function sortOnStringValue(arr) {
    return arr.sort(function(a, b) {
      var aType = typeof a;
      var bType = typeof b;
      var aVal = String(a);
      var bVal = String(b);
      if(aType != bType) {
        return aType < bType;
      }
      if(aVal === bVal) return 0;
      return a < b ? -1 : 1;
    });
  }

  // Iteration methods

  // Tests may include sparse arrays, so need a way to iterate
  // over them safely here.
  function arrayEach(arr, fn, sparse) {
    var length = arr.length, i = 0;
    while(i < length) {
      if(!(i in arr)) {
        return iterateOverArray(arr, fn, i);
      } else if(fn.call(arr, arr[i], i, arr) === false) {
        break;
      }
      i++;
    }
  }

  function iterateOverArray(arr, fn, fromIndex) {
    var indexes = [], i;
    for(i in arr) {
      if(isArrayIndex(arr, i) && i >= fromIndex) {
        indexes.push(parseInt(i));
      }
    }
    arrayEach(indexes.sort(), function(index) {
      return fn.call(arr, arr[index], index, arr);
    });
    return arr;
  }

  function isArrayIndex(arr, i) {
    return i in arr && (i >>> 0) == i && i != 0xffffffff;
  }

  if(typeof console === 'undefined') {
    var consoleFn = function() {
      var messages = Array.prototype.slice.call(arguments);
      messages = messages.map(function(arg) {
        return String(arg);
      })
      $('<p/>').text(messages.join(',')).appendTo(document.body);
    }
    console = {
      log: consoleFn,
      info: consoleFn
    }
  }

  this.syncTestsFinished = syncTestsFinished;
  this.equalWithMargin = equalWithMargin;
  this.raisesError = raisesError;
  this.getProperty = getProperty;
  this.setIsEqual = setIsEqual;
  this.notEqual = notEqual;
  this.package = package;
  this.method = method;
  this.group = group;
  this.equal = equal;
  this.test = test;
  this.run = run;

})();
<|endoftext|>


<commit_before>angular.module('anol.getfeatureinfo')
/**
 * @ngdoc directive
 * @name anol.getfeatureinfo.directive:anolGetFeatureInfo
 *
 * @restrict A
 * @requires $http
 * @required $window
 * @requires anol.map.MapService
 * @requires anol.map.LayersService
 * @requires anol.map.ControlsService
 *
 * @description
 * Makes GetFeatureInfo request on all non vector layers with 'featureinfo' property
 * and show result if not empty depending on 'target' specified in 'featureinfo'
 *
 * @param {function} customTargetFilled Callback called after featureinfo result added to custom element
 * @param {string} templateUrl Url to template to use instead of default one
 * @param {function} beforeRequest Callback called before featureinfo requests are fulfilled
 * @param {string} proxyUrl Url for proxy to use for requests.
                            When proxyUrl is used, name of requested anol layer
 *                          is appended as path to proxyUrl. E.g.: proxyUrl = '/foo', for layer with name 'bar' requested url is '/foo/bar/'.
 *                          Also only url params are submitted.
 *
 * Layer property **featureinfo** - {Object} - Contains properties:
 * - **target** - {string} - Target for featureinfo result. ('_blank', '_popup', [element-id])
 */
.directive('anolGetFeatureInfo', [
    '$http', '$window', 'MapService', 'LayersService', 'ControlsService',
    function($http, $window, MapService, LayersService, ControlsService) {
    return {
        restrict: 'A',
        replace: true,
        scope: {
            customTargetFilled: '&',
            beforeRequest: '&',
            proxyUrl: '@'
        },
        templateUrl: function(tElement, tAttrs) {
            var defaultUrl = 'src/modules/getfeatureinfo/templates/popup.html';
            return tAttrs.templateUrl || defaultUrl;
        },
        link: {
            pre: function(scope, element) {
                scope.map = MapService.getMap();
                // get callback from wrapper function
                scope.customTargetCallback = scope.customTargetFilled();
                scope.beforeRequest = scope.beforeRequest();
                var view = scope.map.getView();
                var popupContent = element.find('.anol-popup-content');
                var popupOverlay = new ol.Overlay({
                    element: element.context,
                    autoPan: true,
                    autoPanAnimation: {
                        duration: 250
                    }
                });

                if(angular.isDefined(scope.proxyUrl) && !scope.proxyUrl.endsWith('/')) {
                    scope.proxyUrl += '/';
                }

                scope.map.addOverlay(popupOverlay);

                scope.close = function() {
                    element.css('display', 'none');
                };

                scope.handleClick = function(evt) {
                    var viewResolution = view.getResolution();
                    var coordinate = evt.coordinate;
                    var divTargetCleared = false;

                    element.css('display', 'none');
                    popupContent.empty();

                    if(angular.isFunction(scope.beforeRequest)) {
                        scope.beforeRequest();
                    }

                    angular.forEach(LayersService.flattedLayers, function(layer) {
                        if(!layer.getVisible()) {
                            return;
                        }
                        if(layer.olLayer instanceof ol.layer.Vector) {
                            return;
                        }
                        if(!layer.featureinfo) {
                            return;
                        }

                        // TODO do not request each layer seperate
                        var params = layer.olLayer.getSource().getParams();
                        var queryLayers = params.layers || params.LAYERS;
                        queryLayers = queryLayers.split(',');

                        var url = layer.olLayer.getSource().getGetFeatureInfoUrl(
                            coordinate, viewResolution, view.getProjection(),
                            {
                                'INFO_FORMAT': 'text/html',
                                'QUERY_LAYERS': queryLayers
                            }
                        );
                        if(angular.isDefined(scope.proxyUrl)) {
                            url = scope.proxyUrl + layer.name + '/?' + url.split('?')[1];
                        }
                        if(angular.isDefined(url)) {
                            $http.get(url).success(function(response) {
                                if(angular.isString(response) && response !== '' && !response.startsWith('<?xml')) {
                                    var iframe;
                                    if(layer.featureinfo.target === '_popup') {
                                        iframe = $('');
                                    }
                                    switch(layer.featureinfo.target) {
                                        case '_blank':
                                            $window.open(url, '_blank');
                                        break;
                                        case '_popup':
                                            popupContent.append(iframe);
                                            if(element.css('display') === 'none') {
                                                element.css('display', '');
                                                popupOverlay.setPosition(coordinate);
                                            }
                                        break;
                                        default:
                                            var temp = $('

');
                                            var target = angular.element(layer.featureinfo.target);
                                            if(divTargetCleared === false) {
                                                target.empty();
                                                divTargetCleared = true;
                                            }
                                            var content = angular.element(response);
                                            temp.append(content);
                                            temp.find('meta').remove();
                                            temp.find('link').remove();
                                            temp.find('title').remove();
                                            temp.find('script').remove();
                                            target.append(temp.children());
                                            if(angular.isFunction(scope.customTargetCallback)) {
                                                scope.customTargetCallback();
                                            }
                                        break;
                                    }
                                }
                            });
                        }
                    });
                };
            },
            post: function(scope) {
                var handlerKey;
                var control = new anol.control.Control({
                    subordinate: true,
                    olControl: null
                });
                control.onDeactivate(function() {
                    scope.map.unByKey(handlerKey);
                });
                control.onActivate(function() {
                    handlerKey = scope.map.on('singleclick', scope.handleClick, this);
                });

                control.activate();

                ControlsService.addControl(control);
            }
        }
    };
}]);
<commit_msg>Removed todo<commit_after>angular.module('anol.getfeatureinfo')
/**
 * @ngdoc directive
 * @name anol.getfeatureinfo.directive:anolGetFeatureInfo
 *
 * @restrict A
 * @requires $http
 * @required $window
 * @requires anol.map.MapService
 * @requires anol.map.LayersService
 * @requires anol.map.ControlsService
 *
 * @description
 * Makes GetFeatureInfo request on all non vector layers with 'featureinfo' property
 * and show result if not empty depending on 'target' specified in 'featureinfo'
 *
 * @param {function} customTargetFilled Callback called after featureinfo result added to custom element
 * @param {string} templateUrl Url to template to use instead of default one
 * @param {function} beforeRequest Callback called before featureinfo requests are fulfilled
 * @param {string} proxyUrl Url for proxy to use for requests.
                            When proxyUrl is used, name of requested anol layer
 *                          is appended as path to proxyUrl. E.g.: proxyUrl = '/foo', for layer with name 'bar' requested url is '/foo/bar/'.
 *                          Also only url params are submitted.
 *
 * Layer property **featureinfo** - {Object} - Contains properties:
 * - **target** - {string} - Target for featureinfo result. ('_blank', '_popup', [element-id])
 */
.directive('anolGetFeatureInfo', [
    '$http', '$window', 'MapService', 'LayersService', 'ControlsService',
    function($http, $window, MapService, LayersService, ControlsService) {
    return {
        restrict: 'A',
        replace: true,
        scope: {
            customTargetFilled: '&',
            beforeRequest: '&',
            proxyUrl: '@'
        },
        templateUrl: function(tElement, tAttrs) {
            var defaultUrl = 'src/modules/getfeatureinfo/templates/popup.html';
            return tAttrs.templateUrl || defaultUrl;
        },
        link: {
            pre: function(scope, element) {
                scope.map = MapService.getMap();
                // get callback from wrapper function
                scope.customTargetCallback = scope.customTargetFilled();
                scope.beforeRequest = scope.beforeRequest();
                var view = scope.map.getView();
                var popupContent = element.find('.anol-popup-content');
                var popupOverlay = new ol.Overlay({
                    element: element.context,
                    autoPan: true,
                    autoPanAnimation: {
                        duration: 250
                    }
                });

                if(angular.isDefined(scope.proxyUrl) && !scope.proxyUrl.endsWith('/')) {
                    scope.proxyUrl += '/';
                }

                scope.map.addOverlay(popupOverlay);

                scope.close = function() {
                    element.css('display', 'none');
                };

                scope.handleClick = function(evt) {
                    var viewResolution = view.getResolution();
                    var coordinate = evt.coordinate;
                    var divTargetCleared = false;

                    element.css('display', 'none');
                    popupContent.empty();

                    if(angular.isFunction(scope.beforeRequest)) {
                        scope.beforeRequest();
                    }

                    angular.forEach(LayersService.flattedLayers, function(layer) {
                        if(!layer.getVisible()) {
                            return;
                        }
                        if(layer.olLayer instanceof ol.layer.Vector) {
                            return;
                        }
                        if(!layer.featureinfo) {
                            return;
                        }

                        var params = layer.olLayer.getSource().getParams();
                        var queryLayers = params.layers || params.LAYERS;
                        queryLayers = queryLayers.split(',');

                        var url = layer.olLayer.getSource().getGetFeatureInfoUrl(
                            coordinate, viewResolution, view.getProjection(),
                            {
                                'INFO_FORMAT': 'text/html',
                                'QUERY_LAYERS': queryLayers
                            }
                        );
                        if(angular.isDefined(scope.proxyUrl)) {
                            url = scope.proxyUrl + layer.name + '/?' + url.split('?')[1];
                        }
                        if(angular.isDefined(url)) {
                            $http.get(url).success(function(response) {
                                if(angular.isString(response) && response !== '' && !response.startsWith('<?xml')) {
                                    var iframe;
                                    if(layer.featureinfo.target === '_popup') {
                                        iframe = $('');
                                    }
                                    switch(layer.featureinfo.target) {
                                        case '_blank':
                                            $window.open(url, '_blank');
                                        break;
                                        case '_popup':
                                            popupContent.append(iframe);
                                            if(element.css('display') === 'none') {
                                                element.css('display', '');
                                                popupOverlay.setPosition(coordinate);
                                            }
                                        break;
                                        default:
                                            var temp = $('

');
                                            var target = angular.element(layer.featureinfo.target);
                                            if(divTargetCleared === false) {
                                                target.empty();
                                                divTargetCleared = true;
                                            }
                                            var content = angular.element(response);
                                            temp.append(content);
                                            temp.find('meta').remove();
                                            temp.find('link').remove();
                                            temp.find('title').remove();
                                            temp.find('script').remove();
                                            target.append(temp.children());
                                            if(angular.isFunction(scope.customTargetCallback)) {
                                                scope.customTargetCallback();
                                            }
                                        break;
                                    }
                                }
                            });
                        }
                    });
                };
            },
            post: function(scope) {
                var handlerKey;
                var control = new anol.control.Control({
                    subordinate: true,
                    olControl: null
                });
                control.onDeactivate(function() {
                    scope.map.unByKey(handlerKey);
                });
                control.onActivate(function() {
                    handlerKey = scope.map.on('singleclick', scope.handleClick, this);
                });

                control.activate();

                ControlsService.addControl(control);
            }
        }
    };
}]);
<|endoftext|>


<commit_before>'use strict';

/*

CassandraStore is a Cassandra tile storage.
 */

var util = require('util');
var BBPromise = require('bluebird');
var core = require('kartotherian-core');
var cassandra = require('cassandra-driver');
var multistream = require('multistream');
var promistreamus = require('promistreamus');

var prepared = {prepare: true};

BBPromise.promisifyAll(cassandra.Client.prototype);

function CassandraStore(uri, callback) {
    var self = this;
    var params;
    this.batchMode = 0;
    this.batch = [];

    this.throwError = function (msg) {
        throw new Error(util.format.apply(null, arguments) + JSON.stringify(uri));
    };

    this.attachUri = function (err) {
        err.moduleUri = JSON.stringify(uri);
        throw err;
    };

    return BBPromise.try(function () {
        params = core.normalizeUri(uri).query;
        if (!params.cp) {
            self.throwError("Uri must include at least one 'cp' connect point query parameter");
        } else if (typeof params.cp === 'string') {
            self.contactPoints = [params.cp];
        } else {
            self.contactPoints = params.cp;
        }
        if (!params.keyspace || !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.keyspace)) {
            self.throwError("Uri must have a valid 'keyspace' query parameter");
        }
        if (params.table && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.table)) {
            self.throwError("Optional uri 'table' param must be a valid value");
        }
        if (params.repclass && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.repclass)) {
            self.throwError("Uri 'repclass' must be a valid value");
        }
        self.keyspace = params.keyspace;
        self.createIfMissing = !!params.createIfMissing;
        self.table = params.table || 'tiles';
        self.repclass = params.repclass || 'SimpleStrategy';
        self.repfactor = typeof params.repfactor === 'undefined' ? 3 : parseInt(params.repfactor);
        var dw = params.durablewrite;
        self.durablewrite = (typeof dw === 'undefined' || (dw && dw !== 'false' && dw !== '0')) ? 'true' : 'false';
        self.minzoom = typeof params.minzoom === 'undefined' ? 0 : parseInt(params.minzoom);
        self.maxzoom = typeof params.maxzoom === 'undefined' ? 15 : parseInt(params.maxzoom);
        self.blocksize = typeof params.blocksize === 'undefined' ? 32768 : parseInt(params.blocksize);
        self.maxBatchSize = typeof params.maxBatchSize === 'undefined' ? undefined : parseInt(params.maxBatchSize);
        var clientOpts = {contactPoints: self.contactPoints};
        if (params.username || params.password) {
            clientOpts.authProvider = new cassandra.auth.PlainTextAuthProvider(params.username, params.password);
        }
        self.client = new cassandra.Client(clientOpts);
        self.headers = {
            'Content-Type': 'application/x-protobuf',
            'Content-Encoding': 'gzip'
        };
        return self.client.connectAsync();
    }).then(function () {
        if (!self.createIfMissing) {
            return true;
        }
        return self.client.executeAsync(
            "CREATE KEYSPACE IF NOT EXISTS " + self.keyspace +
            " WITH REPLICATION = {'class': '" + self.repclass + "'," +
            " 'replication_factor': " + self.repfactor + "}" +
            " AND DURABLE_WRITES = " + self.durablewrite);
    }).then(function () {
        return self.client.executeAsync("USE " + self.keyspace);
    }).then(function () {
        if (!self.createIfMissing) {
            return true;
        }
        var createTableSql = "CREATE TABLE IF NOT EXISTS " + self.table + " (" +
            " zoom int," +
            (self.blocksize ? " block int," : "") +
            " idx int," +
            " tile blob," +
            (self.blocksize
                ? " PRIMARY KEY ((zoom, block), idx)"
                : " PRIMARY KEY (zoom, idx)") +
            ")";
        return self.client.executeAsync(createTableSql);
    }).catch(function (err) {
        return self.closeAsync().finally(function () {
            throw err;
        });
    }).then(function () {
        var whereClause = ' WHERE zoom = ? AND idx = ?';
        if (self.blocksize)
            whereClause += ' AND block = ?';
        self.queries = {
            getTile: 'SELECT tile FROM ' + self.table + whereClause,
            getWriteTime: 'SELECT WRITETIME(tile) AS wt FROM ' + self.table + whereClause,
            getTileAndWt: 'SELECT tile, WRITETIME(tile) AS wt FROM ' + self.table + whereClause,
            set: 'UPDATE ' + self.table + ' SET tile = ?' + whereClause,
            delete: 'DELETE FROM ' + self.table + whereClause
        };

        return self;
    }).catch(this.attachUri).nodeify(callback);
}

CassandraStore.registerProtocols = function(tilelive) {
    tilelive.protocols['cassandra:'] = CassandraStore;
};

CassandraStore.prototype.getTile = function(z, x, y, callback) {
    var self = this;
    return this.queryTileAsync({zoom: z, idx: core.xyToIndex(x, y)}).then(function(row) {
        if (row) {
            return [row.tile, self.headers];
        } else {
            core.throwNoTile();
        }
    }).catch(this.attachUri).nodeify(callback, {spread: true});
};

CassandraStore.prototype.getInfo = function(callback) {
    callback(null, {
        "bounds": "-180,-85.0511,180,85.0511",
        "center": "0,0,2",
        "description": "",
        "maxzoom": this.maxzoom,
        "minzoom": this.minzoom,
        "name": "cassandra",
        "template": "",
        "version": "1.0.0"
    });
};

CassandraStore.prototype.putTile = function(z, x, y, tile, callback) {
    var self = this;
    BBPromise.try(function() {
        var query, params;
        var idx = core.xyToIndex(x, y);
        if (tile && tile.length > 0) {
            query = self.queries.set;
            params = [tile, z, idx];
        } else {
            query = self.queries.delete;
            params = [z, idx];
        }
        if (self.blocksize)
            params.push(Math.floor(idx / self.blocksize));
        if (!self.batchMode || !self.maxBatchSize) {
            return self.client.executeAsync(query, params, prepared);
        } else {
            self.batch.push({query: query, params: params});
            if (Object.keys(self.batch).length > self.maxBatchSize) {
                return self.flushAsync();
            }
        }
    }).catch(this.attachUri).nodeify(callback);
};

CassandraStore.prototype.close = function(callback) {
    var cl = this.client;
    if (!cl) {
        callback(null);
    } else {
        var self = this;
        BBPromise.try(function () {
            return (self.batchMode && self.maxBatchSize) ? self.flushAsync() : true;
        }).then(function () {
            delete self.client;
            self.batchMode = 0;
            return cl.shutdownAsync();
        }).catch(this.attachUri).nodeify(callback);
    }
};

CassandraStore.prototype.startWriting = function(callback) {
    this.batchMode++;
    callback(null);
};

CassandraStore.prototype.flush = function(callback) {
    var batch = this.batch;
    if (Object.keys(batch).length > 0) {
        this.batch = [];
        this.client
            .batchAsync(batch, prepared)
            .catch(this.attachUri)
            .nodeify(callback);
    } else {
        callback();
    }
};

CassandraStore.prototype.stopWriting = function(callback) {
    var self = this;
    BBPromise.try(function () {
        if (self.batchMode === 0) {
            self.throwError('stopWriting() called more times than startWriting()')
        }
        self.batchMode--;
        return self.flushAsync();
    })
        .catch(this.attachUri)
        .nodeify(callback);
};

CassandraStore.prototype.queryTileAsync = function(options) {
    var self = this, getTile, getWriteTime, getSize;

    return BBPromise.try(function() {
        if (!core.isInteger(options.zoom))
            self.throwError('Options must contain integer zoom parameter. Opts=%j', options);
        if (!core.isInteger(options.idx))
            self.throwError('Options must contain an integer idx parameter. Opts=%j', options);
        var maxEnd = Math.pow(4, options.zoom);
        if (options.idx < 0 || options.idx >= maxEnd)
            self.throwError('Options must satisfy: 0 <= idx < %d. Opts=%j', maxEnd, options);
        getTile = typeof options.getTile === 'undefined' ? true : options.getTile;
        getWriteTime = typeof options.getWriteTime === 'undefined' ? false : options.getWriteTime;
        getSize = typeof options.getSize === 'undefined' ? false : options.getSize;
        var query;
        if (getWriteTime && (getTile || getSize))
            query = self.queries.getTileAndWt;
        else if (getTile || getSize)
            query = self.queries.getTile;
        else if (getWriteTime)
            query = self.queries.getWriteTime;
        else
            self.throwError('Either getTile or getWriteTime or both must be requested. Opts=%j', options);
        var params = [options.zoom, options.idx];
        if (self.blocksize)
            params.push(Math.floor(options.idx / self.blocksize));
        return self.client.executeAsync(query, params, prepared);
    }).then(function(res) {
        if ('rows' in res && res.rows.length === 1) {
            var row = res.rows[0];
            var resp = {};
            if (getTile) resp.tile = row.tile;
            if (getSize) resp.size = row.tile.length; // TODO: Use UDF in the next Cassandra ver
            if (getWriteTime) resp.writeTime = new Date(row.wt / 1000);
            return resp;
        } else {
            return false;
        }
    }).catch(this.attachUri);
};

/**
 * Query database for all tiles that match conditions
 * @param options - an object that must have an integer 'zoom' value.
 * Optional values:
 *  idxFrom - int index to start iteration from (inclusive)
 *  idxBefore - int index to stop iteration at (exclusive)
 *  dateFrom - Date value - return only tiles whose write time is on or after this date (inclusive)
 *  dateBefore - Date value - return only tiles whose write time is before this date (exclusive)
 *  biggerThan - number - return only tiles whose compressed size is bigger than this value (inclusive)
 *  smallerThan - number - return only tiles whose compressed size is smaller than this value (exclusive)
 * @returns {Function} - a function that returns a promise. If promise resolves to undefined, there are no more values
 * in the stream.
 */
CassandraStore.prototype.query = function(options) {
    var self = this,
        dateBefore, dateFrom;

    if (!core.isInteger(options.zoom))
        self.throwError('Options must contain integer zoom parameter. Opts=%j', options);
    if (typeof options.idxFrom !== 'undefined' && !core.isInteger(options.idxFrom))
        self.throwError('Options may contain an integer idxFrom parameter. Opts=%j', options);
    if (typeof options.idxBefore !== 'undefined' && !core.isInteger(options.idxBefore))
        self.throwError('Options may contain an integer idxBefore parameter. Opts=%j', options);
    if (typeof options.dateBefore !== 'undefined' && Object.prototype.toString.call(options.dateBefore) !== '[object Date]')
        self.throwError('Options may contain a Date dateBefore parameter. Opts=%j', options);
    if (typeof options.dateFrom !== 'undefined' && Object.prototype.toString.call(options.dateFrom) !== '[object Date]')
        self.throwError('Options may contain a Date dateFrom parameter. Opts=%j', options);
    if (typeof options.biggerThan !== 'undefined' && typeof options.biggerThan !== 'number')
        self.throwError('Options may contain a biggerThan numeric parameter. Opts=%j', options);
    if ((typeof options.smallerThan !== 'undefined' && typeof options.smallerThan !== 'number') || options.smallerThan <= 0)
        self.throwError('Options may contain a smallerThan numeric parameter that is bigger than 0. Opts=%j', options);
    var maxEnd = Math.pow(4, options.zoom);
    var start = options.idxFrom || 0;
    var end = options.idxBefore || maxEnd;
    if (start > end || end > maxEnd)
        self.throwError('Options must satisfy: idxFrom <= idxBefore <= %d. Opts=%j', maxEnd, options);
    if (options.dateFrom >= options.dateBefore)
        self.throwError('Options must satisfy: dateFrom < dateBefore. Opts=%j', options);
    dateFrom = options.dateFrom ? options.dateFrom.valueOf() * 1000 : false;
    dateBefore = options.dateBefore ? options.dateBefore.valueOf() * 1000 : false;

    var fields = 'idx';
    if (options.getTiles || options.smallerThan || options.biggerThan) {
        // If tile size check is requested, we have to get the whole tile at this point...
        // TODO: in the next Cassandra, UDFs should help with this
        // Optimization - if biggerThan is 0, it will not be used
        fields += ', tile';
    }
    if (dateBefore !== false || dateFrom !== false) {
        fields += ', WRITETIME(tile) AS wt';
    }

    var createStream = function(blockStart, blockEnd) {
        var conds = 'zoom = ?',
            params = [options.zoom];
        if (self.blocksize) {
            conds += ' AND block = ?';
            params.push(Math.floor(blockStart / self.blocksize));
        }
        if (start > blockStart) {
            conds += ' AND idx >= ?';
            params.push(start);
        }
        if (end < blockEnd) {
            conds += ' AND idx < ?';
            params.push(end);
        }
        var query = 'SELECT ' + fields + ' FROM ' + self.table + ' WHERE ' + conds;
        return self.client.stream(query, params, {prepare: true, autoPage: true});
    };

    var ms;
    if (self.blocksize) {
        var blockIdx = Math.floor(start / self.blocksize);
        var toBlockIdx = Math.floor((end - 1) / self.blocksize);
        ms = multistream.obj(function() {
            if (blockIdx > toBlockIdx) return false;
            var s = createStream(blockIdx * self.blocksize,
                Math.min(maxEnd, (blockIdx + 1) * self.blocksize));
            blockIdx++;
            return s;
        });
    } else {
        ms = createStream(0, maxEnd);
    }

    return promistreamus(ms, function(value) {
        if ((dateBefore !== false && value.wt >= dateBefore) ||
            (dateFrom !== false && value.wt < dateFrom) ||
            (options.smallerThan && value.tile.length >= options.smallerThan) ||
            (options.biggerThan && value.tile.length < options.biggerThan)
        ) {
            return undefined;
        }
        var res = {
            zoom: options.zoom,
            idx: value.idx
        };
        if (options.getTiles) {
            res.tile = value.tile;
        }
        return res;
    });
};

BBPromise.promisifyAll(CassandraStore.prototype);
module.exports = CassandraStore;
<commit_msg>info storage in cassandra<commit_after>'use strict';

/*

CassandraStore is a Cassandra tile storage.
 */

var util = require('util');
var BBPromise = require('bluebird');
var core = require('kartotherian-core');
var cassandra = require('cassandra-driver');
var multistream = require('multistream');
var promistreamus = require('promistreamus');

var prepared = {prepare: true};

BBPromise.promisifyAll(cassandra.Client.prototype);

function CassandraStore(uri, callback) {
    var self = this;
    this.batchMode = 0;
    this.batch = [];

    this.throwError = function (msg) {
        throw new Error(util.format.apply(null, arguments) + JSON.stringify(uri));
    };

    this.attachUri = function (err) {
        err.moduleUri = JSON.stringify(self._params);
        throw err;
    };

    return BBPromise.try(function () {
        var params = core.normalizeUri(uri).query;
        self._params = params;

        if (!params.cp) {
            self.throwError("Uri must include at least one 'cp' connect point query parameter");
        } else if (typeof params.cp === 'string') {
            self.contactPoints = [params.cp];
        } else {
            self.contactPoints = params.cp;
        }
        if (!params.keyspace || !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.keyspace)) {
            self.throwError("Uri must have a valid 'keyspace' query parameter");
        }
        if (params.table && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.table)) {
            self.throwError("Optional uri 'table' param must be a valid value");
        }
        if (params.repclass && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(params.repclass)) {
            self.throwError("Uri 'repclass' must be a valid value");
        }
        self.keyspace = params.keyspace;
        self.createIfMissing = !!params.createIfMissing;
        self.table = params.table || 'tiles';
        self.repclass = params.repclass || 'SimpleStrategy';
        self.repfactor = typeof params.repfactor === 'undefined' ? 3 : parseInt(params.repfactor);
        var dw = params.durablewrite;
        self.durablewrite = (typeof dw === 'undefined' || (dw && dw !== 'false' && dw !== '0')) ? 'true' : 'false';
        self.minzoom = typeof params.minzoom === 'undefined' ? 0 : parseInt(params.minzoom);
        self.maxzoom = typeof params.maxzoom === 'undefined' ? 15 : parseInt(params.maxzoom);
        self.blocksize = typeof params.blocksize === 'undefined' ? 32768 : parseInt(params.blocksize);
        self.maxBatchSize = typeof params.maxBatchSize === 'undefined' ? undefined : parseInt(params.maxBatchSize);
        var clientOpts = {contactPoints: self.contactPoints};
        if (params.username || params.password) {
            clientOpts.authProvider = new cassandra.auth.PlainTextAuthProvider(params.username, params.password);
            // make sure not to expose it in the error reporting
            delete params.password;
        }
        self.client = new cassandra.Client(clientOpts);
        self.headers = {
            'Content-Type': 'application/x-protobuf',
            'Content-Encoding': 'gzip'
        };
        return self.client.connectAsync();
    }).then(function () {
        if (!self.createIfMissing) {
            return true;
        }
        return self.client.executeAsync(
            "CREATE KEYSPACE IF NOT EXISTS " + self.keyspace +
            " WITH REPLICATION = {'class': '" + self.repclass + "'," +
            " 'replication_factor': " + self.repfactor + "}" +
            " AND DURABLE_WRITES = " + self.durablewrite);
    }).then(function () {
        return self.client.executeAsync("USE " + self.keyspace);
    }).then(function () {
        if (!self.createIfMissing) {
            return true;
        }
        var createTableSql = "CREATE TABLE IF NOT EXISTS " + self.table + " (" +
            " zoom int," +
            (self.blocksize ? " block int," : "") +
            " idx int," +
            " tile blob," +
            (self.blocksize
                ? " PRIMARY KEY ((zoom, block), idx)"
                : " PRIMARY KEY (zoom, idx)") +
            ")";
        return self.client.executeAsync(createTableSql);
    }).catch(function (err) {
        return self.closeAsync().finally(function () {
            throw err;
        });
    }).then(function () {
        var whereClause = ' WHERE zoom = ? AND idx = ?';
        if (self.blocksize)
            whereClause += ' AND block = ?';
        self.queries = {
            getTile: 'SELECT tile FROM ' + self.table + whereClause,
            getWriteTime: 'SELECT WRITETIME(tile) AS wt FROM ' + self.table + whereClause,
            getTileAndWt: 'SELECT tile, WRITETIME(tile) AS wt FROM ' + self.table + whereClause,
            set: 'UPDATE ' + self.table + ' SET tile = ?' + whereClause,
            delete: 'DELETE FROM ' + self.table + whereClause
        };

        return self;
    }).catch(this.attachUri).nodeify(callback);
}

CassandraStore.registerProtocols = function(tilelive) {
    tilelive.protocols['cassandra:'] = CassandraStore;
};

CassandraStore.prototype.getTile = function(z, x, y, callback) {
    var self = this;
    return this.queryTileAsync({zoom: z, idx: core.xyToIndex(x, y, z)}).then(function(row) {
        if (row) {
            return [row.tile, self.headers];
        } else {
            core.throwNoTile();
        }
    }).catch(this.attachUri).nodeify(callback, {spread: true});
};

CassandraStore.prototype.getInfo = function(callback) {
    var self = this;
    return this.queryTileAsync({info: true}).then(function (row) {
        if (row) {
            return JSON.parse(row.tile.toString());
        } else {
            return {
                "bounds": "-180,-85.0511,180,85.0511",
                "center": "0,0,2",
                "description": "",
                "maxzoom": self.maxzoom,
                "minzoom": self.minzoom,
                "name": "cassandra",
                "template": "",
                "version": "1.0.0"
            };
        }
    }).catch(this.attachUri).nodeify(callback);
};

CassandraStore.prototype.putTile = function(z, x, y, tile, callback) {
    return this._storeDataAsync(z, core.xyToIndex(x, y, z), tile).nodeify(callback);
};

CassandraStore.prototype.putInfo = function(data, callback) {
    // hack: Store source info under zoom 0 with ID 1 (which is invalid for tiles)
    return this._storeDataAsync(0, 1, new Buffer(JSON.stringify(data))).nodeify(callback)
};

CassandraStore.prototype._storeDataAsync = function(zoom, idx, data) {
    var self = this;
    return BBPromise.try(function () {
        var query, params;
        if (data && data.length > 0) {
            query = self.queries.set;
            params = [data, zoom, idx];
        } else {
            query = self.queries.delete;
            params = [zoom, idx];
        }
        if (self.blocksize)
            params.push(Math.floor(idx / self.blocksize));
        if (!self.batchMode || !self.maxBatchSize) {
            return self.client.executeAsync(query, params, prepared);
        } else {
            self.batch.push({query: query, params: params});
            if (Object.keys(self.batch).length > self.maxBatchSize) {
                return self.flushAsync();
            }
        }
    }).catch(this.attachUri);
};

CassandraStore.prototype.close = function(callback) {
    var cl = this.client;
    if (!cl) {
        callback(null);
    } else {
        var self = this;
        BBPromise.try(function () {
            return (self.batchMode && self.maxBatchSize) ? self.flushAsync() : true;
        }).then(function () {
            delete self.client;
            self.batchMode = 0;
            return cl.shutdownAsync();
        }).catch(this.attachUri).nodeify(callback);
    }
};

CassandraStore.prototype.startWriting = function(callback) {
    this.batchMode++;
    callback(null);
};

CassandraStore.prototype.flush = function(callback) {
    var batch = this.batch;
    if (Object.keys(batch).length > 0) {
        this.batch = [];
        this.client
            .batchAsync(batch, prepared)
            .catch(this.attachUri)
            .nodeify(callback);
    } else {
        callback();
    }
};

CassandraStore.prototype.stopWriting = function(callback) {
    var self = this;
    BBPromise.try(function () {
        if (self.batchMode === 0) {
            self.throwError('stopWriting() called more times than startWriting()')
        }
        self.batchMode--;
        return self.flushAsync();
    })
        .catch(this.attachUri)
        .nodeify(callback);
};

CassandraStore.prototype.queryTileAsync = function(options) {
    var self = this, getTile, getWriteTime, getSize;

    return BBPromise.try(function() {
        if (options.info) {
            options.zoom = 0;
            options.idx = 1;
        } else {
            if (!core.isInteger(options.zoom))
                self.throwError('Options must contain integer zoom parameter. Opts=%j', options);
            if (!core.isInteger(options.idx))
                self.throwError('Options must contain an integer idx parameter. Opts=%j', options);
            var maxEnd = Math.pow(4, options.zoom);
            if (options.idx < 0 || options.idx >= maxEnd)
                self.throwError('Options must satisfy: 0 <= idx < %d. Opts=%j', maxEnd, options);
        }
        getTile = typeof options.getTile === 'undefined' ? true : options.getTile;
        getWriteTime = typeof options.getWriteTime === 'undefined' ? false : options.getWriteTime;
        getSize = typeof options.getSize === 'undefined' ? false : options.getSize;
        var query;
        if (getWriteTime && (getTile || getSize))
            query = self.queries.getTileAndWt;
        else if (getTile || getSize)
            query = self.queries.getTile;
        else if (getWriteTime)
            query = self.queries.getWriteTime;
        else
            self.throwError('Either getTile or getWriteTime or both must be requested. Opts=%j', options);
        var params = [options.zoom, options.idx];
        if (self.blocksize)
            params.push(Math.floor(options.idx / self.blocksize));
        return self.client.executeAsync(query, params, prepared);
    }).then(function(res) {
        if ('rows' in res && res.rows.length === 1) {
            var row = res.rows[0];
            var resp = {};
            if (getTile) resp.tile = row.tile;
            if (getSize) resp.size = row.tile.length; // TODO: Use UDF in the next Cassandra ver
            if (getWriteTime) resp.writeTime = new Date(row.wt / 1000);
            return resp;
        } else {
            return false;
        }
    }).catch(this.attachUri);
};

/**
 * Query database for all tiles that match conditions
 * @param options - an object that must have an integer 'zoom' value.
 * Optional values:
 *  idxFrom - int index to start iteration from (inclusive)
 *  idxBefore - int index to stop iteration at (exclusive)
 *  dateFrom - Date value - return only tiles whose write time is on or after this date (inclusive)
 *  dateBefore - Date value - return only tiles whose write time is before this date (exclusive)
 *  biggerThan - number - return only tiles whose compressed size is bigger than this value (inclusive)
 *  smallerThan - number - return only tiles whose compressed size is smaller than this value (exclusive)
 * @returns {Function} - a function that returns a promise. If promise resolves to undefined, there are no more values
 * in the stream.
 */
CassandraStore.prototype.query = function(options) {
    var self = this,
        dateBefore, dateFrom;

    if (!core.isInteger(options.zoom))
        self.throwError('Options must contain integer zoom parameter. Opts=%j', options);
    if (typeof options.idxFrom !== 'undefined' && !core.isInteger(options.idxFrom))
        self.throwError('Options may contain an integer idxFrom parameter. Opts=%j', options);
    if (typeof options.idxBefore !== 'undefined' && !core.isInteger(options.idxBefore))
        self.throwError('Options may contain an integer idxBefore parameter. Opts=%j', options);
    if (typeof options.dateBefore !== 'undefined' && Object.prototype.toString.call(options.dateBefore) !== '[object Date]')
        self.throwError('Options may contain a Date dateBefore parameter. Opts=%j', options);
    if (typeof options.dateFrom !== 'undefined' && Object.prototype.toString.call(options.dateFrom) !== '[object Date]')
        self.throwError('Options may contain a Date dateFrom parameter. Opts=%j', options);
    if (typeof options.biggerThan !== 'undefined' && typeof options.biggerThan !== 'number')
        self.throwError('Options may contain a biggerThan numeric parameter. Opts=%j', options);
    if ((typeof options.smallerThan !== 'undefined' && typeof options.smallerThan !== 'number') || options.smallerThan <= 0)
        self.throwError('Options may contain a smallerThan numeric parameter that is bigger than 0. Opts=%j', options);
    var maxEnd = Math.pow(4, options.zoom);
    var start = options.idxFrom || 0;
    var end = options.idxBefore || maxEnd;
    if (start > end || end > maxEnd)
        self.throwError('Options must satisfy: idxFrom <= idxBefore <= %d. Opts=%j', maxEnd, options);
    if (options.dateFrom >= options.dateBefore)
        self.throwError('Options must satisfy: dateFrom < dateBefore. Opts=%j', options);
    dateFrom = options.dateFrom ? options.dateFrom.valueOf() * 1000 : false;
    dateBefore = options.dateBefore ? options.dateBefore.valueOf() * 1000 : false;

    var fields = 'idx';
    if (options.getTiles || options.smallerThan || options.biggerThan) {
        // If tile size check is requested, we have to get the whole tile at this point...
        // TODO: in the next Cassandra, UDFs should help with this
        // Optimization - if biggerThan is 0, it will not be used
        fields += ', tile';
    }
    if (dateBefore !== false || dateFrom !== false) {
        fields += ', WRITETIME(tile) AS wt';
    }

    var createStream = function(blockStart, blockEnd) {
        var conds = 'zoom = ?',
            params = [options.zoom];
        if (self.blocksize) {
            conds += ' AND block = ?';
            params.push(Math.floor(blockStart / self.blocksize));
        }
        if (start > blockStart) {
            conds += ' AND idx >= ?';
            params.push(start);
        }
        if (end < blockEnd) {
            conds += ' AND idx < ?';
            params.push(end);
        }
        var query = 'SELECT ' + fields + ' FROM ' + self.table + ' WHERE ' + conds;
        return self.client.stream(query, params, {prepare: true, autoPage: true});
    };

    var ms;
    if (self.blocksize) {
        var blockIdx = Math.floor(start / self.blocksize);
        var toBlockIdx = Math.floor((end - 1) / self.blocksize);
        ms = multistream.obj(function() {
            if (blockIdx > toBlockIdx) return false;
            var s = createStream(blockIdx * self.blocksize,
                Math.min(maxEnd, (blockIdx + 1) * self.blocksize));
            blockIdx++;
            return s;
        });
    } else {
        ms = createStream(0, maxEnd);
    }

    return promistreamus(ms, function(value) {
        if ((dateBefore !== false && value.wt >= dateBefore) ||
            (dateFrom !== false && value.wt < dateFrom) ||
            (options.smallerThan && value.tile.length >= options.smallerThan) ||
            (options.biggerThan && value.tile.length < options.biggerThan)
        ) {
            return undefined;
        }
        var res = {
            zoom: options.zoom,
            idx: value.idx
        };
        if (options.getTiles) {
            res.tile = value.tile;
        }
        return res;
    });
};

BBPromise.promisifyAll(CassandraStore.prototype);
module.exports = CassandraStore;
<|endoftext|>