<commit_before>window.onload = function() {

	// Video
	var video = document.getElementById("video");

	// Buttons
	var playButton = document.getElementById("play-pause");
	var lastStepBtn = document.getElementById('play-back');
	var muteButton = document.getElementById("mute");
	var fullScreenButton = document.getElementById("full-screen");

	// Sliders
	var seekBar = document.getElementById("seek-bar");
	var volumeBar = document.getElementById("volume-bar");

	// Event listener for the play/pause button
	playButton.addEventListener("click", function() {
		if (video.paused == true) {
			// Play the video
			video.play();
		} else {
			// Pause the video
			video.pause();
		}
	});

	// Event listener for the mute button
	muteButton.addEventListener("click", function() {
		if (video.muted == false) {
			// Mute the video
			video.muted = true;

			// Update the button text
			muteButton.innerHTML = "Unmute";
		} else {
			// Unmute the video
			video.muted = false;

			// Update the button text
			muteButton.innerHTML = "Mute";
		}
	});

	// Event listener for the full-screen button
	fullScreenButton.addEventListener("click", function() {
		if (video.requestFullscreen) {
			video.requestFullscreen();
		} else if (video.mozRequestFullScreen) {
			video.mozRequestFullScreen(); // Firefox
		} else if (video.webkitRequestFullscreen) {
			video.webkitRequestFullscreen(); // Chrome and Safari
		}
	});

	// // Event listener for the seek bar
	// seekBar.addEventListener("change", function() {
	// 	// Calculate the new time
	// 	var time = video.duration * (seekBar.value / 100);

	// 	// Update the video time
	// 	video.currentTime = time;
	// });

	// Event listener for the volume bar
	volumeBar.addEventListener("change", function() {
		// Update the video volume
		video.volume = volumeBar.value;
	})

	var setupVideoPlayer = function(){
		var steps = document.getElementById('video-controls').getAttribute('data-stops').split(' '),
				currentStep = 1,
				isSectionEnded = false;

		$(lastStepBtn).hide();
		//Append step indicator to the video player's time track
		appendSteps(steps);

		//Step indicator onclick, set video's currentTime to the step time.
		//This part has the same logic with selecting an accordion
		$('.step-indicator').on('click', function(){
			var that = this;
			var step = that.getAttribute('data-step') -1;
			$('.accordion a')[step].click();
		})

		//
		$(lastStepBtn).on('click', function(){
			currentStep -= 1;
			video.currentTime = steps[currentStep];

			isSectionEnded = true;

			var accordion = currentStep + 1;
			toggleAccordion(accordion);

			_updatePlayBtnText();
		})

		// Keyup enter key to pause and play video
		document.addEventListener('keyup', function(e){
			if(e.keyCode === 32){
				video.paused ? video.play():video.pause();
			}
		})
		//Stop when it comes to next step
		video.addEventListener('timeupdate', function(){
			// Update the seek bar as the video plays

			// Calculate the slider value
			var value = (100 / video.duration) * video.currentTime;

			// Update the slider value
			seekBar.value = value;

			for (var i = 1; i < steps.length; i++) {
				if(Math.floor(video.currentTime) == steps[i] && currentStep == i){
					video.pause();
					_updatePlayBtnText();
					isSectionEnded = true;
					return;
				}
			};
		})

		//Play to see next step
		video.addEventListener('play', function(){
			if(isSectionEnded){
				currentStep ++;
				toggleAccordion(currentStep);
				isSectionEnded = false;
			}
			$('.play-container').addClass('hide');
			playButton.innerHTML = 'Pause';
		})

		// Show play button on video pause
		video.addEventListener('pause', function(){
			$('.play-container').removeClass('hide');
			_updatePlayBtnText();

		})

		// Pause the video when the seek handle is being dragged
		seekBar.addEventListener("mousedown", function() {
			video.pause();
			$(video).unbind('timeupdate');
		});

		// Play the video when the seek handle is dropped
		// TODO: track the current step on handler release
		seekBar.addEventListener("mouseup", function() {
			// Calculate the new time
			var time = video.duration * (seekBar.value / 100);

			currentStep = _calculateCurrentStep(time);
			console.log(currentStep);
			// Update the video time
			video.currentTime = time;
			video.play();
			$(video).bind('timeupdate');
		});

		//Select accordion to according point of the video
		$('.instruction').on('click', function(e){
			var target = e.target.text.trim().split(' ')[1] - 1;
			currentStep = target;
			isSectionEnded = true;
			video.pause();
			video.currentTime = steps[target];
		})

		function _updatePlayBtnText(){
			if(currentStep > 0 && currentStep !== steps.length && isSectionEnded){
				playButton.innerHTML = 'Play Next Step';
				$(lastStepBtn).show();
			}else{
				playButton.innerHTML = 'Play';
				$(lastStepBtn).hide();
			}
		}

		function _calculateCurrentStep(time){
			//Calculate which step it is in
			for (var i = steps.length - 1; i >= 0; i--) {
				if(time > steps[i]){
					return currentStep = i+1;
				}
			};
		}

	}

	var appendSteps = function(array){
		for (var i = 0; i < array.length; i++) {
			var stepTime = array[i],
					stepPosition = stepTime/video.duration * 100 - 0.1,
					$tagSpan = $('').css('left', stepPosition + '%').text(i + 1).attr({'data-time':stepTime, 'data-step':i+1});
			$('.seekbar-container').append($tagSpan);
		};
	}

	//Toggle accordion with the according step
	var toggleAccordion = function(step){
		var accordion = $('.collapse')[step -1];
		$('.collapse').not(accordion).removeClass('in');
		if(!$(accordion).hasClass('in')){
			$(accordion).addClass('in');
		}
	}

	setupVideoPlayer();
}

<commit_msg>fixed the collapse issues and replay issue<commit_after>window.onload = function() {

	// Video
	var video = document.getElementById("video");

	// Buttons
	var playButton = document.getElementById("play-pause");
	var lastStepBtn = document.getElementById('play-back');
	var muteButton = document.getElementById("mute");
	var fullScreenButton = document.getElementById("full-screen");

	// Sliders
	var seekBar = document.getElementById("seek-bar");
	var volumeBar = document.getElementById("volume-bar");

	// Event listener for the play/pause button
	playButton.addEventListener("click", function() {
		if (video.paused == true) {
			// Play the video
			video.play();
		} else {
			// Pause the video
			video.pause();
		}
	});

	// Event listener for the mute button
	muteButton.addEventListener("click", function() {
		if (video.muted == false) {
			// Mute the video
			video.muted = true;

			// Update the button text
			muteButton.innerHTML = "Unmute";
		} else {
			// Unmute the video
			video.muted = false;

			// Update the button text
			muteButton.innerHTML = "Mute";
		}
	});

	// Event listener for the full-screen button
	fullScreenButton.addEventListener("click", function() {
		if (video.requestFullscreen) {
			video.requestFullscreen();
		} else if (video.mozRequestFullScreen) {
			video.mozRequestFullScreen(); // Firefox
		} else if (video.webkitRequestFullscreen) {
			video.webkitRequestFullscreen(); // Chrome and Safari
		}
	});

	// Event listener for the volume bar
	volumeBar.addEventListener("change", function() {
		// Update the video volume
		video.volume = volumeBar.value;
	})

	var setupVideoPlayer = function(){
		var steps = document.getElementById('video-controls').getAttribute('data-stops').split(' '),
				currentStep = 1,
				isSectionEnded = false;

		$(lastStepBtn).hide();
		//Append step indicator to the video player's time track
		appendSteps(steps);

		//Step indicator onclick, set video's currentTime to the step time.
		//This part has the same logic with selecting an accordion
		$('.step-indicator').on('click', function(){
			var that = this;
			var step = that.getAttribute('data-step') -1;
			$('.accordion a')[step].click();
		})

		//
		$(lastStepBtn).on('click', function(){
			currentStep -= 1;
			video.currentTime = steps[currentStep];

			isSectionEnded = true;

			var accordion = currentStep + 1;
			toggleAccordion(accordion);

			_updatePlayBtnText();
		})

		// Keyup enter key to pause and play video
		document.addEventListener('keyup', function(e){
			if(e.keyCode === 32){
				video.paused ? video.play():video.pause();
			}
		})
		//Stop when it comes to next step
		video.addEventListener('timeupdate', function(){
			// Update the seek bar as the video plays

			// Calculate the slider value
			var value = (100 / video.duration) * video.currentTime;

			// Update the slider value
			seekBar.value = value;

			for (var i = 1; i < steps.length; i++) {
				if(Math.floor(video.currentTime) == steps[i] && currentStep == i){
					video.pause();
					_updatePlayBtnText();
					return isSectionEnded = true;
				}
			};
		})

		//Play to see next step
		video.addEventListener('play', function(){
			if(isSectionEnded){
				currentStep ++;
				isSectionEnded = false;
			}else{
				_calculateCurrentStep(video.currentTime);
				console.log(currentStep);
			}
			$('.play-container').addClass('hide');
			playButton.innerHTML = 'Pause';
			toggleAccordion(currentStep);
		})

		// Show play button on video pause
		video.addEventListener('pause', function(){
			$('.play-container').removeClass('hide');
			_updatePlayBtnText();
		})

		// Pause the video when the seek handle is being dragged
		seekBar.addEventListener("mousedown", function() {
			video.pause();
		});

		// Play the video when the seek handle is dropped
		seekBar.addEventListener("mouseup", function() {
			// Calculate the new time
			var time = video.duration * (seekBar.value / 100);

			// Update the video time
			video.currentTime = time;
			video.play();
		});

		//Select accordion to according point of the video
		$('.instruction').on('click', function(e){
			var target = e.target.text.trim().split(' ')[1] - 1;
			currentStep = target;
			isSectionEnded = true;
			video.pause();
			video.currentTime = steps[target];
			toggleAccordion(currentStep);
		})

		function _updatePlayBtnText(){
			if(currentStep > 0 && currentStep !== steps.length && isSectionEnded){
				playButton.innerHTML = 'Play Next Step';
				$(lastStepBtn).show();
			}else{
				playButton.innerHTML = 'Play';
				$(lastStepBtn).hide();
			}
		}

		function _calculateCurrentStep(time){
			//Calculate which step it is in
			if(video.currentTime !== 0){
				for (var i = steps.length - 1; i >= 0; i--) {
					if(time > steps[i]){
						return currentStep = i+1;
					}
				}
			}
			else{
				return currentStep = 1;
			}
		}

	}

	var appendSteps = function(array){
		for (var i = 0; i < array.length; i++) {
			var stepTime = array[i],
					stepPosition = stepTime/video.duration * 100 - 0.1,
					$tagSpan = $('').css('left', stepPosition + '%').text(i + 1).attr({'data-time':stepTime, 'data-step':i+1});
			$('.seekbar-container').append($tagSpan);
		};
	}

	//Toggle accordion with the according step
	var toggleAccordion = function(step){
		var accordion = $('.collapse')[step -1];
		$('.collapse').not(accordion).each(function(){
			var that = $(this);
			if(that.hasClass('in')){
				$(this).collapse('hide');
			}
		});
		if(!$(accordion).hasClass('in')){
			$(accordion).collapse('show');
		}
	}

	setupVideoPlayer();
}

