<commit_before>exports.names = ['lastplay'];
exports.hidden = false;
exports.enabled = true;
exports.matchStart = true;
exports.cd_all = 5;
exports.cd_user = 10;
exports.cd_manager = 5;
exports.min_role = PERMISSIONS.RDJ;
exports.handler = function (data) {
    var params = _.rest(data.message.split(' '), 1);
    if (params.length < 1) {
        chatMessage('/me usage: .lastplay username');
        return;
    }

    username = params.join(' ');
    usernameFormatted = S(username).chompLeft('@').s;

    User.find({where: {username: usernameFormatted}}).on('success', function (dbUser) {
        if (dbUser === null) {
            modMessage(data, 'Invalid user specified.');
        } else {
            Play.find({
                where: {user_id: dbUser['id']},
                order: 'updated_at DESC',
                limit: 1
            }).on('success', function(dbPlay) {
                if (dbPlay === null) {
                    modMessage(data, usernameFormatted + ' has not played a song in this room.');
                } else {
                    Song.find({where: {id: dbPlay['song_id']}}).on('success', function(dbSong) {
                        if (dbSong === null) {
                            modMessage(data, usernameFormatted + ' has not played a song in this room.');
                        } else {
                            var song_link = null;
                            if (dbSong['format'] == 1) {
                                song_link = 'https://youtu.be/' + dbSong['cid'];
                                modMessage(data, usernameFormatted + ' last played ' + dbSong['author'] + ' - ' + dbSong['title'] + ' (' + song_link + ') ' + moment.utc(dbPlay['updated_at']).calendar() + ' (' + moment.utc(dbPlay['updated_at']).fromNow() + ')');
                            } else {
                                var client_id = config.apiKeys.soundcloud;
                                request('https://api.soundcloud.com/tracks/'+dbSong['cid']+'.json?client_id='+client_id, function (error, response, body) {
                                    var json_data = JSON.parse(body);
                                    song_link = json_data.permalink_url;
                                    modMessage(data, usernameFormatted + ' last played ' + dbSong['author'] + ' - ' + dbSong['title'] + ' (' + song_link + ') ' + moment.utc(dbPlay['updated_at']).calendar() + ' (' + moment.utc(dbPlay['updated_at']).fromNow() + ')');
                                    });
                            }
                        }
                    });
                }
            });
        }
    });
};
<commit_msg>added .lastplayed as an alias to .lastplay<commit_after>exports.names = ['lastplay', 'lastplayed'];
exports.hidden = false;
exports.enabled = true;
exports.matchStart = true;
exports.cd_all = 5;
exports.cd_user = 10;
exports.cd_manager = 5;
exports.min_role = PERMISSIONS.RDJ;
exports.handler = function (data) {
    var params = _.rest(data.message.split(' '), 1);
    if (params.length < 1) {
        chatMessage('/me usage: .lastplay username');
        return;
    }

    username = params.join(' ');
    usernameFormatted = S(username).chompLeft('@').s;

    User.find({where: {username: usernameFormatted}}).on('success', function (dbUser) {
        if (dbUser === null) {
            modMessage(data, 'Invalid user specified.');
        } else {
            Play.find({
                where: {user_id: dbUser['id']},
                order: 'updated_at DESC',
                limit: 1
            }).on('success', function(dbPlay) {
                if (dbPlay === null) {
                    modMessage(data, usernameFormatted + ' has not played a song in this room.');
                } else {
                    Song.find({where: {id: dbPlay['song_id']}}).on('success', function(dbSong) {
                        if (dbSong === null) {
                            modMessage(data, usernameFormatted + ' has not played a song in this room.');
                        } else {
                            var song_link = null;
                            if (dbSong['format'] == 1) {
                                song_link = 'https://youtu.be/' + dbSong['cid'];
                                modMessage(data, usernameFormatted + ' last played ' + dbSong['author'] + ' - ' + dbSong['title'] + ' (' + song_link + ') ' + moment.utc(dbPlay['updated_at']).calendar() + ' (' + moment.utc(dbPlay['updated_at']).fromNow() + ')');
                            } else {
                                var client_id = config.apiKeys.soundcloud;
                                request('https://api.soundcloud.com/tracks/'+dbSong['cid']+'.json?client_id='+client_id, function (error, response, body) {
                                    var json_data = JSON.parse(body);
                                    song_link = json_data.permalink_url;
                                    modMessage(data, usernameFormatted + ' last played ' + dbSong['author'] + ' - ' + dbSong['title'] + ' (' + song_link + ') ' + moment.utc(dbPlay['updated_at']).calendar() + ' (' + moment.utc(dbPlay['updated_at']).fromNow() + ')');
                                    });
                            }
                        }
                    });
                }
            });
        }
    });
};
<|endoftext|>


<commit_before>const got = require('got');
const fuzz = require('fuzzball');
const { fail, success } = require('../lib/healthchecks');
const { logger } = require('../middleware/logger');

const {
  SPACEX_KEY,
  LAUNCH_LIBRARY_HEALTHCHECK,
  SPACEX_API: API,
} = process.env;
const LAUNCH_LIBRARY_API = 'https://ll.thespacedevs.com/2.1.0/launch/upcoming';

/**
 * Attach Launch Library v2 launch id's to upcoming launches
 * @return {Promise<void>}
 */
module.exports = async () => {
  try {
    const log = {
      name: 'launch-library',
      updated: false,
    };
    const upcomingLaunches = await got.post(`${API}/launches/query`, {
      json: {
        query: {
          upcoming: true,
        },
        options: {
          sort: {
            flight_number: 'asc',
          },
          limit: 1,
        },
      },
      resolveBodyOnly: true,
      responseType: 'json',
      throwHttpErrors: false,
    });
    if (upcomingLaunches.docs.length === 1) {
      const llLaunches = await got(LAUNCH_LIBRARY_API, {
        searchParams: {
          lsp__name: 'SpaceX',
          ordering: 'net',
          format: 'json',
        },
        responseType: 'json',
        throwHttpErrors: false,
      });
      if (llLaunches.statusCode === 200 && llLaunches.body.results.length) {
        const upcomingLaunch = upcomingLaunches.docs[0];
        const choices = llLaunches.body.results.map((result) => result.name.split(' | ')[1]);
        const results = fuzz.extract(upcomingLaunch.name, choices);
        if (results.length) {
          const launchLibraryId = llLaunches.body.results[results[0][2]].id;
          await got.patch(`${API}/launches/${upcomingLaunch.id}`, {
            json: {
              launch_library_id: launchLibraryId,
            },
            headers: {
              'spacex-key': SPACEX_KEY,
            },
          });
          [[, log.ratio]] = results;
          log.spacexdataName = upcomingLaunch.name;
          [[log.llName]] = results;
          log.launch_library_id = launchLibraryId;
          log.updated = true;
        }
      }
    }
    await success(LAUNCH_LIBRARY_HEALTHCHECK, log);
    logger.info(log);
  } catch (error) {
    const formatted = {
      name: 'launch-library',
      error: error.message,
      stack: error.stack,
    };
    await fail(LAUNCH_LIBRARY_HEALTHCHECK, formatted);
    logger.error(formatted);
  }
};
<commit_msg>Only update LL id if it doesn't exist<commit_after>const got = require('got');
const fuzz = require('fuzzball');
const { fail, success } = require('../lib/healthchecks');
const { logger } = require('../middleware/logger');

const {
  SPACEX_KEY,
  LAUNCH_LIBRARY_HEALTHCHECK,
  SPACEX_API: API,
} = process.env;
const LAUNCH_LIBRARY_API = 'https://ll.thespacedevs.com/2.1.0/launch/upcoming';

/**
 * Attach Launch Library v2 launch id's to upcoming launches
 * @return {Promise<void>}
 */
module.exports = async () => {
  try {
    const log = {
      name: 'launch-library',
      updated: false,
    };
    const upcomingLaunches = await got.post(`${API}/launches/query`, {
      json: {
        query: {
          upcoming: true,
        },
        options: {
          sort: {
            flight_number: 'asc',
          },
          limit: 1,
        },
      },
      resolveBodyOnly: true,
      responseType: 'json',
      throwHttpErrors: false,
    });
    if (upcomingLaunches.docs.length === 1) {
      const llLaunches = await got(LAUNCH_LIBRARY_API, {
        searchParams: {
          lsp__name: 'SpaceX',
          ordering: 'net',
          format: 'json',
        },
        responseType: 'json',
        throwHttpErrors: false,
      });
      if (llLaunches.statusCode === 200 && llLaunches.body.results.length) {
        const upcomingLaunch = upcomingLaunches.docs[0];
        if (!upcomingLaunch?.launch_library_id) {
          const choices = llLaunches.body.results.map((result) => result.name.split(' | ')[1]);
          const results = fuzz.extract(upcomingLaunch.name, choices);
          if (results.length) {
            const launchLibraryId = llLaunches.body.results[results[0][2]].id;
            await got.patch(`${API}/launches/${upcomingLaunch.id}`, {
              json: {
                launch_library_id: launchLibraryId,
              },
              headers: {
                'spacex-key': SPACEX_KEY,
              },
            });
            [[, log.ratio]] = results;
            log.spacexdataName = upcomingLaunch.name;
            [[log.llName]] = results;
            log.launch_library_id = launchLibraryId;
            log.updated = true;
          }
        }
      }
    }
    await success(LAUNCH_LIBRARY_HEALTHCHECK, log);
    logger.info(log);
  } catch (error) {
    const formatted = {
      name: 'launch-library',
      error: error.message,
      stack: error.stack,
    };
    await fail(LAUNCH_LIBRARY_HEALTHCHECK, formatted);
    logger.error(formatted);
  }
};
<|endoftext|>


<commit_before>var app = getApp()
var socketOpen = false;//WebSocket 打开状态
Page({
  data: {
    messageTip: '',
    messageTextArr:[],
    messageContent:'',
    userinfo:{}
  },
  //sendMessage
  formSubmit: function(e) {
    var that = this;
    console.log('formSubmit',e);
    var msg =  e.detail.value.messageContent;//获得输入文字
    console.log('send message:' +msg );
     if (socketOpen) {

       //如果使用Senparc.WebSocket，必须严格按照以下data数据字段发送（只能多不能少）
       var submitData = JSON.stringify({
          Messsage:msg,//必填
          SessionId:wx.getStorageSync("sessionId"),//选填，不需要可输入''
          FormId:e.detail.formId//选填formId用于发送模板消息，不需要可输入''
        });

      wx.sendSocketMessage({
        data:submitData
      });
      that.setData({
        messageContent:''
      })
    } else {
      that.setData({
        messageTip:'WebSocket 链接失败，请重新连接！'
      })
    }
  },
  onLoad: function () {
    console.log('onLoad')
    var that = this

    //连接 Websocket
    wx.connectSocket({
      url: 'wss://sdk.weixin.senparc.com/SenparcWebSocket',
      header:{
        'content-type': 'application/json'
      },
      method:"GET"
    });
//WebSocket 连接成功
wx.onSocketOpen(function(res) {
  console.log('WebSocket 连接成功！')
  socketOpen = true;
  that.setData({
    messageTip:'WebSocket 连接成功！'
  })
    })
//收到 WebSocket 推送消息
wx.onSocketMessage(function(res) {
  console.log('收到服务器内容：' + res.data)
  var jsonResult = JSON.parse(res.data);
  var currentIndex= that.data.messageTextArr.length+1;
  var newArr = that.data.messageTextArr;
  newArr.push(
    {
      index:currentIndex,
      content:jsonResult.content,
      time:jsonResult.time
    });
    console.log(that);
  that.setData({
    messageTextArr:newArr
  });
})
    //WebSocket 已关闭
    wx.onSocketClose(function(res) {
      console.log('WebSocket 已关闭！')
      socketOpen = false;
    })
    //WebSocket 打开失败
    wx.onSocketError(function(res){
      console.log('WebSocket连接打开失败，请检查！')
    })
  }
})
<commit_msg>调试WebSocket<commit_after>var app = getApp()
var socketOpen = false;//WebSocket 打开状态
Page({
  data: {
    messageTip: '',
    messageTextArr:[],
    messageContent:'TEST',
    userinfo:{}
  },
  //sendMessage
  formSubmit: function(e) {
    var that = this;
    console.log('formSubmit',e);
    var msg =  e.detail.value.messageContent;//获得输入文字
    console.log('send message:' +msg );
     if (socketOpen) {

       //如果使用Senparc.WebSocket，必须严格按照以下data数据字段发送（只能多不能少）
       var submitData = JSON.stringify({
          Messsage:msg,//必填
          SessionId:wx.getStorageSync("sessionId"),//选填，不需要可输入''
          FormId:e.detail.formId//选填formId用于发送模板消息，不需要可输入''
        });

      wx.sendSocketMessage({
        data:submitData
      });
      that.setData({
        messageContent:''
      })
    } else {
      that.setData({
        messageTip:'WebSocket 链接失败，请重新连接！'
      })
    }
  },
  onLoad: function () {
    console.log('onLoad')
    var that = this

    //连接 Websocket
    wx.connectSocket({
      url: 'wss://sdk.weixin.senparc.com/SenparcWebSocket',
      header:{
        'content-type': 'application/json'
      },
      method:"GET"
    });
//WebSocket 连接成功
wx.onSocketOpen(function(res) {
  console.log('WebSocket 连接成功！')
  socketOpen = true;
  that.setData({
    messageTip:'WebSocket 连接成功！'
  })
    })
//收到 WebSocket 推送消息
wx.onSocketMessage(function(res) {
  console.log('收到服务器内容：' + res.data)
  var jsonResult = JSON.parse(res.data);
  var currentIndex= that.data.messageTextArr.length+1;
  var newArr = that.data.messageTextArr;
  newArr.unshift(
    {
      index:currentIndex,
      content:jsonResult.content,
      time:jsonResult.time
    });
    console.log(that);
  that.setData({
    messageTextArr:newArr
  });
})
    //WebSocket 已关闭
    wx.onSocketClose(function(res) {
      console.log('WebSocket 已关闭！')
      socketOpen = false;
    })
    //WebSocket 打开失败
    wx.onSocketError(function(res){
      console.log('WebSocket连接打开失败，请检查！')
    })
  }
})
<|endoftext|>


<commit_before>// Copyright 2020, University of Colorado Boulder

/**
 * A HomeScreenButton is displayed on the HomeScreen for choosing a screen. The button can be in a selected or
 * unselected state - it's large with a yellow frame in its selected state, and small in its unselected state.
 * Selecting the button when in its "selected" state will result in that screen being chosen.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */

import BooleanProperty from '../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../axon/js/DerivedProperty.js';
import Utils from '../../dot/js/Utils.js';
import Shape from '../../kite/js/Shape.js';
import merge from '../../phet-core/js/merge.js';
import PhetColorScheme from '../../scenery-phet/js/PhetColorScheme.js';
import PhetFont from '../../scenery-phet/js/PhetFont.js';
import Touch from '../../scenery/js/input/Touch.js';
import FireListener from '../../scenery/js/listeners/FireListener.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Text from '../../scenery/js/nodes/Text.js';
import VBox from '../../scenery/js/nodes/VBox.js';
import EventType from '../../tandem/js/EventType.js';
import Tandem from '../../tandem/js/Tandem.js';
import Frame from './Frame.js';
import joist from './joist.js';
import joistStrings from './joist-strings.js';

// constants
const simScreenString = joistStrings.a11y.simScreen;
const LARGE_ICON_HEIGHT = 140;

class HomeScreenButton extends VBox {

  /**
   * @param {Screen} screen
   * @param {HomeScreenModel} homeScreenModel
   * @param {Object} [options]
   * @constructor
   */
  constructor( screen, homeScreenModel, options ) {
    options = merge( {
      cursor: 'pointer',
      resize: false, // don't resize the VBox or it will shift down when the border becomes thicker
      showUnselectedHomeScreenIconFrame: false, // put a frame around unselected home screen icons

      // a11y
      tagName: 'button',
      appendDescription: true,
      containerTagName: 'li',

      // phet-io
      tandem: Tandem.REQUIRED,
      phetioEventType: EventType.USER,
      phetioDocumentation: 'A button on the home screen for choosing a simulation screen'
    }, options );

    // derives a boolean value from homeScreenModel.selectedScreenProperty that says if this button is selected on the
    // home screen.
    const isSelectedProperty = new DerivedProperty( [ homeScreenModel.selectedScreenProperty ], selectedScreen => {
      return selectedScreen === screen;
    } );

    // true if this button has focus or mouseover
    const isHighlightedProperty = new BooleanProperty( false );

    // maps the number of screens to a scale for the small icons. The scale is percentage of LARGE_ICON_HEIGHT.
    let smallIconScale = Utils.linear( 2, 4, 0.875, 0.50, homeScreenModel.simScreens.length );
    if ( homeScreenModel.simScreens.length >= 5 ) {
      smallIconScale = 0.4;
    }
    const smallIconHeight = smallIconScale * LARGE_ICON_HEIGHT;

    // create an icon for each size
    const smallIcon = new Node( {
      children: [ screen.homeScreenIcon ],
      scale: smallIconHeight / screen.homeScreenIcon.height
    } );
    const largeIcon = new Node( {
      children: [ screen.homeScreenIcon ],
      scale: LARGE_ICON_HEIGHT / screen.homeScreenIcon.height
    } );

    // create a frame for each size
    const smallFrame = new Rectangle( 0, 0, smallIcon.width, smallIcon.height, {
      stroke: options.showUnselectedHomeScreenIconFrame ? PhetColorScheme.SCREEN_ICON_FRAME : null,
      lineWidth: 0.7
    } );
    const largeFrame = new Frame( largeIcon );

    // create one node for the each of large and small frame + icon pairs
    const smallNode = new Node( { children: [ smallFrame, smallIcon ] } );
    const largeNode = new Node( { children: [ largeFrame, largeIcon ] } );

    // container for the icon and frame, children updated when isSelectedProperty changes
    const nodeContainer = new Node();

    // text for the button
    const text = new Text( screen.name, {
      tandem: options.tandem.createTandem( 'text' )
    } );

    super( merge( { children: [ nodeContainer, text ] }, options ) );

    // create large and small settings
    const settings = {
      small: {
        node: [ smallNode ],
        font: new PhetFont( 18 ),
        spacing: 3
      },
      large: {
        node: [ largeNode ],
        font: new PhetFont( 42 ),
        spacing: 0
      }
    };

    // sets the opacity of the icon and fill of the text
    const setOpacityAndFill = () => {
      const opacity = ( isSelectedProperty.value || isHighlightedProperty.value ) ? 1 : 0.5;
      largeIcon.opacity = opacity;
      smallIcon.opacity = opacity;
      text.fill = ( isSelectedProperty.value || isHighlightedProperty.value ) ? 'white' : 'gray';
    };

    // update pieces that change when the button is selected or unselected
    isSelectedProperty.link( isSelected => {
      const data = isSelected ? settings.large : settings.small;

      // apply settings for the current size
      nodeContainer.children = data.node;
      text.font = data.font;
      text.maxWidth = nodeContainer.width;
      setOpacityAndFill();
      this.setSpacing( data.spacing );

      // update the VBox layout after changing the size of its children
      this.updateLayout();
    } );

    // update the appearance when the button is highlighted
    isHighlightedProperty.link( isHighlighted => {
      largeFrame.setHighlighted( isHighlighted );
      setOpacityAndFill();
    } );

    // if the button is already selected, then set the sim's screen to be its corresponding screen. otherwise,
    // make the button selected
    const buttonDown = () => {
      if ( isSelectedProperty.value ) {
        homeScreenModel.screenProperty.value = screen;
      }
      else {
        homeScreenModel.selectedScreenProperty.value = screen;
      }
    };

    const fireListener = new FireListener( {
      fireOnDown: true, // to match prior behavior
      fire: buttonDown,
      tandem: options.tandem.createTandem( 'inputListener' )
    } );
    this.addInputListener( fireListener );
    this.addInputListener( { click: event => { fireListener.fire( event ); } } );
    this.addInputListener( { focus: event => { !isSelectedProperty.value && fireListener.fire( event ); } } );
    this.addInputListener( {
      focus: () => isHighlightedProperty.set( true ),
      blur: () => isHighlightedProperty.set( false ),
      over: () => isHighlightedProperty.set( true ),
      out: () => isHighlightedProperty.set( false )
    } );

    // If you touch an unselected button, it become selected. If then without lifting your finger you swipe over to the
    // next button, that one becomes selected instead.
    this.addInputListener( {
      over: event => {
        if ( event.pointer instanceof Touch ) {
          homeScreenModel.selectedScreenProperty.value = screen;
        }
      }
    } );

    // a11y support for click listeners on the screen buttons
    const toggleListener = () => {
      this.focus();
    };
    this.addInputListener( { focus: toggleListener } );
    this.addInputListener( { click: toggleListener } );

    // a11y - add the right aria attributes to the buttons
    this.setAccessibleAttribute( 'aria-roledescription', simScreenString );

    // set the mouseArea and touchArea to be the whole local bounds of this node, because if it just relies on the
    // bounds of the icon and text, then there is a gap in between them. Since the button can change size, this
    // assignment needs to happen anytime the bounds change.
    this.on( 'bounds', () => {
      this.mouseArea = this.touchArea = Shape.bounds( this.localBounds );
    } );
  }
}

joist.register( 'ScreenButton', HomeScreenButton );
export default HomeScreenButton;<commit_msg>add aria-describedby between each screen's button and p elements, https://github.com/phetsims/joist/issues/496<commit_after>// Copyright 2020, University of Colorado Boulder

/**
 * A HomeScreenButton is displayed on the HomeScreen for choosing a screen. The button can be in a selected or
 * unselected state - it's large with a yellow frame in its selected state, and small in its unselected state.
 * Selecting the button when in its "selected" state will result in that screen being chosen.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Chris Klusendorf (PhET Interactive Simulations)
 */

import BooleanProperty from '../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../axon/js/DerivedProperty.js';
import Utils from '../../dot/js/Utils.js';
import Shape from '../../kite/js/Shape.js';
import merge from '../../phet-core/js/merge.js';
import PhetColorScheme from '../../scenery-phet/js/PhetColorScheme.js';
import PhetFont from '../../scenery-phet/js/PhetFont.js';
import AccessiblePeer from '../../scenery/js/accessibility/AccessiblePeer.js';
import Touch from '../../scenery/js/input/Touch.js';
import FireListener from '../../scenery/js/listeners/FireListener.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Text from '../../scenery/js/nodes/Text.js';
import VBox from '../../scenery/js/nodes/VBox.js';
import EventType from '../../tandem/js/EventType.js';
import Tandem from '../../tandem/js/Tandem.js';
import Frame from './Frame.js';
import joist from './joist.js';
import joistStrings from './joist-strings.js';

// constants
const simScreenString = joistStrings.a11y.simScreen;
const LARGE_ICON_HEIGHT = 140;

class HomeScreenButton extends VBox {