// !function ($) {
// 	"use strict";

// 	/* STEP PLAYER PUBLIC CLASS DEFINITION
// 	 * =================================== */

// 	var StepPlayer = function(element, options){
// 		this.element = element;
// 	}

// 	StepPlayer.prototype = {
// 		constructor: StepPlayer,
// 		setup: function(){
// 			// Detect video player

// 			// Append video controller with step widgets

// 			// Establish relationship with the collapse sections
// 		},
// 		createVideoController: function(){

// 		}
// 	}

// 	/* STEP PLAYER PLUGIN DEFINITION
// 	 * ============================= */

// 	 var old = $.fn.stepplayer;

// 	 $.fn.stepplayer = function(option){
// 	 	return this.each(function(){
// 	 		var $this = $(this),

// 	 	})
// 	 }
// }(window.jQuery);

<|endoftext|>


<commit_before>////////////////////////////////////////////////////////////////////////////////
/// @brief Traversal "classes"
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2011-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Jan Steemann
/// @author Copyright 2011-2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var internal = require("internal");

// -----------------------------------------------------------------------------
// --SECTION--                                      constructors and destructors
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief traversal constructor
////////////////////////////////////////////////////////////////////////////////

function ArangoTraverser (config) {
  var defaults = {
    order: ArangoTraverser.PRE_ORDER,
    itemOrder: ArangoTraverser.FORWARD,
    strategy: ArangoTraverser.DEPTH_FIRST,
    uniqueness: {
      vertices: ArangoTraverser.UNIQUE_NONE,
      edges: ArangoTraverser.UNIQUE_NONE
    },
    visitor: TrackingVisitor,
    filter: VisitAllFilter,
    expander: CollectionOutboundExpander
  };

  if (typeof config !== "object") {
    throw "invalid configuration";
  }

  // apply defaults
  for (var d in defaults) {
    if (! defaults.hasOwnProperty(d)) {
      continue;
    }

    if (! config.hasOwnProperty(d)) {
      config[d] = defaults[d];
    }
  }

  if (typeof config.visitor !== "function") {
    throw "invalid visitor";
  }

  if (Array.isArray(config.filter)) {
    config.filter.forEach( function (f) {
      if (typeof f !== "function") {
        throw "invalid filter";
      }
    });
    var innerFilters = config.filter.slice()
    var combinedFilter = function(config, vertex, path) {
      return CombineFilters(innerFilters, config, vertex, path);
    };
    config.filter = combinedFilter;
  }

  if (typeof config.filter !== "function") {
    throw "invalid filter";
  }

  if (typeof config.expander !== "function") {
    throw "invalid expander";
  }

  this.config = config;
}

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                    public methods
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief execute the traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.prototype.traverse = function (result, startVertex) {
  // check the start vertex
  if (startVertex == undefined || startVertex == null) {
    throw "invalid startVertex specified for traversal";
  }

  // get the traversal strategy
  var strategy;
  if (this.config.strategy === ArangoTraverser.BREADTH_FIRST) {
    strategy = BreadthFirstSearch();
  }
  else {
    strategy = DepthFirstSearch();
  }

  // run the traversal
  strategy.run(this.config, result, startVertex);
};

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                 private functions
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief parse a filter result
////////////////////////////////////////////////////////////////////////////////