  /**
   * @param {Screen} screen
   * @param {HomeScreenModel} homeScreenModel
   * @param {Object} [options]
   * @constructor
   */
  constructor( screen, homeScreenModel, options ) {

    options = merge( {
      cursor: 'pointer',
      resize: false, // don't resize the VBox or it will shift down when the border becomes thicker
      showUnselectedHomeScreenIconFrame: false, // put a frame around unselected home screen icons

      // a11y
      tagName: 'button',
      appendDescription: true,
      containerTagName: 'li',

      // phet-io
      tandem: Tandem.REQUIRED,
      phetioEventType: EventType.USER,
      phetioDocumentation: 'A button on the home screen for choosing a simulation screen'
    }, options );

    // derives a boolean value from homeScreenModel.selectedScreenProperty that says if this button is selected on the
    // home screen.
    const isSelectedProperty = new DerivedProperty( [ homeScreenModel.selectedScreenProperty ], selectedScreen => {
      return selectedScreen === screen;
    } );

    // true if this button has focus or mouseover
    const isHighlightedProperty = new BooleanProperty( false );

    // maps the number of screens to a scale for the small icons. The scale is percentage of LARGE_ICON_HEIGHT.
    let smallIconScale = Utils.linear( 2, 4, 0.875, 0.50, homeScreenModel.simScreens.length );
    if ( homeScreenModel.simScreens.length >= 5 ) {
      smallIconScale = 0.4;
    }
    const smallIconHeight = smallIconScale * LARGE_ICON_HEIGHT;

    // create an icon for each size
    const smallIcon = new Node( {
      children: [ screen.homeScreenIcon ],
      scale: smallIconHeight / screen.homeScreenIcon.height
    } );
    const largeIcon = new Node( {
      children: [ screen.homeScreenIcon ],
      scale: LARGE_ICON_HEIGHT / screen.homeScreenIcon.height
    } );

    // create a frame for each size
    const smallFrame = new Rectangle( 0, 0, smallIcon.width, smallIcon.height, {
      stroke: options.showUnselectedHomeScreenIconFrame ? PhetColorScheme.SCREEN_ICON_FRAME : null,
      lineWidth: 0.7
    } );
    const largeFrame = new Frame( largeIcon );

    // create one node for the each of large and small frame + icon pairs
    const smallNode = new Node( { children: [ smallFrame, smallIcon ] } );
    const largeNode = new Node( { children: [ largeFrame, largeIcon ] } );

    // container for the icon and frame, children updated when isSelectedProperty changes
    const nodeContainer = new Node();

    // text for the button
    const text = new Text( screen.name, {
      tandem: options.tandem.createTandem( 'text' )
    } );

    super( merge( { children: [ nodeContainer, text ] }, options ) );

    this.addAriaDescribedbyAssociation( {
      otherNode: this,
      otherElementName: AccessiblePeer.DESCRIPTION_SIBLING,
      thisElementName: AccessiblePeer.PRIMARY_SIBLING
    } );

    // create large and small settings
    const settings = {
      small: {
        node: [ smallNode ],
        font: new PhetFont( 18 ),
        spacing: 3
      },
      large: {
        node: [ largeNode ],
        font: new PhetFont( 42 ),
        spacing: 0
      }
    };

    // sets the opacity of the icon and fill of the text
    const setOpacityAndFill = () => {
      const opacity = ( isSelectedProperty.value || isHighlightedProperty.value ) ? 1 : 0.5;
      largeIcon.opacity = opacity;
      smallIcon.opacity = opacity;
      text.fill = ( isSelectedProperty.value || isHighlightedProperty.value ) ? 'white' : 'gray';
    };

    // update pieces that change when the button is selected or unselected
    isSelectedProperty.link( isSelected => {
      const data = isSelected ? settings.large : settings.small;

      // apply settings for the current size
      nodeContainer.children = data.node;
      text.font = data.font;
      text.maxWidth = nodeContainer.width;
      setOpacityAndFill();
      this.setSpacing( data.spacing );

      // update the VBox layout after changing the size of its children
      this.updateLayout();
    } );

    // update the appearance when the button is highlighted
    isHighlightedProperty.link( isHighlighted => {
      largeFrame.setHighlighted( isHighlighted );
      setOpacityAndFill();
    } );

    // if the button is already selected, then set the sim's screen to be its corresponding screen. otherwise,
    // make the button selected
    const buttonDown = () => {
      if ( isSelectedProperty.value ) {
        homeScreenModel.screenProperty.value = screen;
      }
      else {
        homeScreenModel.selectedScreenProperty.value = screen;
      }
    };

    const fireListener = new FireListener( {
      fireOnDown: true, // to match prior behavior
      fire: buttonDown,
      tandem: options.tandem.createTandem( 'inputListener' )
    } );
    this.addInputListener( fireListener );
    this.addInputListener( { click: event => { fireListener.fire( event ); } } );
    this.addInputListener( { focus: event => { !isSelectedProperty.value && fireListener.fire( event ); } } );
    this.addInputListener( {
      focus: () => isHighlightedProperty.set( true ),
      blur: () => isHighlightedProperty.set( false ),
      over: () => isHighlightedProperty.set( true ),
      out: () => isHighlightedProperty.set( false )
    } );

    // If you touch an unselected button, it become selected. If then without lifting your finger you swipe over to the
    // next button, that one becomes selected instead.
    this.addInputListener( {
      over: event => {
        if ( event.pointer instanceof Touch ) {
          homeScreenModel.selectedScreenProperty.value = screen;
        }
      }
    } );

    // a11y support for click listeners on the screen buttons
    const toggleListener = () => {
      this.focus();
    };
    this.addInputListener( { focus: toggleListener } );
    this.addInputListener( { click: toggleListener } );

    // a11y - add the right aria attributes to the buttons
    this.setAccessibleAttribute( 'aria-roledescription', simScreenString );

    // set the mouseArea and touchArea to be the whole local bounds of this node, because if it just relies on the
    // bounds of the icon and text, then there is a gap in between them. Since the button can change size, this
    // assignment needs to happen anytime the bounds change.
    this.on( 'bounds', () => {
      this.mouseArea = this.touchArea = Shape.bounds( this.localBounds );
    } );
  }
}

joist.register( 'ScreenButton', HomeScreenButton );
export default HomeScreenButton;<|endoftext|>


<commit_before>module.exports = setup;

function setup(app) {
	app.get('/callbacks/instagram/ingester', verify);
	app.post('/callbacks/instagram/ingester', ingest);
}

function verify(req, res, next) {
	var inst = req.app.get('instagram');

	inst.subscriptions.handshake(req, res);
}

function ingest(req, res, next) {
	var inst = req.app.get('instagram');
	var changes = req.body;

	res.status(200).end();

	for (var i = 0; i < changes.length; i++) {
		var change = changes[i];

		console.log('notified of change to #' + change.object_id);

		inst.tags.recent({
			name: change.object_id,

			complete: function (images, pagination) {
				var objects = [];

				console.log('retrieved ' + images.length + ' images for #' + change.object_id);

				for (var i = 0; i < images.length; i++) {
					var image = images[i];

					var thumb = image.images.thumbnail.url;
					var standard = image.images.standard_resolution.url;

					objects.push({
						instagramId: image.id,
						date: new Date(image.created_time * 1000),
						thumbnailUrl: thumb,
						imageUrl: standard
					});
				}

				req.app.get('models').Media
					.bulkCreate(objects)
					.error(function (err) {
						console.log('error saving media');
						console.log(err);
					});
			},

			error: function (errorMessage, errorObject, caller) {
				console.log('error getting media');
				console.log(errorMessage);
				console.log(errorObject);
			}
		});
	}
}
<commit_msg>Saving media<commit_after>module.exports = setup;

function setup(app) {
	app.get('/callbacks/instagram/ingester', verify);
	app.post('/callbacks/instagram/ingester', ingest);
}

function verify(req, res, next) {
	var inst = req.app.get('instagram');

	inst.subscriptions.handshake(req, res);
}

function ingest(req, res, next) {
	var inst = req.app.get('instagram');
	var changes = req.body;

	res.status(200).end();

	for (var i = 0; i < changes.length; i++) {
		var change = changes[i];

		console.log('notified of change to #' + change.object_id);

		inst.tags.recent({
			name: change.object_id,

			complete: function (images, pagination) {

				console.log('retrieved ' + images.length + ' images for #' + change.object_id);

				for (var i = 0; i < images.length; i++) {
					var image = images[i];

					var thumb = image.images.thumbnail.url;
					var standard = image.images.standard_resolution.url;

					req.app.get('models').Tag
						.findOrCreate({ name: change.object_id })
						.success(function(tag, created) {
							var object = {
								instagramId: image.id,
								date: new Date(image.created_time * 1000),
								thumbnailUrl: thumb,
								imageUrl: standard
							};

							req.app.get('models').Media.create(object)
								.success(function(media, created) {
									tag.addMedia(media).error(function (err) {
										console.log('error adding media to tag');
										console.log(err);
									});
								})
								.error(function (err) {
									console.log('error saving media');
									console.log(err);
								});
						})
						.error(function (err) {
							console.log(err);
						});
				}
			},

			error: function (errorMessage, errorObject, caller) {
				console.log('error getting media');
				console.log(errorMessage);
				console.log(errorObject);
			}
		});
	}
}
<|endoftext|>


<commit_before>// Get the required shit together
const Discord = require('eris');
const config = require('config');
const utils = require('./utils.js');
require('colors');

const client = new Discord.Client(config.get('api').discord.token);
const prefixes = config.get('discord').prefix.user.concat(config.get('discord').prefix.guild);
let prefix = null;

// Setup commands and util objects.
const commands = require('./cogs.js');

client.on('shardReady', (id) => {
	console.log(`Shard ${id} is online`.green);
});

client.on('ready', () => {
	// Set up regex for the bot.
	// It's "man's essential illness"
	// Use this regex for testing in regexr.com
	// /^(mss).?(ping)\s?([\s\S]*)/
	// /(\w+)rly/
	prefix = new RegExp(`^(${prefixes.join('|')}).?(${Object.keys(commands).join('|')})\\s?([\\s\\S]*)`, 'i');

	// Add mentions to the prefix list
	prefixes.push(`<@${client.user.id}>`);
	console.log('All shards are online'.green.bold);

	// Set up currently playing game
	client.editStatus('online', {
		name: `${prefixes[0]} help`,
		type: 0,
		url: config.get('api').mailgun.domain
	});

	// Send DBOTS info if it was provided.
	if (config.get('api').botsdiscordpw) {
		utils.botsdiscordpw(client);
		setInterval(() => {
			utils.botsdiscordpw(client);
		}, 1800000);
	}

	// Send FAKEDBOTS info if it was provided.
	if (config.get('api').discordbotsorg) {
		utils.discordbotsorg(client);
		setInterval(() => {
			utils.discordbotsorg(client);
		}, 1800000);
	}

	// Check for bot collection servers
	utils.collection.check(client);
	setInterval(() => {
		utils.collection.check(client);
	}, 1800000);

	client.on('messageCreate', (message) => {
		// It crashed on this before. No explaination.
		if (!message.author) return;
		// Disallow if the author is a bot
		if (message.author.bot) return;

		// Test the message content on the regular expression for prefixed commands.
		const pre = prefix.exec(message.content);

		// If there's a result, do this crap.
		if (pre) {
			message.prefix = pre[1];
			message.command = pre[2];
			message.input = pre[3] || null;
			message.name = utils.dmail.name(message.author.username);
			message.context = config.get('discord').prefix.user.includes(message.prefix.toLowerCase()) ? 'user' : 'guild';
			message.inbox = message.context === 'user' ? message.author.id : (message.channel.guild && message.channel.guild.id) || 'Not inside a guild';

			// Run the actual command
			commands[message.command.toLowerCase()].command(message, client);
		}
	});
});

client.connect();
module.exports = client;
<commit_msg>Ready once<commit_after>// Get the required shit together
const Discord = require('eris');
const config = require('config');
const utils = require('./utils.js');
require('colors');

const client = new Discord.Client(config.get('api').discord.token);
const prefixes = config.get('discord').prefix.user.concat(config.get('discord').prefix.guild);
let prefix = null;

// Setup commands and util objects.
const commands = require('./cogs.js');

client.on('shardReady', (id) => {
	console.log(`Shard ${id} is online`.green);
});

client.once('ready', () => {
	// Set up regex for the bot.
	// It's "man's essential illness"
	// Use this regex for testing in regexr.com
	// /^(mss).?(ping)\s?([\s\S]*)/
	// /(\w+)rly/
	prefix = new RegExp(`^(${prefixes.join('|')}).?(${Object.keys(commands).join('|')})\\s?([\\s\\S]*)`, 'i');

	// Add mentions to the prefix list
	prefixes.push(`<@${client.user.id}>`);
	console.log('All shards are online'.green.bold);

	// Set up currently playing game
	client.editStatus('online', {
		name: `${prefixes[0]} help`,
		type: 0,
		url: config.get('api').mailgun.domain
	});

	// Send DBOTS info if it was provided.
	if (config.get('api').botsdiscordpw) {
		utils.botsdiscordpw(client);
		setInterval(() => {
			utils.botsdiscordpw(client);
		}, 1800000);
	}

	// Send FAKEDBOTS info if it was provided.
	if (config.get('api').discordbotsorg) {
		utils.discordbotsorg(client);
		setInterval(() => {
			utils.discordbotsorg(client);
		}, 1800000);
	}

	// Check for bot collection servers
	utils.collection.check(client);
	setInterval(() => {
		utils.collection.check(client);
	}, 1800000);

	client.on('messageCreate', (message) => {
		// It crashed on this before. No explaination.
		if (!message.author) return;
		// Disallow if the author is a bot
		if (message.author.bot) return;

		// Test the message content on the regular expression for prefixed commands.
		const pre = prefix.exec(message.content);

		// If there's a result, do this crap.
		if (pre) {
			message.prefix = pre[1];
			message.command = pre[2];
			message.input = pre[3] || null;
			message.name = utils.dmail.name(message.author.username);
			message.context = config.get('discord').prefix.user.includes(message.prefix.toLowerCase()) ? 'user' : 'guild';
			message.inbox = message.context === 'user' ? message.author.id : (message.channel.guild && message.channel.guild.id) || 'Not inside a guild';

			// Run the actual command
			commands[message.command.toLowerCase()].command(message, client);
		}
	});
});

client.connect();
module.exports = client;
<|endoftext|>


<commit_before>exports.names = ['moveback'];
exports.hidden = true;
exports.enabled = true;
exports.matchStart = true;
exports.cd_all = 20;
exports.cd_user = 3600;
exports.cd_manager = 0;
exports.min_role = PERMISSIONS.NONE;
exports.handler = function (data) {
    var position = bot.getWaitListPosition(data.from.id);
    if (position !== -1 && position !== 0) {
        move_user(data.from.id, 50);
        chatMessage('/me Moving ' + data.from.username + ' back to 50.');
    }
};
<commit_msg>.moveback now uses the new cooldown feature<commit_after>exports.names = ['moveback'];
exports.hidden = true;
exports.enabled = true;
exports.matchStart = true;
exports.cd_all = 20;
exports.cd_user = 3600;
exports.cd_manager = 0;
exports.min_role = PERMISSIONS.NONE;
exports.handler = function (data) {
    var position = bot.getWaitListPosition(data.from.id);
    if (position !== -1 && position !== 0 && position !== 50) {
        move_user(data.from.id, 50);
        chatMessage('/me Moving ' + data.from.username + ' back to 50.');
    } else {
        modMessage(data, 'You need to be in the waitlist to use .moveback.');
        return {cd_all: 5, cd_user: 20};
    }
};
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */
const mongoose  = require('mongoose');
const Schema    = mongoose.Schema;
// create a schema
const ScoreSchema = new Schema({
    team: { type: Schema.Types.ObjectId, required: true},
    points: {type: Number, required: true},
    created_at: Date
});

ScoreSchema.pre('save', function(next) {
    if (!this.created_at) {
        this.created_at = new Date();
    }
    next();
});

// create a model using schema
const Score = mongoose.model('Score', ScoreSchema);

// make this available to our users in our Node applications
module.exports = Score;
<commit_msg>Add extra line spacing<commit_after>/**
 * Module dependencies.
 */
const mongoose  = require('mongoose');
const Schema    = mongoose.Schema;

// create a schema
const ScoreSchema = new Schema({
    team: { type: Schema.Types.ObjectId, required: true},
    points: {type: Number, required: true},
    created_at: Date
});

ScoreSchema.pre('save', function(next) {
    if (!this.created_at) {
        this.created_at = new Date();
    }
    next();
});

// create a model using schema
const Score = mongoose.model('Score', ScoreSchema);

// make this available to our users in our Node applications
module.exports = Score;
<|endoftext|>


<commit_before>// Copyright 2013-2016, University of Colorado Boulder

/**
 * WavelengthSlider is a slider-like control used for setting visible wavelength.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ArrowButton = require( 'SUN/buttons/ArrowButton' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var SpectrumNode = require( 'SCENERY_PHET/SpectrumNode' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var TandemDragHandler = require( 'TANDEM/scenery/input/TandemDragHandler' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Tandem = require( 'TANDEM/Tandem' );

  // strings
  var wavelengthSliderPattern0Wavelength1UnitsString = require( 'string!SCENERY_PHET/WavelengthSlider.pattern_0wavelength_1units' );
  var unitsNmString = require( 'string!SCENERY_PHET/units_nm' );

  // phet-io modules
  var TNode = require( 'ifphetio!PHET_IO/types/scenery/nodes/TNode' );

  // constants
  var DEFAULT_THUMB_WIDTH = 35;
  var DEFAULT_THUMB_HEIGHT = 45;

  /**
   * @param {Property.<number>} wavelength - in nm
   * @param {Object} [options]
   * @constructor
   */
  function WavelengthSlider( wavelength, options ) {

    options = options || {};

    // Compute defaults for pointer area dilation.  Must be done before _.extends call
    // because these values are proportional to thumb size.
    var DEFAULT_THUMB_TOUCH_AREA_X_DILATION = 0.5 * ( options.thumbWidth || DEFAULT_THUMB_WIDTH );
    var DEFAULT_THUMB_TOUCH_AREA_Y_DILATION = 0.25 * ( options.thumbHeight || DEFAULT_THUMB_HEIGHT );

    // options that are specific to this type
    options = _.extend( {

      pointerAreasOverTrack: false,//TODO delete this, see https://github.com/phetsims/scenery-phet/issues/261

      minWavelength: VisibleColor.MIN_WAVELENGTH,
      maxWavelength: VisibleColor.MAX_WAVELENGTH,

      // track
      trackWidth: 150,
      trackHeight: 30,
      trackOpacity: 1,
      trackBorderStroke: 'black',

      // thumb
      thumbWidth: DEFAULT_THUMB_WIDTH,
      thumbHeight: DEFAULT_THUMB_HEIGHT,
      thumbTouchAreaXDilation: DEFAULT_THUMB_TOUCH_AREA_X_DILATION,
      thumbTouchAreaYDilation: DEFAULT_THUMB_TOUCH_AREA_Y_DILATION,

      // value
      valueFont: new PhetFont( 20 ),
      valueFill: 'black',
      valueVisible: true,
      valueYSpacing: 2, // {number} space between value and top of track

      // tweakers
      tweakersVisible: true,
      maxTweakersHeight: 30,

      // cursor
      cursorVisible: true,
      cursorStroke: 'black',

      // phet-io
      tandem: null

    }, options );

    Tandem.validateOptions( options ); // The tandem is required when brand==='phet-io'

    // validate wavelengths
    assert && assert( options.minWavelength < options.maxWavelength );
    assert && assert( options.minWavelength >= VisibleColor.MIN_WAVELENGTH && options.minWavelength <= VisibleColor.MAX_WAVELENGTH );
    assert && assert( options.maxWavelength >= VisibleColor.MIN_WAVELENGTH && options.maxWavelength <= VisibleColor.MAX_WAVELENGTH );

    var thisNode = this;
    Node.call( thisNode );

    var thumb = new Thumb( options.thumbWidth, options.thumbHeight );
    var valueDisplay = ( options.valueVisible ) ? new ValueDisplay( wavelength, options.valueFont, options.valueFill ) : null;
    var track = new SpectrumNode( options.trackWidth, options.trackHeight, options.minWavelength, options.maxWavelength, options.trackOpacity );
    var cursor = ( options.cursorVisible ) ? new Cursor( 3, track.height, options.cursorStroke ) : null;

    // tweaker buttons for single-unit increments
    var plusButton;
    var minusButton;
    if ( options.tweakersVisible ) {

      plusButton = new ArrowButton( 'right', function() {
        wavelength.set( wavelength.get() + 1 );
      }, {
        maxHeight: options.maxTweakersHeight,
        tandem: options.tandem && options.tandem.createTandem( 'plusButton' )
      } );

      minusButton = new ArrowButton( 'left', function() {
        wavelength.set( wavelength.get() - 1 );
      }, {
        maxHeight: options.maxTweakersHeight,
        tandem: options.tandem && options.tandem.createTandem( 'minusButton' )
      } );
    }

    /*
     * Put a border around the track.
     * We don't stroke the track itself because stroking the track will affect its bounds,
     * and will thus affect the drag handle behavior.
     * Having a separate border also gives subclasses a place to add markings (eg, tick marks)
     * without affecting the track's bounds.
     */
    var trackBorder = new Rectangle( 0, 0, track.width, track.height, {
      stroke: options.trackBorderStroke,
      lineWidth: 1,
      pickable: false
    } );

    // rendering order
    thisNode.addChild( track );
    thisNode.addChild( trackBorder );
    thisNode.addChild( thumb );
    if ( valueDisplay ) { thisNode.addChild( valueDisplay ); }
    if ( cursor ) { thisNode.addChild( cursor ); }
    if ( options.tweakersVisible ) {
      thisNode.addChild( plusButton );
      thisNode.addChild( minusButton );
    }

    // layout
    if ( cursor ) { cursor.top = track.top; }
    thumb.top = track.bottom;
    if ( valueDisplay ) { valueDisplay.bottom = track.top - options.valueYSpacing; }
    if ( options.tweakersVisible ) {
      plusButton.left = track.right + 8;
      plusButton.centerY = track.centerY;
      minusButton.right = track.left - 8;
      minusButton.centerY = track.centerY;
    }

    // transforms between position and wavelength
    var positionToWavelength = function( x ) {
      return Math.floor( Util.clamp( Util.linear( 0, track.width, options.minWavelength, options.maxWavelength, x ), options.minWavelength, options.maxWavelength ) );
    };
    var wavelengthToPosition = function( wavelength ) {
      return Math.floor( Util.clamp( Util.linear( options.minWavelength, options.maxWavelength, 0, track.width, wavelength ), 0, track.width ) );
    };

    // track interactivity
    // TODO: Other tracks continue to drag instead of just setting a single value, why not this?
    track.cursor = 'pointer';
    track.addInputListener( new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'trackInputListener' ) : null,
      start: function( event ) {
        var x = track.globalToParentPoint( event.pointer.point ).x;
        var newValue = positionToWavelength( x );
        wavelength.set( newValue );
      }
    } ) );

    // thumb interactivity
    thumb.cursor = 'pointer';
    var clickXOffset = 0; // x-offset between initial click and thumb's origin
    thumb.addInputListener( new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'thumbInputListener' ) : null,
      allowTouchSnag: true,

      start: function( event ) {
        clickXOffset = thumb.globalToParentPoint( event.pointer.point ).x - thumb.x;
      },

      drag: function( event ) {
        var x = thumb.globalToParentPoint( event.pointer.point ).x - clickXOffset;
        var value = positionToWavelength( x );
        wavelength.set( value );
      }
    } ) );

    // sync with model
    var updateUI = function( wavelength ) {
      // positions
      var x = wavelengthToPosition( wavelength );
      thumb.centerX = x;
      if ( cursor ) { cursor.centerX = x; }
      if ( valueDisplay ) { valueDisplay.centerX = x; }
      // thumb color
      thumb.fill = VisibleColor.wavelengthToColor( wavelength );
      // tweaker buttons
      if ( options.tweakersVisible ) {
        plusButton.enabled = ( wavelength < options.maxWavelength );
        minusButton.enabled = ( wavelength > options.minWavelength );
      }
    };
    var wavelengthListener = function( wavelength ) {
      updateUI( wavelength );
    };
    wavelength.link( wavelengthListener );

    /*
     * The horizontal bounds of the wavelength control changes as the slider knob is dragged.
     * To prevent this, we determine the extents of the control's bounds at min and max values,
     * then add an invisible horizontal strut.
     */
    // determine bounds at min and max wavelength settings
    updateUI( options.minWavelength );
    var minX = thisNode.left;
    updateUI( options.maxWavelength );
    var maxX = thisNode.right;

    // restore the wavelength
    updateUI( wavelength.get() );

    // add a horizontal strut
    var strut = new Rectangle( minX, 0, maxX - minX, 1, { pickable: false } );
    thisNode.addChild( strut );
    strut.moveToBack();

    // thumb touchArea
    if ( options.thumbTouchAreaXDilation || options.thumbTouchAreaYDilation ) {
      thumb.touchArea = thumb.localBounds
        .dilatedXY( options.thumbTouchAreaXDilation, options.thumbTouchAreaYDilation )
        .shiftedY( 0.25 * thumb.height )
    }

    thisNode.mutate( options );
    options.tandem && options.tandem.addInstance( this, TNode );

    this.disposeWavelengthSlider = function() {
      plusButton && plusButton.dispose();
      minusButton && minusButton.dispose();
      wavelength.unlink( wavelengthListener );
      options.tandem && options.tandem.removeInstance( this );
    };
  }

  sceneryPhet.register( 'WavelengthSlider', WavelengthSlider );

  /**
   * The slider thumb (aka knob)
   * @param {number} width
   * @param {number} height
   * @constructor
   */
  function Thumb( width, height ) {
    // set the radius of the arcs based on the height or width, whichever is smaller
    var radiusScale = 0.15;
    var radius = ( width < height ) ? radiusScale * width : radiusScale * height;

    // calculate some parameters of the upper triangles of the thumb for getting arc offsets
    var hypotenuse = Math.sqrt( Math.pow( 0.5 * width, 2 ) + Math.pow( 0.3 * height, 2 ) );
    var angle = Math.acos( width * 0.5 / hypotenuse );
    var heightOffset = radius * Math.sin( angle );

    // draw the thumb shape starting at the right upper corner of the pentagon below the arc,
    // this way we can get the arc coordinates for the arc in this corner from the other side, which
    // will be easier to calculate arcing from bottom to top
    var shape = new Shape()
      .moveTo( 0.5 * width, 0.3 * height + heightOffset )
      .lineTo( 0.5 * width, 1 * height - radius )
      .arc( 0.5 * width - radius, 1 * height - radius, radius, 0, Math.PI / 2 )
      .lineTo( -0.5 * width + radius, 1 * height )
      .arc( -0.5 * width + radius, 1 * height - radius, radius, Math.PI / 2, Math.PI )
      .lineTo( -0.5 * width, 0.3 * height + heightOffset )
      .arc( -0.5 * width + radius, 0.3 * height + heightOffset, radius, Math.PI, Math.PI + angle );

    // save the coordinates for the point above the left side arc, for use on the other side
    var sideArcPoint = shape.getLastPoint();

    shape.lineTo( 0, 0 )
      .lineTo( -sideArcPoint.x, sideArcPoint.y )
      .arc( 0.5 * width - radius, 0.3 * height + heightOffset, radius, -angle, 0 )
      .close();

    Path.call( this, shape, { stroke: 'black', lineWidth: 1, fill: 'black' } );
  }

  sceneryPhet.register( 'WavelengthSlider.Thumb', Thumb );

  inherit( Path, Thumb );

  /**
   * Displays the value and units.
   * @param property
   * @param {string} font
   * @param {string} fill
   * @constructor
   */
  function ValueDisplay( property, font, fill ) {
    var thisNode = this;
    Text.call( this, '?', { font: font, fill: fill } );
    property.link( function( value ) {
      thisNode.text = StringUtils.format( wavelengthSliderPattern0Wavelength1UnitsString, Util.toFixed( value, 0 ), unitsNmString );
    } );
  }

  sceneryPhet.register( 'WavelengthSlider.ValueDisplay', ValueDisplay );

  inherit( Text, ValueDisplay );

  /**
   * Rectangular 'cursor' that appears in the track directly above the thumb. Origin is at top center of cursor.
   * @param {number} width
   * @param {number} height
   * @constructor
   */
  function Cursor( width, height, stroke ) {
    Rectangle.call( this, -width / 2, 0, width, height, { stroke: stroke, lineWidth: 1 } );
  }

  sceneryPhet.register( 'WavelengthSlider.Cursor', Cursor );

  inherit( Rectangle, Cursor );

  return inherit( Node, WavelengthSlider, {

    // @public
    dispose: function() {
      this.disposeWavelengthSlider();
    }
  } );
} );
<commit_msg>add options for mouseArea dilation of thumb, #261<commit_after>// Copyright 2013-2016, University of Colorado Boulder