function ParseFilterResult (args) {
  var result = {
    visit: true,
    expand: true
  };

  function processArgument (arg) {
    if (arg == undefined || arg == null) {
      return;
    }

    if (typeof(arg) === 'string') {
      if (arg === ArangoTraverser.EXCLUDE) {
        result.visit = false;
        return;
      }
      else if (arg === ArangoTraverser.PRUNE) {
        result.expand = false;
        return;
      }
      else if (arg === '') {
        return;
      }
    }
    else if (Array.isArray(arg)) {
      for (var i = 0; i < arg.length; ++i) {
        processArgument(arg[i]);
      }
      return;
    }

    throw "invalid filter result";
  }

  processArgument(args);

  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief apply the uniqueness checks
////////////////////////////////////////////////////////////////////////////////

function CheckUniqueness (uniqueness, visited, vertex, edge) {
  if (uniqueness.vertices !== ArangoTraverser.UNIQUE_NONE) {
    if (visited.vertices[vertex._id] === true) {
      return false;
    }

    visited.vertices[vertex._id] = true;
  }

  if (edge != null && uniqueness.edges !== ArangoTraverser.UNIQUE_NONE) {
    if (visited.edges[edge._id] === true) {
      return false;
    }

    visited.edges[edge._id] = true;
  }

  return true;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief check if we must process items in reverse order
////////////////////////////////////////////////////////////////////////////////

function CheckReverse (config) {
  if (config.order === ArangoTraverser.POST_ORDER) {
    // post order
    if (config.itemOrder === ArangoTraverser.FORWARD) {
      return true;
    }
  }
  else if (config.order  === ArangoTraverser.PRE_ORDER) {
    // pre order
    if (config.itemOrder === ArangoTraverser.BACKWARD &&
        config.strategy  === ArangoTraverser.BREADTH_FIRST) {
      return true;
    }
    if (config.itemOrder === ArangoTraverser.FORWARD &&
        config.strategy  === ArangoTraverser.DEPTH_FIRST) {
      return true;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief implementation details for breadth-first strategy
////////////////////////////////////////////////////////////////////////////////

function BreadthFirstSearch () {
  return {

    createPath: function (items, idx) {
      var path = { edges: [ ], vertices: [ ] };
      var pathItem = items[idx];

      while (true) {
        if (pathItem.edge != null) {
          path.edges.unshift(pathItem.edge);
        }
        path.vertices.unshift(pathItem.vertex);
        idx = pathItem.parentIndex;
        if (idx < 0) {
          break;
        }
        pathItem = items[idx];
      }

      return path;
    },

    run: function (config, result, startVertex) {
      var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ];
      var visited = { edges: { }, vertices: { } };

      var index = 0;
      var step = 1;
      var reverse = CheckReverse(config);

      while ((step == 1 && index < toVisit.length) ||
             (step == -1 && index >= 0)) {
        var current = toVisit[index];
        var vertex  = current.vertex;
        var edge    = current.edge;

        if (current.visit == null) {
          current.visit = false;

          // first apply uniqueness check
          if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) {
            if (index < toVisit.length - 1) {
              index += step;
            }
            else {
              step = -1;
            }
            continue;
          }

          var path = this.createPath(toVisit, index);
          var filterResult = ParseFilterResult(config.filter(config, vertex, path));
          if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) {
            // preorder
            config.visitor(config, result, vertex, path);
          }
          else {
            // postorder
            current.visit = filterResult.visit || false;
          }

          if (filterResult.expand) {
            var connected = config.expander(config, vertex, path);
            if (connected.length > 0) {
              reverse && connected.reverse();
              for (var i = 0; i < connected.length; ++i) {
                connected[i].parentIndex = index;
                toVisit.push(connected[i]);
              }
            }
          }

          if (config.order === ArangoTraverser.POST_ORDER) {
            if (index < toVisit.length - 1) {
              index += step;
            }
            else {
              step = -1;
            }
          }
        }
        else {
          if (config.order === ArangoTraverser.POST_ORDER && current.visit) {
            var path = this.createPath(toVisit, index);
            config.visitor(config, result, vertex, path);
          }
          index += step;

        }

      }
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief implementation details for depth-first strategy
////////////////////////////////////////////////////////////////////////////////

function DepthFirstSearch () {
  return {
    run: function (config, result, startVertex) {
      var toVisit = [ { edge: null, vertex: startVertex, visit: null } ];
      var path    = { edges: [ ], vertices: [ ] };
      var visited = { edges: { }, vertices: { } };
      var reverse = CheckReverse(config);

      while (toVisit.length > 0) {
        // peek at the top of the stack
        var current = toVisit[toVisit.length - 1];
        var vertex  = current.vertex;
        var edge    = current.edge;

        // check if we visit the element for the first time
        if (current.visit == null) {
          current.visit = false;

          // first apply uniqueness check
          if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) {
            // skip element if not unique
            toVisit.pop();
            continue;
          }

          // push the current element onto the path stack
          if (edge != null) {
            path.edges.push(edge);
          }
          path.vertices.push(vertex);

          var filterResult = ParseFilterResult(config.filter(config, vertex, path));
          if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) {
            // preorder visit
            config.visitor(config, result, vertex, path);
          }
          else {
            // postorder. mark the element visitation flag because we'll got to check it later
            current.visit = filterResult.visit || false;
          }

          // expand the element's children?
          if (filterResult.expand) {
            var connected = config.expander(config, vertex, path);
            if (connected.length > 0) {
              reverse && connected.reverse();
              for (var i = 0; i < connected.length; ++i) {
                connected[i].visit = null;
                toVisit.push(connected[i]);
              }
            }
          }
        }
        else {
          // we have already seen this element
          if (config.order === ArangoTraverser.POST_ORDER && current.visit) {
            // postorder visitation
            config.visitor(config, result, vertex, path);
          }

          // pop the element from the stack
          toVisit.pop();
          if (path.edges.length > 0) {
            path.edges.pop();
          }
          path.vertices.pop();
        }

      }
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default outbound expander function
////////////////////////////////////////////////////////////////////////////////

function CollectionOutboundExpander (config, vertex, path) {
  var connections = [ ];
  var outEdges = config.edgeCollection.outEdges(vertex._id);

  if (outEdges.length > 1 && config.sort) {
    outEdges.sort(config.sort);
  }

  outEdges.forEach(function (edge) {
    var vertex = internal.db._document(edge._to);
    connections.push({ edge: edge, vertex: vertex });
  });

  return connections;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default inbound expander function
////////////////////////////////////////////////////////////////////////////////

function CollectionInboundExpander (config, vertex, path) {
  var connections = [ ];
  var inEdges = config.edgeCollection.inEdges(vertex._id);

  if (inEdges.length > 1 && config.sort) {
    inEdges.sort(config.sort);
  }

  inEdges.forEach(function (edge) {
    var vertex = internal.db._document(edge._from);
    connections.push({ edge: edge, vertex: vertex });
  });

  return connections;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default visitor that just tracks every visit
////////////////////////////////////////////////////////////////////////////////

function TrackingVisitor (config, result, vertex, path) {
  if (! result || ! result.visited) {
    return;
  }

  function clone (obj) {
    if (obj == null || typeof(obj) !== "object") {
      return obj;
    }

    if (Array.isArray(obj)) {
      var copy = [];
      for (var i = 0; i < obj.length; ++i) {
        copy[i] = clone(obj[i]);
      }
    }
    else if (obj instanceof Object) {
      var copy = {};
      for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) {
          copy[attr] = clone(obj[attr]);
        }
      }
    }

    return copy;
  }

  if (result.visited.vertices) {
    result.visited.vertices.push(clone(vertex));
  }
  if (result.visited.paths) {
    result.visited.paths.push(clone(path));
  }
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to visit & expand all vertices
////////////////////////////////////////////////////////////////////////////////

function VisitAllFilter () {
  return "";
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to visit & expand all vertices up to a given depth
////////////////////////////////////////////////////////////////////////////////
function MaxDepthFilter (config, vertex, path) {
  if (path.vertices.length > config.maxDepth) {
    return "prune";
  }
};

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to exclude all vertices up to a given depth
////////////////////////////////////////////////////////////////////////////////
function MinDepthFilter (config, vertex, path) {
  if (path.vertices.length <= config.minDepth) {
    return "exclude";
  }
};

////////////////////////////////////////////////////////////////////////////////
/// @brief combine an array of filters
////////////////////////////////////////////////////////////////////////////////
function CombineFilters (filters, config, vertex, path) {
  var result = [];
  filters.forEach( function (f) {
    var tmp = f(config, vertex, path);
    if (!Array.isArray(tmp)) {
      tmp = [tmp];
    }
    result = result.concat(tmp);
  });
  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                  public constants
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief every element can be revisited
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_NONE          = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief element can only be revisited if not already in current path
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_PATH          = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief element can only be revisited if not already visited
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_GLOBAL        = 2;

////////////////////////////////////////////////////////////////////////////////
/// @brief visitation strategy breadth first
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.BREADTH_FIRST        = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief visitation strategy depth first
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.DEPTH_FIRST          = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief pre-order traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.PRE_ORDER            = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief post-order traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.POST_ORDER           = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief forward item processing order
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.FORWARD              = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief backward item processing order
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.BACKWARD             = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief prune "constant"
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.PRUNE                = 'prune';

////////////////////////////////////////////////////////////////////////////////
/// @brief exclude "constant"
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.EXCLUDE              = 'exclude';

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                    MODULE EXPORTS
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

exports.Traverser                  = ArangoTraverser;
exports.CollectionOutboundExpander = CollectionOutboundExpander;
exports.CollectionInboundExpander  = CollectionInboundExpander;
exports.VisitAllFilter             = VisitAllFilter;
exports.TrackingVisitor            = TrackingVisitor;
exports.MinDepthFilter             = MinDepthFilter;
exports.MaxDepthFilter             = MaxDepthFilter;

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                       END-OF-FILE
// -----------------------------------------------------------------------------

// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @\\}\\)"
// End:
<commit_msg>First version of expand graphs by labels<commit_after>////////////////////////////////////////////////////////////////////////////////
/// @brief Traversal "classes"
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2011-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Jan Steemann
/// @author Copyright 2011-2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var internal = require("internal");

// -----------------------------------------------------------------------------
// --SECTION--                                      constructors and destructors
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief traversal constructor
////////////////////////////////////////////////////////////////////////////////

function ArangoTraverser (config) {
  var defaults = {
    order: ArangoTraverser.PRE_ORDER,
    itemOrder: ArangoTraverser.FORWARD,
    strategy: ArangoTraverser.DEPTH_FIRST,
    uniqueness: {
      vertices: ArangoTraverser.UNIQUE_NONE,
      edges: ArangoTraverser.UNIQUE_NONE
    },
    visitor: TrackingVisitor,
    filter: VisitAllFilter,
    expander: CollectionOutboundExpander
  };

  if (typeof config !== "object") {
    throw "invalid configuration";
  }

  // apply defaults
  for (var d in defaults) {
    if (! defaults.hasOwnProperty(d)) {
      continue;
    }

    if (! config.hasOwnProperty(d)) {
      config[d] = defaults[d];
    }
  }

  if (typeof config.visitor !== "function") {
    throw "invalid visitor";
  }

  if (Array.isArray(config.filter)) {
    config.filter.forEach( function (f) {
      if (typeof f !== "function") {
        throw "invalid filter";
      }
    });
    var innerFilters = config.filter.slice()
    var combinedFilter = function(config, vertex, path) {
      return CombineFilters(innerFilters, config, vertex, path);
    };
    config.filter = combinedFilter;
  }

  if (typeof config.filter !== "function") {
    throw "invalid filter";
  }

  if (typeof config.expander !== "function") {
    throw "invalid expander";
  }

  this.config = config;
}

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                    public methods
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief execute the traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.prototype.traverse = function (result, startVertex) {
  // check the start vertex
  if (startVertex == undefined || startVertex == null) {
    throw "invalid startVertex specified for traversal";
  }

  // get the traversal strategy
  var strategy;
  if (this.config.strategy === ArangoTraverser.BREADTH_FIRST) {
    strategy = BreadthFirstSearch();
  }
  else {
    strategy = DepthFirstSearch();
  }

  // run the traversal
  strategy.run(this.config, result, startVertex);
};

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                 private functions
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief parse a filter result
////////////////////////////////////////////////////////////////////////////////

function ParseFilterResult (args) {
  var result = {
    visit: true,
    expand: true
  };

  function processArgument (arg) {
    if (arg == undefined || arg == null) {
      return;
    }

    if (typeof(arg) === 'string') {
      if (arg === ArangoTraverser.EXCLUDE) {
        result.visit = false;
        return;
      }
      else if (arg === ArangoTraverser.PRUNE) {
        result.expand = false;
        return;
      }
      else if (arg === '') {
        return;
      }
    }
    else if (Array.isArray(arg)) {
      for (var i = 0; i < arg.length; ++i) {
        processArgument(arg[i]);
      }
      return;
    }

    throw "invalid filter result";
  }

  processArgument(args);

  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief apply the uniqueness checks
////////////////////////////////////////////////////////////////////////////////

function CheckUniqueness (uniqueness, visited, vertex, edge) {
  if (uniqueness.vertices !== ArangoTraverser.UNIQUE_NONE) {
    if (visited.vertices[vertex._id] === true) {
      return false;
    }

    visited.vertices[vertex._id] = true;
  }

  if (edge != null && uniqueness.edges !== ArangoTraverser.UNIQUE_NONE) {
    if (visited.edges[edge._id] === true) {
      return false;
    }

    visited.edges[edge._id] = true;
  }

  return true;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief check if we must process items in reverse order
////////////////////////////////////////////////////////////////////////////////

function CheckReverse (config) {
  if (config.order === ArangoTraverser.POST_ORDER) {
    // post order
    if (config.itemOrder === ArangoTraverser.FORWARD) {
      return true;
    }
  }
  else if (config.order  === ArangoTraverser.PRE_ORDER) {
    // pre order
    if (config.itemOrder === ArangoTraverser.BACKWARD &&
        config.strategy  === ArangoTraverser.BREADTH_FIRST) {
      return true;
    }
    if (config.itemOrder === ArangoTraverser.FORWARD &&
        config.strategy  === ArangoTraverser.DEPTH_FIRST) {
      return true;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief implementation details for breadth-first strategy
////////////////////////////////////////////////////////////////////////////////

function BreadthFirstSearch () {
  return {

    createPath: function (items, idx) {
      var path = { edges: [ ], vertices: [ ] };
      var pathItem = items[idx];

      while (true) {
        if (pathItem.edge != null) {
          path.edges.unshift(pathItem.edge);
        }
        path.vertices.unshift(pathItem.vertex);
        idx = pathItem.parentIndex;
        if (idx < 0) {
          break;
        }
        pathItem = items[idx];
      }

      return path;
    },

    run: function (config, result, startVertex) {
      var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ];
      var visited = { edges: { }, vertices: { } };

      var index = 0;
      var step = 1;
      var reverse = CheckReverse(config);

      while ((step == 1 && index < toVisit.length) ||
             (step == -1 && index >= 0)) {
        var current = toVisit[index];
        var vertex  = current.vertex;
        var edge    = current.edge;

        if (current.visit == null) {
          current.visit = false;

          // first apply uniqueness check
          if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) {
            if (index < toVisit.length - 1) {
              index += step;
            }
            else {
              step = -1;
            }
            continue;
          }

          var path = this.createPath(toVisit, index);
          var filterResult = ParseFilterResult(config.filter(config, vertex, path));
          if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) {
            // preorder
            config.visitor(config, result, vertex, path);
          }
          else {
            // postorder
            current.visit = filterResult.visit || false;
          }

          if (filterResult.expand) {
            var connected = config.expander(config, vertex, path);
            if (connected.length > 0) {
              reverse && connected.reverse();
              for (var i = 0; i < connected.length; ++i) {
                connected[i].parentIndex = index;
                toVisit.push(connected[i]);
              }
            }
          }

          if (config.order === ArangoTraverser.POST_ORDER) {
            if (index < toVisit.length - 1) {
              index += step;
            }
            else {
              step = -1;
            }
          }
        }
        else {
          if (config.order === ArangoTraverser.POST_ORDER && current.visit) {
            var path = this.createPath(toVisit, index);
            config.visitor(config, result, vertex, path);
          }
          index += step;

        }

      }
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief implementation details for depth-first strategy
////////////////////////////////////////////////////////////////////////////////

function DepthFirstSearch () {
  return {
    run: function (config, result, startVertex) {
      var toVisit = [ { edge: null, vertex: startVertex, visit: null } ];
      var path    = { edges: [ ], vertices: [ ] };
      var visited = { edges: { }, vertices: { } };
      var reverse = CheckReverse(config);

      while (toVisit.length > 0) {
        // peek at the top of the stack
        var current = toVisit[toVisit.length - 1];
        var vertex  = current.vertex;
        var edge    = current.edge;

        // check if we visit the element for the first time
        if (current.visit == null) {
          current.visit = false;

          // first apply uniqueness check
          if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) {
            // skip element if not unique
            toVisit.pop();
            continue;
          }

          // push the current element onto the path stack
          if (edge != null) {
            path.edges.push(edge);
          }
          path.vertices.push(vertex);

          var filterResult = ParseFilterResult(config.filter(config, vertex, path));
          if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) {
            // preorder visit
            config.visitor(config, result, vertex, path);
          }
          else {
            // postorder. mark the element visitation flag because we'll got to check it later
            current.visit = filterResult.visit || false;
          }

          // expand the element's children?
          if (filterResult.expand) {
            var connected = config.expander(config, vertex, path);
            if (connected.length > 0) {
              reverse && connected.reverse();
              for (var i = 0; i < connected.length; ++i) {
                connected[i].visit = null;
                toVisit.push(connected[i]);
              }
            }
          }
        }
        else {
          // we have already seen this element
          if (config.order === ArangoTraverser.POST_ORDER && current.visit) {
            // postorder visitation
            config.visitor(config, result, vertex, path);
          }

          // pop the element from the stack
          toVisit.pop();
          if (path.edges.length > 0) {
            path.edges.pop();
          }
          path.vertices.pop();
        }

      }
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default outbound expander function
////////////////////////////////////////////////////////////////////////////////

function CollectionOutboundExpander (config, vertex, path) {
  var connections = [ ];
  var outEdges = config.edgeCollection.outEdges(vertex._id);

  if (outEdges.length > 1 && config.sort) {
    outEdges.sort(config.sort);
  }

  outEdges.forEach(function (edge) {
    var vertex = internal.db._document(edge._to);
    connections.push({ edge: edge, vertex: vertex });
  });

  return connections;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default inbound expander function
////////////////////////////////////////////////////////////////////////////////

function CollectionInboundExpander (config, vertex, path) {
  var connections = [ ];
  var inEdges = config.edgeCollection.inEdges(vertex._id);

  if (inEdges.length > 1 && config.sort) {
    inEdges.sort(config.sort);
  }

  inEdges.forEach(function (edge) {
    var vertex = internal.db._document(edge._from);
    connections.push({ edge: edge, vertex: vertex });
  });

  return connections;
}

///////////////////////////////////////////////////////////////////////////////////////////
/// @brief default expander that expands all edges labeled with one label in config.labels
///////////////////////////////////////////////////////////////////////////////////////////

var ExpandEdgesWithLabels = function (config, vertex, path) {
  var result = [ ];
  if (!Array.isArray(config.labels)) {
    config.labels = [config.labels];
  }
  var edgesList = edges[vertex._id];
  if (edgesList != undefined) {
    for (i = 0; i < edgesList.length; ++i) {
      if (!!~config.labels.indexOf(edgesList[i].label)) {
        result.push({ edge: edgesList[i], vertex: vertices[edgesList[i]._to] });
      }
    }
  }
  return result;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief default visitor that just tracks every visit
////////////////////////////////////////////////////////////////////////////////

function TrackingVisitor (config, result, vertex, path) {
  if (! result || ! result.visited) {
    return;
  }

  function clone (obj) {
    if (obj == null || typeof(obj) !== "object") {
      return obj;
    }

    if (Array.isArray(obj)) {
      var copy = [];
      for (var i = 0; i < obj.length; ++i) {
        copy[i] = clone(obj[i]);
      }
    }
    else if (obj instanceof Object) {
      var copy = {};
      for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) {
          copy[attr] = clone(obj[attr]);
        }
      }
    }

    return copy;
  }

  if (result.visited.vertices) {
    result.visited.vertices.push(clone(vertex));
  }
  if (result.visited.paths) {
    result.visited.paths.push(clone(path));
  }
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to visit & expand all vertices
////////////////////////////////////////////////////////////////////////////////

function VisitAllFilter () {
  return "";
}

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to visit & expand all vertices up to a given depth
////////////////////////////////////////////////////////////////////////////////
function MaxDepthFilter (config, vertex, path) {
  if (path.vertices.length > config.maxDepth) {
    return "prune";
  }
};

////////////////////////////////////////////////////////////////////////////////
/// @brief default filter to exclude all vertices up to a given depth
////////////////////////////////////////////////////////////////////////////////
function MinDepthFilter (config, vertex, path) {
  if (path.vertices.length <= config.minDepth) {
    return "exclude";
  }
};

////////////////////////////////////////////////////////////////////////////////
/// @brief combine an array of filters
////////////////////////////////////////////////////////////////////////////////
function CombineFilters (filters, config, vertex, path) {
  var result = [];
  filters.forEach( function (f) {
    var tmp = f(config, vertex, path);
    if (!Array.isArray(tmp)) {
      tmp = [tmp];
    }
    result = result.concat(tmp);
  });
  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                  public constants
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief every element can be revisited
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_NONE          = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief element can only be revisited if not already in current path
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_PATH          = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief element can only be revisited if not already visited
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.UNIQUE_GLOBAL        = 2;

////////////////////////////////////////////////////////////////////////////////
/// @brief visitation strategy breadth first
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.BREADTH_FIRST        = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief visitation strategy depth first
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.DEPTH_FIRST          = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief pre-order traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.PRE_ORDER            = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief post-order traversal
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.POST_ORDER           = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief forward item processing order
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.FORWARD              = 0;

////////////////////////////////////////////////////////////////////////////////
/// @brief backward item processing order
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.BACKWARD             = 1;

////////////////////////////////////////////////////////////////////////////////
/// @brief prune "constant"
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.PRUNE                = 'prune';

////////////////////////////////////////////////////////////////////////////////
/// @brief exclude "constant"
////////////////////////////////////////////////////////////////////////////////

ArangoTraverser.EXCLUDE              = 'exclude';

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                    MODULE EXPORTS
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoTraverser
/// @{
////////////////////////////////////////////////////////////////////////////////

exports.Traverser                  = ArangoTraverser;
exports.CollectionOutboundExpander = CollectionOutboundExpander;
exports.CollectionInboundExpander  = CollectionInboundExpander;
exports.VisitAllFilter             = VisitAllFilter;
exports.TrackingVisitor            = TrackingVisitor;
exports.MinDepthFilter             = MinDepthFilter;
exports.MaxDepthFilter             = MaxDepthFilter;
exports.ExpandEdgesWithLabels      = ExpandEdgesWithLabels;

////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////

// -----------------------------------------------------------------------------
// --SECTION--                                                       END-OF-FILE
// -----------------------------------------------------------------------------

// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @\\}\\)"
// End:
<|endoftext|>


<commit_before>const moment = require('moment-timezone');
const Promise = require('bluebird');

const dateFormat = 'YYYY-MM-DD HH:mm:ss';
class MembersStats {
    constructor({db, settingsCache, isSQLite}) {
        this._db = db;
        this._settingsCache = settingsCache;
        this._isSQLite = isSQLite;
    }

    /**
     * Fetches count of all members
     */
    async getTotalMembers() {
        const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members');
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     * @param {Number} totalMembers - number of registered members
     * @param {String} siteTimezone - site's current timezone
     */
    async getTotalMembersInRange({days, totalMembers, siteTimezone}) {
        if (days === 'all-time') {
            return totalMembers;
        }

        const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
        const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members WHERE created_at >= ?', [startOfRange]);
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     * Fetches member signups for current day
     *
     * @param {String} siteTimezone - site's current timezone
     */
    async getNewMembersToday({siteTimezone}) {
        const startOfToday = moment.tz(siteTimezone).startOf('day').utc().format(dateFormat);
        const result = await this._db.knex.raw('SELECT count(id) AS total FROM members WHERE created_at >= ?', [startOfToday]);
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     * @param {Number} totalMembers - number of registered members
     * @param {String} siteTimezone - site's current timezone
     */
    async getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}) {
        const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
        const tzOffsetMins = moment.tz(siteTimezone).utcOffset();

        let result;

        if (this._isSQLite) {
            const dateModifier = `${Math.sign(tzOffsetMins) === -1 ? '' : '+'}${tzOffsetMins} minutes`;

            result = await this._db.knex('members')
                .select(this._db.knex.raw('DATE(created_at, ?) AS created_at, COUNT(DATE(created_at, ?)) AS count', [dateModifier, dateModifier]))
                .where((builder) => {
                    if (days !== 'all-time') {
                        builder.whereRaw('created_at >= ?', [startOfRange]);
                    }
                }).groupByRaw('DATE(created_at, ?)', [dateModifier]);
        } else {
            const mins = Math.abs(tzOffsetMins) % 60;
            const hours = (Math.abs(tzOffsetMins) - mins) / 60;
            const utcOffset = `${Math.sign(tzOffsetMins) === -1 ? '-' : '+'}${hours}:${mins < 10 ? '0' : ''}${mins}`;

            result = await this._db.knex('members')
                .select(this._db.knex.raw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?)) AS created_at, COUNT(CONVERT_TZ(created_at, \'+00:00\', ?)) AS count', [utcOffset, utcOffset]))
                .where((builder) => {
                    if (days !== 'all-time') {
                        builder.whereRaw('created_at >= ?', [startOfRange]);
                    }
                })
                .groupByRaw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?))', [utcOffset]);
        }

        // sql doesn't return rows with a 0 count so we build an object
        // with sparse results to reference by date rather than performing
        // multiple finds across an array
        const resultObject = {};
        result.forEach((row) => {
            resultObject[moment(row.created_at).format('YYYY-MM-DD')] = row.count;
        });

        // loop over every date in the range so we can return a contiguous range object
        const totalInRange = Object.values(resultObject).reduce((acc, value) => acc + value, 0);
        let runningTotal = totalMembers - totalInRange;
        let currentRangeDate;

        if (days === 'all-time') {
            // start from the date of first created member
            currentRangeDate = moment(moment(result[0].created_at).format('YYYY-MM-DD')).tz(siteTimezone);
        } else {
            currentRangeDate = moment.tz(siteTimezone).subtract(days - 1, 'days');
        }

        let endDate = moment.tz(siteTimezone).add(1, 'hour');
        const output = {};

        while (currentRangeDate.isBefore(endDate)) {
            let dateStr = currentRangeDate.format('YYYY-MM-DD');
            runningTotal += resultObject[dateStr] || 0;
            output[dateStr] = runningTotal;

            currentRangeDate = currentRangeDate.add(1, 'day');
        }

        return output;
    }

    /**
     * Fetches member's signup statistics
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     */
    async fetch(days) {
        const siteTimezone = this._settingsCache.get('timezone');
        const totalMembers = await this.getTotalMembers();

        // perform final calculations in parallel
        const results = await Promise.props({
            total: totalMembers,
            total_in_range: this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
            total_on_date: this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
            new_today: this.getNewMembersToday({siteTimezone})
        });

        return results;
    }
}

module.exports = MembersStats;
<commit_msg>Added jsdoc to MembersStats class<commit_after>const moment = require('moment-timezone');
const Promise = require('bluebird');

const dateFormat = 'YYYY-MM-DD HH:mm:ss';
class MembersStats {
    /**
     * @param {Object} config
     * @param {Object} config.db - an instance holding knex connection to the database
     * @param {Object} config.settingsCache - an instance of the Ghost Settings Cache
     * @param {Boolean} config.isSQLite - flag identifying if storage is connected to SQLite
     */
    constructor({db, settingsCache, isSQLite}) {
        this._db = db;
        this._settingsCache = settingsCache;
        this._isSQLite = isSQLite;
    }

    /**
     * Fetches count of all members
     */
    async getTotalMembers() {
        const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members');
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     * @param {Number} totalMembers - number of registered members
     * @param {String} siteTimezone - site's current timezone
     */
    async getTotalMembersInRange({days, totalMembers, siteTimezone}) {
        if (days === 'all-time') {
            return totalMembers;
        }

        const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
        const result = await this._db.knex.raw('SELECT COUNT(id) AS total FROM members WHERE created_at >= ?', [startOfRange]);
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     * Fetches member signups for current day
     *
     * @param {String} siteTimezone - site's current timezone
     */
    async getNewMembersToday({siteTimezone}) {
        const startOfToday = moment.tz(siteTimezone).startOf('day').utc().format(dateFormat);
        const result = await this._db.knex.raw('SELECT count(id) AS total FROM members WHERE created_at >= ?', [startOfToday]);
        return this._isSQLite ? result[0].total : result[0][0].total;
    }

    /**
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     * @param {Number} totalMembers - number of registered members
     * @param {String} siteTimezone - site's current timezone
     */
    async getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}) {
        const startOfRange = moment.tz(siteTimezone).subtract(days - 1, 'days').startOf('day').utc().format(dateFormat);
        const tzOffsetMins = moment.tz(siteTimezone).utcOffset();

        let result;

        if (this._isSQLite) {
            const dateModifier = `${Math.sign(tzOffsetMins) === -1 ? '' : '+'}${tzOffsetMins} minutes`;

            result = await this._db.knex('members')
                .select(this._db.knex.raw('DATE(created_at, ?) AS created_at, COUNT(DATE(created_at, ?)) AS count', [dateModifier, dateModifier]))
                .where((builder) => {
                    if (days !== 'all-time') {
                        builder.whereRaw('created_at >= ?', [startOfRange]);
                    }
                }).groupByRaw('DATE(created_at, ?)', [dateModifier]);
        } else {
            const mins = Math.abs(tzOffsetMins) % 60;
            const hours = (Math.abs(tzOffsetMins) - mins) / 60;
            const utcOffset = `${Math.sign(tzOffsetMins) === -1 ? '-' : '+'}${hours}:${mins < 10 ? '0' : ''}${mins}`;

            result = await this._db.knex('members')
                .select(this._db.knex.raw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?)) AS created_at, COUNT(CONVERT_TZ(created_at, \'+00:00\', ?)) AS count', [utcOffset, utcOffset]))
                .where((builder) => {
                    if (days !== 'all-time') {
                        builder.whereRaw('created_at >= ?', [startOfRange]);
                    }
                })
                .groupByRaw('DATE(CONVERT_TZ(created_at, \'+00:00\', ?))', [utcOffset]);
        }

        // sql doesn't return rows with a 0 count so we build an object
        // with sparse results to reference by date rather than performing
        // multiple finds across an array
        const resultObject = {};
        result.forEach((row) => {
            resultObject[moment(row.created_at).format('YYYY-MM-DD')] = row.count;
        });

        // loop over every date in the range so we can return a contiguous range object
        const totalInRange = Object.values(resultObject).reduce((acc, value) => acc + value, 0);
        let runningTotal = totalMembers - totalInRange;
        let currentRangeDate;

        if (days === 'all-time') {
            // start from the date of first created member
            currentRangeDate = moment(moment(result[0].created_at).format('YYYY-MM-DD')).tz(siteTimezone);
        } else {
            currentRangeDate = moment.tz(siteTimezone).subtract(days - 1, 'days');
        }

        let endDate = moment.tz(siteTimezone).add(1, 'hour');
        const output = {};

        while (currentRangeDate.isBefore(endDate)) {
            let dateStr = currentRangeDate.format('YYYY-MM-DD');
            runningTotal += resultObject[dateStr] || 0;
            output[dateStr] = runningTotal;

            currentRangeDate = currentRangeDate.add(1, 'day');
        }

        return output;
    }

    /**
     * Fetches member's signup statistics
     *
     * @param {Number | String} days  - number of days to fetch of 'all-time' to get for all existing records
     */
    async fetch(days) {
        const siteTimezone = this._settingsCache.get('timezone');
        const totalMembers = await this.getTotalMembers();

        // perform final calculations in parallel
        const results = await Promise.props({
            total: totalMembers,
            total_in_range: this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
            total_on_date: this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
            new_today: this.getNewMembersToday({siteTimezone})
        });

        return results;
    }
}

module.exports = MembersStats;
<|endoftext|>


<commit_before><commit_msg>Fixed randomization to have 6go:1nogo (test_block_stim)<commit_after><|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

const assert = require('assert');
const StateTree = require('state-tree');

class Foo {
  func() {
  }
}

const tree = StateTree({
  // Can not pass functions directly
  //func() {
  //},
  foo: new Foo(),
  money: 1000,
  party: {
    members: [
      {
        name: 'Taro',
        job: 'fighter',
      },
      {
        name: 'Hanako',
        job: 'mage',
      },
    ],
  },
});

assert.strictEqual(tree.get('money'), 1000);
assert.deepStrictEqual(tree.get('party.members.1'), {
  name: 'Hanako',
  job: 'mage',
});

tree.push('party.members', {
  name: 'Jiro',
  job: 'thief',
});
assert.deepStrictEqual(tree.get('party.members.2'), {
  name: 'Jiro',
  job: 'thief',
});

//
// Does it not broken unrelated refs?
//
const taro = tree.get('party.members.0');
assert.deepStrictEqual(taro, {
  name: 'Taro',
  job: 'fighter',
});

tree.unshift('party.members', {
  name: 'Hiroshi',
  job: 'ranger',
});

const taro2 = tree.get('party.members.1');
assert.strictEqual(taro, taro2);

const tree2 = tree.get('');
console.log(tree2);
<commit_msg>Update a sample<commit_after>#!/usr/bin/env node

'use strict';

const assert = require('assert');
const StateTree = require('state-tree');

class Foo {
  func() {
  }
}

const tree = StateTree({
  // Can not pass functions directly
  //func() {
  //},
  fame: 10,
  foo: new Foo(),
  money: 1000,
  party: {
    members: [
      {
        name: 'Taro',
        job: 'fighter',
      },
      {
        name: 'Hanako',
        job: 'mage',
      },
    ],
  },
});

assert.strictEqual(tree.get('money'), 1000);
assert.deepStrictEqual(tree.get('party.members.1'), {
  name: 'Hanako',
  job: 'mage',
});

tree.push('party.members', {
  name: 'Jiro',
  job: 'thief',
});
assert.deepStrictEqual(tree.get('party.members.2'), {
  name: 'Jiro',
  job: 'thief',
});

//
// Does it not broken unrelated refs?
//
const taro = tree.get('party.members.0');
assert.deepStrictEqual(taro, {
  name: 'Taro',
  job: 'fighter',
});

tree.unshift('party.members', {
  name: 'Hiroshi',
  job: 'ranger',
});

const taro2 = tree.get('party.members.1');
assert.strictEqual(taro, taro2);

//
// Flushing changes
//
tree.set('fame', 101);  // another value
tree.set('money', 1000);  // same value
assert.deepStrictEqual(tree.flushChanges(), {
  fame: true,
  money: true,  // true!
  party: {
    members: {
      0: true,  // Unshifted "Hiroshi"
      2: true,  // Pushed "Jiro" before "Hiroshi", (current index is 3)
    },
  },
});

// Reset changes
assert.deepStrictEqual(tree.flushChanges(), {});

tree.push('party.members', {
  name: 'Goro',
  job: 'knight',
});
tree.set('party.members.1.job', 'lord');

assert.deepStrictEqual(tree.flushChanges(), {
  party: {
    members: {
      1: {
        job: true,
      },
      4: true,
    },
  },
});

console.log(tree.get(''));
<|endoftext|>


<commit_before>[
{"id": "align_relative_to", "title": "Выровнять по отношению к ..."},
{"id": "angle", "title": "Изменение угла поворота"},
{"id": "angleLabel", "textContent": "Угол:"},
{"id": "bkgnd_color", "title": "Изменение цвета фона / непрозрачность"},
{"id": "circle_cx", "title": "CX изменении круга координата"},
{"id": "circle_cy", "title": "Изменения кружка CY координаты"},
{"id": "circle_r", "title": "Изменение круга's радиус"},
{"id": "cornerRadiusLabel", "textContent": "Уголок Радиус:"},
{"id": "curve_segments", "textContent": "Curve"},
{"id": "ellipse_cx", "title": "Изменения эллипса CX координаты"},
{"id": "ellipse_cy", "title": "Изменения эллипса CY координаты"},
{"id": "ellipse_rx", "title": "Х Изменения эллипса радиусом"},
{"id": "ellipse_ry", "title": "Изменения у эллипса радиусом"},
{"id": "fill_color", "title": "Изменение цвета заливки"},
{"id": "fill_tool_bottom", "textContent": "заполнять:"},
{"id": "fitToContent", "textContent": "По размеру содержания"},
{"id": "fit_to_all", "textContent": "По размеру все содержимое"},
{"id": "fit_to_canvas", "textContent": "Размер холста"},
{"id": "fit_to_layer_content", "textContent": "По размеру слой содержания"},
{"id": "fit_to_sel", "textContent": "Выбор размера"},
{"id": "font_family", "title": "Изменения Семейство шрифтов"},
{"id": "font_size", "title": "Изменить размер шрифта"},
{"id": "group_opacity", "title": "Изменение выбранного пункта непрозрачности"},
{"id": "icon_large", "textContent": "Large"},
{"id": "icon_medium", "textContent": "Medium"},
{"id": "icon_small", "textContent": "Small"},
{"id": "icon_xlarge", "textContent": "Extra Large"},
{"id": "iheightLabel", "textContent": "высота:"},
{"id": "image_height", "title": "Изменение высоты изображения"},
{"id": "image_opt_embed", "textContent": "Embed data (local files)"},
{"id": "image_opt_ref", "textContent": "Use file reference"},
{"id": "image_url", "title": "Изменить URL"},
{"id": "image_width", "title": "Изменения ширина изображения"},
{"id": "includedImages", "textContent": "Included Images"},
{"id": "iwidthLabel", "textContent": "ширина:"},
{"id": "largest_object", "textContent": "крупнейший объект"},
{"id": "layer_delete", "title": "Удалить слой"},
{"id": "layer_down", "title": "Переместить слой на"},
{"id": "layer_new", "title": "Новый слой"},
{"id": "layer_rename", "title": "Переименовать Слой"},
{"id": "layer_up", "title": "Перемещение слоя до"},
{"id": "layersLabel", "textContent": "Слои:"},
{"id": "line_x1", "title": "Изменения линия начинает координаты х"},
{"id": "line_x2", "title": "Изменение за период, закончившийся линия координаты х"},
{"id": "line_y1", "title": "Изменения линия начинает Y координата"},
{"id": "line_y2", "title": "Изменение за период, закончившийся линия Y координата"},
{"id": "page", "textContent": "страница"},
{"id": "palette", "title": "Нажмите для изменения цвета заливки, Shift-Click изменить обводку"},
{"id": "path_node_x", "title": "Change node's x coordinate"},
{"id": "path_node_y", "title": "Change node's y coordinate"},
{"id": "rect_height", "title": "Изменения прямоугольник высотой"},
{"id": "rect_rx", "title": "Изменения прямоугольник Corner Radius"},
{"id": "rect_width", "title": "Изменение ширины прямоугольника"},
{"id": "relativeToLabel", "textContent": "по сравнению с:"},
{"id": "rheightLabel", "textContent": "Высота:"},
{"id": "rwidthLabel", "textContent": "Ширина:"},
{"id": "seg_type", "title": "Change Segment type"},
{"id": "selLayerLabel", "textContent": "Move elements to:"},
{"id": "selLayerNames", "title": "Move selected elements to a different layer"},
{"id": "selectedPredefined", "textContent": "Выберите предопределенный:"},
{"id": "selected_objects", "textContent": "избранными объектами"},
{"id": "selected_x", "title": "Change X coordinate"},
{"id": "selected_y", "title": "Change Y coordinate"},
{"id": "smallest_object", "textContent": "маленький объект"},
{"id": "straight_segments", "textContent": "Straight"},
{"id": "stroke_color", "title": "Изменение цвета инсульт"},
{"id": "stroke_style", "title": "Изменение стиля инсульт тире"},
{"id": "stroke_tool_bottom", "textContent": "удар:"},
{"id": "stroke_width", "title": "Изменения ширина штриха"},
{"id": "svginfo_bg_note", "textContent": "Note: Background will not be saved with image."},
{"id": "svginfo_change_background", "textContent": "Editor Background"},
{"id": "svginfo_dim", "textContent": "Canvas Dimensions"},
{"id": "svginfo_editor_prefs", "textContent": "Editor Preferences"},
{"id": "svginfo_height", "textContent": "Высота:"},
{"id": "svginfo_icons", "textContent": "Icon size"},
{"id": "svginfo_image_props", "textContent": "Image Properties"},
{"id": "svginfo_lang", "textContent": "Language"},
{"id": "svginfo_title", "textContent": "Title"},
{"id": "svginfo_width", "textContent": "Ширина:"},
{"id": "text", "title": "Изменение содержания текста"},
{"id": "tool_alignbottom", "title": "Выровнять по нижнему краю"},
{"id": "tool_aligncenter", "title": "Выровнять по центру"},
{"id": "tool_alignleft", "title": "По левому краю"},
{"id": "tool_alignmiddle", "title": "Выровнять Ближнего"},
{"id": "tool_alignright", "title": "По правому краю"},
{"id": "tool_aligntop", "title": "Выровнять по верхнему краю"},
{"id": "tool_bold", "title": "Жирный текст"},
{"id": "tool_circle", "title": "Круг"},
{"id": "tool_clear", "textContent": "Новое изображение"},
{"id": "tool_clone", "title": "Клон Элемента"},
{"id": "tool_clone_multi", "title": "Клон Элементы"},
{"id": "tool_delete", "title": "Удалить элемент"},
{"id": "tool_delete_multi", "title": "Удалить выбранные элементы"},
{"id": "tool_docprops", "textContent": "Свойства документа"},
{"id": "tool_docprops_cancel", "textContent": "Отмена"},
{"id": "tool_docprops_save", "textContent": "Сохранить"},
{"id": "tool_ellipse", "title": "Эллипс"},
{"id": "tool_fhellipse", "title": "Свободной руки Эллипс"},
{"id": "tool_fhpath", "title": "Pencil Tool"},
{"id": "tool_fhrect", "title": "Свободной руки Прямоугольник"},
{"id": "tool_group", "title": "Группа элементов"},
{"id": "tool_image", "title": "Image Tool"},
{"id": "tool_italic", "title": "Наклонный текст"},
{"id": "tool_line", "title": "Line Tool"},
{"id": "tool_move_bottom", "title": "Переместить вниз"},
{"id": "tool_move_top", "title": "Переместить наверх"},
{"id": "tool_node_clone", "title": "Clone Node"},
{"id": "tool_node_delete", "title": "Delete Node"},
{"id": "tool_node_link", "title": "Link Control Points"},
{"id": "tool_open", "textContent": "Открыть изображение"},
{"id": "tool_path", "title": "Path Tool"},
{"id": "tool_rect", "title": "Прямоугольник"},
{"id": "tool_redo", "title": "Повтор"},
{"id": "tool_reorient", "title": "Reorient path"},
{"id": "tool_save", "textContent": "Сохранить рисунок"},
{"id": "tool_select", "title": "Выберите инструмент"},
{"id": "tool_source", "title": "Изменить исходный"},
{"id": "tool_source_cancel", "textContent": "Отмена"},
{"id": "tool_source_save", "textContent": "Сохранить"},
{"id": "tool_square", "title": "Площадь"},
{"id": "tool_text", "title": "Текст Tool"},
{"id": "tool_topath", "title": "Convert to Path"},
{"id": "tool_undo", "title": "Отменить"},
{"id": "tool_ungroup", "title": "Элементы Разгруппировать"},
{"id": "tool_wireframe", "title": "Wireframe Mode"},
{"id": "tool_zoom", "title": "Zoom Tool"},
{"id": "tools_ellipse_show", "title": "Эллипс / круг инструментов"},
{"id": "tools_rect_show", "title": "Площадь / Rect Tool"},
{"id": "zoom", "title": "Изменение масштаба"},
{"id": "zoomLabel", "textContent": "Увеличить:"},
{"id": "sidepanel_handle", "textContent": "L a y e r s", "title": "Drag left/right to resize side panel"},
{
 "js_strings": {
  "QerrorsRevertToSource": "There were parsing errors in your SVG source.\nRevert back to original SVG source?",
  "QignoreSourceChanges": "Ignore changes made to SVG source?",
  "QmoveElemsToLayer": "Move selected elements to layer '%s'?",
  "QwantToClear": "Do you want to clear the drawing?\nThis will also erase your undo history!",
  "cancel": "Cancel",
  "dupeLayerName": "There is already a layer named that!",
  "enterNewImgURL": "Enter the new image URL",
  "enterNewLayerName": "Please enter the new layer name",
  "enterUniqueLayerName": "Please enter a unique layer name",
  "featNotSupported": "Feature not supported",
  "invalidAttrValGiven": "Invalid value given",
  "key_backspace": "backspace",
  "key_del": "delete",
  "key_down": "down",
  "key_up": "up",
  "layer": "Layer",
  "layerHasThatName": "Layer already has that name",
  "noContentToFitTo": "No content to fit to",
  "ok": "OK",
  "pathCtrlPtTooltip": "Drag control point to adjust curve properties",
  "pathNodeTooltip": "Drag node to move it. Double-click node to change segment type"
 }
}
]<commit_msg>Update the first draft of the Russian translation by Laurent Dufloux.<commit_after>[
{"id":"layer_new","title":"Создать слой"},
{"id":"layer_delete","title":"Удалить слой"},
{"id":"layer_rename","title":"Переименовать Слой"},
{"id":"layer_up","title":"Поднять слой"},
{"id":"layer_down","title":"Опустить слой"},
{"id":"tool_clear","textContent":"Создать изображение"},
{"id":"tool_open","textContent":"Открыть изображение"},
{"id":"tool_save","textContent":"Сохранить изображение"},
{"id":"tool_docprops","textContent":"Свойства документа"},
{"id":"tool_source","title":"Изменить исходный код"},
{"id":"tool_undo","title":"Отменить"},
{"id":"tool_redo","title":"Вернуть"},
{"id":"tool_clone","title":"Создать копию элемента"},
{"id":"tool_delete","title":"Удалить элемент"},
{"id":"tool_move_top","title":"Поднять"},
{"id":"tool_move_bottom","title":"Опустить"},
{"id":"group_opacity","title":"Изменить прозрачность элемента"},
{"id":"angle","title":"Изменить угол поворота"},
{"id":"tool_clone_multi","title":"Создать копию элементов"},
{"id":"tool_delete_multi","title":"Удалить выбранные элементы"},
{"id":"tool_alignleft","title":"По левому краю"},
{"id":"tool_aligncenter","title":"Центрировать по вертикальной оси"},
{"id":"tool_alignright","title":"По правому краю"},
{"id":"tool_aligntop","title":"Выровнять по верхнему краю"},
{"id":"tool_alignmiddle","title":"Центрировать по горизонтальной оси"},
{"id":"tool_alignbottom","title":"Выровнять по нижнему краю"},
{"id":"align_relative_to","title":"Выровнять по отношению к ..."},
{"id":"tool_group","title":"Создать группу элементов"},
{"id":"tool_ungroup","title":"Разгруппировать элементы"},
{"id":"rect_width","title":"Изменение ширину прямоугольника"},
{"id":"rect_height","title":"Изменениe высоту прямоугольника"},
{"id":"rect_rx","title":"Изменить радиус закругленности углов прямоугольника"},
{"id":"image_width","title":"Изменить ширину изображения"},
{"id":"image_height","title":"Изменить высоту изображения"},
{"id":"image_url","title":"Изменить URL"},
{"id":"circle_cx","title":"Изменить горизонтальный координат (CX) окружности"},
{"id":"circle_cy","title":"Изменить вертикальный координат (CY) окружности"},
{"id":"circle_r","title":"Изменить радиус окружности"},
{"id":"ellipse_cx","title":"Изменить горизонтальный координат (CX) эллипса"},
{"id":"ellipse_cy","title":"Изменить вертикальный координат (CY) эллипса"},
{"id":"ellipse_rx","title":"Изменить горизонтальный радиус эллипса"},
{"id":"ellipse_ry","title":"Изменить вертикальный радиус эллипса"},
{"id":"line_x1","title":"Изменить горизонтальный координат X начальной точки линии"},
{"id":"line_y1","title":"Изменить вертикальный координат Y начальной точки линии"},
{"id":"line_x2","title":"Изменить горизонтальный координат X конечной точки линии"},
{"id":"line_y2","title":"Изменить вертикальный координат Y конечной точки линии"},
{"id":"tool_bold","title":"Жирный"},
{"id":"tool_italic","title":"Курсив"},
{"id":"font_family","title":"Изменить семейство шрифтов"},
{"id":"font_size","title":"Изменить размер шрифта"},
{"id":"text","title":"Изменить содержание текста"},
{"id":"tool_select","title":"Выделить"},
{"id":"tool_fhpath","title":"Карандаш"},
{"id":"tool_line","title":"Линия"},
{"id":"tools_rect_show","title":"Прямоугольник / квадрат"},
{"id":"tools_ellipse_show","title":"Эллипс / окружность"},
{"id":"tool_text","title":"Текст"},
{"id":"tool_path","title":"Контуры"},
{"id":"tool_image","title":"Изображение"},
{"id":"tool_zoom","title":"Лупа"},
{"id":"zoom","title":"Изменить масштаб"},
{"id":"fill_color","title":"Изменить цвет заливки"},
{"id":"stroke_color","title":"Изменить цвет обведения"},
{"id":"stroke_width","title":"Изменить толщину обведения"},
{"id":"stroke_style","title":"Изменить стиль обведения"},
{"id":"palette","title":"Нажмите для изменения цвета заливки, Shift-Click изменить цвета обведения"},
{"id":"tool_square","title":"Квардат"},
{"id":"tool_rect","title":"Прямоугольник"},
{"id":"tool_fhrect","title":"Прямоугольник от руки"},
{"id":"tool_circle","title":"Окружность"},
{"id":"tool_ellipse","title":"Эллипс"},
{"id":"tool_fhellipse","title":"Эллипс от руки"},
{"id":"bkgnd_color","title":"Изменить цвет фона или прозрачность"},
{"id":"rwidthLabel","textContent":"Ширина:"},
{"id":"rheightLabel","textContent":"Высота:"},
{"id":"cornerRadiusLabel","textContent":"Радиус закругленности угла"},
{"id":"iwidthLabel","textContent":"Ширина:"},
{"id":"iheightLabel","textContent":"Высота:"},
{"id":"svginfo_width","textContent":"Ширина:"},
{"id":"svginfo_height","textContent":"Высота:"},
{"id":"angleLabel","textContent":"Угол:"},
{"id":"relativeToLabel","textContent":"По отношению к "},
{"id":"zoomLabel","textContent":"Масштаб:"},
{"id":"layersLabel","textContent":"Слои:"},
{"id":"selectedPredefined","textContent":"Выбирать предопределенный размер"},
{"id":"fitToContent","textContent":"По размеру содержания"},
{"id":"tool_source_save","textContent":"Сохранить"},
{"id":"tool_docprops_save","textContent":"Сохранить"},
{"id":"tool_docprops_cancel","textContent":"Отменить"},
{"id":"tool_source_cancel","textContent":"Отменить"},
{"id":"fit_to_all","textContent":"По размеру всех слоев"},
{"id":"fit_to_layer_content","textContent":"По размеру содержания слоя"},
{"id":"fit_to_sel","textContent":"По размеру выделенного"},
{"id":"fit_to_canvas","textContent":"По размеру холста"},
{"id":"selected_objects","textContent":"Выделенные объекты"},
{"id":"largest_object","textContent":"Больший объект"},
{"id":"smallest_object","textContent":"Самый маленький объект"},
{"id":"page","textContent":"страница"},
{"id":"fill_tool_bottom","textContent":"Заливка:"},
{"id":"stroke_tool_bottom","textContent":"Обводка:"},
{"id":"path_node_x","title":"Изменить горизонтальный координат узла"},
{"id":"path_node_y","title":"Изменить вертикальный координат узла"},
{"id":"seg_type","title":"Изменить вид"},
{"id":"straight_segments","textContent":"Прямый"},
{"id":"curve_segments","textContent":"Кривой"},
{"id":"tool_node_clone","title":"Создать копию узла"},
{"id":"tool_node_delete","title":"Удалить узел"},
{"id":"selLayerLabel","textContent":"Переместить выделенные элементы:"},
{"id":"selLayerNames","title":"Переместить выделенные элементы на другой слой"},
{"id":"sidepanel_handle","title":"Перетащить налево или направо","textContent":"С л о и"},
{"id":"tool_wireframe","title":"Каркас"},
{"id":"svginfo_image_props","textContent":"Свойства изображения"},
{"id":"svginfo_title","textContent":"Заглавие"},
{"id":"svginfo_dim","textContent":"Размеры холста"},
{"id":"includedImages","textContent":"Вложенные изображения"},
{"id":"image_opt_embed","textContent":"Местные файлы"},
{"id":"image_opt_ref","textContent":"Ссылки"},
{"id":"svginfo_editor_prefs","textContent":"Параметры"},
{"id":"svginfo_lang","textContent":"Язык"},
{"id":"svginfo_change_background","textContent":"Фон"},
{"id":"svginfo_bg_note","textContent":"(Фон не сохранится вместе с изображением.)"},
{"id":"svginfo_icons","textContent":"Размер значков"},
{"id":"icon_small","textContent":"Малые"},
{"id":"icon_medium","textContent":"Средние"},
{"id":"icon_large","textContent":"Большие"},
{"id":"icon_xlarge","textContent":"Огромные"},
{"id":"selected_x","title":"Изменить горизонтальный координат"},
{"id":"selected_y","title":"Изменить вертикальный координат"},
{"id":"tool_topath","title":"В контур"},
{"id":"tool_reorient","title":"Изменить ориентация контура"},
{"id":"tool_node_link","title":"Связать узлы"},
{"js_strings": {
	"invalidAttrValGiven":"Данный ошибочный",
	"noContentToFitTo":"Нет содержания, по которому выровнять.",
	"layer":"Слой",
	"dupeLayerName":"Слой с этим именем уже существует.",
	"enterUniqueLayerName":"Пожалуйста, введите имя для слоя.",
	"enterNewLayerName":"Пожалуйста, введите новое имя.",
	"layerHasThatName":"Слой уже называется этим именем.",
	"QmoveElemsToLayer":"Переместить выделенные элементы на слой '%s'?",
	"QwantToClear":"Вы хотите очистить?\nИстория действий будет забыта!",
	"QerrorsRevertToSource":"Была проблема при парсинге вашего SVG исходного кода.\nЗаменить его предыдущим SVG кодом?",
	"QignoreSourceChanges":"Забыть без сохранения?",
	"featNotSupported":"Пока невозможно.",
	"enterNewImgURL":"Введите новый URL изображения",
	"ok":"OK",
	"cancel":"Отменить",
	"pathNodeTooltip":"Потащите узел. Чтобы изменить вид отрезка, сделайте двойной щелчок.",
	"pathCtrlPtTooltip":"Потащите, чтобы переместить рычаг",
	"key_up":"Вверх",
	"key_down":"Вниз",
	"key_backspace":"Backspace",
	"key_del":"Delete"
	}
}
]

<|endoftext|>