/**
 * WavelengthSlider is a slider-like control used for setting visible wavelength.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var ArrowButton = require( 'SUN/buttons/ArrowButton' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );
  var SpectrumNode = require( 'SCENERY_PHET/SpectrumNode' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var TandemDragHandler = require( 'TANDEM/scenery/input/TandemDragHandler' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var Text = require( 'SCENERY/nodes/Text' );
  var Util = require( 'DOT/Util' );
  var VisibleColor = require( 'SCENERY_PHET/VisibleColor' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Tandem = require( 'TANDEM/Tandem' );

  // strings
  var wavelengthSliderPattern0Wavelength1UnitsString = require( 'string!SCENERY_PHET/WavelengthSlider.pattern_0wavelength_1units' );
  var unitsNmString = require( 'string!SCENERY_PHET/units_nm' );

  // phet-io modules
  var TNode = require( 'ifphetio!PHET_IO/types/scenery/nodes/TNode' );

  // constants
  var DEFAULT_THUMB_WIDTH = 35;
  var DEFAULT_THUMB_HEIGHT = 45;

  /**
   * @param {Property.<number>} wavelength - in nm
   * @param {Object} [options]
   * @constructor
   */
  function WavelengthSlider( wavelength, options ) {

    options = options || {};

    // Compute defaults for pointer area dilation.  Must be done before _.extends call
    // because these values are proportional to thumb size.
    var DEFAULT_THUMB_TOUCH_AREA_X_DILATION = 0.5 * ( options.thumbWidth || DEFAULT_THUMB_WIDTH );
    var DEFAULT_THUMB_TOUCH_AREA_Y_DILATION = 0.25 * ( options.thumbHeight || DEFAULT_THUMB_HEIGHT );

    // options that are specific to this type
    options = _.extend( {

      pointerAreasOverTrack: false,//TODO delete this, see https://github.com/phetsims/scenery-phet/issues/261

      minWavelength: VisibleColor.MIN_WAVELENGTH,
      maxWavelength: VisibleColor.MAX_WAVELENGTH,

      // track
      trackWidth: 150,
      trackHeight: 30,
      trackOpacity: 1,
      trackBorderStroke: 'black',

      // thumb
      thumbWidth: DEFAULT_THUMB_WIDTH,
      thumbHeight: DEFAULT_THUMB_HEIGHT,
      thumbTouchAreaXDilation: DEFAULT_THUMB_TOUCH_AREA_X_DILATION,
      thumbTouchAreaYDilation: DEFAULT_THUMB_TOUCH_AREA_Y_DILATION,
      thumbMouseAreaXDilation: 0,
      thumbMouseAreaYDilation: 0,

      // value
      valueFont: new PhetFont( 20 ),
      valueFill: 'black',
      valueVisible: true,
      valueYSpacing: 2, // {number} space between value and top of track

      // tweakers
      tweakersVisible: true,
      maxTweakersHeight: 30,

      // cursor
      cursorVisible: true,
      cursorStroke: 'black',

      // phet-io
      tandem: null

    }, options );

    Tandem.validateOptions( options ); // The tandem is required when brand==='phet-io'

    // validate wavelengths
    assert && assert( options.minWavelength < options.maxWavelength );
    assert && assert( options.minWavelength >= VisibleColor.MIN_WAVELENGTH && options.minWavelength <= VisibleColor.MAX_WAVELENGTH );
    assert && assert( options.maxWavelength >= VisibleColor.MIN_WAVELENGTH && options.maxWavelength <= VisibleColor.MAX_WAVELENGTH );

    var thisNode = this;
    Node.call( thisNode );

    var thumb = new Thumb( options.thumbWidth, options.thumbHeight );
    var valueDisplay = ( options.valueVisible ) ? new ValueDisplay( wavelength, options.valueFont, options.valueFill ) : null;
    var track = new SpectrumNode( options.trackWidth, options.trackHeight, options.minWavelength, options.maxWavelength, options.trackOpacity );
    var cursor = ( options.cursorVisible ) ? new Cursor( 3, track.height, options.cursorStroke ) : null;

    // tweaker buttons for single-unit increments
    var plusButton;
    var minusButton;
    if ( options.tweakersVisible ) {

      plusButton = new ArrowButton( 'right', function() {
        wavelength.set( wavelength.get() + 1 );
      }, {
        maxHeight: options.maxTweakersHeight,
        tandem: options.tandem && options.tandem.createTandem( 'plusButton' )
      } );

      minusButton = new ArrowButton( 'left', function() {
        wavelength.set( wavelength.get() - 1 );
      }, {
        maxHeight: options.maxTweakersHeight,
        tandem: options.tandem && options.tandem.createTandem( 'minusButton' )
      } );
    }

    /*
     * Put a border around the track.
     * We don't stroke the track itself because stroking the track will affect its bounds,
     * and will thus affect the drag handle behavior.
     * Having a separate border also gives subclasses a place to add markings (eg, tick marks)
     * without affecting the track's bounds.
     */
    var trackBorder = new Rectangle( 0, 0, track.width, track.height, {
      stroke: options.trackBorderStroke,
      lineWidth: 1,
      pickable: false
    } );

    // rendering order
    thisNode.addChild( track );
    thisNode.addChild( trackBorder );
    thisNode.addChild( thumb );
    if ( valueDisplay ) { thisNode.addChild( valueDisplay ); }
    if ( cursor ) { thisNode.addChild( cursor ); }
    if ( options.tweakersVisible ) {
      thisNode.addChild( plusButton );
      thisNode.addChild( minusButton );
    }

    // layout
    if ( cursor ) { cursor.top = track.top; }
    thumb.top = track.bottom;
    if ( valueDisplay ) { valueDisplay.bottom = track.top - options.valueYSpacing; }
    if ( options.tweakersVisible ) {
      plusButton.left = track.right + 8;
      plusButton.centerY = track.centerY;
      minusButton.right = track.left - 8;
      minusButton.centerY = track.centerY;
    }

    // transforms between position and wavelength
    var positionToWavelength = function( x ) {
      return Math.floor( Util.clamp( Util.linear( 0, track.width, options.minWavelength, options.maxWavelength, x ), options.minWavelength, options.maxWavelength ) );
    };
    var wavelengthToPosition = function( wavelength ) {
      return Math.floor( Util.clamp( Util.linear( options.minWavelength, options.maxWavelength, 0, track.width, wavelength ), 0, track.width ) );
    };

    // track interactivity
    // TODO: Other tracks continue to drag instead of just setting a single value, why not this?
    track.cursor = 'pointer';
    track.addInputListener( new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'trackInputListener' ) : null,
      start: function( event ) {
        var x = track.globalToParentPoint( event.pointer.point ).x;
        var newValue = positionToWavelength( x );
        wavelength.set( newValue );
      }
    } ) );

    // thumb interactivity
    thumb.cursor = 'pointer';
    var clickXOffset = 0; // x-offset between initial click and thumb's origin
    thumb.addInputListener( new TandemDragHandler( {
      tandem: options.tandem ? options.tandem.createTandem( 'thumbInputListener' ) : null,
      allowTouchSnag: true,

      start: function( event ) {
        clickXOffset = thumb.globalToParentPoint( event.pointer.point ).x - thumb.x;
      },

      drag: function( event ) {
        var x = thumb.globalToParentPoint( event.pointer.point ).x - clickXOffset;
        var value = positionToWavelength( x );
        wavelength.set( value );
      }
    } ) );

    // sync with model
    var updateUI = function( wavelength ) {
      // positions
      var x = wavelengthToPosition( wavelength );
      thumb.centerX = x;
      if ( cursor ) { cursor.centerX = x; }
      if ( valueDisplay ) { valueDisplay.centerX = x; }
      // thumb color
      thumb.fill = VisibleColor.wavelengthToColor( wavelength );
      // tweaker buttons
      if ( options.tweakersVisible ) {
        plusButton.enabled = ( wavelength < options.maxWavelength );
        minusButton.enabled = ( wavelength > options.minWavelength );
      }
    };
    var wavelengthListener = function( wavelength ) {
      updateUI( wavelength );
    };
    wavelength.link( wavelengthListener );

    /*
     * The horizontal bounds of the wavelength control changes as the slider knob is dragged.
     * To prevent this, we determine the extents of the control's bounds at min and max values,
     * then add an invisible horizontal strut.
     */
    // determine bounds at min and max wavelength settings
    updateUI( options.minWavelength );
    var minX = thisNode.left;
    updateUI( options.maxWavelength );
    var maxX = thisNode.right;

    // restore the wavelength
    updateUI( wavelength.get() );

    // add a horizontal strut
    var strut = new Rectangle( minX, 0, maxX - minX, 1, { pickable: false } );
    thisNode.addChild( strut );
    strut.moveToBack();

    // thumb touchArea
    if ( options.thumbTouchAreaXDilation || options.thumbTouchAreaYDilation ) {
      thumb.touchArea = thumb.localBounds
        .dilatedXY( options.thumbTouchAreaXDilation, options.thumbTouchAreaYDilation )
        .shiftedY( options.thumbTouchAreaYDilation );
    }

    // thumb mouseArea
    if ( options.thumbMouseAreaXDilation || options.thumbMouseAreaYDilation ) {
      thumb.mouseArea = thumb.localBounds
        .dilatedXY( options.thumbMouseAreaXDilation, options.thumbMouseAreaYDilation )
        .shiftedY( options.thumbMouseAreaYDilation );
    }

    thisNode.mutate( options );
    options.tandem && options.tandem.addInstance( this, TNode );

    this.disposeWavelengthSlider = function() {
      plusButton && plusButton.dispose();
      minusButton && minusButton.dispose();
      wavelength.unlink( wavelengthListener );
      options.tandem && options.tandem.removeInstance( this );
    };
  }

  sceneryPhet.register( 'WavelengthSlider', WavelengthSlider );

  /**
   * The slider thumb (aka knob)
   * @param {number} width
   * @param {number} height
   * @constructor
   */
  function Thumb( width, height ) {
    // set the radius of the arcs based on the height or width, whichever is smaller
    var radiusScale = 0.15;
    var radius = ( width < height ) ? radiusScale * width : radiusScale * height;

    // calculate some parameters of the upper triangles of the thumb for getting arc offsets
    var hypotenuse = Math.sqrt( Math.pow( 0.5 * width, 2 ) + Math.pow( 0.3 * height, 2 ) );
    var angle = Math.acos( width * 0.5 / hypotenuse );
    var heightOffset = radius * Math.sin( angle );

    // draw the thumb shape starting at the right upper corner of the pentagon below the arc,
    // this way we can get the arc coordinates for the arc in this corner from the other side, which
    // will be easier to calculate arcing from bottom to top
    var shape = new Shape()
      .moveTo( 0.5 * width, 0.3 * height + heightOffset )
      .lineTo( 0.5 * width, 1 * height - radius )
      .arc( 0.5 * width - radius, 1 * height - radius, radius, 0, Math.PI / 2 )
      .lineTo( -0.5 * width + radius, 1 * height )
      .arc( -0.5 * width + radius, 1 * height - radius, radius, Math.PI / 2, Math.PI )
      .lineTo( -0.5 * width, 0.3 * height + heightOffset )
      .arc( -0.5 * width + radius, 0.3 * height + heightOffset, radius, Math.PI, Math.PI + angle );

    // save the coordinates for the point above the left side arc, for use on the other side
    var sideArcPoint = shape.getLastPoint();

    shape.lineTo( 0, 0 )
      .lineTo( -sideArcPoint.x, sideArcPoint.y )
      .arc( 0.5 * width - radius, 0.3 * height + heightOffset, radius, -angle, 0 )
      .close();

    Path.call( this, shape, { stroke: 'black', lineWidth: 1, fill: 'black' } );
  }

  sceneryPhet.register( 'WavelengthSlider.Thumb', Thumb );

  inherit( Path, Thumb );

  /**
   * Displays the value and units.
   * @param property
   * @param {string} font
   * @param {string} fill
   * @constructor
   */
  function ValueDisplay( property, font, fill ) {
    var thisNode = this;
    Text.call( this, '?', { font: font, fill: fill } );
    property.link( function( value ) {
      thisNode.text = StringUtils.format( wavelengthSliderPattern0Wavelength1UnitsString, Util.toFixed( value, 0 ), unitsNmString );
    } );
  }

  sceneryPhet.register( 'WavelengthSlider.ValueDisplay', ValueDisplay );

  inherit( Text, ValueDisplay );

  /**
   * Rectangular 'cursor' that appears in the track directly above the thumb. Origin is at top center of cursor.
   * @param {number} width
   * @param {number} height
   * @constructor
   */
  function Cursor( width, height, stroke ) {
    Rectangle.call( this, -width / 2, 0, width, height, { stroke: stroke, lineWidth: 1 } );
  }

  sceneryPhet.register( 'WavelengthSlider.Cursor', Cursor );

  inherit( Rectangle, Cursor );

  return inherit( Node, WavelengthSlider, {

    // @public
    dispose: function() {
      this.disposeWavelengthSlider();
    }
  } );
} );
<|endoftext|>


<commit_before>var carto = require('carto');
var debug = require('debug')('windshaft:renderer:torque_factory');
var _ = require('underscore');
var PSQL = require('cartodb-psql');
var torque_reference = require('torque.js').cartocss_reference;
var format = require('../../utils/format');

var RendererParams = require('../renderer_params');

var sql = require('../../utils/sql');
var Renderer = require('./renderer');
var PSQLAdaptor = require('./psql_adaptor');
var PngRenderer = require('./png_renderer');
var BaseAdaptor = require('../base_adaptor');

var SubstitutionTokens = require('../../utils/substitution_tokens');

/// API: initializes the renderer, it should be called once
//
/// @param options initialization options.
///     - sqlClass: class used to access postgres, by default is PSQL
///         the class should provide the following interface
///          - constructor(params) where params should contain:
///            host, port, database, user, password.
///            the class is always constructed with dbParams passed to
///            getRender as-is
///          - query(sql, callback(err, data), readonly)
///            gets an SQL query and return a javascript object with
///            the same structure of a JSON format response from
///            CartoDB-SQL-API, for reference see
///            http://github.com/CartoDB/CartoDB-SQL-API/blob/1.8.2/doc/API.md#json
///            The 'readonly' parameter (false by default) requests
///            that running the query should not allowed to change the database.
///     - dbPoolParams: database connection pool params
///          - size: maximum number of resources to create at any given time
///          - idleTimeout: max milliseconds a resource can go unused before it should be destroyed
///          - reapInterval: frequency to check for idle resources
///
function TorqueFactory(options) {
    // jshint newcap:false
    this.options = options || {};
    _.defaults(this.options, {
        sqlClass: PSQLAdaptor(PSQL, options.dbPoolParams)
    });
    if (this.options.sqlClass) {
        this.sqlClass = this.options.sqlClass;
    }
}

module.exports = TorqueFactory;
const NAME = 'torque';
module.exports.NAME = NAME;

var formatToRenderer = {
    'torque.json': Renderer,
    'torque.bin': Renderer,
    'json.torque': Renderer,
    'bin.torque': Renderer,
    'png': PngRenderer,
    'torque.png': PngRenderer
};

TorqueFactory.prototype = {
    /// API: renderer name, use for information purposes
    name: NAME,

    /// API: tile formats this module is able to render
    // TODO: deprecate 'json.torque' and 'bin.torque' with 1.18.0
    supported_formats: Object.keys(formatToRenderer),

    getName: function() {
        return this.name;
    },

    supportsFormat: function(format) {
        return !!formatToRenderer[format];
    },

    getAdaptor: function(renderer, format, onTileErrorStrategy) {
        return new BaseAdaptor(renderer, format, onTileErrorStrategy);
    },

    getRenderer: function(mapConfig, format, options, callback) {
        var dbParams = RendererParams.dbParamsFromReqParams(options.params);
        var layer = options.layer;

        if (_.isUndefined(layer)) {
            return callback(new Error("torque renderer only supports a single layer"));
        }
        if (!formatToRenderer[format]) {
            return callback(new Error("format not supported: " + format));
        }
        var layerObj = mapConfig.getLayer(layer);
        if ( ! layerObj ) {
            return callback(new Error("layer index is greater than number of layers"));
        }
        if ( layerObj.type !== 'torque' ) {
            return callback(new Error("layer " + layer + " is not a torque layer"));
        }

        _.extend(dbParams, mapConfig.getLayerDatasource(layer));

        var pgSQL = sql(dbParams, this.sqlClass);
        fetchMapConfigAttributes(pgSQL, layerObj, function (err, params) {
            if (err) {
                return callback(err);
            }

            var RendererClass = formatToRenderer[format];

            callback(null, new RendererClass(layerObj, pgSQL, params, layer));
        });
    }
};

//
// given cartocss return javascript properties
//
// @param required optional array of required properties
//
// @throw Error if required properties are not found
//
function attrsFromCartoCSS(cartocss, required) {
  var attrs_keys = {
    '-torque-frame-count': 'steps',
    '-torque-resolution': 'resolution',
    '-torque-animation-duration': 'animationDuration',
    '-torque-aggregation-function': 'countby',
    '-torque-time-attribute': 'column',
    '-torque-data-aggregation': 'data_aggregation'
  };
  carto.tree.Reference.setData(torque_reference.version.latest);
  var env = {
      benchmark: false,
      validation_data: false,
      effects: [],
      errors: [],
      error: function(e) {
        this.errors.push(e);
      }
  };
  var symbolizers = torque_reference.version.latest.layer;
  var root = (carto.Parser(env)).parse(cartocss);
  var definitions = root.toList(env);
  var rules = getMapProperties(definitions, env);
  var attrs = {};
  for (var k in attrs_keys) {
    if (rules[k]) {
      attrs[attrs_keys[k]] = rules[k].value.toString();
      var element = rules[k].value.value[0];
      var type = symbolizers[k].type;
      if(!checkValidType(element, type)){
          throw new Error("Unexpected type for property '" + k + "', expected " + type);
      }
    } else if ( required && required.indexOf(attrs_keys[k]) !== -1 ) {
        throw new Error("Missing required property '" + k + "' in torque layer CartoCSS");
    }

  }
  return attrs;
}

function checkValidType(e, type){
    if (["number", "float"].indexOf(type) > -1) {
      return typeof e.value === "number";
    }
    else if (type === "string"){
      return e.value !== "undefined" && typeof e.value === "string";
    }
    else if (type.constructor === Array){
      return type.indexOf(e.value) > -1 || e.value === "linear";
    }
    else if (type === "color"){
      return checkValidColor(e);
    }
    return true;
}

function checkValidColor(e){
  var expectedArguments = { rgb: 3, hsl: 3, rgba: 4, hsla: 4};
  return typeof e.rgb !== "undefined" || expectedArguments[e.name] === e.args;
}

//
// get rules from Map definition
// stores errors in env.error
//
function getMapProperties(definitions, env) {
  var rules = {};
  definitions.filter(function(r) {
      return r.elements.join('') === 'Map';
  }).forEach(function(r) {
      for (var i = 0; i < r.rules.length; i++) {
          var key = r.rules[i].name;
          rules[key] = r.rules[i].ev(env);
      }
  });
  return rules;
}

//
// check layer and raise an exception is some error is found
//
// @throw Error if missing or malformed CartoCSS
//
function _checkLayerAttributes(layerConfig) {
  var cartoCSS = layerConfig.options.cartocss;
  if (!cartoCSS) {
    throw new Error("cartocss can't be undefined");
  }
  var checks = ['steps', 'resolution', 'column', 'countby'];
  return attrsFromCartoCSS(cartoCSS, checks);
}

// returns an array of errors if the mapconfig is not supported or contains errors
// errors is empty is the configuration is ok
// dbParams: host, dbname, user and pass
// layer: optional, if is specified only a layer is checked
function fetchMapConfigAttributes(sql, layer, callback) {
  // check attrs
  var attrs;
  try {
      attrs = _checkLayerAttributes(layer);
  }  catch (e) {
      return callback(e);
  }

  // fetch layer attributes to check the query and so on is ok
  var layer_sql = layer.options.sql;
  fetchLayerAttributes(sql, layer_sql, attrs, function(err, layerAttrs) {
    if (err) {
      return callback(err);
    }
    callback(null, _.extend(attrs, layerAttrs));
  });
}

function fetchLayerAttributes(sql, layer_sql, attrs, callback) {
    var is_time;

    layer_sql = SubstitutionTokens.replace(layer_sql, {
        bbox: 'ST_MakeEnvelope(0,0,0,0)',
        scale_denominator: '0',
        pixel_width: '1',
        pixel_height: '1'
    });

    // first step, fetch the schema to know if torque colum is time column
    var query = `select * from (${layer_sql}) __torque_wrap_sql limit 0`;
    sql(query, function (err, data) {
        // second step, get time bounds to calculate step
        if (err) {
            if (err.message) {
                err.message = 'TorqueRenderer: ' + err.message;
            }
            return callback(err);
        }
        if (!data) {
            debug(`ERROR: layer query '${layer_sql}' returned no data`);
            const noDataError = new Error("TorqueRenderer: Layer query returned no data");
            return callback(noDataError);
        }
        if (!data.fields.hasOwnProperty(attrs.column)) {
            debug(`ERROR: layer query ${layer_sql} does not include time-attribute column '${attrs.column}'`);
            const columnError = new Error(`TorqueRenderer: Layer query did not return the requested time-attribute column '${attrs.column}'`);
            return callback(columnError);
        }

        is_time = data.fields[attrs.column].type === 'date';
        let max_col = `max(${attrs.column})`;
        let min_col = `min(${attrs.column})`;
        if (is_time) {
            max_col = `date_part('epoch', ${max_col})`;
            min_col = `date_part('epoch', ${min_col})`;
        }

        var query = `
            SELECT count(*) as num_steps, ${max_col} max_date, ${min_col} min_date
            FROM  (${layer_sql}) __torque_wrap_sql
        `;
        sql(query, function generateMetadata(err, data) {
            // prepare metadata needed to render the tiles
            if (err) {
                if (err.message) {
                    err.message = 'TorqueRenderer: ' + err.message;
                }
                return callback(err);
            }
            data = data.rows[0];
            let attributeStep = (data.max_date - data.min_date + 1) / Math.min(attrs.steps, data.num_steps >> 0);
            attributeStep = Math.abs(attributeStep) === Infinity ? 0 : attributeStep;
            callback(null, {
                start: data.min_date,
                end: data.max_date,
                step: attributeStep || 1,
                data_steps: data.num_steps >> 0,
                is_time: is_time
            });
        });
    });
}
<commit_msg>var scope<commit_after>var carto = require('carto');
var debug = require('debug')('windshaft:renderer:torque_factory');
var _ = require('underscore');
var PSQL = require('cartodb-psql');
var torque_reference = require('torque.js').cartocss_reference;
var format = require('../../utils/format');

var RendererParams = require('../renderer_params');

var sql = require('../../utils/sql');
var Renderer = require('./renderer');
var PSQLAdaptor = require('./psql_adaptor');
var PngRenderer = require('./png_renderer');
var BaseAdaptor = require('../base_adaptor');

var SubstitutionTokens = require('../../utils/substitution_tokens');

/// API: initializes the renderer, it should be called once
//
/// @param options initialization options.
///     - sqlClass: class used to access postgres, by default is PSQL
///         the class should provide the following interface
///          - constructor(params) where params should contain:
///            host, port, database, user, password.
///            the class is always constructed with dbParams passed to
///            getRender as-is
///          - query(sql, callback(err, data), readonly)
///            gets an SQL query and return a javascript object with
///            the same structure of a JSON format response from
///            CartoDB-SQL-API, for reference see
///            http://github.com/CartoDB/CartoDB-SQL-API/blob/1.8.2/doc/API.md#json
///            The 'readonly' parameter (false by default) requests
///            that running the query should not allowed to change the database.
///     - dbPoolParams: database connection pool params
///          - size: maximum number of resources to create at any given time
///          - idleTimeout: max milliseconds a resource can go unused before it should be destroyed
///          - reapInterval: frequency to check for idle resources
///
function TorqueFactory(options) {
    // jshint newcap:false
    this.options = options || {};
    _.defaults(this.options, {
        sqlClass: PSQLAdaptor(PSQL, options.dbPoolParams)
    });
    if (this.options.sqlClass) {
        this.sqlClass = this.options.sqlClass;
    }
}

module.exports = TorqueFactory;
const NAME = 'torque';
module.exports.NAME = NAME;

var formatToRenderer = {
    'torque.json': Renderer,
    'torque.bin': Renderer,
    'json.torque': Renderer,
    'bin.torque': Renderer,
    'png': PngRenderer,
    'torque.png': PngRenderer
};

TorqueFactory.prototype = {
    /// API: renderer name, use for information purposes
    name: NAME,

    /// API: tile formats this module is able to render
    // TODO: deprecate 'json.torque' and 'bin.torque' with 1.18.0
    supported_formats: Object.keys(formatToRenderer),

    getName: function() {
        return this.name;
    },

    supportsFormat: function(format) {
        return !!formatToRenderer[format];
    },

    getAdaptor: function(renderer, format, onTileErrorStrategy) {
        return new BaseAdaptor(renderer, format, onTileErrorStrategy);
    },

    getRenderer: function(mapConfig, format, options, callback) {
        var dbParams = RendererParams.dbParamsFromReqParams(options.params);
        var layer = options.layer;

        if (_.isUndefined(layer)) {
            return callback(new Error("torque renderer only supports a single layer"));
        }
        if (!formatToRenderer[format]) {
            return callback(new Error("format not supported: " + format));
        }
        var layerObj = mapConfig.getLayer(layer);
        if ( ! layerObj ) {
            return callback(new Error("layer index is greater than number of layers"));
        }
        if ( layerObj.type !== 'torque' ) {
            return callback(new Error("layer " + layer + " is not a torque layer"));
        }

        _.extend(dbParams, mapConfig.getLayerDatasource(layer));

        var pgSQL = sql(dbParams, this.sqlClass);
        fetchMapConfigAttributes(pgSQL, layerObj, function (err, params) {
            if (err) {
                return callback(err);
            }

            var RendererClass = formatToRenderer[format];

            callback(null, new RendererClass(layerObj, pgSQL, params, layer));
        });
    }
};

//
// given cartocss return javascript properties
//
// @param required optional array of required properties
//
// @throw Error if required properties are not found
//
function attrsFromCartoCSS(cartocss, required) {
  var attrs_keys = {
    '-torque-frame-count': 'steps',
    '-torque-resolution': 'resolution',
    '-torque-animation-duration': 'animationDuration',
    '-torque-aggregation-function': 'countby',
    '-torque-time-attribute': 'column',
    '-torque-data-aggregation': 'data_aggregation'
  };
  carto.tree.Reference.setData(torque_reference.version.latest);
  var env = {
      benchmark: false,
      validation_data: false,
      effects: [],
      errors: [],
      error: function(e) {
        this.errors.push(e);
      }
  };
  var symbolizers = torque_reference.version.latest.layer;
  var root = (carto.Parser(env)).parse(cartocss);
  var definitions = root.toList(env);
  var rules = getMapProperties(definitions, env);
  var attrs = {};
  for (var k in attrs_keys) {
    if (rules[k]) {
      attrs[attrs_keys[k]] = rules[k].value.toString();
      var element = rules[k].value.value[0];
      var type = symbolizers[k].type;
      if(!checkValidType(element, type)){
          throw new Error("Unexpected type for property '" + k + "', expected " + type);
      }
    } else if ( required && required.indexOf(attrs_keys[k]) !== -1 ) {
        throw new Error("Missing required property '" + k + "' in torque layer CartoCSS");
    }

  }
  return attrs;
}

function checkValidType(e, type){
    if (["number", "float"].indexOf(type) > -1) {
      return typeof e.value === "number";
    }
    else if (type === "string"){
      return e.value !== "undefined" && typeof e.value === "string";
    }
    else if (type.constructor === Array){
      return type.indexOf(e.value) > -1 || e.value === "linear";
    }
    else if (type === "color"){
      return checkValidColor(e);
    }
    return true;
}

function checkValidColor(e){
  var expectedArguments = { rgb: 3, hsl: 3, rgba: 4, hsla: 4};
  return typeof e.rgb !== "undefined" || expectedArguments[e.name] === e.args;
}

//
// get rules from Map definition
// stores errors in env.error
//
function getMapProperties(definitions, env) {
  var rules = {};
  definitions.filter(function(r) {
      return r.elements.join('') === 'Map';
  }).forEach(function(r) {
      for (var i = 0; i < r.rules.length; i++) {
          var key = r.rules[i].name;
          rules[key] = r.rules[i].ev(env);
      }
  });
  return rules;
}

//
// check layer and raise an exception is some error is found
//
// @throw Error if missing or malformed CartoCSS
//
function _checkLayerAttributes(layerConfig) {
  var cartoCSS = layerConfig.options.cartocss;
  if (!cartoCSS) {
    throw new Error("cartocss can't be undefined");
  }
  var checks = ['steps', 'resolution', 'column', 'countby'];
  return attrsFromCartoCSS(cartoCSS, checks);
}

// returns an array of errors if the mapconfig is not supported or contains errors
// errors is empty is the configuration is ok
// dbParams: host, dbname, user and pass
// layer: optional, if is specified only a layer is checked
function fetchMapConfigAttributes(sql, layer, callback) {
  // check attrs
  var attrs;
  try {
      attrs = _checkLayerAttributes(layer);
  }  catch (e) {
      return callback(e);
  }

  // fetch layer attributes to check the query and so on is ok
  var layer_sql = layer.options.sql;
  fetchLayerAttributes(sql, layer_sql, attrs, function(err, layerAttrs) {
    if (err) {
      return callback(err);
    }
    callback(null, _.extend(attrs, layerAttrs));
  });
}

function fetchLayerAttributes(sql, layer_sql, attrs, callback) {
    layer_sql = SubstitutionTokens.replace(layer_sql, {
        bbox: 'ST_MakeEnvelope(0,0,0,0)',
        scale_denominator: '0',
        pixel_width: '1',
        pixel_height: '1'
    });

    // first step, fetch the schema to know if torque colum is time column
    var query = `select * from (${layer_sql}) __torque_wrap_sql limit 0`;
    sql(query, function (err, data) {
        // second step, get time bounds to calculate step
        if (err) {
            if (err.message) {
                err.message = 'TorqueRenderer: ' + err.message;
            }
            return callback(err);
        }
        if (!data) {
            debug(`ERROR: layer query '${layer_sql}' returned no data`);
            const noDataError = new Error("TorqueRenderer: Layer query returned no data");
            return callback(noDataError);
        }
        if (!data.fields.hasOwnProperty(attrs.column)) {
            debug(`ERROR: layer query ${layer_sql} does not include time-attribute column '${attrs.column}'`);
            const columnError = new Error(`TorqueRenderer: Layer query did not return the requested time-attribute column '${attrs.column}'`);
            return callback(columnError);
        }

        const is_time = data.fields[attrs.column].type === 'date';
        let max_col = `max(${attrs.column})`;
        let min_col = `min(${attrs.column})`;
        if (is_time) {
            max_col = `date_part('epoch', ${max_col})`;
            min_col = `date_part('epoch', ${min_col})`;
        }

        var query = `
            SELECT count(*) as num_steps, ${max_col} max_date, ${min_col} min_date
            FROM  (${layer_sql}) __torque_wrap_sql
        `;
        sql(query, function generateMetadata(err, data) {
            // prepare metadata needed to render the tiles
            if (err) {
                if (err.message) {
                    err.message = 'TorqueRenderer: ' + err.message;
                }
                return callback(err);
            }
            data = data.rows[0];
            let attributeStep = (data.max_date - data.min_date + 1) / Math.min(attrs.steps, data.num_steps >> 0);
            attributeStep = Math.abs(attributeStep) === Infinity ? 0 : attributeStep;
            callback(null, {
                start: data.min_date,
                end: data.max_date,
                step: attributeStep || 1,
                data_steps: data.num_steps >> 0,
                is_time: is_time
            });
        });
    });
}
<|endoftext|>


<commit_before>'use strict';

[{
  zoneRegex: {
    en: /^The Navel \(Hard\)$/,
  },
  zoneId: ZoneId.TheNavelHard,
  timelineFile: 'titan-hm.txt',
  timelineTriggers: [
    {
      id: 'TitanHm Mountain Buster',
      regex: /Mountain Buster/,
      beforeSeconds: 7,
      condition: function(data) {
        return data.role == 'healer' || data.role == 'tank';
      },
      response: Responses.tankBuster(),
    },
    {
      id: 'TitanHm Mountain Buster Avoid',
      regex: /Mountain Buster/,
      beforeSeconds: 7,
      condition: function(data) {
        return data.role != 'healer' && data.role != 'tank';
      },
      response: Responses.tankCleave(),
    },
    {
      id: 'TitanHm Rock Buster',
      regex: /Rock Buster/,
      beforeSeconds: 6,
      response: Responses.tankCleave('info'),
    },
    {
      id: 'TitanHm Tumult',
      regex: /Tumult/,
      beforeSeconds: 5,
      condition: function(data) {
        return data.role == 'healer' || data.role == 'tank' || data.CanAddle();
      },
      response: Responses.aoe(),
    },
  ],
  triggers: [
    {
      id: 'TitanHm Damage Down',
      netRegex: NetRegexes.gainsEffect({ effectId: '3E' }),
      condition: function(data) {
        return data.CanCleanse();
      },
      infoText: function(data, matches) {
        return {
          en: 'Cleanse ' + data.ShortName(matches.target),
        };
      },
    },
  ],
  timelineReplace: [
    {
      'locale': 'de',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'Bomber-Brocken',
        'Titan': 'Titan',
      },
      'replaceText': {
        '\\(clock\\)': '(Uhrzeiger)',
        'Burst': 'Einschlag',
        'Bury': 'Begraben',
        'Earthen Fury': 'Gaias Zorn',
        'Geocrush': 'Geo-Stoß',
        '(?<! )Landslide': 'Bergsturz',
        'Mountain Buster': 'Bergsprenger',
        'Rock Buster': 'Steinsprenger',
        'Rock Throw': 'Granitgefängnis',
        '(?<! )Tumult': 'Urerschütterung',
        'Upheaval': 'Urtrauma',
        'Weight Of The Land': 'Gaias Gewicht',
      },
    },
    {
      'locale': 'fr',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'Bombo Rocher',
        'Titan': 'Titan',
      },
      'replaceText': {
        '\\(clock\\)': '(horloge)',
        'Burst': 'Explosion',
        'Bury': 'Ensevelissement',
        'Earthen Fury': 'Fureur tellurique',
        'Geocrush': 'Broie-terre',
        '(?<! )Landslide': 'Glissement de terrain',
        'Mountain Buster': 'Casse-montagnes',
        'Rock Buster': 'Casse-roc',
        'Rock Throw': 'Jeté de rocs',
        '(?<! )Tumult': 'Tumulte',
        'Upheaval': 'Bouleversement',
        'Weight Of The Land': 'Poids de la terre',
      },
    },
    {
      'locale': 'ja',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'ボムボルダー',
        'Titan': 'タイタン',
      },
      'replaceText': {
        'Burst': '大爆発',
        'Bury': '衝撃',
        'Earthen Fury': '大地の怒り',
        'Geocrush': 'ジオクラッシュ',
        '(?<! )Landslide': 'ランドスライド',
        'Mountain Buster': 'マウンテンバスター',
        'Rock Buster': 'ロックバスター',
        'Rock Throw': 'グラナイト・ジェイル',
        '(?<! )Tumult': '激震',
        'Upheaval': '大激震',
        'Weight Of The Land': '大地の重み',
      },
    },
    {
      'locale': 'cn',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': '爆破岩石',
        'Titan': '泰坦',
      },
      'replaceText': {
        'Burst': '爆炸',
        'Bury': '塌方',
        'Earthen Fury': '大地之怒',
        'Geocrush': '大地粉碎',
        '(?<! )Landslide': '地裂',
        'Mountain Buster': '山崩',
        'Rock Buster': '碎岩',
        'Rock Throw': '花岗岩牢狱',
        '(?<! )Tumult': '怒震',
        'Upheaval': '大怒震',
        'Weight Of The Land': '大地之重',
      },
    },
    {
      'locale': 'ko',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': '바위폭탄',
        'Titan': '타이탄',
      },
      'replaceText': {
        'Burst': '대폭발',
        'Bury': '충격',
        'Earthen Fury': '대지의 분노',
        'Geocrush': '대지 붕괴',
        '(?<! )Landslide': '산사태',
        'Mountain Buster': '산 쪼개기',
        'Rock Buster': '바위 쪼개기',
        'Rock Throw': '화강암 감옥',
        '(?<! )Tumult': '격진',
        'Upheaval': '대격진',
        'Weight Of The Land': '대지의 무게',
      },
    },
  ],
}];
<commit_msg>raidboss: CN translation for Titan-nm‘s triggers & zoneRegex (#1539)<commit_after>'use strict';

[{
  zoneRegex: {
    en: /^The Navel \(Hard\)$/,
    cn: /^泰坦歼灭战$/,
  },
  zoneId: ZoneId.TheNavelHard,
  timelineFile: 'titan-hm.txt',
  timelineTriggers: [
    {
      id: 'TitanHm Mountain Buster',
      regex: /Mountain Buster/,
      beforeSeconds: 7,
      condition: function(data) {
        return data.role == 'healer' || data.role == 'tank';
      },
      response: Responses.tankBuster(),
    },
    {
      id: 'TitanHm Mountain Buster Avoid',
      regex: /Mountain Buster/,
      beforeSeconds: 7,
      condition: function(data) {
        return data.role != 'healer' && data.role != 'tank';
      },
      response: Responses.tankCleave(),
    },
    {
      id: 'TitanHm Rock Buster',
      regex: /Rock Buster/,
      beforeSeconds: 6,
      response: Responses.tankCleave('info'),
    },
    {
      id: 'TitanHm Tumult',
      regex: /Tumult/,
      beforeSeconds: 5,
      condition: function(data) {
        return data.role == 'healer' || data.role == 'tank' || data.CanAddle();
      },
      response: Responses.aoe(),
    },
  ],
  triggers: [
    {
      id: 'TitanHm Damage Down',
      netRegex: NetRegexes.gainsEffect({ effectId: '3E' }),
      condition: function(data) {
        return data.CanCleanse();
      },
      infoText: function(data, matches) {
        return {
          en: 'Cleanse ' + data.ShortName(matches.target),
          cn: '康复' + data.ShortName(matches.target),
        };
      },
    },
  ],
  timelineReplace: [
    {
      'locale': 'de',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'Bomber-Brocken',
        'Titan': 'Titan',
      },
      'replaceText': {
        '\\(clock\\)': '(Uhrzeiger)',
        'Burst': 'Einschlag',
        'Bury': 'Begraben',
        'Earthen Fury': 'Gaias Zorn',
        'Geocrush': 'Geo-Stoß',
        '(?<! )Landslide': 'Bergsturz',
        'Mountain Buster': 'Bergsprenger',
        'Rock Buster': 'Steinsprenger',
        'Rock Throw': 'Granitgefängnis',
        '(?<! )Tumult': 'Urerschütterung',
        'Upheaval': 'Urtrauma',
        'Weight Of The Land': 'Gaias Gewicht',
      },
    },
    {
      'locale': 'fr',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'Bombo Rocher',
        'Titan': 'Titan',
      },
      'replaceText': {
        '\\(clock\\)': '(horloge)',
        'Burst': 'Explosion',
        'Bury': 'Ensevelissement',
        'Earthen Fury': 'Fureur tellurique',
        'Geocrush': 'Broie-terre',
        '(?<! )Landslide': 'Glissement de terrain',
        'Mountain Buster': 'Casse-montagnes',
        'Rock Buster': 'Casse-roc',
        'Rock Throw': 'Jeté de rocs',
        '(?<! )Tumult': 'Tumulte',
        'Upheaval': 'Bouleversement',
        'Weight Of The Land': 'Poids de la terre',
      },
    },
    {
      'locale': 'ja',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': 'ボムボルダー',
        'Titan': 'タイタン',
      },
      'replaceText': {
        'Burst': '大爆発',
        'Bury': '衝撃',
        'Earthen Fury': '大地の怒り',
        'Geocrush': 'ジオクラッシュ',
        '(?<! )Landslide': 'ランドスライド',
        'Mountain Buster': 'マウンテンバスター',
        'Rock Buster': 'ロックバスター',
        'Rock Throw': 'グラナイト・ジェイル',
        '(?<! )Tumult': '激震',
        'Upheaval': '大激震',
        'Weight Of The Land': '大地の重み',
      },
    },
    {
      'locale': 'cn',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': '爆破岩石',
        'Titan': '泰坦',
      },
      'replaceText': {
        'Burst': '爆炸',
        'Bury': '塌方',
        'Earthen Fury': '大地之怒',
        'Geocrush': '大地粉碎',
        '(?<! )Landslide': '地裂',
        'Mountain Buster': '山崩',
        'Rock Buster': '碎岩',
        'Rock Throw': '花岗岩牢狱',
        '(?<! )Tumult': '怒震',
        'Upheaval': '大怒震',
        'Weight Of The Land': '大地之重',
      },
    },
    {
      'locale': 'ko',
      'missingTranslations': true,
      'replaceSync': {
        'Bomb Boulder': '바위폭탄',
        'Titan': '타이탄',
      },
      'replaceText': {
        'Burst': '대폭발',
        'Bury': '충격',
        'Earthen Fury': '대지의 분노',
        'Geocrush': '대지 붕괴',
        '(?<! )Landslide': '산사태',
        'Mountain Buster': '산 쪼개기',
        'Rock Buster': '바위 쪼개기',
        'Rock Throw': '화강암 감옥',
        '(?<! )Tumult': '격진',
        'Upheaval': '대격진',
        'Weight Of The Land': '대지의 무게',
      },
    },
  ],
}];
<|endoftext|>