3
0
Fork 0
mirror of https://github.com/JulianGro/overte.git synced 2025-05-01 06:27:31 +02:00

Merge branch 'master' into vs2015

This commit is contained in:
Brad Davis 2015-06-10 10:11:58 -07:00
commit 1846ead27c
658 changed files with 39140 additions and 16840 deletions
BUILD_OSX.mdBUILD_WIN.mdCMakeLists.txt
assignment-client/src
cmake
domain-server
examples

View file

@ -12,6 +12,12 @@ We have a [homebrew formulas repository](https://github.com/highfidelity/homebre
*Our [qt5 homebrew formula](https://raw.github.com/highfidelity/homebrew-formulas/master/qt5.rb) is for a patched version of Qt 5.4.x stable that removes wireless network scanning that can reduce real-time audio performance. We recommended you use this formula to install Qt.*
###Qt
Assuming you've installed Qt 5 using the homebrew instructions above, you'll need to set QT_CMAKE_PREFIX_PATH so CMake can find your installation of Qt. For Qt 5.4.1 installed via homebrew, set QT_CMAKE_PREFIX_PATH as follows.
export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.4.1/lib/cmake
###Xcode
If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles.
@ -19,4 +25,4 @@ If Xcode is your editor of choice, you can ask CMake to generate Xcode project f
After running cmake, you will have the make files or Xcode project file necessary to build all of the components. Open the hifi.xcodeproj file, choose ALL_BUILD from the Product > Scheme menu (or target drop down), and click Run.
If the build completes successfully, you will have built targets for all components located in the `build/${target_name}/Debug` directories.
If the build completes successfully, you will have built targets for all components located in the `build/${target_name}/Debug` directories.

View file

@ -75,21 +75,6 @@ To prevent these problems, install OpenSSL yourself. Download the following bina
Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version.
###vhacd
Download it directly from https://github.com/virneo/v-hacd
To build it run the following commands
1. cd src\
2. mkdir build
3. cd build
4. cmake ..
Build using visual studio 2013. Build ALL_BUILD and INSTALL targets both in Release and Debug.
This will create an output folder with include and lib directory inside it.
Either copy the contents of output folder to ENV %HIFI_LIB_DIR%/vhacd or create an environment variable VHACD_ROOT_DIR to this output directory.
###Build High Fidelity using Visual Studio
Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake.

View file

@ -40,15 +40,17 @@ if (WIN32)
endif ()
message (WINDOW_SDK_PATH= ${WINDOW_SDK_PATH})
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${WINDOW_SDK_PATH})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
# /wd4351 disables warning C4351: new behavior: elements of array will be default initialized
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /wd4351")
# /LARGEADDRESSAWARE enables 32-bit apps to use more than 2GB of memory.
# Caveats: http://stackoverflow.com/questions/2288728/drawbacks-of-using-largeaddressaware-for-32-bit-windows-executables
# TODO: Remove when building 64-bit.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LARGEADDRESSAWARE")
elseif (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
#SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -pedantic")
#SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-unknown-pragmas")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fno-strict-aliasing -Wno-unused-parameter -ggdb")
else ()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fno-strict-aliasing -Wno-unused-parameter")
if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb")
endif ()
endif(WIN32)
if ((NOT MSVC12) AND (NOT MSVC14))
@ -176,8 +178,13 @@ option(GET_GVERB "Get Gverb library automatically as external project" 1)
option(GET_SOXR "Get Soxr library automatically as external project" 1)
option(GET_TBB "Get Threading Building Blocks library automatically as external project" 1)
option(GET_LIBOVR "Get LibOVR library automatically as external project" 1)
option(USE_NSIGHT "Attempt to find the nSight libraries" 1)
option(GET_VHACD "Get V-HACD library automatically as external project" 1)
option(GET_POLYVOX "Get polyvox library automatically as external project" 1)
option(GET_OPENVR "Get OpenVR library automatically as external project" 1)
option(GET_BOOSTCONFIG "Get Boost-config library automatically as external project" 1)
option(GET_OGLPLUS "Get OGLplus library automatically as external project" 1)
option(USE_NSIGHT "Attempt to find the nSight libraries" 1)
if (WIN32)
option(GET_GLEW "Get GLEW library automatically as external project" 1)

View file

@ -73,17 +73,6 @@ void Agent::readPendingDatagrams() {
}
}
} else if (datagramPacketType == PacketTypeEntityAddResponse) {
// this will keep creatorTokenIDs to IDs mapped correctly
EntityItemID::handleAddEntityResponse(receivedPacket);
// also give our local entity tree a chance to remap any internal locally created entities
_entityViewer.getTree()->handleAddEntityResponse(receivedPacket);
// Make sure our Node and NodeList knows we've heard from this node.
SharedNodePointer sourceNode = nodeList->sendingNodeForPacket(receivedPacket);
sourceNode->setLastHeardMicrostamp(usecTimestampNow());
} else if (datagramPacketType == PacketTypeOctreeStats
|| datagramPacketType == PacketTypeEntityData
|| datagramPacketType == PacketTypeEntityErase
@ -224,5 +213,7 @@ void Agent::run() {
void Agent::aboutToFinish() {
_scriptEngine.stop();
NetworkAccessManager::getInstance().clearAccessCache();
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(NULL);
}

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <assert.h>
#include <QProcess>
#include <QSettings>
#include <QSharedMemory>
@ -18,6 +20,7 @@
#include <AccountManager.h>
#include <AddressManager.h>
#include <Assignment.h>
#include <AvatarHashMap.h>
#include <EntityScriptingInterface.h>
#include <LogHandler.h>
#include <LogUtils.h>
@ -29,31 +32,30 @@
#include <SoundCache.h>
#include "AssignmentFactory.h"
#include "AssignmentThread.h"
#include "AssignmentClient.h"
const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client";
const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000;
SharedAssignmentPointer AssignmentClient::_currentAssignment;
int hifiSockAddrMeta = qRegisterMetaType<HifiSockAddr>("HifiSockAddr");
AssignmentClient::AssignmentClient(int ppid, Assignment::Type requestAssignmentType, QString assignmentPool,
QUuid walletUUID, QString assignmentServerHostname, quint16 assignmentServerPort) :
_assignmentServerHostname(DEFAULT_ASSIGNMENT_SERVER_HOSTNAME),
_localASPortSharedMem(NULL),
_localACMPortSharedMem(NULL)
AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QString assignmentPool,
QUuid walletUUID, QString assignmentServerHostname, quint16 assignmentServerPort,
quint16 assignmentMonitorPort) :
_assignmentServerHostname(DEFAULT_ASSIGNMENT_SERVER_HOSTNAME)
{
LogUtils::init();
QSettings::setDefaultFormat(QSettings::IniFormat);
// create a NodeList as an unassigned client
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
auto addressManager = DependencyManager::set<AddressManager>();
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned);
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned); // Order is important
auto animationCache = DependencyManager::set<AnimationCache>();
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
auto entityScriptingInterface = DependencyManager::set<EntityScriptingInterface>();
// make up a uuid for this child so the parent can tell us apart. This id will be changed
@ -73,18 +75,18 @@ AssignmentClient::AssignmentClient(int ppid, Assignment::Type requestAssignmentT
qDebug() << "The destination wallet UUID for credits is" << uuidStringWithoutCurlyBraces(walletUUID);
_requestAssignment.setWalletUUID(walletUUID);
}
// check for an overriden assignment server hostname
if (assignmentServerHostname != "") {
// change the hostname for our assignment server
_assignmentServerHostname = assignmentServerHostname;
}
_assignmentServerSocket = HifiSockAddr(_assignmentServerHostname, assignmentServerPort, true);
nodeList->setAssignmentServerSocket(_assignmentServerSocket);
qDebug() << "Assignment server socket is" << _assignmentServerSocket;
// call a timer function every ASSIGNMENT_REQUEST_INTERVAL_MSECS to ask for assignment, if required
qDebug() << "Waiting for assignment -" << _requestAssignment;
@ -101,32 +103,57 @@ AssignmentClient::AssignmentClient(int ppid, Assignment::Type requestAssignmentT
// connections to AccountManager for authentication
connect(&AccountManager::getInstance(), &AccountManager::authRequired,
this, &AssignmentClient::handleAuthenticationRequest);
// Create Singleton objects on main thread
NetworkAccessManager::getInstance();
// Hook up a timer to send this child's status to the Monitor once per second
setUpStatsToMonitor(ppid);
// did we get an assignment-client monitor port?
if (assignmentMonitorPort > 0) {
_assignmentClientMonitorSocket = HifiSockAddr(DEFAULT_ASSIGNMENT_CLIENT_MONITOR_HOSTNAME, assignmentMonitorPort);
qDebug() << "Assignment-client monitor socket is" << _assignmentClientMonitorSocket;
// Hook up a timer to send this child's status to the Monitor once per second
setUpStatsToMonitor();
}
}
void AssignmentClient::stopAssignmentClient() {
qDebug() << "Exiting.";
qDebug() << "Forced stop of assignment-client.";
_requestTimer.stop();
_statsTimerACM.stop();
QCoreApplication::quit();
if (_currentAssignment) {
// grab the thread for the current assignment
QThread* currentAssignmentThread = _currentAssignment->thread();
// ask the current assignment to stop
QMetaObject::invokeMethod(_currentAssignment, "stop", Qt::BlockingQueuedConnection);
// ask the current assignment to delete itself on its thread
_currentAssignment->deleteLater();
// when this thread is destroyed we don't need to run our assignment complete method
disconnect(currentAssignmentThread, &QThread::destroyed, this, &AssignmentClient::assignmentCompleted);
// wait on the thread from that assignment - it will be gone once the current assignment deletes
currentAssignmentThread->quit();
currentAssignmentThread->wait();
}
}
void AssignmentClient::setUpStatsToMonitor(int ppid) {
// Figure out the address to send out stats to
quint16 localMonitorServerPort = DEFAULT_ASSIGNMENT_CLIENT_MONITOR_PORT;
auto nodeList = DependencyManager::get<NodeList>();
void AssignmentClient::aboutToQuit() {
stopAssignmentClient();
nodeList->getLocalServerPortFromSharedMemory(QString(ASSIGNMENT_CLIENT_MONITOR_LOCAL_PORT_SMEM_KEY) + "-" +
QString::number(ppid), _localACMPortSharedMem, localMonitorServerPort);
_assignmentClientMonitorSocket = HifiSockAddr(DEFAULT_ASSIGNMENT_CLIENT_MONITOR_HOSTNAME, localMonitorServerPort, true);
// clear the log handler so that Qt doesn't call the destructor on LogHandler
qInstallMessageHandler(0);
}
void AssignmentClient::setUpStatsToMonitor() {
// send a stats packet every 1 seconds
connect(&_statsTimerACM, &QTimer::timeout, this, &AssignmentClient::sendStatsPacketToACM);
_statsTimerACM.start(1000);
@ -147,24 +174,27 @@ void AssignmentClient::sendStatsPacketToACM() {
void AssignmentClient::sendAssignmentRequest() {
if (!_currentAssignment) {
auto nodeList = DependencyManager::get<NodeList>();
if (_assignmentServerHostname == "localhost") {
// we want to check again for the local domain-server port in case the DS has restarted
quint16 localAssignmentServerPort;
if (nodeList->getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, _localASPortSharedMem,
localAssignmentServerPort)) {
if (nodeList->getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, localAssignmentServerPort)) {
if (localAssignmentServerPort != _assignmentServerSocket.getPort()) {
qDebug() << "Port for local assignment server read from shared memory is"
<< localAssignmentServerPort;
_assignmentServerSocket.setPort(localAssignmentServerPort);
nodeList->setAssignmentServerSocket(_assignmentServerSocket);
}
} else {
qDebug() << "Failed to read local assignment server port from shared memory"
<< "- will send assignment request to previous assignment server socket.";
}
}
nodeList->sendAssignment(_requestAssignment);
}
}
@ -182,8 +212,11 @@ void AssignmentClient::readPendingDatagrams() {
if (nodeList->packetVersionAndHashMatch(receivedPacket)) {
if (packetTypeForPacket(receivedPacket) == PacketTypeCreateAssignment) {
qDebug() << "Received a PacketTypeCreateAssignment - attempting to unpack.";
// construct the deployed assignment from the packet data
_currentAssignment = SharedAssignmentPointer(AssignmentFactory::unpackAssignment(receivedPacket));
_currentAssignment = AssignmentFactory::unpackAssignment(receivedPacket);
if (_currentAssignment) {
qDebug() << "Received an assignment -" << *_currentAssignment;
@ -196,14 +229,26 @@ void AssignmentClient::readPendingDatagrams() {
qDebug() << "Destination IP for assignment is" << nodeList->getDomainHandler().getIP().toString();
// start the deployed assignment
AssignmentThread* workerThread = new AssignmentThread(_currentAssignment, this);
QThread* workerThread = new QThread;
workerThread->setObjectName("ThreadedAssignment Worker");
connect(workerThread, &QThread::started, _currentAssignment.data(), &ThreadedAssignment::run);
connect(_currentAssignment.data(), &ThreadedAssignment::finished, workerThread, &QThread::quit);
connect(_currentAssignment.data(), &ThreadedAssignment::finished,
this, &AssignmentClient::assignmentCompleted);
// Once the ThreadedAssignment says it is finished - we ask it to deleteLater
// This is a queued connection so that it is put into the event loop to be processed by the worker
// thread when it is ready.
connect(_currentAssignment.data(), &ThreadedAssignment::finished, _currentAssignment.data(),
&ThreadedAssignment::deleteLater, Qt::QueuedConnection);
// once it is deleted, we quit the worker thread
connect(_currentAssignment.data(), &ThreadedAssignment::destroyed, workerThread, &QThread::quit);
// have the worker thread remove itself once it is done
connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater);
// once the worker thread says it is done, we consider the assignment completed
connect(workerThread, &QThread::destroyed, this, &AssignmentClient::assignmentCompleted);
_currentAssignment->moveToThread(workerThread);
// move the NodeList to the thread used for the _current assignment
@ -222,8 +267,9 @@ void AssignmentClient::readPendingDatagrams() {
} else if (packetTypeForPacket(receivedPacket) == PacketTypeStopNode) {
if (senderSockAddr.getAddress() == QHostAddress::LocalHost ||
senderSockAddr.getAddress() == QHostAddress::LocalHostIPv6) {
qDebug() << "Network told me to exit.";
emit stopAssignmentClient();
qDebug() << "AssignmentClientMonitor at" << senderSockAddr << "requested stop via PacketTypeStopNode.";
QCoreApplication::quit();
} else {
qDebug() << "Got a stop packet from other than localhost.";
}
@ -260,21 +306,23 @@ void AssignmentClient::handleAuthenticationRequest() {
}
void AssignmentClient::assignmentCompleted() {
// we expect that to be here the previous assignment has completely cleaned up
assert(_currentAssignment.isNull());
// reset our current assignment pointer to NULL now that it has been deleted
_currentAssignment = NULL;
// reset the logging target to the the CHILD_TARGET_NAME
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME);
qDebug("Assignment finished or never started - waiting for new assignment.");
qDebug() << "Assignment finished or never started - waiting for new assignment.";
auto nodeList = DependencyManager::get<NodeList>();
// have us handle incoming NodeList datagrams again
disconnect(&nodeList->getNodeSocket(), 0, _currentAssignment.data(), 0);
// have us handle incoming NodeList datagrams again, and make sure our ThreadedAssignment isn't handling them
connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClient::readPendingDatagrams);
// clear our current assignment shared pointer now that we're done with it
// if the assignment thread is still around it has its own shared pointer to the assignment
_currentAssignment.clear();
// reset our NodeList by switching back to unassigned and clearing the list
nodeList->setOwnerType(NodeType::Unassigned);
nodeList->reset();

View file

@ -13,6 +13,7 @@
#define hifi_AssignmentClient_h
#include <QtCore/QCoreApplication>
#include <QtCore/QPointer>
#include "ThreadedAssignment.h"
@ -22,10 +23,9 @@ class AssignmentClient : public QObject {
Q_OBJECT
public:
AssignmentClient(int ppid, Assignment::Type requestAssignmentType, QString assignmentPool,
QUuid walletUUID, QString assignmentServerHostname, quint16 assignmentServerPort);
static const SharedAssignmentPointer& getCurrentAssignment() { return _currentAssignment; }
AssignmentClient(Assignment::Type requestAssignmentType, QString assignmentPool,
QUuid walletUUID, QString assignmentServerHostname, quint16 assignmentServerPort,
quint16 assignmentMonitorPort);
private slots:
void sendAssignmentRequest();
void readPendingDatagrams();
@ -34,14 +34,16 @@ private slots:
void sendStatsPacketToACM();
void stopAssignmentClient();
public slots:
void aboutToQuit();
private:
void setUpStatsToMonitor(int ppid);
void setUpStatsToMonitor();
Assignment _requestAssignment;
static SharedAssignmentPointer _currentAssignment;
QPointer<ThreadedAssignment> _currentAssignment;
QString _assignmentServerHostname;
HifiSockAddr _assignmentServerSocket;
QSharedMemory* _localASPortSharedMem; // memory shared with domain server
QSharedMemory* _localACMPortSharedMem; // memory shared with assignment client monitor
QTimer _requestTimer; // timer for requesting and assignment
QTimer _statsTimerACM; // timer for sending stats to assignment client monitor

View file

@ -10,6 +10,7 @@
//
#include <QCommandLineParser>
#include <QThread>
#include <LogHandler.h>
#include <SharedUtil.h>
@ -78,9 +79,8 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
const QCommandLineOption maxChildsOption(ASSIGNMENT_MAX_FORKS_OPTION, "maximum number of children", "child-count");
parser.addOption(maxChildsOption);
const QCommandLineOption ppidOption(PARENT_PID_OPTION, "parent's process id", "pid");
parser.addOption(ppidOption);
const QCommandLineOption monitorPortOption(ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION, "assignment-client monitor port", "port");
parser.addOption(monitorPortOption);
if (!parser.parse(QCoreApplication::arguments())) {
qCritical() << parser.errorText() << endl;
@ -112,9 +112,9 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
maxForks = parser.value(maxChildsOption).toInt();
}
int ppid = 0;
if (parser.isSet(ppidOption)) {
ppid = parser.value(ppidOption).toInt();
unsigned short monitorPort = 0;
if (parser.isSet(monitorPortOption)) {
monitorPort = parser.value(monitorPortOption).toUShort();
}
if (!numForks && minForks) {
@ -161,12 +161,11 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
if (argumentVariantMap.contains(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) {
assignmentServerPort = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION).toString().toUInt();
}
if (parser.isSet(assignmentServerPortOption)) {
assignmentServerPort = parser.value(assignmentServerPortOption).toInt();
}
if (parser.isSet(numChildsOption)) {
if (minForks && minForks > numForks) {
qCritical() << "--min can't be more than -n";
@ -180,14 +179,22 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
}
}
QThread::currentThread()->setObjectName("main thread");
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
if (numForks || minForks || maxForks) {
AssignmentClientMonitor monitor(numForks, minForks, maxForks, requestAssignmentType, assignmentPool,
walletUUID, assignmentServerHostname, assignmentServerPort);
exec();
AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks,
requestAssignmentType, assignmentPool,
walletUUID, assignmentServerHostname,
assignmentServerPort);
monitor->setParent(this);
connect(this, &QCoreApplication::aboutToQuit, monitor, &AssignmentClientMonitor::aboutToQuit);
} else {
AssignmentClient client(ppid, requestAssignmentType, assignmentPool,
walletUUID, assignmentServerHostname, assignmentServerPort);
exec();
AssignmentClient* client = new AssignmentClient(requestAssignmentType, assignmentPool,
walletUUID, assignmentServerHostname,
assignmentServerPort, monitorPort);
client->setParent(this);
connect(this, &QCoreApplication::aboutToQuit, client, &AssignmentClient::aboutToQuit);
}
}

View file

@ -23,7 +23,7 @@ const QString CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION = "p";
const QString ASSIGNMENT_NUM_FORKS_OPTION = "n";
const QString ASSIGNMENT_MIN_FORKS_OPTION = "min";
const QString ASSIGNMENT_MAX_FORKS_OPTION = "max";
const QString PARENT_PID_OPTION = "ppid";
const QString ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION = "monitor-port";
class AssignmentClientApp : public QCoreApplication {

View file

@ -11,8 +11,9 @@
#include <signal.h>
#include <LogHandler.h>
#include <AddressManager.h>
#include <JSONBreakableMarshal.h>
#include <LogHandler.h>
#include "AssignmentClientMonitor.h"
#include "AssignmentClientApp.h"
@ -22,6 +23,7 @@
const QString ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME = "assignment-client-monitor";
const int WAIT_FOR_CHILD_MSECS = 1000;
AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmentClientForks,
const unsigned int minAssignmentClientForks,
@ -39,22 +41,20 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
_assignmentServerPort(assignmentServerPort)
{
qDebug() << "_requestAssignmentType =" << _requestAssignmentType;
// start the Logging class with the parent's target name
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME);
// make sure we output process IDs for a monitor otherwise it's insane to parse
LogHandler::getInstance().setShouldOutputPID(true);
// create a NodeList so we can receive stats from children
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
auto addressManager = DependencyManager::set<AddressManager>();
auto nodeList = DependencyManager::set<LimitedNodeList>(DEFAULT_ASSIGNMENT_CLIENT_MONITOR_PORT);
auto nodeList = DependencyManager::set<LimitedNodeList>();
connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClientMonitor::readPendingDatagrams);
qint64 pid = QCoreApplication::applicationPid ();
nodeList->putLocalPortIntoSharedMemory(QString(ASSIGNMENT_CLIENT_MONITOR_LOCAL_PORT_SMEM_KEY) + "-" + QString::number(pid),
this, nodeList->getNodeSocket().localPort());
// use QProcess to fork off a process for each of the child assignment clients
for (unsigned int i = 0; i < _numAssignmentClientForks; i++) {
spawnChildClient();
@ -69,20 +69,60 @@ AssignmentClientMonitor::~AssignmentClientMonitor() {
stopChildProcesses();
}
void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) {
QElapsedTimer waitTimer;
waitTimer.start();
// loop as long as we still have processes around and we're inside the wait window
while(_childProcesses.size() > 0 && !waitTimer.hasExpired(waitMsecs)) {
// continue processing events so we can handle a process finishing up
QCoreApplication::processEvents();
}
}
void AssignmentClientMonitor::childProcessFinished() {
QProcess* childProcess = qobject_cast<QProcess*>(sender());
qint64 processID = _childProcesses.key(childProcess);
if (processID > 0) {
qDebug() << "Child process" << processID << "has finished. Removing from internal map.";
_childProcesses.remove(processID);
}
}
void AssignmentClientMonitor::stopChildProcesses() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([&](const SharedNodePointer& node) {
qDebug() << "asking child" << node->getUUID() << "to exit.";
node->activateLocalSocket();
QByteArray diePacket = byteArrayWithPopulatedHeader(PacketTypeStopNode);
nodeList->writeUnverifiedDatagram(diePacket, *node->getActiveSocket());
});
// ask child processes to terminate
foreach(QProcess* childProcess, _childProcesses) {
qDebug() << "Attempting to terminate child process" << childProcess->processId();
childProcess->terminate();
}
simultaneousWaitOnChildren(WAIT_FOR_CHILD_MSECS);
if (_childProcesses.size() > 0) {
// ask even more firmly
foreach(QProcess* childProcess, _childProcesses) {
qDebug() << "Attempting to kill child process" << childProcess->processId();
childProcess->kill();
}
simultaneousWaitOnChildren(WAIT_FOR_CHILD_MSECS);
}
}
void AssignmentClientMonitor::aboutToQuit() {
stopChildProcesses();
// clear the log handler so that Qt doesn't call the destructor on LogHandler
qInstallMessageHandler(0);
}
void AssignmentClientMonitor::spawnChildClient() {
QProcess *assignmentClient = new QProcess(this);
QProcess* assignmentClient = new QProcess(this);
// unparse the parts of the command-line that the child cares about
QStringList _childArguments;
if (_assignmentPool != "") {
@ -106,21 +146,23 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append(QString::number(_requestAssignmentType));
}
// tell children which shared memory key to use
qint64 pid = QCoreApplication::applicationPid ();
_childArguments.append("--" + PARENT_PID_OPTION);
_childArguments.append(QString::number(pid));
// tell children which assignment monitor port to use
// for now they simply talk to us on localhost
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
_childArguments.append(QString::number(DependencyManager::get<NodeList>()->getLocalSockAddr().getPort()));
// make sure that the output from the child process appears in our output
assignmentClient->setProcessChannelMode(QProcess::ForwardedChannels);
assignmentClient->start(QCoreApplication::applicationFilePath(), _childArguments);
// make sure we hear that this process has finished when it does
connect(assignmentClient, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(childProcessFinished()));
qDebug() << "Spawned a child client with PID" << assignmentClient->pid();
_childProcesses.insert(assignmentClient->processId(), assignmentClient);
}
void AssignmentClientMonitor::checkSpares() {
auto nodeList = DependencyManager::get<NodeList>();
QUuid aSpareId = "";
@ -152,7 +194,8 @@ void AssignmentClientMonitor::checkSpares() {
qDebug() << "asking child" << aSpareId << "to exit.";
SharedNodePointer childNode = nodeList->nodeWithUUID(aSpareId);
childNode->activateLocalSocket();
QByteArray diePacket = byteArrayWithPopulatedHeader(PacketTypeStopNode);
QByteArray diePacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeStopNode);
nodeList->writeUnverifiedDatagram(diePacket, childNode);
}
}
@ -186,7 +229,7 @@ void AssignmentClientMonitor::readPendingDatagrams() {
} else {
// tell unknown assignment-client child to exit.
qDebug() << "asking unknown child to exit.";
QByteArray diePacket = byteArrayWithPopulatedHeader(PacketTypeStopNode);
QByteArray diePacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeStopNode);
nodeList->writeUnverifiedDatagram(diePacket, senderSockAddr);
}
}
@ -196,13 +239,9 @@ void AssignmentClientMonitor::readPendingDatagrams() {
// update our records about how to reach this child
matchingNode->setLocalSocket(senderSockAddr);
// push past the packet header
QDataStream packetStream(receivedPacket);
packetStream.skipRawData(numBytesForPacketHeader(receivedPacket));
// decode json
QVariantMap unpackedVariantMap;
packetStream >> unpackedVariantMap;
QJsonObject unpackedStatsJSON = QJsonObject::fromVariantMap(unpackedVariantMap);
QVariantMap packetVariantMap =
JSONBreakableMarshal::fromStringBuffer(receivedPacket.mid(numBytesForPacketHeader(receivedPacket)));
QJsonObject unpackedStatsJSON = QJsonObject::fromVariantMap(packetVariantMap);
// get child's assignment type out of the decoded json
QString childType = unpackedStatsJSON["assignment_type"].toString();

View file

@ -32,14 +32,20 @@ public:
QString assignmentPool, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort);
~AssignmentClientMonitor();
void stopChildProcesses();
private slots:
void readPendingDatagrams();
void checkSpares();
void childProcessFinished();
public slots:
void aboutToQuit();
private:
void spawnChildClient();
void simultaneousWaitOnChildren(int waitMsecs);
QTimer _checkSparesTimer; // every few seconds see if it need fewer or more spare children
const unsigned int _numAssignmentClientForks;
@ -52,6 +58,7 @@ private:
QString _assignmentServerHostname;
quint16 _assignmentServerPort;
QMap<qint64, QProcess*> _childProcesses;
};
#endif // hifi_AssignmentClientMonitor_h

View file

@ -1,19 +0,0 @@
//
// AssignmentThread.cpp
// assignment-client/src
//
// Created by Stephen Birarda on 2014.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AssignmentThread.h"
AssignmentThread::AssignmentThread(const SharedAssignmentPointer& assignment, QObject* parent) :
QThread(parent),
_assignment(assignment)
{
}

View file

@ -1,26 +0,0 @@
//
// AssignmentThread.h
// assignment-client/src
//
// Created by Stephen Birarda on 2014.
// Copyright 2014 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_AssignmentThread_h
#define hifi_AssignmentThread_h
#include <QtCore/QThread>
#include <ThreadedAssignment.h>
class AssignmentThread : public QThread {
public:
AssignmentThread(const SharedAssignmentPointer& assignment, QObject* parent);
private:
SharedAssignmentPointer _assignment;
};
#endif // hifi_AssignmentThread_h

File diff suppressed because it is too large Load diff

View file

@ -29,49 +29,46 @@ class AudioMixer : public ThreadedAssignment {
Q_OBJECT
public:
AudioMixer(const QByteArray& packet);
void deleteLater() { qDebug() << "DELETE LATER CALLED?"; QObject::deleteLater(); }
public slots:
/// threaded run of assignment
void run();
void readPendingDatagrams() { }; // this will not be called since our datagram processing thread will handle
void readPendingDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
void sendStatsPacket();
static const InboundAudioStream::Settings& getStreamSettings() { return _streamSettings; }
private:
/// adds one stream to the mix for a listening node
int addStreamToMixForListeningNodeWithStream(AudioMixerClientData* listenerNodeData,
const QUuid& streamUUID,
PositionalAudioStream* streamToAdd,
AvatarAudioStream* listeningNodeStream);
/// prepares and sends a mix to one Node
int prepareMixForListeningNode(Node* node);
/// Send Audio Environment packet for a single node
void sendAudioEnvironmentPacket(SharedNodePointer node);
// used on a per stream basis to run the filter on before mixing, large enough to handle the historical
// data from a phase delay as well as an entire network buffer
int16_t _preMixSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO + (SAMPLE_PHASE_DELAY_AT_90 * 2)];
// client samples capacity is larger than what will be sent to optimize mixing
// we are MMX adding 4 samples at a time so we need client samples to have an extra 4
int16_t _mixSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO + (SAMPLE_PHASE_DELAY_AT_90 * 2)];
void perSecondActions();
bool shouldMute(float quietestFrame);
QString getReadPendingDatagramsCallsPerSecondsStatsString() const;
QString getReadPendingDatagramsPacketsPerCallStatsString() const;
QString getReadPendingDatagramsTimeStatsString() const;
QString getReadPendingDatagramsHashMatchTimeStatsString() const;
void parseSettingsObject(const QJsonObject& settingsObject);
float _trailingSleepRatio;
float _minAudibilityThreshold;
float _performanceThrottlingRatio;
@ -80,7 +77,7 @@ private:
int _numStatFrames;
int _sumListeners;
int _sumMixes;
QHash<QString, AABox> _audioZones;
struct ZonesSettings {
QString source;
@ -94,12 +91,12 @@ private:
float wetLevel;
};
QVector<ReverbSettings> _zoneReverbSettings;
static InboundAudioStream::Settings _streamSettings;
static bool _printStreamStats;
static bool _enableFilter;
quint64 _lastPerSecondCallbackTime;
bool _sendAudioStreamStats;

View file

@ -9,7 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QDebug>
#include <QtCore/QDebug>
#include <QtCore/QJsonArray>
#include <PacketHeaders.h>
#include <UUID.h>
@ -156,7 +157,7 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer&
quint8 appendFlag = 0;
// pack header
int numBytesPacketHeader = populatePacketHeader(packet, PacketTypeAudioStreamStats);
int numBytesPacketHeader = nodeList->populatePacketHeader(packet, PacketTypeAudioStreamStats);
char* headerEndAt = packet + numBytesPacketHeader;
// calculate how many stream stat structs we can fit in each packet
@ -196,70 +197,86 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer&
}
}
QString AudioMixerClientData::getAudioStreamStatsString() const {
QString result;
QJsonObject AudioMixerClientData::getAudioStreamStats() const {
QJsonObject result;
QJsonObject downstreamStats;
AudioStreamStats streamStats = _downstreamAudioStreamStats;
result += "DOWNSTREAM.desired:" + QString::number(streamStats._desiredJitterBufferFrames)
+ " available_avg_10s:" + QString::number(streamStats._framesAvailableAverage)
+ " available:" + QString::number(streamStats._framesAvailable)
+ " starves:" + QString::number(streamStats._starveCount)
+ " not_mixed:" + QString::number(streamStats._consecutiveNotMixedCount)
+ " overflows:" + QString::number(streamStats._overflowCount)
+ " silents_dropped: ?"
+ " lost%:" + QString::number(streamStats._packetStreamStats.getLostRate() * 100.0f, 'f', 2)
+ " lost%_30s:" + QString::number(streamStats._packetStreamWindowStats.getLostRate() * 100.0f, 'f', 2)
+ " min_gap:" + formatUsecTime(streamStats._timeGapMin)
+ " max_gap:" + formatUsecTime(streamStats._timeGapMax)
+ " avg_gap:" + formatUsecTime(streamStats._timeGapAverage)
+ " min_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMin)
+ " max_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMax)
+ " avg_gap_30s:" + formatUsecTime(streamStats._timeGapWindowAverage);
downstreamStats["desired"] = streamStats._desiredJitterBufferFrames;
downstreamStats["available_avg_10s"] = streamStats._framesAvailableAverage;
downstreamStats["available"] = (double) streamStats._framesAvailable;
downstreamStats["starves"] = (double) streamStats._starveCount;
downstreamStats["not_mixed"] = (double) streamStats._consecutiveNotMixedCount;
downstreamStats["overflows"] = (double) streamStats._overflowCount;
downstreamStats["lost%"] = streamStats._packetStreamStats.getLostRate() * 100.0f;
downstreamStats["lost%_30s"] = streamStats._packetStreamWindowStats.getLostRate() * 100.0f;
downstreamStats["min_gap"] = formatUsecTime(streamStats._timeGapMin);
downstreamStats["max_gap"] = formatUsecTime(streamStats._timeGapMax);
downstreamStats["avg_gap"] = formatUsecTime(streamStats._timeGapAverage);
downstreamStats["min_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMin);
downstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax);
downstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage);
result["downstream"] = downstreamStats;
AvatarAudioStream* avatarAudioStream = getAvatarAudioStream();
if (avatarAudioStream) {
QJsonObject upstreamStats;
AudioStreamStats streamStats = avatarAudioStream->getAudioStreamStats();
result += " UPSTREAM.mic.desired:" + QString::number(streamStats._desiredJitterBufferFrames)
+ " desired_calc:" + QString::number(avatarAudioStream->getCalculatedJitterBufferFrames())
+ " available_avg_10s:" + QString::number(streamStats._framesAvailableAverage)
+ " available:" + QString::number(streamStats._framesAvailable)
+ " starves:" + QString::number(streamStats._starveCount)
+ " not_mixed:" + QString::number(streamStats._consecutiveNotMixedCount)
+ " overflows:" + QString::number(streamStats._overflowCount)
+ " silents_dropped:" + QString::number(streamStats._framesDropped)
+ " lost%:" + QString::number(streamStats._packetStreamStats.getLostRate() * 100.0f, 'f', 2)
+ " lost%_30s:" + QString::number(streamStats._packetStreamWindowStats.getLostRate() * 100.0f, 'f', 2)
+ " min_gap:" + formatUsecTime(streamStats._timeGapMin)
+ " max_gap:" + formatUsecTime(streamStats._timeGapMax)
+ " avg_gap:" + formatUsecTime(streamStats._timeGapAverage)
+ " min_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMin)
+ " max_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMax)
+ " avg_gap_30s:" + formatUsecTime(streamStats._timeGapWindowAverage);
upstreamStats["mic.desired"] = streamStats._desiredJitterBufferFrames;
upstreamStats["desired_calc"] = avatarAudioStream->getCalculatedJitterBufferFrames();
upstreamStats["available_avg_10s"] = streamStats._framesAvailableAverage;
upstreamStats["available"] = (double) streamStats._framesAvailable;
upstreamStats["starves"] = (double) streamStats._starveCount;
upstreamStats["not_mixed"] = (double) streamStats._consecutiveNotMixedCount;
upstreamStats["overflows"] = (double) streamStats._overflowCount;
upstreamStats["silents_dropped"] = (double) streamStats._framesDropped;
upstreamStats["lost%"] = streamStats._packetStreamStats.getLostRate() * 100.0f;
upstreamStats["lost%_30s"] = streamStats._packetStreamWindowStats.getLostRate() * 100.0f;
upstreamStats["min_gap"] = formatUsecTime(streamStats._timeGapMin);
upstreamStats["max_gap"] = formatUsecTime(streamStats._timeGapMax);
upstreamStats["avg_gap"] = formatUsecTime(streamStats._timeGapAverage);
upstreamStats["min_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMin);
upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax);
upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage);
result["upstream"] = upstreamStats;
} else {
result = "mic unknown";
result["upstream"] = "mic unknown";
}
QHash<QUuid, PositionalAudioStream*>::ConstIterator i;
QJsonArray injectorArray;
for (i = _audioStreams.constBegin(); i != _audioStreams.constEnd(); i++) {
if (i.value()->getType() == PositionalAudioStream::Injector) {
QJsonObject upstreamStats;
AudioStreamStats streamStats = i.value()->getAudioStreamStats();
result += " UPSTREAM.inj.desired:" + QString::number(streamStats._desiredJitterBufferFrames)
+ " desired_calc:" + QString::number(i.value()->getCalculatedJitterBufferFrames())
+ " available_avg_10s:" + QString::number(streamStats._framesAvailableAverage)
+ " available:" + QString::number(streamStats._framesAvailable)
+ " starves:" + QString::number(streamStats._starveCount)
+ " not_mixed:" + QString::number(streamStats._consecutiveNotMixedCount)
+ " overflows:" + QString::number(streamStats._overflowCount)
+ " silents_dropped:" + QString::number(streamStats._framesDropped)
+ " lost%:" + QString::number(streamStats._packetStreamStats.getLostRate() * 100.0f, 'f', 2)
+ " lost%_30s:" + QString::number(streamStats._packetStreamWindowStats.getLostRate() * 100.0f, 'f', 2)
+ " min_gap:" + formatUsecTime(streamStats._timeGapMin)
+ " max_gap:" + formatUsecTime(streamStats._timeGapMax)
+ " avg_gap:" + formatUsecTime(streamStats._timeGapAverage)
+ " min_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMin)
+ " max_gap_30s:" + formatUsecTime(streamStats._timeGapWindowMax)
+ " avg_gap_30s:" + formatUsecTime(streamStats._timeGapWindowAverage);
upstreamStats["inj.desired"] = streamStats._desiredJitterBufferFrames;
upstreamStats["desired_calc"] = i.value()->getCalculatedJitterBufferFrames();
upstreamStats["available_avg_10s"] = streamStats._framesAvailableAverage;
upstreamStats["available"] = (double) streamStats._framesAvailable;
upstreamStats["starves"] = (double) streamStats._starveCount;
upstreamStats["not_mixed"] = (double) streamStats._consecutiveNotMixedCount;
upstreamStats["overflows"] = (double) streamStats._overflowCount;
upstreamStats["silents_dropped"] = (double) streamStats._framesDropped;
upstreamStats["lost%"] = streamStats._packetStreamStats.getLostRate() * 100.0f;
upstreamStats["lost%_30s"] = streamStats._packetStreamWindowStats.getLostRate() * 100.0f;
upstreamStats["min_gap"] = formatUsecTime(streamStats._timeGapMin);
upstreamStats["max_gap"] = formatUsecTime(streamStats._timeGapMax);
upstreamStats["avg_gap"] = formatUsecTime(streamStats._timeGapAverage);
upstreamStats["min_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMin);
upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax);
upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage);
injectorArray.push_back(upstreamStats);
}
}
result["injectors"] = injectorArray;
return result;
}

View file

@ -12,6 +12,8 @@
#ifndef hifi_AudioMixerClientData_h
#define hifi_AudioMixerClientData_h
#include <QtCore/QJsonObject>
#include <AABox.h>
#include <AudioFormat.h> // For AudioFilterHSF1s and _penumbraFilter
#include <AudioBuffer.h> // For AudioFilterHSF1s and _penumbraFilter
@ -46,7 +48,7 @@ public:
void removeDeadInjectedStreams();
QString getAudioStreamStatsString() const;
QJsonObject getAudioStreamStats() const;
void sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode);

View file

@ -9,6 +9,9 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <cfloat>
#include <random>
#include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
@ -27,7 +30,8 @@
const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / 60.0f) * 1000;
const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 60;
const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / (float) AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) * 1000;
AvatarMixer::AvatarMixer(const QByteArray& packet) :
ThreadedAssignment(packet),
@ -45,16 +49,14 @@ AvatarMixer::AvatarMixer(const QByteArray& packet) :
}
AvatarMixer::~AvatarMixer() {
if (_broadcastTimer) {
_broadcastTimer->deleteLater();
}
_broadcastThread.quit();
_broadcastThread.wait();
}
void attachAvatarDataToNode(Node* newNode) {
if (!newNode->getLinkedData()) {
newNode->setLinkedData(new AvatarMixerClientData());
}
}
const float BILLBOARD_AND_IDENTITY_SEND_PROBABILITY = 1.0f / 300.0f;
// NOTE: some additional optimizations to consider.
@ -76,7 +78,12 @@ void AvatarMixer::broadcastAvatarData() {
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
// NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was
// able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value
// is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client.
// It is reported in the domain-server stats for the avatar-mixer.
_trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio)
+ (idleTime * CURRENT_FRAME_RATIO / (float) AVATAR_DATA_SEND_INTERVAL_MSECS);
@ -85,14 +92,14 @@ void AvatarMixer::broadcastAvatarData() {
if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) {
if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) {
// we're struggling - change our min required loudness to reduce some load
// we're struggling - change our performance throttling ratio
_performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio));
qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
hasRatioChanged = true;
} else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) {
// we've recovered and can back off the required loudness
// we've recovered and can back off the performance throttling
_performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF;
if (_performanceThrottlingRatio < 0) {
@ -100,7 +107,7 @@ void AvatarMixer::broadcastAvatarData() {
}
qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
hasRatioChanged = true;
}
@ -115,9 +122,13 @@ void AvatarMixer::broadcastAvatarData() {
static QByteArray mixedAvatarByteArray;
int numPacketHeaderBytes = populatePacketHeader(mixedAvatarByteArray, PacketTypeBulkAvatarData);
auto nodeList = DependencyManager::get<NodeList>();
int numPacketHeaderBytes = nodeList->populatePacketHeader(mixedAvatarByteArray, PacketTypeBulkAvatarData);
// setup for distributed random floating point values
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::uniform_real_distribution<float> distribution;
nodeList->eachMatchingNode(
[&](const SharedNodePointer& node)->bool {
@ -142,16 +153,71 @@ void AvatarMixer::broadcastAvatarData() {
// reset packet pointers for this node
mixedAvatarByteArray.resize(numPacketHeaderBytes);
AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getPosition();
// TODO use this along with the distance in the calculation of whether to send an update
// about a given otherNode to this node
// FIXME does this mean we should sort the othernodes by distance before iterating
// over them?
// float outputBandwidth =
node->getOutboundBandwidth();
// reset the internal state for correct random number distribution
distribution.reset();
// reset the max distance for this frame
float maxAvatarDistanceThisFrame = 0.0f;
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
// keep track of the number of other avatar frames skipped
int numAvatarsWithSkippedFrames = 0;
// use the data rate specifically for avatar data for FRD adjustment checks
float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps();
// Check if it is time to adjust what we send this client based on the observed
// bandwidth to this node. We do this once a second, which is also the window for
// the bandwidth reported by node->getOutboundBandwidth();
if (nodeData->getNumFramesSinceFRDAdjustment() > AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) {
const float FRD_ADJUSTMENT_ACCEPTABLE_RATIO = 0.8f;
const float HYSTERISIS_GAP = (1 - FRD_ADJUSTMENT_ACCEPTABLE_RATIO);
const float HYSTERISIS_MIDDLE_PERCENTAGE = (1 - (HYSTERISIS_GAP * 0.5f));
// get the current full rate distance so we can work with it
float currentFullRateDistance = nodeData->getFullRateDistance();
if (avatarDataRateLastSecond > _maxKbpsPerNode) {
// is the FRD greater than the farthest avatar?
// if so, before we calculate anything, set it to that distance
currentFullRateDistance = std::min(currentFullRateDistance, nodeData->getMaxAvatarDistance());
// we're adjusting the full rate distance to target a bandwidth in the middle
// of the hysterisis gap
currentFullRateDistance *= (_maxKbpsPerNode * HYSTERISIS_MIDDLE_PERCENTAGE) / avatarDataRateLastSecond;
nodeData->setFullRateDistance(currentFullRateDistance);
nodeData->resetNumFramesSinceFRDAdjustment();
} else if (currentFullRateDistance < nodeData->getMaxAvatarDistance()
&& avatarDataRateLastSecond < _maxKbpsPerNode * FRD_ADJUSTMENT_ACCEPTABLE_RATIO) {
// we are constrained AND we've recovered to below the acceptable ratio
// lets adjust the full rate distance to target a bandwidth in the middle of the hyterisis gap
currentFullRateDistance *= (_maxKbpsPerNode * HYSTERISIS_MIDDLE_PERCENTAGE) / avatarDataRateLastSecond;
nodeData->setFullRateDistance(currentFullRateDistance);
nodeData->resetNumFramesSinceFRDAdjustment();
}
} else {
nodeData->incrementNumFramesSinceFRDAdjustment();
}
// this is an AGENT we have received head data from
// send back a packet with other active node data to this node
nodeList->eachMatchingNode(
@ -163,36 +229,73 @@ void AvatarMixer::broadcastAvatarData() {
return false;
}
// Check throttling value
if (!(_performanceThrottlingRatio == 0 || randFloat() < (1.0f - _performanceThrottlingRatio))) {
return false;
}
return true;
},
[&](const SharedNodePointer& otherNode) {
++numOtherAvatars;
AvatarMixerClientData* otherNodeData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
MutexTryLocker lock(otherNodeData->getMutex());
if (!lock.isLocked()) {
return;
}
AvatarData& otherAvatar = otherNodeData->getAvatar();
// Decide whether to send this avatar's data based on it's distance from us
// The full rate distance is the distance at which EVERY update will be sent for this avatar
// at a distance of twice the full rate distance, there will be a 50% chance of sending this avatar's update
const float FULL_RATE_DISTANCE = 2.0f;
// at twice the full rate distance, there will be a 50% chance of sending this avatar's update
glm::vec3 otherPosition = otherAvatar.getPosition();
float distanceToAvatar = glm::length(myPosition - otherPosition);
if (!(distanceToAvatar == 0.0f || randFloat() < FULL_RATE_DISTANCE / distanceToAvatar)) {
// potentially update the max full rate distance for this frame
maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar);
if (distanceToAvatar != 0.0f
&& distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) {
return;
}
PacketSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(otherNode->getUUID());
PacketSequenceNumber lastSeqFromSender = otherNode->getLastSequenceNumberForPacketType(PacketTypeAvatarData);
if (lastSeqToReceiver > lastSeqFromSender) {
// Did we somehow get out of order packets from the sender?
// We don't expect this to happen - in RELEASE we add this to a trackable stat
// and in DEBUG we crash on the assert
otherNodeData->incrementNumOutOfOrderSends();
assert(false);
}
// make sure we haven't already sent this data from this sender to this receiver
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
return;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
// we're going to send this avatar
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNode->getLastSequenceNumberForPacketType(PacketTypeAvatarData));
QByteArray avatarByteArray;
avatarByteArray.append(otherNode->getUUID().toRfc4122());
avatarByteArray.append(otherAvatar.toByteArray());
if (avatarByteArray.size() + mixedAvatarByteArray.size() > MAX_PACKET_SIZE) {
nodeList->writeDatagram(mixedAvatarByteArray, node);
numAvatarDataBytes += mixedAvatarByteArray.size();
// reset the packet
mixedAvatarByteArray.resize(numPacketHeaderBytes);
@ -212,9 +315,10 @@ void AvatarMixer::broadcastAvatarData() {
&& (forceSend
|| otherNodeData->getBillboardChangeTimestamp() > _lastFrameTimestamp
|| randFloat() < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) {
QByteArray billboardPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard);
QByteArray billboardPacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeAvatarBillboard);
billboardPacket.append(otherNode->getUUID().toRfc4122());
billboardPacket.append(otherNodeData->getAvatar().getBillboard());
nodeList->writeDatagram(billboardPacket, node);
++_sumBillboardPackets;
@ -225,19 +329,36 @@ void AvatarMixer::broadcastAvatarData() {
|| otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp
|| randFloat() < BILLBOARD_AND_IDENTITY_SEND_PROBABILITY)) {
QByteArray identityPacket = byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity);
QByteArray identityPacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeAvatarIdentity);
QByteArray individualData = otherNodeData->getAvatar().identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, otherNode->getUUID().toRfc4122());
identityPacket.append(individualData);
nodeList->writeDatagram(identityPacket, node);
++_sumIdentityPackets;
}
});
// send the last packet
nodeList->writeDatagram(mixedAvatarByteArray, node);
});
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes + mixedAvatarByteArray.size());
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
if (numOtherAvatars == 0) {
// update the full rate distance to FLOAT_MAX since we didn't have any other avatars to send
nodeData->setMaxAvatarDistance(FLT_MAX);
} else {
nodeData->setMaxAvatarDistance(maxAvatarDistanceThisFrame);
}
}
);
_lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch();
}
@ -245,13 +366,36 @@ void AvatarMixer::broadcastAvatarData() {
void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
if (killedNode->getType() == NodeType::Agent
&& killedNode->getLinkedData()) {
auto nodeList = DependencyManager::get<NodeList>();
// this was an avatar we were sending to other people
// send a kill packet for it to our other nodes
QByteArray killPacket = byteArrayWithPopulatedHeader(PacketTypeKillAvatar);
QByteArray killPacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeKillAvatar);
killPacket += killedNode->getUUID().toRfc4122();
DependencyManager::get<NodeList>()->broadcastToNodes(killPacket,
NodeSet() << NodeType::Agent);
nodeList->broadcastToNodes(killPacket, NodeSet() << NodeType::Agent);
// we also want to remove sequence number data for this avatar on our other avatars
// so invoke the appropriate method on the AvatarMixerClientData for other avatars
nodeList->eachMatchingNode(
[&](const SharedNodePointer& node)->bool {
if (!node->getLinkedData()) {
return false;
}
if (node->getUUID() == killedNode->getUUID()) {
return false;
}
return true;
},
[&](const SharedNodePointer& node) {
QMetaObject::invokeMethod(node->getLinkedData(),
"removeLastBroadcastSequenceNumber",
Qt::AutoConnection,
Q_ARG(const QUuid&, QUuid(killedNode->getUUID())));
}
);
}
}
@ -325,9 +469,38 @@ void AvatarMixer::sendStatsPacket() {
statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100;
statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio;
QJsonObject avatarsObject;
auto nodeList = DependencyManager::get<NodeList>();
// add stats for each listerner
nodeList->eachNode([&](const SharedNodePointer& node) {
QJsonObject avatarStats;
const QString NODE_OUTBOUND_KBPS_STAT_KEY = "outbound_kbps";
// add the key to ask the domain-server for a username replacement, if it has it
avatarStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundBandwidth();
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
if (clientData) {
MutexTryLocker lock(clientData->getMutex());
if (lock.isLocked()) {
clientData->loadJSONStats(avatarStats);
// add the diff between the full outbound bandwidth and the measured bandwidth for AvatarData send only
avatarStats["delta_full_vs_avatar_data_kbps"] =
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY].toDouble() - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
}
}
avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats;
});
statsObject["avatars"] = avatarsObject;
ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject);
_sumListeners = 0;
_sumBillboardPackets = 0;
_sumIdentityPackets = 0;
@ -340,17 +513,54 @@ void AvatarMixer::run() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
nodeList->linkedDataCreateCallback = attachAvatarDataToNode;
nodeList->linkedDataCreateCallback = [] (Node* node) {
node->setLinkedData(new AvatarMixerClientData());
};
// setup the timer that will be fired on the broadcast thread
QTimer* broadcastTimer = new QTimer();
broadcastTimer->setInterval(AVATAR_DATA_SEND_INTERVAL_MSECS);
broadcastTimer->moveToThread(&_broadcastThread);
_broadcastTimer = new QTimer;
_broadcastTimer->setInterval(AVATAR_DATA_SEND_INTERVAL_MSECS);
_broadcastTimer->moveToThread(&_broadcastThread);
// connect appropriate signals and slots
connect(broadcastTimer, &QTimer::timeout, this, &AvatarMixer::broadcastAvatarData, Qt::DirectConnection);
connect(&_broadcastThread, SIGNAL(started()), broadcastTimer, SLOT(start()));
connect(_broadcastTimer, &QTimer::timeout, this, &AvatarMixer::broadcastAvatarData, Qt::DirectConnection);
connect(&_broadcastThread, SIGNAL(started()), _broadcastTimer, SLOT(start()));
// wait until we have the domain-server settings, otherwise we bail
DomainHandler& domainHandler = nodeList->getDomainHandler();
qDebug() << "Waiting for domain settings from domain-server.";
// block until we get the settingsRequestComplete signal
QEventLoop loop;
connect(&domainHandler, &DomainHandler::settingsReceived, &loop, &QEventLoop::quit);
connect(&domainHandler, &DomainHandler::settingsReceiveFail, &loop, &QEventLoop::quit);
domainHandler.requestDomainSettings();
loop.exec();
if (domainHandler.getSettingsObject().isEmpty()) {
qDebug() << "Failed to retreive settings object from domain-server. Bailing on assignment.";
setFinished(true);
return;
}
// parse the settings to pull out the values we need
parseDomainServerSettings(domainHandler.getSettingsObject());
// start the broadcastThread
_broadcastThread.start();
}
void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
const QString AVATAR_MIXER_SETTINGS_KEY = "avatar_mixer";
const QString NODE_SEND_BANDWIDTH_KEY = "max_node_send_bandwidth";
const float DEFAULT_NODE_SEND_BANDWIDTH = 1.0f;
QJsonValue nodeBandwidthValue = domainSettings[AVATAR_MIXER_SETTINGS_KEY].toObject()[NODE_SEND_BANDWIDTH_KEY];
if (!nodeBandwidthValue.isDouble()) {
qDebug() << NODE_SEND_BANDWIDTH_KEY << "is not a double - will continue with default value";
}
_maxKbpsPerNode = nodeBandwidthValue.toDouble(DEFAULT_NODE_SEND_BANDWIDTH) * KILO_PER_MEGA;
qDebug() << "The maximum send bandwidth per node is" << _maxKbpsPerNode << "kbps.";
}

View file

@ -35,6 +35,7 @@ public slots:
private:
void broadcastAvatarData();
void parseDomainServerSettings(const QJsonObject& domainSettings);
QThread _broadcastThread;
@ -47,6 +48,10 @@ private:
int _numStatFrames;
int _sumBillboardPackets;
int _sumIdentityPackets;
float _maxKbpsPerNode = 0.0f;
QTimer* _broadcastTimer = nullptr;
};
#endif // hifi_AvatarMixer_h

View file

@ -13,15 +13,6 @@
#include "AvatarMixerClientData.h"
AvatarMixerClientData::AvatarMixerClientData() :
NodeData(),
_hasReceivedFirstPackets(false),
_billboardChangeTimestamp(0),
_identityChangeTimestamp(0)
{
}
int AvatarMixerClientData::parseData(const QByteArray& packet) {
// compute the offset to the data payload
int offset = numBytesForPacketHeader(packet);
@ -33,3 +24,25 @@ bool AvatarMixerClientData::checkAndSetHasReceivedFirstPackets() {
_hasReceivedFirstPackets = true;
return oldValue;
}
PacketSequenceNumber AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const {
// return the matching PacketSequenceNumber, or the default if we don't have it
auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeUUID);
if (nodeMatch != _lastBroadcastSequenceNumbers.end()) {
return nodeMatch->second;
} else {
return DEFAULT_SEQUENCE_NUMBER;
}
}
void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
jsonObject["display_name"] = _avatar.getDisplayName();
jsonObject["full_rate_distance"] = _fullRateDistance;
jsonObject["max_avatar_distance"] = _maxAvatarDistance;
jsonObject["num_avatars_sent_last_frame"] = _numAvatarsSentLastFrame;
jsonObject["avg_other_avatar_starves_per_second"] = getAvgNumOtherAvatarStarvesPerSecond();
jsonObject["avg_other_avatar_skips_per_second"] = getAvgNumOtherAvatarSkipsPerSecond();
jsonObject["total_num_out_of_order_sends"] = _numOutOfOrderSends;
jsonObject[OUTBOUND_AVATAR_DATA_STATS_KEY] = getOutboundAvatarDataKbps();
}

View file

@ -12,32 +12,89 @@
#ifndef hifi_AvatarMixerClientData_h
#define hifi_AvatarMixerClientData_h
#include <algorithm>
#include <cfloat>
#include <unordered_map>
#include <QtCore/QJsonObject>
#include <QtCore/QUrl>
#include <AvatarData.h>
#include <NodeData.h>
#include <NumericalConstants.h>
#include <PacketHeaders.h>
#include <SimpleMovingAverage.h>
#include <UUIDHasher.h>
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
class AvatarMixerClientData : public NodeData {
Q_OBJECT
public:
AvatarMixerClientData();
int parseData(const QByteArray& packet);
AvatarData& getAvatar() { return _avatar; }
bool checkAndSetHasReceivedFirstPackets();
PacketSequenceNumber getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const;
void setLastBroadcastSequenceNumber(const QUuid& nodeUUID, PacketSequenceNumber sequenceNumber)
{ _lastBroadcastSequenceNumbers[nodeUUID] = sequenceNumber; }
Q_INVOKABLE void removeLastBroadcastSequenceNumber(const QUuid& nodeUUID) { _lastBroadcastSequenceNumbers.erase(nodeUUID); }
quint64 getBillboardChangeTimestamp() const { return _billboardChangeTimestamp; }
void setBillboardChangeTimestamp(quint64 billboardChangeTimestamp) { _billboardChangeTimestamp = billboardChangeTimestamp; }
quint64 getIdentityChangeTimestamp() const { return _identityChangeTimestamp; }
void setIdentityChangeTimestamp(quint64 identityChangeTimestamp) { _identityChangeTimestamp = identityChangeTimestamp; }
void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; }
float getFullRateDistance() const { return _fullRateDistance; }
void setMaxAvatarDistance(float maxAvatarDistance) { _maxAvatarDistance = maxAvatarDistance; }
float getMaxAvatarDistance() const { return _maxAvatarDistance; }
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
int getNumAvatarsSentLastFrame() const { return _numAvatarsSentLastFrame; }
void recordNumOtherAvatarStarves(int numAvatarsHeldBack) { _otherAvatarStarves.updateAverage((float) numAvatarsHeldBack); }
float getAvgNumOtherAvatarStarvesPerSecond() const { return _otherAvatarStarves.getAverageSampleValuePerSecond(); }
void recordNumOtherAvatarSkips(int numOtherAvatarSkips) { _otherAvatarSkips.updateAverage((float) numOtherAvatarSkips); }
float getAvgNumOtherAvatarSkipsPerSecond() const { return _otherAvatarSkips.getAverageSampleValuePerSecond(); }
void incrementNumOutOfOrderSends() { ++_numOutOfOrderSends; }
int getNumFramesSinceFRDAdjustment() const { return _numFramesSinceAdjustment; }
void incrementNumFramesSinceFRDAdjustment() { ++_numFramesSinceAdjustment; }
void resetNumFramesSinceFRDAdjustment() { _numFramesSinceAdjustment = 0; }
void recordSentAvatarData(int numBytes) { _avgOtherAvatarDataRate.updateAverage((float) numBytes); }
float getOutboundAvatarDataKbps() const
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
void loadJSONStats(QJsonObject& jsonObject) const;
private:
AvatarData _avatar;
bool _hasReceivedFirstPackets;
quint64 _billboardChangeTimestamp;
quint64 _identityChangeTimestamp;
std::unordered_map<QUuid, PacketSequenceNumber, UUIDHasher> _lastBroadcastSequenceNumbers;
bool _hasReceivedFirstPackets = false;
quint64 _billboardChangeTimestamp = 0;
quint64 _identityChangeTimestamp = 0;
float _fullRateDistance = FLT_MAX;
float _maxAvatarDistance = FLT_MAX;
int _numAvatarsSentLastFrame = 0;
int _numFramesSinceAdjustment = 0;
SimpleMovingAverage _otherAvatarStarves;
SimpleMovingAverage _otherAvatarSkips;
int _numOutOfOrderSends = 0;
SimpleMovingAverage _avgOtherAvatarDataRate;
};
#endif // hifi_AvatarMixerClientData_h

View file

@ -27,6 +27,11 @@ EntityServer::EntityServer(const QByteArray& packet)
}
EntityServer::~EntityServer() {
if (_pruneDeletedEntitiesTimer) {
_pruneDeletedEntitiesTimer->stop();
_pruneDeletedEntitiesTimer->deleteLater();
}
EntityTree* tree = (EntityTree*)_tree;
tree->removeNewlyCreatedHook(this);
}
@ -48,35 +53,13 @@ Octree* EntityServer::createTree() {
}
void EntityServer::beforeRun() {
QTimer* pruneDeletedEntitiesTimer = new QTimer(this);
connect(pruneDeletedEntitiesTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedEntities()));
_pruneDeletedEntitiesTimer = new QTimer();
connect(_pruneDeletedEntitiesTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedEntities()));
const int PRUNE_DELETED_MODELS_INTERVAL_MSECS = 1 * 1000; // once every second
pruneDeletedEntitiesTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS);
_pruneDeletedEntitiesTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS);
}
void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) {
unsigned char outputBuffer[MAX_PACKET_SIZE];
unsigned char* copyAt = outputBuffer;
int numBytesPacketHeader = populatePacketHeader(reinterpret_cast<char*>(outputBuffer), PacketTypeEntityAddResponse);
int packetLength = numBytesPacketHeader;
copyAt += numBytesPacketHeader;
// encode the creatorTokenID
uint32_t creatorTokenID = newEntity.getCreatorTokenID();
memcpy(copyAt, &creatorTokenID, sizeof(creatorTokenID));
copyAt += sizeof(creatorTokenID);
packetLength += sizeof(creatorTokenID);
// encode the entity ID
QUuid entityID = newEntity.getID();
QByteArray encodedID = entityID.toRfc4122();
memcpy(copyAt, encodedID.constData(), encodedID.size());
copyAt += sizeof(entityID);
packetLength += sizeof(entityID);
DependencyManager::get<NodeList>()->writeDatagram((char*) outputBuffer, packetLength, senderNode);
}

View file

@ -51,6 +51,7 @@ protected:
private:
EntitySimulation* _entitySimulation;
QTimer* _pruneDeletedEntitiesTimer = nullptr;
};
#endif // hifi_EntityServer_h

View file

@ -9,10 +9,15 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AssignmentClientApp.h"
#include <QtCore/QDebug>
int main(int argc, char* argv[]) {
AssignmentClientApp app(argc, argv);
return 0;
int acReturn = app.exec();
qDebug() << "assignment-client process" << app.applicationPid() << "exiting with status code" << acReturn;
return acReturn;
}

View file

@ -10,6 +10,8 @@
//
#include <limits>
#include <NumericalConstants.h>
#include <PacketHeaders.h>
#include <PerfStat.h>
@ -275,9 +277,11 @@ int OctreeInboundPacketProcessor::sendNackPackets() {
char* dataAt = packet;
int bytesRemaining = MAX_PACKET_SIZE;
auto nodeList = DependencyManager::get<NodeList>();
// pack header
int numBytesPacketHeader = populatePacketHeader(packet, _myServer->getMyEditNackType());
int numBytesPacketHeader = nodeList->populatePacketHeader(packet, _myServer->getMyEditNackType());
dataAt += numBytesPacketHeader;
bytesRemaining -= numBytesPacketHeader;
@ -299,7 +303,7 @@ int OctreeInboundPacketProcessor::sendNackPackets() {
numSequenceNumbersAvailable -= numSequenceNumbers;
// send it
DependencyManager::get<NodeList>()->writeUnverifiedDatagram(packet, dataAt - packet, destinationNode);
nodeList->writeUnverifiedDatagram(packet, dataAt - packet, destinationNode);
packetsSent++;
qDebug() << "NACK Sent back to editor/client... destinationNode=" << nodeUUID;

View file

@ -74,7 +74,7 @@ public:
NodeToSenderStatsMap& getSingleSenderStats() { return _singleSenderStats; }
void shuttingDown() { _shuttingDown = true;}
virtual void terminating() { _shuttingDown = true; ReceivedPacketProcessor::terminating(); }
protected:

View file

@ -9,11 +9,15 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "PacketHeaders.h"
#include "SharedUtil.h"
#include "OctreeQueryNode.h"
#include <cstring>
#include <cstdio>
#include <PacketHeaders.h>
#include <SharedUtil.h>
#include <UUID.h>
#include "OctreeSendThread.h"
OctreeQueryNode::OctreeQueryNode() :
@ -91,8 +95,8 @@ void OctreeQueryNode::sendThreadFinished() {
}
}
void OctreeQueryNode::initializeOctreeSendThread(const SharedAssignmentPointer& myAssignment, const SharedNodePointer& node) {
_octreeSendThread = new OctreeSendThread(myAssignment, node);
void OctreeQueryNode::initializeOctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) {
_octreeSendThread = new OctreeSendThread(myServer, node);
// we want to be notified when the thread finishes
connect(_octreeSendThread, &GenericThread::finished, this, &OctreeQueryNode::sendThreadFinished);
@ -182,14 +186,16 @@ void OctreeQueryNode::resetOctreePacket() {
_currentPacketIsCompressed = getWantCompression();
OCTREE_PACKET_FLAGS flags = 0;
if (_currentPacketIsColor) {
setAtBit(flags,PACKET_IS_COLOR_BIT);
setAtBit(flags, PACKET_IS_COLOR_BIT);
}
if (_currentPacketIsCompressed) {
setAtBit(flags,PACKET_IS_COMPRESSED_BIT);
setAtBit(flags, PACKET_IS_COMPRESSED_BIT);
}
_octreePacketAvailableBytes = MAX_PACKET_SIZE;
int numBytesPacketHeader = populatePacketHeader(reinterpret_cast<char*>(_octreePacket), _myPacketType);
int numBytesPacketHeader = DependencyManager::get<NodeList>()->populatePacketHeader(reinterpret_cast<char*>(_octreePacket),
_myPacketType);
_octreePacketAt = _octreePacket + numBytesPacketHeader;
_octreePacketAvailableBytes -= numBytesPacketHeader;
@ -198,7 +204,7 @@ void OctreeQueryNode::resetOctreePacket() {
*flagsAt = flags;
_octreePacketAt += sizeof(OCTREE_PACKET_FLAGS);
_octreePacketAvailableBytes -= sizeof(OCTREE_PACKET_FLAGS);
// pack in sequence number
OCTREE_PACKET_SEQUENCE* sequenceAt = (OCTREE_PACKET_SEQUENCE*)_octreePacketAt;
*sequenceAt = _sequenceNumber;
@ -252,11 +258,16 @@ bool OctreeQueryNode::updateCurrentViewFrustum() {
float originalFOV = getCameraFov();
float wideFOV = originalFOV + VIEW_FRUSTUM_FOV_OVERSEND;
newestViewFrustum.setFieldOfView(wideFOV); // hack
newestViewFrustum.setAspectRatio(getCameraAspectRatio());
newestViewFrustum.setNearClip(getCameraNearClip());
newestViewFrustum.setFarClip(getCameraFarClip());
newestViewFrustum.setEyeOffsetPosition(getCameraEyeOffsetPosition());
if (0.0f != getCameraAspectRatio() &&
0.0f != getCameraNearClip() &&
0.0f != getCameraFarClip()) {
newestViewFrustum.setProjection(glm::perspective(
glm::radians(wideFOV), // hack
getCameraAspectRatio(),
getCameraNearClip(),
getCameraFarClip()));
}
// if there has been a change, then recalculate
if (!newestViewFrustum.isVerySimilar(_currentViewFrustum)) {
@ -314,8 +325,7 @@ void OctreeQueryNode::updateLastKnownViewFrustum() {
}
// save that we know the view has been sent.
quint64 now = usecTimestampNow();
setLastTimeBagEmpty(now); // is this what we want? poor names
setLastTimeBagEmpty();
}

View file

@ -22,11 +22,11 @@
#include <OctreePacketData.h>
#include <OctreeQuery.h>
#include <OctreeSceneStats.h>
#include <ThreadedAssignment.h> // for SharedAssignmentPointer
#include "SentPacketHistory.h"
#include <qqueue.h>
class OctreeSendThread;
class OctreeServer;
class OctreeQueryNode : public OctreeQuery {
Q_OBJECT
@ -77,7 +77,7 @@ public:
bool moveShouldDump() const;
quint64 getLastTimeBagEmpty() const { return _lastTimeBagEmpty; }
void setLastTimeBagEmpty(quint64 lastTimeBagEmpty) { _lastTimeBagEmpty = lastTimeBagEmpty; }
void setLastTimeBagEmpty() { _lastTimeBagEmpty = _sceneSendStartTime; }
bool getCurrentPacketIsColor() const { return _currentPacketIsColor; }
bool getCurrentPacketIsCompressed() const { return _currentPacketIsCompressed; }
@ -89,7 +89,7 @@ public:
OctreeSceneStats stats;
void initializeOctreeSendThread(const SharedAssignmentPointer& myAssignment, const SharedNodePointer& node);
void initializeOctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node);
bool isOctreeSendThreadInitalized() { return _octreeSendThread; }
void dumpOutOfView();
@ -98,6 +98,8 @@ public:
void setLastRootTimestamp(quint64 timestamp) { _lastRootTimestamp = timestamp; }
unsigned int getlastOctreePacketLength() const { return _lastOctreePacketLength; }
int getDuplicatePacketCount() const { return _duplicatePacketCount; }
void sceneStart(quint64 sceneSendStartTime) { _sceneSendStartTime = sceneSendStartTime; }
void nodeKilled();
void forceNodeShutdown();
@ -158,6 +160,8 @@ private:
SentPacketHistory _sentPacketHistory;
QQueue<OCTREE_PACKET_SEQUENCE> _nackedSequenceNumbers;
quint64 _sceneSendStartTime = 0;
};
#endif // hifi_OctreeQueryNode_h

View file

@ -10,9 +10,9 @@
//
#include <NodeList.h>
#include <NumericalConstants.h>
#include <PacketHeaders.h>
#include <PerfStat.h>
#include <SharedUtil.h>
#include "OctreeSendThread.h"
#include "OctreeServer.h"
@ -21,9 +21,8 @@
quint64 startSceneSleepTime = 0;
quint64 endSceneSleepTime = 0;
OctreeSendThread::OctreeSendThread(const SharedAssignmentPointer& myAssignment, const SharedNodePointer& node) :
_myAssignment(myAssignment),
_myServer(static_cast<OctreeServer*>(myAssignment.data())),
OctreeSendThread::OctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node) :
_myServer(myServer),
_node(node),
_nodeUUID(node->getUUID()),
_packetData(),
@ -31,9 +30,14 @@ OctreeSendThread::OctreeSendThread(const SharedAssignmentPointer& myAssignment,
_isShuttingDown(false)
{
QString safeServerName("Octree");
// set our QThread object name so we can identify this thread while debugging
setObjectName(QString("Octree Send Thread (%1)").arg(uuidStringWithoutCurlyBraces(node->getUUID())));
if (_myServer) {
safeServerName = _myServer->getMyServerName();
}
qDebug() << qPrintable(safeServerName) << "server [" << _myServer << "]: client connected "
"- starting sending thread [" << this << "]";
@ -53,7 +57,6 @@ OctreeSendThread::~OctreeSendThread() {
OctreeServer::stopTrackingThread(this);
_node.clear();
_myAssignment.clear();
}
void OctreeSendThread::setIsShuttingDown() {
@ -66,15 +69,13 @@ bool OctreeSendThread::process() {
return false; // exit early if we're shutting down
}
// check that our server and assignment is still valid
if (!_myServer || !_myAssignment) {
return false; // exit early if it's not, it means the server is shutting down
}
OctreeServer::didProcess(this);
quint64 start = usecTimestampNow();
// we'd better have a server at this point, or we're in trouble
assert(_myServer);
// don't do any send processing until the initial load of the octree is complete...
if (_myServer->isInitialLoadComplete()) {
if (_node) {
@ -290,7 +291,7 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus
}
// calculate max number of packets that can be sent during this interval
int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxOctreePacketsPerSecond() / INTERVALS_PER_SECOND));
int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND));
int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval());
int truePacketsSent = 0;
@ -342,8 +343,7 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus
if (!viewFrustumChanged && !nodeData->getWantDelta()) {
// only set our last sent time if we weren't resetting due to frustum change
quint64 now = usecTimestampNow();
nodeData->setLastTimeBagEmpty(now);
nodeData->setLastTimeBagEmpty();
}
// track completed scenes and send out the stats packet accordingly
@ -367,9 +367,11 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus
// TODO: add these to stats page
//::startSceneSleepTime = _usleepTime;
nodeData->sceneStart(usecTimestampNow() - CHANGE_FUDGE);
// start tracking our stats
nodeData->stats.sceneStarted(isFullScene, viewFrustumChanged, _myServer->getOctree()->getRoot(), _myServer->getJurisdiction());
nodeData->stats.sceneStarted(isFullScene, viewFrustumChanged,
_myServer->getOctree()->getRoot(), _myServer->getJurisdiction());
// This is the start of "resending" the scene.
bool dontRestartSceneOnMove = false; // this is experimental
@ -560,6 +562,13 @@ int OctreeSendThread::packetDistributor(OctreeQueryNode* nodeData, bool viewFrus
}
if (somethingToSend) {
qDebug() << "Hit PPS Limit, packetsSentThisInterval =" << packetsSentThisInterval
<< " maxPacketsPerInterval = " << maxPacketsPerInterval
<< " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval;
}
// Here's where we can/should allow the server to send other data...
// send the environment packet
// TODO: should we turn this into a while loop to better handle sending multiple special packets

View file

@ -26,7 +26,7 @@ class OctreeServer;
class OctreeSendThread : public GenericThread {
Q_OBJECT
public:
OctreeSendThread(const SharedAssignmentPointer& myAssignment, const SharedNodePointer& node);
OctreeSendThread(OctreeServer* myServer, const SharedNodePointer& node);
virtual ~OctreeSendThread();
void setIsShuttingDown();
@ -43,7 +43,6 @@ protected:
virtual bool process();
private:
SharedAssignmentPointer _myAssignment;
OctreeServer* _myServer;
SharedNodePointer _node;
QUuid _nodeUUID;

View file

@ -21,6 +21,7 @@
#include <HTTPConnection.h>
#include <LogHandler.h>
#include <NetworkingConstants.h>
#include <NumericalConstants.h>
#include <UUID.h>
#include "../AssignmentClient.h"
@ -212,14 +213,6 @@ void OctreeServer::trackProcessWaitTime(float time) {
_averageProcessWaitTime.updateAverage(time);
}
void OctreeServer::attachQueryNodeToNode(Node* newNode) {
if (!newNode->getLinkedData() && _instance) {
OctreeQueryNode* newQueryNodeData = _instance->createOctreeQueryNode();
newQueryNodeData->init();
newNode->setLinkedData(newQueryNodeData);
}
}
OctreeServer::OctreeServer(const QByteArray& packet) :
ThreadedAssignment(packet),
_argc(0),
@ -266,16 +259,19 @@ OctreeServer::~OctreeServer() {
}
if (_jurisdictionSender) {
_jurisdictionSender->terminating();
_jurisdictionSender->terminate();
_jurisdictionSender->deleteLater();
}
if (_octreeInboundPacketProcessor) {
_octreeInboundPacketProcessor->terminating();
_octreeInboundPacketProcessor->terminate();
_octreeInboundPacketProcessor->deleteLater();
}
if (_persistThread) {
_persistThread->terminating();
_persistThread->terminate();
_persistThread->deleteLater();
}
@ -832,42 +828,42 @@ void OctreeServer::parsePayload() {
void OctreeServer::readPendingDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) {
auto nodeList = DependencyManager::get<NodeList>();
// If we know we're shutting down we just drop these packets on the floor.
// This stops us from initializing send threads we just shut down.
if (nodeList->packetVersionAndHashMatch(receivedPacket)) {
PacketType packetType = packetTypeForPacket(receivedPacket);
SharedNodePointer matchingNode = nodeList->sendingNodeForPacket(receivedPacket);
if (packetType == getMyQueryMessageType()) {
// If we got a query packet, then we're talking to an agent, and we
// need to make sure we have it in our nodeList.
if (matchingNode) {
nodeList->updateNodeWithDataFromPacket(matchingNode, receivedPacket);
OctreeQueryNode* nodeData = (OctreeQueryNode*)matchingNode->getLinkedData();
if (nodeData && !nodeData->isOctreeSendThreadInitalized()) {
if (!_isShuttingDown) {
if (nodeList->packetVersionAndHashMatch(receivedPacket)) {
PacketType packetType = packetTypeForPacket(receivedPacket);
SharedNodePointer matchingNode = nodeList->sendingNodeForPacket(receivedPacket);
if (packetType == getMyQueryMessageType()) {
// If we got a query packet, then we're talking to an agent, and we
// need to make sure we have it in our nodeList.
if (matchingNode) {
nodeList->updateNodeWithDataFromPacket(matchingNode, receivedPacket);
// NOTE: this is an important aspect of the proper ref counting. The send threads/node data need to
// know that the OctreeServer/Assignment will not get deleted on it while it's still active. The
// solution is to get the shared pointer for the current assignment. We need to make sure this is the
// same SharedAssignmentPointer that was ref counted by the assignment client.
SharedAssignmentPointer sharedAssignment = AssignmentClient::getCurrentAssignment();
nodeData->initializeOctreeSendThread(sharedAssignment, matchingNode);
OctreeQueryNode* nodeData = (OctreeQueryNode*) matchingNode->getLinkedData();
if (nodeData && !nodeData->isOctreeSendThreadInitalized()) {
nodeData->initializeOctreeSendThread(this, matchingNode);
}
}
}
} else if (packetType == PacketTypeOctreeDataNack) {
// If we got a nack packet, then we're talking to an agent, and we
// need to make sure we have it in our nodeList.
if (matchingNode) {
OctreeQueryNode* nodeData = (OctreeQueryNode*)matchingNode->getLinkedData();
if (nodeData) {
nodeData->parseNackPacket(receivedPacket);
} else if (packetType == PacketTypeOctreeDataNack) {
// If we got a nack packet, then we're talking to an agent, and we
// need to make sure we have it in our nodeList.
if (matchingNode) {
OctreeQueryNode* nodeData = (OctreeQueryNode*)matchingNode->getLinkedData();
if (nodeData) {
nodeData->parseNackPacket(receivedPacket);
}
}
} else if (packetType == PacketTypeJurisdictionRequest) {
_jurisdictionSender->queueReceivedPacket(matchingNode, receivedPacket);
} else if (_octreeInboundPacketProcessor && getOctree()->handlesEditPacketType(packetType)) {
_octreeInboundPacketProcessor->queueReceivedPacket(matchingNode, receivedPacket);
} else {
// let processNodeData handle it.
DependencyManager::get<NodeList>()->processNodeData(senderSockAddr, receivedPacket);
}
} else if (packetType == PacketTypeJurisdictionRequest) {
_jurisdictionSender->queueReceivedPacket(matchingNode, receivedPacket);
} else if (_octreeInboundPacketProcessor && getOctree()->handlesEditPacketType(packetType)) {
_octreeInboundPacketProcessor->queueReceivedPacket(matchingNode, receivedPacket);
} else {
// let processNodeData handle it.
DependencyManager::get<NodeList>()->processNodeData(senderSockAddr, receivedPacket);
}
}
}
@ -1095,8 +1091,6 @@ void OctreeServer::readConfiguration() {
}
void OctreeServer::run() {
qInstallMessageHandler(LogHandler::verboseMessageHandler);
_safeServerName = getMyServerName();
// Before we do anything else, create our tree...
@ -1130,7 +1124,11 @@ void OctreeServer::run() {
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
nodeList->linkedDataCreateCallback = &OctreeServer::attachQueryNodeToNode;
nodeList->linkedDataCreateCallback = [] (Node* node) {
OctreeQueryNode* newQueryNodeData = _instance->createOctreeQueryNode();
newQueryNodeData->init();
node->setLinkedData(newQueryNodeData);
};
srand((unsigned)time(0));
@ -1218,16 +1216,34 @@ void OctreeServer::forceNodeShutdown(SharedNodePointer node) {
void OctreeServer::aboutToFinish() {
qDebug() << qPrintable(_safeServerName) << "server STARTING about to finish...";
_isShuttingDown = true;
qDebug() << qPrintable(_safeServerName) << "inform Octree Inbound Packet Processor that we are shutting down...";
_octreeInboundPacketProcessor->shuttingDown();
// we're going down - set the NodeList linkedDataCallback to NULL so we do not create any more OctreeQueryNode objects.
// This ensures that when we forceNodeShutdown below for each node we don't get any more newly connecting nodes
auto nodeList = DependencyManager::get<NodeList>();
nodeList->linkedDataCreateCallback = NULL;
DependencyManager::get<NodeList>()->eachNode([this](const SharedNodePointer& node) {
if (_octreeInboundPacketProcessor) {
_octreeInboundPacketProcessor->terminating();
}
if (_jurisdictionSender) {
_jurisdictionSender->terminating();
}
// force a shutdown of all of our OctreeSendThreads - at this point it has to be impossible for a
// linkedDataCreateCallback to be called for a new node
nodeList->eachNode([this](const SharedNodePointer& node) {
qDebug() << qPrintable(_safeServerName) << "server about to finish while node still connected node:" << *node;
forceNodeShutdown(node);
});
if (_persistThread) {
_persistThread->aboutToFinish();
_persistThread->terminating();
}
qDebug() << qPrintable(_safeServerName) << "server ENDING about to finish...";

View file

@ -75,8 +75,6 @@ public:
virtual bool hasSpecialPacketToSend(const SharedNodePointer& node) { return false; }
virtual int sendSpecialPacket(const SharedNodePointer& node, OctreeQueryNode* queryNode, int& packetsSent) { return 0; }
static void attachQueryNodeToNode(Node* newNode);
static float SKIP_TIME; // use this for trackXXXTime() calls for non-times
static void trackLoopTime(float time) { _averageLoopTime.updateAverage(time); }
@ -146,12 +144,14 @@ protected:
QString getStatusLink();
void setupDatagramProcessingThread();
int _argc;
const char** _argv;
char** _parsedArgV;
QJsonObject _settings;
bool _isShuttingDown = false;
HTTPManager* _httpManager;
int _statusPort;
QString _statusHost;

View file

@ -1,6 +1,6 @@
#
# OSXTBBInstallNameChange.cmake
# cmake/externals/tbb
# OSXInstallNameChange.cmake
# cmake/macros
#
# Copyright 2015 High Fidelity, Inc.
# Created by Stephen Birarda on February 20, 2014
@ -10,36 +10,33 @@
#
# first find the so files in the source dir
set(_TBB_LIBRARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib)
file(GLOB_RECURSE _TBB_LIBRARIES "${_TBB_LIBRARY_DIR}/*.dylib")
message("INSTALL_NAME_LIBRARY_DIR ${INSTALL_NAME_LIBRARY_DIR}")
# raise an error if we found none
if (NOT _TBB_LIBRARIES)
message(FATAL_ERROR "Did not find any TBB libraries")
file(GLOB_RECURSE _LIBRARIES "${INSTALL_NAME_LIBRARY_DIR}/*.dylib")
if (NOT _LIBRARIES)
message(FATAL_ERROR "OSXInstallNameChange -- no libraries found: ${INSTALL_NAME_LIBRARY_DIR}")
endif ()
# find the install_name_tool command
find_program(INSTALL_NAME_TOOL_COMMAND NAMES install_name_tool DOC "Path to the install_name_tool command")
# find the lipo command
find_program(LIPO_COMMAND NAMES lipo DOC "Path to the lipo command")
# enumerate the libraries
foreach(_TBB_LIBRARY ${_TBB_LIBRARIES})
get_filename_component(_TBB_LIBRARY_FILENAME ${_TBB_LIBRARY} NAME)
foreach(_LIBRARY ${_LIBRARIES})
get_filename_component(_LIBRARY_FILENAME ${_LIBRARY} NAME)
set(_INSTALL_NAME_ARGS ${INSTALL_NAME_TOOL_COMMAND} -id ${_TBB_LIBRARY} ${_TBB_LIBRARY_FILENAME})
set(_INSTALL_NAME_ARGS ${INSTALL_NAME_TOOL_COMMAND} -id ${_LIBRARY} ${_LIBRARY_FILENAME})
message(STATUS "${INSTALL_NAME_COMMAND} ${_INSTALL_NAME_ARGS}")
execute_process(
COMMAND ${INSTALL_NAME_COMMAND} ${_INSTALL_NAME_ARGS}
WORKING_DIRECTORY ${_TBB_LIBRARY_DIR}
WORKING_DIRECTORY ${INSTALL_NAME_LIBRARY_DIR}
ERROR_VARIABLE _INSTALL_NAME_ERROR
)
if (_INSTALL_NAME_ERROR)
message(FATAL_ERROR "There was an error changing install name for ${_TBB_LIBRARY_FILENAME} - ${_INSTALL_NAME_ERROR}")
message(FATAL_ERROR "There was an error changing install name for ${_LIBRARY_FILENAME} - ${_INSTALL_NAME_ERROR}")
endif ()
endforeach()

View file

@ -0,0 +1,18 @@
set(EXTERNAL_NAME boostconfig)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://github.com/boostorg/config/archive/boost-1.58.0.zip
URL_MD5 42fa673bae2b7645a22736445e80eb8d
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE INTERNAL)

18
cmake/externals/oglplus/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,18 @@
set(EXTERNAL_NAME oglplus)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://softlayer-dal.dl.sourceforge.net/project/oglplus/oglplus-0.61.x/oglplus-0.61.0.zip
URL_MD5 bb55038c36c660d2b6c7be380414fa60
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include ${SOURCE_DIR}/implement CACHE TYPE INTERNAL)

38
cmake/externals/openvr/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,38 @@
include(ExternalProject)
include(SelectLibraryConfigurations)
set(EXTERNAL_NAME OpenVR)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://github.com/ValveSoftware/openvr/archive/0.9.1.zip
URL_MD5 f986f5a6815e9454c53c5bf58ce02fdc
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/headers CACHE TYPE INTERNAL)
if (WIN32)
# FIXME need to account for different architectures
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/win32/openvr_api.lib CACHE TYPE INTERNAL)
elseif(APPLE)
# FIXME need to account for different architectures
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/osx32/libopenvr_api.dylib CACHE TYPE INTERNAL)
elseif(NOT ANDROID)
# FIXME need to account for different architectures
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/lib/linux32/libopenvr_api.so CACHE TYPE INTERNAL)
endif()

57
cmake/externals/polyvox/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,57 @@
set(EXTERNAL_NAME polyvox)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/polyvox.zip
URL_MD5 904b840328278c9b36fa7a14be730c34
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build
LOG_DOWNLOAD 1
LOG_CONFIGURE 1
LOG_BUILD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
if (APPLE)
set(INSTALL_NAME_LIBRARY_DIR ${INSTALL_DIR}/lib)
message(STATUS "in polyvox INSTALL_NAME_LIBRARY_DIR ${INSTALL_NAME_LIBRARY_DIR}")
ExternalProject_Add_Step(
${EXTERNAL_NAME}
change-install-name
COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking"
COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${INSTALL_NAME_LIBRARY_DIR} -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake
DEPENDEES install
WORKING_DIRECTORY <SOURCE_DIR>
LOG 1
)
endif ()
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
if (WIN32)
set(${EXTERNAL_NAME_UPPER}_CORE_INCLUDE_DIRS ${INSTALL_DIR}/PolyVoxCore/include CACHE FILEPATH
"Path to polyvox core include directory")
set(${EXTERNAL_NAME_UPPER}_UTIL_INCLUDE_DIRS ${INSTALL_DIR}/PolyVoxUtil/include CACHE FILEPATH
"Path to polyvox util include directory")
else ()
set(${EXTERNAL_NAME_UPPER}_CORE_INCLUDE_DIRS ${INSTALL_DIR}/include/PolyVoxCore CACHE FILEPATH
"Path to polyvox core include directory")
set(${EXTERNAL_NAME_UPPER}_UTIL_INCLUDE_DIRS ${INSTALL_DIR}/include/PolyVoxUtil CACHE FILEPATH
"Path to polyvox util include directory")
endif ()
if (WIN32)
set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY ${INSTALL_DIR}/PolyVoxCore/lib/PolyVoxCore.lib CACHE FILEPATH "polyvox core library")
# set(${EXTERNAL_NAME_UPPER}_UTIL_LIBRARY ${INSTALL_DIR}/PolyVoxUtil/lib/PolyVoxUtil.lib CACHE FILEPATH "polyvox util library")
elseif (APPLE)
set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY ${INSTALL_DIR}/lib/libPolyVoxCore.dylib CACHE FILEPATH "polyvox core library")
# set(${EXTERNAL_NAME_UPPER}_UTIL_LIBRARY ${INSTALL_DIR}/lib/libPolyVoxUtil.dylib CACHE FILEPATH "polyvox util library")
else ()
set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY ${INSTALL_DIR}/lib/libPolyVoxCore.so CACHE FILEPATH "polyvox core library")
# set(${EXTERNAL_NAME_UPPER}_UTIL_LIBRARY ${INSTALL_DIR}/lib/libPolyVoxUtil.so CACHE FILEPATH "polyvox util library")
endif ()

View file

@ -66,7 +66,7 @@ if (APPLE)
${EXTERNAL_NAME}
change-install-name
COMMENT "Calling install_name_tool on TBB libraries to fix install name for dylib linking"
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/OSXTBBInstallNameChange.cmake
COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_TBB_LIB_DIR} -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake
DEPENDEES install
WORKING_DIRECTORY <SOURCE_DIR>
LOG 1
@ -115,4 +115,4 @@ endif ()
if (DEFINED ${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/include CACHE TYPE "List of tbb include directories")
endif ()
endif ()

View file

@ -34,4 +34,4 @@ macro(SETUP_HIFI_PROJECT)
foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES})
target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE})
endforeach()
endmacro()
endmacro()

View file

@ -0,0 +1,56 @@
#
# CopyDirectoryBesideTarget.cmake
# cmake/macros
#
# Created by Stephen Birarda on 04/30/15.
# Copyright 2015 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTINATION)
# remove the current directory
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory $<TARGET_FILE_DIR:${TARGET_NAME}>/${_DESTINATION}
)
if (${_SHOULD_SYMLINK})
# first create the destination
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/${_DESTINATION}
)
# the caller wants a symlink, so just add a command to symlink all contents of _DIRECTORY
# in the destination - we can't simply create a symlink for _DESTINATION
# because the remove_directory call above would delete the original files
file(GLOB _DIR_ITEMS ${_DIRECTORY}/*)
foreach(_ITEM ${_DIR_ITEMS})
# get the filename for this item
get_filename_component(_ITEM_FILENAME ${_ITEM} NAME)
# add the command to symlink this item
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E create_symlink
${_ITEM}
$<TARGET_FILE_DIR:${TARGET_NAME}>/${_DESTINATION}/${_ITEM_FILENAME}
)
endforeach()
else ()
# copy the directory
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY}
$<TARGET_FILE_DIR:${TARGET_NAME}>/${_DESTINATION}
)
endif ()
# glob everything in this directory - add a custom command to copy any files
endmacro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY)

View file

@ -0,0 +1,24 @@
#
# Try to find BOOSTCONFIG include path.
# Once done this will define
#
# BOOSTCONFIG_INCLUDE_DIRS
#
# Created by Bradley Austin Davis on 2015/05/22
# Copyright 2015 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
# setup hints for BOOSTCONFIG search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("BOOSTCONFIG")
# locate header
find_path(BOOSTCONFIG_INCLUDE_DIRS "boost/config.hpp" HINTS ${BOOSTCONFIG_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(BOOSTCONFIG DEFAULT_MSG BOOSTCONFIG_INCLUDE_DIRS)
mark_as_advanced(BOOSTCONFIG_INCLUDE_DIRS BOOSTCONFIG_SEARCH_DIRS)

View file

@ -0,0 +1,24 @@
#
# Try to find OGLPLUS include path.
# Once done this will define
#
# OGLPLUS_INCLUDE_DIRS
#
# Created by Bradley Austin Davis on 2015/05/22
# Copyright 2015 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
# setup hints for OGLPLUS search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("oglplus")
# locate header
find_path(OGLPLUS_INCLUDE_DIRS "oglplus/fwd.hpp" HINTS ${OGLPLUS_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(OGLPLUS DEFAULT_MSG OGLPLUS_INCLUDE_DIRS)
mark_as_advanced(OGLPLUS_INCLUDE_DIRS OGLPLUS_SEARCH_DIRS)

View file

@ -0,0 +1,21 @@
#
# FindLibOVR.cmake
#
# Try to find the LibOVR library to use the Oculus
# Once done this will define
#
# OPENVR_FOUND - system found LibOVR
# OPENVR_INCLUDE_DIRS - the LibOVR include directory
# OPENVR_LIBRARIES - Link this to use LibOVR
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
if (NOT ANDROID)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(OPENVR DEFAULT_MSG OPENVR_INCLUDE_DIRS OPENVR_LIBRARIES)
endif()
mark_as_advanced(OPENVR_INCLUDE_DIRS OPENVR_LIBRARIES OPENVR_SEARCH_DIRS)

View file

@ -0,0 +1,54 @@
#
# FindPolyvox.cmake
#
# Try to find the libpolyvox resampling library
#
# You can provide a LIBPOLYVOX_ROOT_DIR which contains lib and include directories
#
# Once done this will define
#
# POLYVOX_FOUND - system found libpolyvox
# POLYVOX_INCLUDE_DIRS - the libpolyvox include directory
# POLYVOX_LIBRARIES - link to this to use libpolyvox
#
# Created on 1/22/2015 by Stephen Birarda
# Copyright 2015 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("polyvox")
find_path(POLYVOX_CORE_INCLUDE_DIRS PolyVoxCore/SimpleVolume.h PATH_SUFFIXES include include/PolyVoxCore HINTS ${POLYVOX_SEARCH_DIRS})
# find_path(POLYVOX_UTIL_INCLUDE_DIRS PolyVoxUtil/Serialization.h PATH_SUFFIXES include include/PolyVoxUtil HINTS ${POLYVOX_SEARCH_DIRS})
find_library(POLYVOX_CORE_LIBRARY NAMES PolyVoxCore PATH_SUFFIXES lib HINTS ${POLYVOX_SEARCH_DIRS})
# find_library(POLYVOX_UTIL_LIBRARY NAMES PolyVoxUtil PATH_SUFFIXES lib HINTS ${POLYVOX_SEARCH_DIRS})
# if (WIN32)
# find_path(POLYVOX_DLL_PATH polyvox.dll PATH_SUFFIXES bin HINTS ${POLYVOX_SEARCH_DIRS})
# endif()
# set(POLYVOX_REQUIREMENTS POLYVOX_CORE_INCLUDE_DIRS POLYVOX_UTIL_INCLUDE_DIRS POLYVOX_CORE_LIBRARY POLYVOX_UTIL_LIBRARY)
set(POLYVOX_REQUIREMENTS POLYVOX_CORE_INCLUDE_DIRS POLYVOX_CORE_LIBRARY)
# if (WIN32)
# list(APPEND POLYVOX_REQUIREMENTS POLYVOX_DLL_PATH)
# endif ()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Polyvox DEFAULT_MSG ${POLYVOX_REQUIREMENTS})
# if (WIN32)
# add_paths_to_fixup_libs(${POLYVOX_DLL_PATH})
# endif ()
# set(POLYVOX_INCLUDE_DIRS ${POLYVOX_CORE_INCLUDE_DIRS} ${POLYVOX_UTIL_INCLUDE_DIRS})
# set(POLYVOX_LIBRARIES ${POLYVOX_CORE_LIBRARY} ${POLYVOX_UTIL_LIBRARY})
set(POLYVOX_INCLUDE_DIRS ${POLYVOX_CORE_INCLUDE_DIRS})
set(POLYVOX_LIBRARIES ${POLYVOX_CORE_LIBRARY})
mark_as_advanced(POLYVOX_INCLUDE_DIRS POLYVOX_LIBRARIES POLYVOX_SEARCH_DIRS)

View file

@ -1,39 +1,18 @@
set(TARGET_NAME domain-server)
if (UPPER_CMAKE_BUILD_TYPE MATCHES DEBUG AND NOT WIN32)
set(_SHOULD_SYMLINK_RESOURCES TRUE)
else ()
set(_SHOULD_SYMLINK_RESOURCES FALSE)
endif ()
# setup the project and link required Qt modules
setup_hifi_project(Network)
# remove current resources dir
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources
)
# copy all files in resources, including web
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${PROJECT_SOURCE_DIR}/resources"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources
)
if (NOT WIN32)
# remove the web directory so we can make it a symlink
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E remove_directory
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web
)
# make the web directory a symlink
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E create_symlink
"${PROJECT_SOURCE_DIR}/resources/web"
$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/web
)
endif ()
# TODO: find a solution that will handle web file changes in resources on windows without a re-build.
# Currently the resources are only copied on post-build. If one is changed but the domain-server is not, they will
# not be re-copied. This is worked-around on OS X/UNIX by using a symlink.
symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources")
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)
@ -52,4 +31,4 @@ include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})
copy_dlls_beside_windows_executable()
copy_dlls_beside_windows_executable()

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,21 +1,10 @@
body {
position: relative;
}
.table-lead {
color: #66CCCC;
padding-bottom: 30px;
}
.table-lead .lead-line {
background-color: #66CCCC;
}
#queued-lead {
color: #F77777;
}
#queued-lead .lead-line {
background-color: #F77777;
background-color: black;
}
.table-lead h3 {
@ -44,14 +33,6 @@ span.port {
color: #666666;
}
.stats-key {
width: 400px;
}
.stale {
color: red;
}
.locked {
color: #428bca;
}
@ -81,7 +62,11 @@ span.port {
}
td.buttons {
width: 14px;
width: 30px;
}
td.buttons.reorder-buttons {
width: 40px;
}
td .glyphicon {
@ -97,7 +82,81 @@ td.reorder-buttons .glyphicon {
display: inherit;
}
td a.glyphicon {
color: black;
text-decoration: none;
}
tr.new-row {
color: #3c763d;
background-color: #dff0d8;
}
.highchart-modal .modal-dialog {
width: 650px;
}
table {
table-layout: fixed;
word-wrap: break-word;
}
#xs-advanced-container {
margin-bottom: 20px;
}
#advanced-toggle-button-xs {
width: 100%;
}
/* styling for bootstrap-switch toggles */
.checkbox-help {
margin-top: 10px;
}
/* CSS only spinner for AJAX requests */
.spinner {
margin: 30px auto 0;
width: 70px;
text-align: center;
}
.spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
animation: bouncedelay 1.4s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}

View file

@ -1,4 +1,12 @@
body.stop-scrolling {
height: 100%;
overflow: hidden; }
.sweet-overlay {
background-color: black;
/* IE8 */
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
/* IE8 */
background-color: rgba(0, 0, 0, 0.4);
position: fixed;
left: 0;
@ -6,11 +14,11 @@
top: 0;
bottom: 0;
display: none;
z-index: 1000; }
z-index: 10000; }
.sweet-alert {
background-color: white;
font-family: sans-serif;
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
width: 478px;
padding: 17px;
border-radius: 5px;
@ -22,7 +30,7 @@
margin-top: -200px;
overflow: hidden;
display: none;
z-index: 2000; }
z-index: 99999; }
@media all and (max-width: 540px) {
.sweet-alert {
width: auto;
@ -36,15 +44,120 @@
text-align: center;
font-weight: 600;
text-transform: none;
position: relative; }
position: relative;
margin: 25px 0;
padding: 0;
line-height: 40px;
display: block; }
.sweet-alert p {
color: #797979;
font-size: 16px;
text-align: center;
font-weight: 300;
position: relative;
text-align: inherit;
float: none;
margin: 0;
padding: 0;
line-height: normal; }
.sweet-alert fieldset {
border: none;
position: relative; }
.sweet-alert .sa-error-container {
background-color: #f1f1f1;
margin-left: -17px;
margin-right: -17px;
overflow: hidden;
padding: 0 10px;
max-height: 0;
webkit-transition: padding 0.15s, max-height 0.15s;
transition: padding 0.15s, max-height 0.15s; }
.sweet-alert .sa-error-container.show {
padding: 10px 0;
max-height: 100px;
webkit-transition: padding 0.2s, max-height 0.2s;
transition: padding 0.25s, max-height 0.25s; }
.sweet-alert .sa-error-container .icon {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #ea7d7d;
color: white;
line-height: 24px;
text-align: center;
margin-right: 3px; }
.sweet-alert .sa-error-container p {
display: inline-block; }
.sweet-alert .sa-input-error {
position: absolute;
top: 29px;
right: 26px;
width: 20px;
height: 20px;
opacity: 0;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
-webkit-transition: all 0.1s;
transition: all 0.1s; }
.sweet-alert .sa-input-error::before, .sweet-alert .sa-input-error::after {
content: "";
width: 20px;
height: 6px;
background-color: #f06e57;
border-radius: 3px;
position: absolute;
top: 50%;
margin-top: -4px;
left: 50%;
margin-left: -9px; }
.sweet-alert .sa-input-error::before {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg); }
.sweet-alert .sa-input-error::after {
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
.sweet-alert .sa-input-error.show {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); }
.sweet-alert input {
width: 100%;
box-sizing: border-box;
border-radius: 3px;
border: 1px solid #d7d7d7;
height: 43px;
margin-top: 10px;
margin-bottom: 17px;
font-size: 18px;
box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.06);
padding: 0 12px;
display: none;
-webkit-transition: all 0.3s;
transition: all 0.3s; }
.sweet-alert input:focus {
outline: none;
box-shadow: 0px 0px 3px #c4e6f5;
border: 1px solid #b4dbed; }
.sweet-alert input:focus::-moz-placeholder {
transition: opacity 0.3s 0.03s ease;
opacity: 0.5; }
.sweet-alert input:focus:-ms-input-placeholder {
transition: opacity 0.3s 0.03s ease;
opacity: 0.5; }
.sweet-alert input:focus::-webkit-input-placeholder {
transition: opacity 0.3s 0.03s ease;
opacity: 0.5; }
.sweet-alert input::-moz-placeholder {
color: #bdbdbd; }
.sweet-alert input:-ms-input-placeholder {
color: #bdbdbd; }
.sweet-alert input::-webkit-input-placeholder {
color: #bdbdbd; }
.sweet-alert.show-input input {
display: block; }
.sweet-alert button {
background-color: #AEDEF4;
color: white;
@ -52,6 +165,7 @@
box-shadow: none;
font-size: 17px;
font-weight: 500;
-webkit-border-radius: 4px;
border-radius: 5px;
padding: 10px 32px;
margin: 26px 5px 0 5px;
@ -71,22 +185,29 @@
background-color: #b6b6b6; }
.sweet-alert button.cancel:focus {
box-shadow: rgba(197, 205, 211, 0.8) 0px 0px 2px, rgba(0, 0, 0, 0.0470588) 0px 0px 0px 1px inset !important; }
.sweet-alert button::-moz-focus-inner {
border: 0; }
.sweet-alert[data-has-cancel-button=false] button {
box-shadow: none !important; }
.sweet-alert .icon {
.sweet-alert[data-has-confirm-button=false][data-has-cancel-button=false] {
padding-bottom: 40px; }
.sweet-alert .sa-icon {
width: 80px;
height: 80px;
border: 4px solid gray;
-webkit-border-radius: 40px;
border-radius: 40px;
border-radius: 50%;
margin: 20px auto;
padding: 0;
position: relative;
box-sizing: content-box; }
.sweet-alert .icon.error {
.sweet-alert .sa-icon.sa-error {
border-color: #F27474; }
.sweet-alert .icon.error .x-mark {
.sweet-alert .sa-icon.sa-error .sa-x-mark {
position: relative;
display: block; }
.sweet-alert .icon.error .line {
.sweet-alert .sa-icon.sa-error .sa-line {
position: absolute;
height: 5px;
width: 47px;
@ -94,37 +215,39 @@
display: block;
top: 37px;
border-radius: 2px; }
.sweet-alert .icon.error .line.left {
.sweet-alert .sa-icon.sa-error .sa-line.sa-left {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
left: 17px; }
.sweet-alert .icon.error .line.right {
.sweet-alert .sa-icon.sa-error .sa-line.sa-right {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
right: 16px; }
.sweet-alert .icon.warning {
.sweet-alert .sa-icon.sa-warning {
border-color: #F8BB86; }
.sweet-alert .icon.warning .body {
.sweet-alert .sa-icon.sa-warning .sa-body {
position: absolute;
width: 5px;
height: 47px;
left: 50%;
top: 10px;
-webkit-border-radius: 2px;
border-radius: 2px;
margin-left: -2px;
background-color: #F8BB86; }
.sweet-alert .icon.warning .dot {
.sweet-alert .sa-icon.sa-warning .sa-dot {
position: absolute;
width: 7px;
height: 7px;
-webkit-border-radius: 50%;
border-radius: 50%;
margin-left: -3px;
left: 50%;
bottom: 10px;
background-color: #F8BB86; }
.sweet-alert .icon.info {
.sweet-alert .sa-icon.sa-info {
border-color: #C9DAE1; }
.sweet-alert .icon.info::before {
.sweet-alert .sa-icon.sa-info::before {
content: "";
position: absolute;
width: 5px;
@ -134,7 +257,7 @@
border-radius: 2px;
margin-left: -2px;
background-color: #C9DAE1; }
.sweet-alert .icon.info::after {
.sweet-alert .sa-icon.sa-info::after {
content: "";
position: absolute;
width: 7px;
@ -143,17 +266,21 @@
margin-left: -3px;
top: 19px;
background-color: #C9DAE1; }
.sweet-alert .icon.success {
.sweet-alert .sa-icon.sa-success {
border-color: #A5DC86; }
.sweet-alert .icon.success::before, .sweet-alert .icon.success::after {
.sweet-alert .sa-icon.sa-success::before, .sweet-alert .sa-icon.sa-success::after {
content: '';
-webkit-border-radius: 40px;
border-radius: 40px;
border-radius: 50%;
position: absolute;
width: 60px;
height: 120px;
background: white;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
.sweet-alert .icon.success::before {
.sweet-alert .sa-icon.sa-success::before {
-webkit-border-radius: 120px 0 0 120px;
border-radius: 120px 0 0 120px;
top: -7px;
left: -33px;
@ -161,7 +288,8 @@
transform: rotate(-45deg);
-webkit-transform-origin: 60px 60px;
transform-origin: 60px 60px; }
.sweet-alert .icon.success::after {
.sweet-alert .sa-icon.sa-success::after {
-webkit-border-radius: 0 120px 120px 0;
border-radius: 0 120px 120px 0;
top: -11px;
left: 30px;
@ -169,17 +297,19 @@
transform: rotate(-45deg);
-webkit-transform-origin: 0px 60px;
transform-origin: 0px 60px; }
.sweet-alert .icon.success .placeholder {
.sweet-alert .sa-icon.sa-success .sa-placeholder {
width: 80px;
height: 80px;
border: 4px solid rgba(165, 220, 134, 0.2);
-webkit-border-radius: 40px;
border-radius: 40px;
border-radius: 50%;
box-sizing: content-box;
position: absolute;
left: -4px;
top: -4px;
z-index: 2; }
.sweet-alert .icon.success .fix {
.sweet-alert .sa-icon.sa-success .sa-fix {
width: 5px;
height: 90px;
background-color: white;
@ -189,26 +319,26 @@
z-index: 1;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg); }
.sweet-alert .icon.success .line {
.sweet-alert .sa-icon.sa-success .sa-line {
height: 5px;
background-color: #A5DC86;
display: block;
border-radius: 2px;
position: absolute;
z-index: 2; }
.sweet-alert .icon.success .line.tip {
.sweet-alert .sa-icon.sa-success .sa-line.sa-tip {
width: 25px;
left: 14px;
top: 46px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
.sweet-alert .icon.success .line.long {
.sweet-alert .sa-icon.sa-success .sa-line.sa-long {
width: 47px;
right: 8px;
top: 38px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg); }
.sweet-alert .icon.custom {
.sweet-alert .sa-icon.sa-custom {
background-size: contain;
border-radius: 0;
border: none;
@ -222,238 +352,274 @@
0% {
transform: scale(0.7);
-webkit-transform: scale(0.7); }
45% {
transform: scale(1.05);
-webkit-transform: scale(1.05); }
80% {
transform: scale(0.95);
-webkit-tranform: scale(0.95); }
100% {
transform: scale(1);
-webkit-transform: scale(1); } }
@-moz-keyframes showSweetAlert {
0% {
transform: scale(0.7);
-webkit-transform: scale(0.7); }
45% {
transform: scale(1.05);
-webkit-transform: scale(1.05); }
80% {
transform: scale(0.95);
-webkit-tranform: scale(0.95); }
-webkit-transform: scale(0.95); }
100% {
transform: scale(1);
-webkit-transform: scale(1); } }
@keyframes showSweetAlert {
0% {
transform: scale(0.7);
-webkit-transform: scale(0.7); }
45% {
transform: scale(1.05);
-webkit-transform: scale(1.05); }
80% {
transform: scale(0.95);
-webkit-tranform: scale(0.95); }
-webkit-transform: scale(0.95); }
100% {
transform: scale(1);
-webkit-transform: scale(1); } }
@-webkit-keyframes hideSweetAlert {
0% {
transform: scale(1);
-webkit-transform: scale(1); }
100% {
transform: scale(0.5);
-webkit-transform: scale(0.5); } }
@-moz-keyframes hideSweetAlert {
0% {
transform: scale(1);
-webkit-transform: scale(1); }
100% {
transform: scale(0.5);
-webkit-transform: scale(0.5); } }
@keyframes hideSweetAlert {
0% {
transform: scale(1);
-webkit-transform: scale(1); }
100% {
transform: scale(0.5);
-webkit-transform: scale(0.5); } }
.showSweetAlert {
-webkit-animation: showSweetAlert 0.3s;
-moz-animation: showSweetAlert 0.3s;
animation: showSweetAlert 0.3s; }
.hideSweetAlert {
@-webkit-keyframes slideFromTop {
0% {
top: 0%; }
100% {
top: 50%; } }
@keyframes slideFromTop {
0% {
top: 0%; }
100% {
top: 50%; } }
@-webkit-keyframes slideToTop {
0% {
top: 50%; }
100% {
top: 0%; } }
@keyframes slideToTop {
0% {
top: 50%; }
100% {
top: 0%; } }
@-webkit-keyframes slideFromBottom {
0% {
top: 70%; }
100% {
top: 50%; } }
@keyframes slideFromBottom {
0% {
top: 70%; }
100% {
top: 50%; } }
@-webkit-keyframes slideToBottom {
0% {
top: 50%; }
100% {
top: 70%; } }
@keyframes slideToBottom {
0% {
top: 50%; }
100% {
top: 70%; } }
.showSweetAlert[data-animation=pop] {
-webkit-animation: showSweetAlert 0.3s;
animation: showSweetAlert 0.3s; }
.showSweetAlert[data-animation=none] {
-webkit-animation: none;
animation: none; }
.showSweetAlert[data-animation=slide-from-top] {
-webkit-animation: slideFromTop 0.3s;
animation: slideFromTop 0.3s; }
.showSweetAlert[data-animation=slide-from-bottom] {
-webkit-animation: slideFromBottom 0.3s;
animation: slideFromBottom 0.3s; }
.hideSweetAlert[data-animation=pop] {
-webkit-animation: hideSweetAlert 0.2s;
-moz-animation: hideSweetAlert 0.2s;
animation: hideSweetAlert 0.2s; }
.hideSweetAlert[data-animation=none] {
-webkit-animation: none;
animation: none; }
.hideSweetAlert[data-animation=slide-from-top] {
-webkit-animation: slideToTop 0.4s;
animation: slideToTop 0.4s; }
.hideSweetAlert[data-animation=slide-from-bottom] {
-webkit-animation: slideToBottom 0.3s;
animation: slideToBottom 0.3s; }
@-webkit-keyframes animateSuccessTip {
0% {
width: 0;
left: 1px;
top: 19px; }
54% {
width: 0;
left: 1px;
top: 19px; }
70% {
width: 50px;
left: -8px;
top: 37px; }
84% {
width: 17px;
left: 21px;
top: 48px; }
100% {
width: 25px;
left: 14px;
top: 45px; } }
@-moz-keyframes animateSuccessTip {
0% {
width: 0;
left: 1px;
top: 19px; }
54% {
width: 0;
left: 1px;
top: 19px; }
70% {
width: 50px;
left: -8px;
top: 37px; }
84% {
width: 17px;
left: 21px;
top: 48px; }
100% {
width: 25px;
left: 14px;
top: 45px; } }
@keyframes animateSuccessTip {
0% {
width: 0;
left: 1px;
top: 19px; }
54% {
width: 0;
left: 1px;
top: 19px; }
70% {
width: 50px;
left: -8px;
top: 37px; }
84% {
width: 17px;
left: 21px;
top: 48px; }
100% {
width: 25px;
left: 14px;
top: 45px; } }
@-webkit-keyframes animateSuccessLong {
0% {
width: 0;
right: 46px;
top: 54px; }
65% {
width: 0;
right: 46px;
top: 54px; }
84% {
width: 55px;
right: 0px;
top: 35px; }
100% {
width: 47px;
right: 8px;
top: 38px; } }
@-moz-keyframes animateSuccessLong {
0% {
width: 0;
right: 46px;
top: 54px; }
65% {
width: 0;
right: 46px;
top: 54px; }
84% {
width: 55px;
right: 0px;
top: 35px; }
100% {
width: 47px;
right: 8px;
top: 38px; } }
@keyframes animateSuccessLong {
0% {
width: 0;
right: 46px;
top: 54px; }
65% {
width: 0;
right: 46px;
top: 54px; }
84% {
width: 55px;
right: 0px;
top: 35px; }
100% {
width: 47px;
right: 8px;
top: 38px; } }
@-webkit-keyframes rotatePlaceholder {
0% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
5% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
12% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); }
100% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); } }
@-moz-keyframes rotatePlaceholder {
0% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
5% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
12% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); }
100% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); } }
@keyframes rotatePlaceholder {
0% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
5% {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg); }
12% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); }
100% {
transform: rotate(-405deg);
-webkit-transform: rotate(-405deg); } }
.animateSuccessTip {
-webkit-animation: animateSuccessTip 0.75s;
-moz-animation: animateSuccessTip 0.75s;
animation: animateSuccessTip 0.75s; }
.animateSuccessLong {
-webkit-animation: animateSuccessLong 0.75s;
-moz-animation: animateSuccessLong 0.75s;
animation: animateSuccessLong 0.75s; }
.icon.success.animate::after {
.sa-icon.sa-success.animate::after {
-webkit-animation: rotatePlaceholder 4.25s ease-in;
-moz-animation: rotatePlaceholder 4.25s ease-in;
animation: rotatePlaceholder 4.25s ease-in; }
@-webkit-keyframes animateErrorIcon {
@ -461,31 +627,25 @@
transform: rotateX(100deg);
-webkit-transform: rotateX(100deg);
opacity: 0; }
100% {
transform: rotateX(0deg);
-webkit-transform: rotateX(0deg);
opacity: 1; } }
@-moz-keyframes animateErrorIcon {
0% {
transform: rotateX(100deg);
-webkit-transform: rotateX(100deg);
opacity: 0; }
100% {
transform: rotateX(0deg);
-webkit-transform: rotateX(0deg);
opacity: 1; } }
@keyframes animateErrorIcon {
0% {
transform: rotateX(100deg);
-webkit-transform: rotateX(100deg);
opacity: 0; }
100% {
transform: rotateX(0deg);
-webkit-transform: rotateX(0deg);
opacity: 1; } }
.animateErrorIcon {
-webkit-animation: animateErrorIcon 0.5s;
-moz-animation: animateErrorIcon 0.5s;
animation: animateErrorIcon 0.5s; }
@-webkit-keyframes animateXMark {
@ -494,108 +654,104 @@
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
50% {
transform: scale(0.4);
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
80% {
transform: scale(1.15);
-webkit-transform: scale(1.15);
margin-top: -6px; }
100% {
transform: scale(1);
-webkit-transform: scale(1);
margin-top: 0;
opacity: 1; } }
@-moz-keyframes animateXMark {
0% {
transform: scale(0.4);
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
50% {
transform: scale(0.4);
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
80% {
transform: scale(1.15);
-webkit-transform: scale(1.15);
margin-top: -6px; }
100% {
transform: scale(1);
-webkit-transform: scale(1);
margin-top: 0;
opacity: 1; } }
@keyframes animateXMark {
0% {
transform: scale(0.4);
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
50% {
transform: scale(0.4);
-webkit-transform: scale(0.4);
margin-top: 26px;
opacity: 0; }
80% {
transform: scale(1.15);
-webkit-transform: scale(1.15);
margin-top: -6px; }
100% {
transform: scale(1);
-webkit-transform: scale(1);
margin-top: 0;
opacity: 1; } }
.animateXMark {
-webkit-animation: animateXMark 0.5s;
-moz-animation: animateXMark 0.5s;
animation: animateXMark 0.5s; }
/*@include keyframes(simpleRotate) {
0% { transform: rotateY(0deg); }
100% { transform: rotateY(-360deg); }
}
.simpleRotate {
@include animation('simpleRotate 0.75s');
}*/
@-webkit-keyframes pulseWarning {
0% {
border-color: #F8D486; }
100% {
border-color: #F8BB86; } }
@-moz-keyframes pulseWarning {
0% {
border-color: #F8D486; }
100% {
border-color: #F8BB86; } }
@keyframes pulseWarning {
0% {
border-color: #F8D486; }
100% {
border-color: #F8BB86; } }
.pulseWarning {
-webkit-animation: pulseWarning 0.75s infinite alternate;
-moz-animation: pulseWarning 0.75s infinite alternate;
animation: pulseWarning 0.75s infinite alternate; }
@-webkit-keyframes pulseWarningIns {
0% {
background-color: #F8D486; }
100% {
background-color: #F8BB86; } }
@-moz-keyframes pulseWarningIns {
0% {
background-color: #F8D486; }
100% {
background-color: #F8BB86; } }
@keyframes pulseWarningIns {
0% {
background-color: #F8D486; }
100% {
background-color: #F8BB86; } }
.pulseWarningIns {
-webkit-animation: pulseWarningIns 0.75s infinite alternate;
-moz-animation: pulseWarningIns 0.75s infinite alternate;
animation: pulseWarningIns 0.75s infinite alternate; }
/* Internet Explorer 9 has some special quirks that are fixed here */
/* The icons are not animated. */
/* This file is automatically merged into sweet-alert.min.js through Gulp */
/* Error icon */
.sweet-alert .sa-icon.sa-error .sa-line.sa-left {
-ms-transform: rotate(45deg) \9; }
.sweet-alert .sa-icon.sa-error .sa-line.sa-right {
-ms-transform: rotate(-45deg) \9; }
/* Success icon */
.sweet-alert .sa-icon.sa-success {
border-color: transparent\9; }
.sweet-alert .sa-icon.sa-success .sa-line.sa-tip {
-ms-transform: rotate(45deg) \9; }
.sweet-alert .sa-icon.sa-success .sa-line.sa-long {
-ms-transform: rotate(-45deg) \9; }

View file

@ -1,4 +1,4 @@
</div>
<script src='/js/jquery.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/domain-server.js'></script>
<script src='/js/domain-server.js'></script>

View file

@ -3,10 +3,13 @@
<head>
<title>domain-server</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap -->
<link href="/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/css/style.css" rel="stylesheet" media="screen">
<link href="/css/sweet-alert.css" rel="stylesheet" media="screen">
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
<link href="/stats/css/json.human.css" rel="stylesheet" media="screen">
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
@ -21,7 +24,7 @@
</button>
<a class="navbar-brand" href="/">domain-server</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
@ -37,4 +40,4 @@
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="container-fluid">

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
function qs(key) {
key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, "\\$&"); // escape RegEx meta chars
var match = location.search.match(new RegExp("[?&]"+key+"=([^&]+)(&|$)"));
return match && decodeURIComponent(match[1].replace(/\+/g, " "));
}

View file

@ -1,711 +0,0 @@
var Settings = {
showAdvanced: false,
ADVANCED_CLASS: 'advanced-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',
DATA_COL_CLASS: 'value-col',
ADD_ROW_BUTTON_CLASS: 'add-row',
ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row',
DEL_ROW_BUTTON_CLASS: 'del-row',
DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row',
MOVE_UP_BUTTON_CLASS: 'move-up',
MOVE_UP_SPAN_CLASSES: 'glyphicon glyphicon-chevron-up move-up',
MOVE_DOWN_BUTTON_CLASS: 'move-down',
MOVE_DOWN_SPAN_CLASSES: 'glyphicon glyphicon-chevron-down move-down',
TABLE_BUTTONS_CLASS: 'buttons',
ADD_DEL_BUTTONS_CLASS: 'add-del-buttons',
ADD_DEL_BUTTONS_CLASSES: 'buttons add-del-buttons',
REORDER_BUTTONS_CLASS: 'reorder-buttons',
REORDER_BUTTONS_CLASSES: 'buttons reorder-buttons',
NEW_ROW_CLASS: 'new-row'
};
var viewHelpers = {
getFormGroup: function(groupName, setting, values, isAdvanced, isLocked) {
setting_name = groupName + "." + setting.name
form_group = "<div class='form-group " + (isAdvanced ? Settings.ADVANCED_CLASS : "") + "'>"
if (_.has(values, groupName) && _.has(values[groupName], setting.name)) {
setting_value = values[groupName][setting.name]
} else if (_.has(setting, 'default')) {
setting_value = setting.default
} else {
setting_value = ""
}
label_class = 'control-label'
if (isLocked) {
label_class += ' locked'
}
common_attrs = " class='" + (setting.type !== 'checkbox' ? 'form-control' : '')
+ " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + setting_name + "' "
+ "id='" + setting_name + "'"
if (setting.type === 'checkbox') {
if (setting.label) {
form_group += "<label class='" + label_class + "'>" + setting.label + "</label>"
}
form_group += "<div class='checkbox" + (isLocked ? " disabled" : "") + "'>"
form_group += "<label for='" + setting_name + "'>"
form_group += "<input type='checkbox'" + common_attrs + (setting_value ? "checked" : "") + (isLocked ? " disabled" : "") + "/>"
form_group += " " + setting.help + "</label>";
form_group += "</div>"
} else {
input_type = _.has(setting, 'type') ? setting.type : "text"
if (setting.label) {
form_group += "<label for='" + setting_name + "' class='" + label_class + "'>" + setting.label + "</label>";
}
if (input_type === 'table') {
form_group += makeTable(setting, setting_name, setting_value, isLocked)
} else {
if (input_type === 'select') {
form_group += "<select class='form-control' data-hidden-input='" + setting_name + "'>'"
_.each(setting.options, function(option) {
form_group += "<option value='" + option.value + "'" +
(option.value == setting_value ? 'selected' : '') + ">" + option.label + "</option>"
})
form_group += "</select>"
form_group += "<input type='hidden'" + common_attrs + "value='" + setting_value + "'>"
} else {
if (input_type == 'integer') {
input_type = "text"
}
form_group += "<input type='" + input_type + "'" + common_attrs +
"placeholder='" + (_.has(setting, 'placeholder') ? setting.placeholder : "") +
"' value='" + setting_value + "'" + (isLocked ? " disabled" : "") + "/>"
}
form_group += "<span class='help-block'>" + setting.help + "</span>"
}
}
form_group += "</div>"
return form_group
}
}
$(document).ready(function(){
/*
* Clamped-width.
* Usage:
* <div data-clampedwidth=".myParent">This long content will force clamped width</div>
*
* Author: LV
*/
$('[data-clampedwidth]').each(function () {
var elem = $(this);
var parentPanel = elem.data('clampedwidth');
var resizeFn = function () {
var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth'));
elem.css('width', sideBarNavWidth);
};
resizeFn();
$(window).resize(resizeFn);
})
$('#settings-form').on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){
addTableRow(this);
})
$('#settings-form').on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){
deleteTableRow(this);
})
$('#settings-form').on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){
moveTableRow(this, true);
})
$('#settings-form').on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){
moveTableRow(this, false);
})
$('#settings-form').on('keypress', 'table input', function(e){
if (e.keyCode == 13) {
// capture enter in table input
// if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click
sibling = $(this).parent('td').next();
if (sibling.hasClass(Settings.DATA_COL_CLASS)) {
// set focus to next input
sibling.find('input').focus()
} else if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click()
// set focus to the first input in the new row
$(this).closest('table').find('tr.inputs input:first').focus()
}
}
});
$('#settings-form').on('change', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){
// this input was changed, add the changed data attribute to it
$(this).attr('data-changed', true)
badgeSidebarForDifferences($(this))
})
$('#advanced-toggle-button').click(function(){
Settings.showAdvanced = !Settings.showAdvanced
var advancedSelector = $('.' + Settings.ADVANCED_CLASS)
if (Settings.showAdvanced) {
advancedSelector.show()
$(this).html("Hide advanced")
} else {
advancedSelector.hide()
$(this).html("Show advanced")
}
$(this).blur()
})
$('#settings-form').on('click', '#choose-domain-btn', function(){
chooseFromHighFidelityDomains($(this))
})
$('#settings-form').on('change', 'select', function(){
$("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change()
})
var panelsSource = $('#panels-template').html()
Settings.panelsTemplate = _.template(panelsSource)
var sidebarTemplate = $('#list-group-template').html()
Settings.sidebarTemplate = _.template(sidebarTemplate)
// $('body').scrollspy({ target: '#setup-sidebar'})
reloadSettings()
})
function reloadSettings() {
$.getJSON('/settings.json', function(data){
_.extend(data, viewHelpers)
$('.nav-stacked').html(Settings.sidebarTemplate(data))
$('#panels').html(Settings.panelsTemplate(data))
Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true);
// add tooltip to locked settings
$('label.locked').tooltip({
placement: 'right',
title: 'This setting is in the master config file and cannot be changed'
})
if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) {
// append the domain selection modal, as long as it's not locked
appendDomainSelectionModal()
}
});
}
function appendDomainSelectionModal() {
var metaverseInput = $("[name='metaverse.id']");
var chooseButton = $("<button type='button' id='choose-domain-btn' class='btn btn-primary' style='margin-top:10px'>Choose ID from my domains</button>");
metaverseInput.after(chooseButton);
}
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
$('body').on('click', '.save-button', function(e){
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
console.log(formJSON);
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
// remove focus from the button
$(this).blur()
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
$.ajax('/settings.json', {
data: JSON.stringify(formJSON),
contentType: 'application/json',
type: 'POST'
}).done(function(data){
if (data.status == "success") {
showRestartModal();
} else {
showErrorMessage("Error", SETTINGS_ERROR_MESSAGE)
reloadSettings();
}
}).fail(function(){
showErrorMessage("Error", SETTINGS_ERROR_MESSAGE)
reloadSettings();
});
return false;
});
function makeTable(setting, setting_name, setting_value, isLocked) {
var isArray = !_.has(setting, 'key')
if (!isArray && setting.can_order) {
setting.can_order = false;
}
var html = "<span class='help-block'>" + setting.help + "</span>"
html += "<table class='table table-bordered " + (isLocked ? "locked-table" : "") + "' data-short-name='" + setting.name + "' name='" + setting_name
+ "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>"
// Column names
html += "<tr class='headers'>"
if (setting.numbered === true) {
html += "<td class='number'><strong>#</strong></td>" // Row number
}
if (setting.key) {
html += "<td class='key'><strong>" + setting.key.label + "</strong></td>" // Key
}
_.each(setting.columns, function(col) {
html += "<td class='data'><strong>" + col.label + "</strong></td>" // Data
})
if (!isLocked) {
if (setting.can_order) {
html += "<td class=" + Settings.REORDER_BUTTONS_CLASSES +
"><span class='glyphicon glyphicon-sort'></span></td>";
}
html += "<td class=" + Settings.ADD_DEL_BUTTONS_CLASSES + "></td></tr>"
}
// populate rows in the table from existing values
var row_num = 1
_.each(setting_value, function(row, indexOrName) {
html += "<tr class='" + Settings.DATA_ROW_CLASS + "'" + (isArray ? "" : "name='" + setting_name + "." + indexOrName + "'") + ">"
if (setting.numbered === true) {
html += "<td class='numbered'>" + row_num + "</td>"
}
if (setting.key) {
html += "<td class='key'>" + indexOrName + "</td>"
}
_.each(setting.columns, function(col) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'>"
if (isArray) {
rowIsObject = setting.columns.length > 1
colValue = rowIsObject ? row[col.name] : row
html += colValue
// for arrays we add a hidden input to this td so that values can be posted appropriately
html += "<input type='hidden' name='" + setting_name + "[" + indexOrName + "]"
+ (rowIsObject ? "." + col.name : "") + "' value='" + colValue + "'/>"
} else if (row.hasOwnProperty(col.name)) {
html += row[col.name]
}
html += "</td>"
})
if (!isLocked) {
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES+
"'><span class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></span><span class='" +
Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><span class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></span></td>"
}
html += "</tr>"
row_num++
})
// populate inputs in the table for new values
if (!isLocked) {
html += makeTableInputs(setting)
}
html += "</table>"
return html;
}
function makeTableInputs(setting) {
var html = "<tr class='inputs'>"
if (setting.numbered === true) {
html += "<td class='numbered'></td>"
}
if (setting.key) {
html += "<td class='key' name='" + setting.key.name + "'>\
<input type='text' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
</td>"
}
_.each(setting.columns, function(col) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
})
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><span class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></span></td>"
html += "</tr>"
return html
}
function badgeSidebarForDifferences(changedElement) {
// figure out which group this input is in
var panelParentID = changedElement.closest('.panel').attr('id')
// get a JSON representation of that section
var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]
var initialPanelJSON = Settings.initialValues[panelParentID]
var badgeValue = 0
// badge for any settings we have that are not the same or are not present in initialValues
for (var setting in panelJSON) {
if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") ||
(!_.isEqual(panelJSON[setting], initialPanelJSON[setting])
&& (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) {
badgeValue += 1
}
}
// update the list-group-item badge to have the new value
if (badgeValue == 0) {
badgeValue = ""
}
$("a[href='#" + panelParentID + "'] .badge").html(badgeValue);
}
function addTableRow(add_glyphicon) {
var row = $(add_glyphicon).closest('tr')
var table = row.parents('table')
var isArray = table.data('setting-type') === 'array'
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS)
if (!isArray) {
// Check key spaces
var key = row.children(".key").children("input").val()
if (key.indexOf(' ') !== -1) {
showErrorMessage("Error", "Key contains spaces")
return
}
// Check keys with the same name
var equals = false;
_.each(columns.children(".key"), function(element) {
if ($(element).text() === key) {
equals = true
return
}
})
if (equals) {
showErrorMessage("Error", "Two keys cannot be identical")
return
}
}
// Check empty fields
var empty = false;
_.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) {
if ($(element).val().length === 0) {
empty = true
return
}
})
if (empty) {
showErrorMessage("Error", "Empty field(s)")
return
}
var input_clone = row.clone()
// Change input row to data row
var table = row.parents("table")
var setting_name = table.attr("name")
var full_name = setting_name + "." + key
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS)
row.removeClass("inputs")
_.each(row.children(), function(element) {
if ($(element).hasClass("numbered")) {
// Index row
var numbers = columns.children(".numbered")
if (numbers.length > 0) {
$(element).html(parseInt(numbers.last().text()) + 1)
} else {
$(element).html(1)
}
} else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
$(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><span class='" + Settings.MOVE_UP_SPAN_CLASSES +
"'></span><span class='" + Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
} else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
// Change buttons
var span = $(element).children("span")
span.removeClass(Settings.ADD_ROW_SPAN_CLASSES)
span.addClass(Settings.DEL_ROW_SPAN_CLASSES)
} else if ($(element).hasClass("key")) {
var input = $(element).children("input")
$(element).html(input.val())
input.remove()
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
// Hide inputs
var input = $(element).children("input")
input.attr("type", "hidden")
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
var key = $(element).attr('name')
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", full_name + "." + $(element).attr("name"))
}
input.attr("data-changed", "true")
$(element).append(input.val())
} else {
console.log("Unknown table element")
}
})
input_clone.find('input').each(function(){
$(this).val($(this).attr('data-default'));
});
if (isArray) {
updateDataChangedForSiblingRows(row, true)
// the addition of any table row should remove the empty-array-row
row.siblings('.empty-array-row').remove()
}
badgeSidebarForDifferences($(table))
row.parent().append(input_clone)
}
function deleteTableRow(delete_glyphicon) {
var row = $(delete_glyphicon).closest('tr')
var table = $(row).closest('table')
var isArray = table.data('setting-type') === 'array'
row.empty();
if (!isArray) {
row.html("<input type='hidden' class='form-control' name='"
+ row.attr('name') + "' data-changed='true' value=''>");
} else {
if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) {
updateDataChangedForSiblingRows(row)
// this isn't the last row - we can just remove it
row.remove()
} else {
// this is the last row, we can't remove it completely since we need to post an empty array
row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS)
row.addClass('empty-array-row')
row.html("<input type='hidden' class='form-control' name='" + table.attr("name").replace('[]', '')
+ "' data-changed='true' value=''>");
}
}
// we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated
badgeSidebarForDifferences($(table))
}
function moveTableRow(move_glyphicon, move_up) {
var row = $(move_glyphicon).closest('tr')
var table = $(row).closest('table')
var isArray = table.data('setting-type') === 'array'
if (!isArray) {
return;
}
if (move_up) {
var prev_row = row.prev()
if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) {
prev_row.before(row)
}
} else {
var next_row = row.next()
if (next_row.hasClass(Settings.DATA_ROW_CLASS)) {
next_row.after(row)
}
}
// we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated
badgeSidebarForDifferences($(table))
}
function updateDataChangedForSiblingRows(row, forceTrue) {
// anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true
// unless it matches the inital set of values
if (!forceTrue) {
// figure out which group this row is in
var panelParentID = row.closest('.panel').attr('id')
// get the short name for the setting from the table
var tableShortName = row.closest('table').data('short-name')
// get a JSON representation of that section
var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName]
var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName]
// if they are equal, we don't need data-changed
isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON)
} else {
isTrue = true
}
row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){
var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input')
if (isTrue) {
hiddenInput.attr('data-changed', isTrue)
} else {
hiddenInput.removeAttr('data-changed')
}
})
}
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
function cleanupFormValues(node) {
if (node.type && node.type === 'checkbox') {
return { name: node.name, value: node.checked ? true : false };
} else {
return false;
}
}
function showErrorMessage(title, message) {
swal(title, message)
}
function chooseFromHighFidelityDomains(clickedButton) {
// setup the modal to help user pick their domain
if (Settings.initialValues.metaverse.access_token) {
// add a spinner to the choose button
clickedButton.html("Loading domains...")
clickedButton.attr('disabled', 'disabled')
// get a list of user domains from data-web
data_web_domains_url = "https://metaverse.highfidelity.com/api/v1/domains?access_token="
$.getJSON(data_web_domains_url + Settings.initialValues.metaverse.access_token, function(data){
modal_buttons = {
cancel: {
label: 'Cancel',
className: 'btn-default'
}
}
if (data.data.domains.length) {
// setup a select box for the returned domains
modal_body = "<p>Choose the High Fidelity domain you want this domain-server to represent.<br/>This will set your domain ID on the settings page.</p>"
domain_select = $("<select id='domain-name-select' class='form-control'></select>")
_.each(data.data.domains, function(domain){
domain_select.append("<option value='" + domain.id + "'>(" + domain.id + ")" + (domain.names.length > 0 ? " [" + domain.names + "]" : "") + "</option>");
})
modal_body += "<label for='domain-name-select'>Domains</label>" + domain_select[0].outerHTML
modal_buttons["success"] = {
label: 'Choose domain',
callback: function() {
domainID = $('#domain-name-select').val()
// set the domain ID on the form
$("[name='metaverse.id']").val(domainID).change();
}
}
} else {
modal_buttons["success"] = {
label: 'Create new domain',
callback: function() {
window.open("https://metaverse.highfidelity.com/user/domains", '_blank');
}
}
modal_body = "<p>You do not have any domains in your High Fidelity account." +
"<br/><br/>Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.</p>"
}
bootbox.dialog({
title: "Choose matching domain",
message: modal_body,
buttons: modal_buttons
})
// remove the spinner from the choose button
clickedButton.html("Choose from my domains")
clickedButton.removeAttr('disabled')
})
} else {
bootbox.alert({
message: "You must have an access token to query your High Fidelity domains.<br><br>" +
"Please follow the instructions on the settings page to add an access token.",
title: "Access token required"
})
}
}

View file

@ -1,33 +0,0 @@
$(document).ready(function(){
/*
* Clamped-width.
* Usage:
* <div data-clampedwidth=".myParent">This long content will force clamped width</div>
*
* Author: LV
*/
$('[data-clampedwidth]').each(function () {
var elem = $(this);
var parentPanel = elem.data('clampedwidth');
var resizeFn = function () {
var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth'));
elem.css('width', sideBarNavWidth);
};
resizeFn();
$(window).resize(resizeFn);
});
var listSource = $('#list-group-template').html();
var listTemplate = _.template(listSource);
reloadSettings();
function reloadSettings() {
$.getJSON('describe-setup.json', function(data){
$('.list-group').html(listTemplate(data));
});
}
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
!function(){function t(t,r,e,n){return(0!==n?".":"")+r.replace(/\./g,"«dot»")}function r(r){"use strict";if("object"==typeof r&&"Array"===r.constructor.name)return Array.prototype.slice.call(r);if("string"==typeof r){r=r.replace(/\.\./g,"«dot»"),r=r.replace(/\['(([^']|\\')*)'\]/g,t),r=r.replace(/\["(([^"]|\\")*)"\]/g,t),r=r.replace(/([$A-Za-z_][0-9A-Za-z_$]*)\(\)/,"$1");var e=r.split(".");return e=s(e).map(function(t){return t.replace(/«dot»/g,".")})}throw new Error("keypath must be an array or a string")}function e(t){"use strict";return t=null===t?"":String(t),t.charAt(0).toUpperCase()+t.slice(1)}function n(t,r){"use strict";var e=y[r];return e?e(t):void 0}function u(t,r,e){"use strict";if(!(t instanceof Array))return void 0;var n=l[r];return n?n(t,e):void 0}function i(t,r){"use strict";if(""===r)return t;if(0===r.indexOf("@"))return n(t,r);var u=t["get"+e(r)];return void 0===u&&(u=t["is"+e(r)]),void 0===u&&(u=t[r]),"function"==typeof u&&(u=u.call(t)),u}function o(t,r){"use strict";return""===r?!0:"function"==typeof t["get"+e(r)]||"function"==typeof t["is"+e(r)]||s.has(t,r)}function a(t,r,n){"use strict";var i=t["set"+e(r)];return i?i.call(t,n):0===r.indexOf("@")?u(t,r,n):(t[r]=n,n)}function f(t,r){"use strict";var e,n,u=!0;for(e in r)if(r.hasOwnProperty(e)&&(n=r[e],s(t).valueForKeyPath(e)!==n)){u=!1;break}return u}var s;"undefined"!=typeof _&&(s=_);var c,y,l,p,d,h=this;if(c="undefined"!=typeof module&&"undefined"!=typeof require,"undefined"==typeof s&&(s=c?require("underscore"):h.underscore),"undefined"==typeof s)throw new Error("underscore.js is not found!");y={"@first":function(t){return s.first(t)},"@last":function(t){return s.last(t)},"@max":function(t){return s.max(t)},"@min":function(t){return s.min(t)},"@size":function(t){return s.size(t)}},l={"@first":function(t,r){return t[0]=r,r},"@last":function(t,r){return t[Math.max(t.length-1,0)]=r,r}},p={valueForKeyPath:function(t,e,n){"use strict";var u,o,a,f=t;if(null===t||void 0===t)return n;for(u=r(e),o=u.shift(),a=void 0;null!==f&&void 0!==f&&"string"==typeof o;)a=f,f=i(f,o),"function"==typeof f&&(f=f.call(a)),o=u.shift();return null===f||void 0===f?n:f},setValueForKeyPath:function(t,e,n){"use strict";var u,i,o=r(e);return u=o.pop(),i=s(t).valueForKeyPath(o.join(".")),null!==i&&void 0!==i?a(i,u,n):void 0},pluckByKeyPath:function(t,r){"use strict";var e,n,u=[];for(e=0;e<t.length;e+=1)n=t[e],u[e]=s(n).valueForKeyPath(r);return u},whereByKeyPath:function(t,r){"use strict";var e,n,u=[];for(e=0;e<t.length;e+=1)n=t[e],f(n,r)&&u.push(n);return u},findWhereByKeyPath:function(t,r){"use strict";var e,n;for(e=0;e<t.length;e+=1)if(n=t[e],f(n,r))return n;return null},sortByKeyPath:function(t,r){"use strict";return"string"!=typeof r?s.sortBy(t,r):s.sortBy(t,function(t){return s.valueForKeyPath(t,r)})},groupByKeyPath:function(t,r){"use strict";return"string"!=typeof r?s.groupBy(t,r):s.groupBy(t,function(t){return s.valueForKeyPath(t,r)})},indexByKeyPath:function(t,r){"use strict";return"string"!=typeof r?s.indexBy(t,r):s.indexBy(t,function(t){return s.valueForKeyPath(t,r)})},countByKeyPath:function(t,r){"use strict";return"string"!=typeof r?s.countBy(t,r):s.countBy(t,function(t){return s.valueForKeyPath(t,r)})},hasKeyPath:function(t,e){"use strict";var n,u,i;return null===t||void 0===t?!1:(n=r(e),u=n.pop(),i=this.valueForKeyPath(t,n),null!==i&&void 0!==i?o(i,u):!1)}},d={getValueForKeyPath:p.valueForKeyPath},s.mixin(p),s.mixin(d),c&&(module.exports=s,module.exports.__debug__={toSegments:r})}();

File diff suppressed because one or more lines are too long

View file

@ -1,68 +1,82 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col" class="hidden-xs" data-spy="affix" data-offset-top="55">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<li>
<a href="#<%-group.name %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% }); %>
</script>
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" hidden=true class="btn btn-info">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
<div class="row">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% split_settings = _.partition(group.settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<div class="panel panel-default <%- (isAdvanced) ? 'advanced-setting' : '' %>" id="<%- group.name %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<%= getFormGroup(group.name, setting, values, false,
(_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %>
<% }); %>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<%= getFormGroup(group.name, setting, values, true,
<div class="row">
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" class="hidden-xs" data-spy="affix" data-offset-top="55" data-clampedwidth="#setup-sidebar-col">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% panelID = group.name ? group.name : group.label %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% }); %>
</script>
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" hidden=true class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<div id="xs-advanced-container" class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button id="advanced-toggle-button-xs" class="btn btn-info advanced-toggle">Show advanced</button>
</div>
<div class="col-xs-12">
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% split_settings = _.partition(group.settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div class="panel panel-default<%- (isAdvanced) ? ' advanced-setting' : '' %><%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false,
(_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %>
<% }); %>
<% }%>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true,
(_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %>
<% }); %>
<% }%>
</div>
</div>
</div>
<% }); %>
</script>
<div id="panels"></div>
</form>
<% }); %>
</script>
<div id="panels"></div>
</form>
</div>
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save and restart</button>
</div>
@ -83,8 +97,10 @@
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sweet-alert.min.js'></script>
<script src='/js/settings.js'></script>
<script src='/js/form2js.min.js'></script>
<!--#include virtual="page-end.html"-->
<script src='js/bootstrap-switch.min.js'></script>
<script src='js/sweetalert.min.js'></script>
<script src='js/settings.js'></script>
<script src='js/form2js.min.js'></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,115 @@
.jh-root, .jh-type-object, .jh-type-array, .jh-key, .jh-value, .jh-root tr {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
font-weight: bold;
}
.jh-key, .jh-value {
margin: 0;
padding: 0.2em;
font-weight: bold;
}
.jh-value {
border-left: 1px solid #ddd;
}
.jh-type-number {
text-align: center;
color: #5286BC;
cursor: pointer;
}
.jh-type-bool-true {
text-align: center;
color: #5A811C;
}
.jh-type-bool-false {
text-align: center;
color: #D45317;
}
.jh-type-bool-image {
width: 20px;
height: 20px;
margin-right: 5px;
vertical-align: bottom;
}
.jh-type-string {
font-style: italic;
color: #6E6E6E;
}
.jh-array-key {
font-style: italic;
font-size: small;
text-align: center;
}
.jh-object-key, .jh-array-key {
color: #444;
vertical-align: top;
}
.jh-type-object tr:nth-child(odd), .jh-type-array tr:nth-child(odd) {
background-color: #f5f5f5;
}
.jh-type-object tr:nth-child(even), .jh-type-array tr:nth-child(even) {
background-color: #fff;
}
.jh-type-object, .jh-type-array {
width: 100%;
border-collapse: collapse;
}
.jh-root {
border: 1px solid #ccc;
margin: 0.2em;
}
th.jh-key {
text-align: left;
}
th.jh-key.stale {
color: red;
}
.jh-type-object tr, .jh-type-array tr {
border: 1px solid #ddd;
border-bottom: none;
}
.jh-type-object tr:last-child, .jh-type-array tr:last-child {
border-bottom: 1px solid #ddd;
}
.jh-type-object tr:hover, .jh-type-array tr:hover {
border: 1px solid #F99927;
}
.jh-empty {
font-style: italic;
color: #999;
font-size: small;
}
.jh-a {
text-decoration: none;
}
.jh-a:hover {
text-decoration: underline;
}
.jh-a span.jh-type-string {
text-decoration: none;
color : #268ddd;
font-style: normal;
}

View file

@ -1,6 +1,15 @@
<!--#include virtual="header.html"-->
<div id="stats-lead" class="table-lead"><h3>Stats</h3><div class="lead-line"></div></div>
<table id="stats-table" class="table"><tbody></tbody></table>
<div class="col-xs-12 table-lead" id="stats-lead"><h3>Stats</h3><div class="lead-line"></div></div>
<div class="col-xs-12">
<p class="help"><em>Click on any of the numerical values in the tables below to view a line graph of the incoming values.</em></p>
</div>
<div class="col-xs-12" id="stats-container"></div>
<!--#include virtual="footer.html"-->
<script src='/js/query-string.js'></script>
<script src='js/stats.js'></script>
<!--#include virtual="page-end.html"-->
<script src='js/json.human.js'></script>
<script src='js/highcharts-custom.js'></script>
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -0,0 +1,247 @@
/*
Highcharts 4.0.4 JS v/Highstock 2.0.4 (2014-09-02)
(c) 2009-2014 Torstein Honsi
License: www.highcharts.com/license
*/
(function(){function v(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function G(){var a,b=arguments,c,d={},e=function(a,b){var c,d;"object"!==typeof a&&(a={});for(d in b)b.hasOwnProperty(d)&&((c=b[d])&&"object"===typeof c&&"[object Array]"!==Object.prototype.toString.call(c)&&"renderTo"!==d&&"number"!==typeof c.nodeType?a[d]=e(a[d]||{},c):a[d]=b[d]);return a};!0===b[0]&&(d=b[1],b=Array.prototype.slice.call(b,2));c=b.length;for(a=0;a<c;a++)d=e(d,b[a]);return d}function E(a,b){return parseInt(a,
b||10)}function pa(a){return"string"===typeof a}function da(a){return a&&"object"===typeof a}function ta(a){return"[object Array]"===Object.prototype.toString.call(a)}function ga(a){return"number"===typeof a}function ab(a){return R.log(a)/R.LN10}function ia(a){return R.pow(10,a)}function qa(a,b){for(var c=a.length;c--;)if(a[c]===b){a.splice(c,1);break}}function w(a){return a!==u&&null!==a}function N(a,b,c){var d,e;if(pa(b))w(c)?a.setAttribute(b,c):a&&a.getAttribute&&(e=a.getAttribute(b));else if(w(b)&&
da(b))for(d in b)a.setAttribute(d,b[d]);return e}function ea(a){return ta(a)?a:[a]}function n(){var a=arguments,b,c,d=a.length;for(b=0;b<d;b++)if(c=a[b],c!==u&&null!==c)return c}function J(a,b){ra&&!Z&&b&&b.opacity!==u&&(b.filter="alpha(opacity="+100*b.opacity+")");v(a.style,b)}function ja(a,b,c,d,e){a=y.createElement(a);b&&v(a,b);e&&J(a,{padding:0,border:"none",margin:0});c&&J(a,c);d&&d.appendChild(a);return a}function nb(a,b){var c=function(){return u};c.prototype=new a;v(c.prototype,b);return c}
function ka(a,b,c,d){var e=U.numberFormat,f=M.lang,g=+a||0,h=-1===b?(g.toString().split(".")[1]||"").length:isNaN(b=X(b))?2:b,k=void 0===c?f.decimalPoint:c,f=void 0===d?f.thousandsSep:d,l=0>g?"-":"",m=String(E(g=X(g).toFixed(h))),q=3<m.length?m.length%3:0;return e!==ka?e(a,b,c,d):l+(q?m.substr(0,q)+f:"")+m.substr(q).replace(/(\d{3})(?=\d)/g,"$1"+f)+(h?k+X(g-m).toFixed(h).slice(2):"")}function sa(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function bb(a,b,c){var d=a[b];a[b]=function(){var a=
Array.prototype.slice.call(arguments);a.unshift(d);return c.apply(this,a)}}function ua(a,b){for(var c="{",d=!1,e,f,g,h,k,l=[];-1!==(c=a.indexOf(c));){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");k=g.length;e=b;for(h=0;h<k;h++)e=e[g[h]];f.length&&(f=f.join(":"),g=/\.([0-9])/,h=M.lang,k=void 0,/f$/.test(f)?(k=(k=f.match(g))?k[1]:-1,null!==e&&(e=ka(e,k,h.decimalPoint,-1<f.indexOf(",")?h.thousandsSep:""))):e=Ka(f,e))}l.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}l.push(a);return l.join("")}
function ob(a,b,c,d){var e;c=n(c,1);e=a/c;b||(b=[1,2,2.5,5,10],!1===d&&(1===c?b=[1,2,5,10]:.1>=c&&(b=[1/c])));for(d=0;d<b.length&&!(a=b[d],e<=(b[d]+(b[d+1]||b[d]))/2);d++);return a*c}function cb(a,b){var c=a.length,d,e;for(e=0;e<c;e++)a[e].ss_i=e;a.sort(function(a,c){d=b(a,c);return 0===d?a.ss_i-c.ss_i:d});for(e=0;e<c;e++)delete a[e].ss_i}function La(a){for(var b=a.length,c=a[0];b--;)a[b]<c&&(c=a[b]);return c}function va(a){for(var b=a.length,c=a[0];b--;)a[b]>c&&(c=a[b]);return c}function Ma(a,b){for(var c in a)a[c]&&
a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Na(a){Oa||(Oa=ja("div"));a&&Oa.appendChild(a);Oa.innerHTML=""}function ha(a){return parseFloat(a.toPrecision(14))}function pb(){var a=M.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";la=M.global.Date||window.Date;wa=6E4*(a&&M.global.timezoneOffset||0);Pa=a?la.UTC:function(a,b,c,g,h,k){return(new la(a,b,n(c,1),n(g,0),n(h,0),n(k,0))).getTime()};db=b+"Minutes";eb=b+"Hours";fb=b+"Day";Da=b+"Date";Qa=b+"Month";Ra=b+"FullYear";qb=c+"Minutes";
rb=c+"Hours";gb=c+"Date";sb=c+"Month";tb=c+"FullYear"}function O(){}function xa(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;c||d||this.addLabel()}function ma(){this.init.apply(this,arguments)}function Ea(){this.init.apply(this,arguments)}var u,y=document,I=window,R=Math,A=R.round,K=R.floor,ya=R.ceil,x=R.max,T=R.min,X=R.abs,Fa=R.cos,Sa=R.sin,ub=R.PI,vb=2*ub/360,na=navigator.userAgent,Bb=I.opera,ra=/msie/i.test(na)&&!Bb,Cb=/AppleWebKit/.test(na),Ta=/Firefox/.test(na),wb=/(Mobile|Android|Windows Phone)/.test(na),
Z=!!y.createElementNS&&!!y.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,Db=Ta&&4>parseInt(na.split("Firefox/")[1],10),ca=!Z&&!ra&&!!y.createElement("canvas").getContext,Ga,xb={},hb=0,Oa,M,Ka,S,ib,z,aa,Eb=function(){return u},Y=[],Ha=0,Fb=/^[0-9]+$/,la,Pa,wa,db,eb,fb,Da,Qa,Ra,qb,rb,gb,sb,tb,P={},U;I.Highcharts?aa(16,!0):U=I.Highcharts={};Ka=function(a,b,c){if(!w(b)||isNaN(b))return"Invalid date";a=n(a,"%Y-%m-%d %H:%M:%S");var d=new la(b-wa),e,f=d[eb](),g=d[fb](),h=d[Da](),k=d[Qa](),
l=d[Ra](),m=M.lang,q=m.weekdays,d=v({a:q[g].substr(0,3),A:q[g],d:sa(h),e:h,b:m.shortMonths[k],B:m.months[k],m:sa(k+1),y:l.toString().substr(2,2),Y:l,H:sa(f),I:sa(f%12||12),l:f%12||12,M:sa(d[db]()),p:12>f?"AM":"PM",P:12>f?"am":"pm",S:sa(d.getSeconds()),L:sa(A(b%1E3),3)},U.dateFormats);for(e in d)for(;-1!==a.indexOf("%"+e);)a=a.replace("%"+e,"function"===typeof d[e]?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};aa=function(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+
a;if(b)throw c;I.console&&console.log(c)};z={millisecond:1,second:1E3,minute:6E4,hour:36E5,day:864E5,week:6048E5,month:26784E5,year:31556952E3};ib={init:function(a,b,c){b=b||"";var d=a.shift,e=-1<b.indexOf("C"),f=e?7:3,g;b=b.split(" ");c=[].concat(c);var h,k,l=function(a){for(g=a.length;g--;)"M"===a[g]&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(l(b),l(c));a.isArea&&(h=b.splice(b.length-6,6),k=c.splice(c.length-6,6));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=[].concat(c).splice(0,f).concat(c);
a.shift=0;if(b.length)for(a=c.length;b.length<a;)d=[].concat(b).splice(b.length-f,f),e&&(d[f-6]=d[f-2],d[f-5]=d[f-1]),b=b.concat(d);h&&(b=b.concat(h),c=c.concat(k));return[b,c]},step:function(a,b,c,d){var e=[],f=a.length;if(1===c)e=d;else if(f===b.length&&1>c)for(;f--;)d=parseFloat(a[f]),e[f]=isNaN(d)?a[f]:c*parseFloat(b[f]-d)+d;else e=b;return e}};(function(a){I.HighchartsAdapter=I.HighchartsAdapter||a&&{init:function(b){var c=a.fx;a.extend(a.easing,{easeOutQuad:function(a,b,c,g,h){return-g*(b/=
h)*(b-2)+c}});a.each(["cur","_default","width","height","opacity"],function(b,e){var f=c.step,g;"cur"===e?f=c.prototype:"_default"===e&&a.Tween&&(f=a.Tween.propHooks[e],e="set");(g=f[e])&&(f[e]=function(a){var c;a=b?a:this;if("align"!==a.prop)return c=a.elem,c.attr?c.attr(a.prop,"cur"===e?u:a.now):g.apply(this,arguments)})});bb(a.cssHooks.opacity,"get",function(a,b,c){return b.attr?b.opacity||0:a.call(this,b,c)});this.addAnimSetter("d",function(a){var c=a.elem,f;a.started||(f=b.init(c,c.d,c.toD),
a.start=f[0],a.end=f[1],a.started=!0);c.attr("d",b.step(a.start,a.end,a.pos,c.toD))});this.each=Array.prototype.forEach?function(a,b){return Array.prototype.forEach.call(a,b)}:function(a,b){var c,g=a.length;for(c=0;c<g;c++)if(!1===b.call(a[c],a[c],c,a))return c};a.fn.highcharts=function(){var a="Chart",b=arguments,c,g;this[0]&&(pa(b[0])&&(a=b[0],b=Array.prototype.slice.call(b,1)),c=b[0],c!==u&&(c.chart=c.chart||{},c.chart.renderTo=this[0],new U[a](c,b[1]),g=this),c===u&&(g=Y[N(this[0],"data-highcharts-chart")]));
return g}},addAnimSetter:function(b,c){a.Tween?a.Tween.propHooks[b]={set:c}:a.fx.step[b]=c},getScript:a.getScript,inArray:a.inArray,adapterRun:function(b,c){return a(b)[c]()},grep:a.grep,map:function(a,c){for(var d=[],e=0,f=a.length;e<f;e++)d[e]=c.call(a[e],a[e],e,a);return d},offset:function(b){return a(b).offset()},addEvent:function(b,c,d){a(b).bind(c,d)},removeEvent:function(b,c,d){var e=y.removeEventListener?"removeEventListener":"detachEvent";y[e]&&b&&!b[e]&&(b[e]=function(){});a(b).unbind(c,
d)},fireEvent:function(b,c,d,e){var f=a.Event(c),g="detached"+c,h;!ra&&d&&(delete d.layerX,delete d.layerY,delete d.returnValue);v(f,d);b[c]&&(b[g]=b[c],b[c]=null);a.each(["preventDefault","stopPropagation"],function(a,b){var c=f[b];f[b]=function(){try{c.call(f)}catch(a){"preventDefault"===b&&(h=!0)}}});a(b).trigger(f);b[g]&&(b[c]=b[g],b[g]=null);!e||f.isDefaultPrevented()||h||e(f)},washMouseEvent:function(a){var c=a.originalEvent||a;c.pageX===u&&(c.pageX=a.pageX,c.pageY=a.pageY);return c},animate:function(b,
c,d){var e=a(b);b.style||(b.style={});c.d&&(b.toD=c.d,c.d=1);e.stop();c.opacity!==u&&b.attr&&(c.opacity+="px");b.hasAnim=1;e.animate(c,d)},stop:function(b){b.hasAnim&&a(b).stop()}}})(I.jQuery);var Ua=I.HighchartsAdapter,ba=Ua||{};Ua&&Ua.init.call(Ua,ib);var Va=ba.adapterRun,Ia=ba.inArray,t=ba.each,jb=ba.grep,Gb=ba.offset,kb=ba.map,F=ba.addEvent,L=ba.removeEvent,B=ba.fireEvent,Hb=ba.washMouseEvent,Wa=ba.animate,Xa=ba.stop,lb={enabled:!0,x:0,y:15,style:{color:"#606060",cursor:"default",fontSize:"11px"}};
M={colors:"#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #8085e8 #8d4653 #91e8e1".split(" "),symbols:["circle","diamond","square","triangle","triangle-down"],lang:{loading:"Loading...",months:"January February March April May June July August September October November December".split(" "),shortMonths:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),weekdays:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),decimalPoint:".",numericSymbols:"kMGTPE".split(""),
resetZoom:"Reset zoom",resetZoomTitle:"Reset zoom level 1:1",thousandsSep:","},global:{useUTC:!0,canvasToolsURL:"http://code.highcharts.com@product.cdnpath@//Highstock 2.0.4/modules/canvas-tools.js",VMLRadialGradientURL:"http://code.highcharts.com@product.cdnpath@//Highstock 2.0.4/gfx/vml-radial-gradient.png"},chart:{borderColor:"#4572A7",borderRadius:0,defaultSeriesType:"line",ignoreHiddenSeries:!0,spacing:[10,10,15,10],backgroundColor:"#FFFFFF",plotBorderColor:"#C0C0C0",resetZoomButton:{theme:{zIndex:20},
position:{align:"right",x:-10,y:10}}},title:{text:"Chart title",align:"center",margin:15,style:{color:"#333333",fontSize:"18px"}},subtitle:{text:"",align:"center",style:{color:"#555555"}},plotOptions:{line:{allowPointSelect:!1,showCheckbox:!1,animation:{duration:1E3},events:{},lineWidth:2,marker:{lineWidth:0,radius:4,lineColor:"#FFFFFF",states:{hover:{enabled:!0,lineWidthPlus:1,radiusPlus:2},select:{fillColor:"#FFFFFF",lineColor:"#000000",lineWidth:2}}},point:{events:{}},dataLabels:G(lb,{align:"center",
enabled:!1,formatter:function(){return null===this.y?"":ka(this.y,-1)},verticalAlign:"bottom",y:0}),cropThreshold:300,pointRange:0,states:{hover:{lineWidthPlus:1,marker:{},halo:{size:10,opacity:.25}},select:{marker:{}}},stickyTracking:!0,turboThreshold:1E3}},labels:{style:{position:"absolute",color:"#3E576F"}},legend:{enabled:!0,align:"center",layout:"horizontal",labelFormatter:function(){return this.name},borderColor:"#909090",borderRadius:0,navigation:{activeColor:"#274b6d",inactiveColor:"#CCC"},
shadow:!1,itemStyle:{color:"#333333",fontSize:"12px",fontWeight:"bold"},itemHoverStyle:{color:"#000"},itemHiddenStyle:{color:"#CCC"},itemCheckboxStyle:{position:"absolute",width:"13px",height:"13px"},symbolPadding:5,verticalAlign:"bottom",x:0,y:0,title:{style:{fontWeight:"bold"}}},loading:{labelStyle:{fontWeight:"bold",position:"relative",top:"45%"},style:{position:"absolute",backgroundColor:"white",opacity:.5,textAlign:"center"}},tooltip:{enabled:!0,animation:Z,backgroundColor:"rgba(249, 249, 249, .85)",
borderWidth:1,borderRadius:3,dateTimeLabelFormats:{millisecond:"%A, %b %e, %H:%M:%S.%L",second:"%A, %b %e, %H:%M:%S",minute:"%A, %b %e, %H:%M",hour:"%A, %b %e, %H:%M",day:"%A, %b %e, %Y",week:"Week from %A, %b %e, %Y",month:"%B %Y",year:"%Y"},headerFormat:'<span style="font-size: 10px">{point.key}</span><br/>',pointFormat:'<span style="color:{series.color}">\u25cf</span> {series.name}: <b>{point.y}</b><br/>',shadow:!0,snap:wb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",
whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var Ya=M.plotOptions;pb();var Ib=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,Jb=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,Kb=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,za=function(a){var b=[],c,d;(function(a){a&&
a.stops?d=kb(a.stops,function(a){return za(a[1])}):(c=Ib.exec(a))?b=[E(c[1]),E(c[2]),E(c[3]),parseFloat(c[4],10)]:(c=Jb.exec(a))?b=[E(c[1],16),E(c[2],16),E(c[3],16),1]:(c=Kb.exec(a))&&(b=[E(c[1]),E(c[2]),E(c[3]),1])})(a);return{get:function(c){var f;d?(f=G(a),f.stops=[].concat(f.stops),t(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?"rgb"===c?"rgb("+b[0]+","+b[1]+","+b[2]+")":"a"===c?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)t(d,function(b){b.brighten(a)});
else if(ga(a)&&0!==a){var c;for(c=0;3>c;c++)b[c]+=E(255*a),0>b[c]&&(b[c]=0),255<b[c]&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}};O.prototype={opacity:1,textProps:"fontSize fontWeight fontFamily color lineHeight width textDecoration textShadow HcTextStroke".split(" "),init:function(a,b){this.element="span"===b?ja(b):y.createElementNS("http://www.w3.org/2000/svg",b);this.renderer=a},animate:function(a,b,c){b=n(b,S,!0);Xa(this);b?(b=G(b,{}),c&&(b.complete=c),Wa(this,
a,b)):(this.attr(a),c&&c());return this},colorGradient:function(a,b,c){var d=this.renderer,e,f,g,h,k,l,m,q,r,p,n=[];a.linearGradient?f="linearGradient":a.radialGradient&&(f="radialGradient");if(f){g=a[f];h=d.gradients;l=a.stops;r=c.radialReference;ta(g)&&(a[f]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"});"radialGradient"===f&&r&&!w(g.gradientUnits)&&(g=G(g,{cx:r[0]-r[2]/2+g.cx*r[2],cy:r[1]-r[2]/2+g.cy*r[2],r:g.r*r[2],gradientUnits:"userSpaceOnUse"}));for(p in g)"id"!==p&&n.push(p,
g[p]);for(p in l)n.push(l[p]);n=n.join(",");h[n]?a=h[n].attr("id"):(g.id=a="highcharts-"+hb++,h[n]=k=d.createElement(f).attr(g).add(d.defs),k.stops=[],t(l,function(a){0===a[1].indexOf("rgba")?(e=za(a[1]),m=e.get("rgb"),q=e.get("a")):(m=a[1],q=1);a=d.createElement("stop").attr({offset:a[0],"stop-color":m,"stop-opacity":q}).add(k);k.stops.push(a)}));c.setAttribute(b,"url("+d.url+"#"+a+")")}},attr:function(a,b){var c,d,e=this.element,f,g=this,h;"string"===typeof a&&b!==u&&(c=a,a={},a[c]=b);if("string"===
typeof a)g=(this[a+"Getter"]||this._defaultGetter).call(this,a,e);else{for(c in a)d=a[c],h=!1,this.symbolName&&/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(c)&&(f||(this.symbolAttr(a),f=!0),h=!0),!this.rotation||"x"!==c&&"y"!==c||(this.doTransform=!0),h||(this[c+"Setter"]||this._defaultSetter).call(this,d,c,e),this.shadows&&/^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(c)&&this.updateShadows(c,d);this.doTransform&&(this.updateTransform(),this.doTransform=!1)}return g},
updateShadows:function(a,b){for(var c=this.shadows,d=c.length;d--;)c[d].setAttribute(a,"height"===a?x(b-(c[d].cutHeight||0),0):"d"===a?this.d:b)},addClass:function(a){var b=this.element,c=N(b,"class")||"";-1===c.indexOf(a)&&N(b,"class",c+" "+a);return this},symbolAttr:function(a){var b=this;t("x y r start end width height innerR anchorX anchorY".split(" "),function(c){b[c]=n(a[c],b[c])});b.attr({d:b.renderer.symbols[b.symbolName](b.x,b.y,b.width,b.height,b)})},clip:function(a){return this.attr("clip-path",
a?"url("+this.renderer.url+"#"+a.id+")":"none")},crisp:function(a){var b,c={},d,e=a.strokeWidth||this.strokeWidth||0;d=A(e)%2/2;a.x=K(a.x||this.x||0)+d;a.y=K(a.y||this.y||0)+d;a.width=K((a.width||this.width||0)-2*d);a.height=K((a.height||this.height||0)-2*d);a.strokeWidth=e;for(b in a)this[b]!==a[b]&&(this[b]=c[b]=a[b]);return c},css:function(a){var b=this.styles,c={},d=this.element,e,f,g="";e=!b;a&&a.color&&(a.fill=a.color);if(b)for(f in a)a[f]!==b[f]&&(c[f]=a[f],e=!0);if(e){e=this.textWidth=a&&
a.width&&"text"===d.nodeName.toLowerCase()&&E(a.width);b&&(a=v(b,c));this.styles=a;e&&(ca||!Z&&this.renderer.forExport)&&delete a.width;if(ra&&!Z)J(this.element,a);else{b=function(a,b){return"-"+b.toLowerCase()};for(f in a)g+=f.replace(/([A-Z])/g,b)+":"+a[f]+";";N(d,"style",g)}e&&this.added&&this.renderer.buildText(this)}return this},on:function(a,b){var c=this,d=c.element;Ga&&"click"===a?(d.ontouchstart=function(a){c.touchEventFired=la.now();a.preventDefault();b.call(d,a)},d.onclick=function(a){(-1===
na.indexOf("Android")||1100<la.now()-(c.touchEventFired||0))&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){this.element.radialReference=a;return this},translate:function(a,b){return this.attr({translateX:a,translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation,g=this.element;e&&(a+=this.attr("width"),b+=this.attr("height"));
a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(g.getAttribute("x")||0)+" "+(g.getAttribute("y")||0)+")");(w(c)||w(d))&&a.push("scale("+n(c,1)+" "+n(d,1)+")");a.length&&g.setAttribute("transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||pa(c))this.alignTo=d=c||"renderer",qa(f,
this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=n(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if("right"===d||"center"===d)f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=A(f);if("bottom"===e||"middle"===e)g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=A(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,
b=this.renderer,c,d,e=this.rotation;c=this.element;var f=this.styles,g=e*vb;d=this.textStr;var h;if(""===d||Fb.test(d))h="num."+d.toString().length+(f?"|"+f.fontSize+"|"+f.fontFamily:"");h&&(a=b.cache[h]);if(!a){if("http://www.w3.org/2000/svg"===c.namespaceURI||b.forExport){try{a=c.getBBox?v({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(k){}if(!a||0>a.width)a={width:0,height:0}}else a=this.htmlGetBBox();b.isSVG&&(c=a.width,d=a.height,ra&&f&&"11px"===f.fontSize&&"16.9"===d.toPrecision(3)&&
(a.height=d=14),e&&(a.width=X(d*Sa(g))+X(c*Fa(g)),a.height=X(d*Fa(g))+X(c*Sa(g))));this.bBox=a;h&&(b.cache[h]=a)}return a},show:function(a){a&&"http://www.w3.org/2000/svg"===this.element.namespaceURI?this.element.removeAttribute("visibility"):this.attr({visibility:a?"inherit":"visible"});return this},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.attr({y:-9999})}})},add:function(a){var b=this.renderer,
c=a||b,d=c.element||b.box,e=this.element,f=this.zIndex,g,h;a&&(this.parentGroup=a);this.parentInverted=a&&a.inverted;void 0!==this.textStr&&b.buildText(this);f&&(c.handleZ=!0,f=E(f));if(c.handleZ)for(a=d.childNodes,g=0;g<a.length;g++)if(b=a[g],c=N(b,"zIndex"),b!==e&&(E(c)>f||!w(f)&&w(c))){d.insertBefore(e,b);h=!0;break}h||d.appendChild(e);this.added=!0;if(this.onAdd)this.onAdd();return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||
{},c=a.shadows,d=a.renderer.isSVG&&"SPAN"===b.nodeName&&a.parentGroup,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;Xa(a);a.clipPath&&(a.clipPath=a.clipPath.destroy());if(a.stops){for(f=0;f<a.stops.length;f++)a.stops[f]=a.stops[f].destroy();a.stops=null}a.safeRemoveChild(b);for(c&&t(c,function(b){a.safeRemoveChild(b)});d&&d.div&&0===d.div.childNodes.length;)b=d.parentGroup,a.safeRemoveChild(d.div),delete d.div,d=b;a.alignTo&&qa(a.renderer.alignedObjects,a);for(e in a)delete a[e];
return null},shadow:function(a,b,c){var d=[],e,f,g=this.element,h,k,l,m;if(a){k=n(a.width,3);l=(a.opacity||.15)/k;m=this.parentInverted?"(-1,-1)":"("+n(a.offsetX,1)+", "+n(a.offsetY,1)+")";for(e=1;e<=k;e++)f=g.cloneNode(0),h=2*k+1-2*e,N(f,{isShadow:"true",stroke:a.color||"black","stroke-opacity":l*e,"stroke-width":h,transform:"translate"+m,fill:"none"}),c&&(N(f,"height",x(N(f,"height")-h,0)),f.cutHeight=h),b?b.element.appendChild(f):g.parentNode.insertBefore(f,g),d.push(f);this.shadows=d}return this},
xGetter:function(a){"circle"===this.element.nodeName&&(a={x:"cx",y:"cy"}[a]||a);return this._defaultGetter(a)},_defaultGetter:function(a){a=n(this[a],this.element?this.element.getAttribute(a):null,0);/^[\-0-9\.]+$/.test(a)&&(a=parseFloat(a));return a},dSetter:function(a,b,c){a&&a.join&&(a=a.join(" "));/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");c.setAttribute(b,a);this[b]=a},dashstyleSetter:function(a){var b;if(a=a&&a.toLowerCase()){a=a.replace("shortdashdotdot","3,1,1,1,1,1,").replace("shortdashdot","3,1,1,1").replace("shortdot",
"1,1,").replace("shortdash","3,1,").replace("longdash","8,3,").replace(/dot/g,"1,3,").replace("dash","4,3,").replace(/,$/,"").split(",");for(b=a.length;b--;)a[b]=E(a[b])*this["stroke-width"];a=a.join(",").replace("NaN","none");this.element.setAttribute("stroke-dasharray",a)}},alignSetter:function(a){this.element.setAttribute("text-anchor",{left:"start",center:"middle",right:"end"}[a])},opacitySetter:function(a,b,c){this[b]=a;c.setAttribute(b,a)},titleSetter:function(a){var b=this.element.getElementsByTagName("title")[0];
b||(b=y.createElementNS("http://www.w3.org/2000/svg","title"),this.element.appendChild(b));b.textContent=n(a,"").replace(/<[^>]*>/g,"")},textSetter:function(a){a!==this.textStr&&(delete this.bBox,this.textStr=a,this.added&&this.renderer.buildText(this))},fillSetter:function(a,b,c){"string"===typeof a?c.setAttribute(b,a):a&&this.colorGradient(a,b,c)},zIndexSetter:function(a,b,c){c.setAttribute(b,a);this[b]=a},_defaultSetter:function(a,b,c){c.setAttribute(b,a)}};O.prototype.yGetter=O.prototype.xGetter;
O.prototype.translateXSetter=O.prototype.translateYSetter=O.prototype.rotationSetter=O.prototype.verticalAlignSetter=O.prototype.scaleXSetter=O.prototype.scaleYSetter=function(a,b){this[b]=a;this.doTransform=!0};O.prototype["stroke-widthSetter"]=O.prototype.strokeSetter=function(a,b,c){this[b]=a;this.stroke&&this["stroke-width"]?(this.strokeWidth=this["stroke-width"],O.prototype.fillSetter.call(this,this.stroke,"stroke",c),c.setAttribute("stroke-width",this["stroke-width"]),this.hasStroke=!0):"stroke-width"===
b&&0===a&&this.hasStroke&&(c.removeAttribute("stroke"),this.hasStroke=!1)};var Ja=function(){this.init.apply(this,arguments)};Ja.prototype={Element:O,init:function(a,b,c,d,e){var f=location,g;d=this.createElement("svg").attr({version:"1.1"}).css(this.getStyle(d));g=d.element;a.appendChild(g);-1===a.innerHTML.indexOf("xmlns")&&N(g,"xmlns","http://www.w3.org/2000/svg");this.isSVG=!0;this.box=g;this.boxWrapper=d;this.alignedObjects=[];this.url=(Ta||Cb)&&y.getElementsByTagName("base").length?f.href.replace(/#.*?$/,
"").replace(/([\('\)])/g,"\\$1").replace(/ /g,"%20"):"";this.createElement("desc").add().element.appendChild(y.createTextNode("Created with Highcharts 4.0.4 /Highstock 2.0.4"));this.defs=this.createElement("defs").add();this.forExport=e;this.gradients={};this.cache={};this.setSize(b,c,!1);var h;Ta&&a.getBoundingClientRect&&(this.subPixelFix=b=function(){J(a,{left:0,top:0});h=a.getBoundingClientRect();J(a,{left:ya(h.left)-h.left+"px",top:ya(h.top)-h.top+"px"})},b(),F(I,"resize",b))},getStyle:function(a){return this.style=
v({fontFamily:'"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif',fontSize:"12px"},a)},isHidden:function(){return!this.boxWrapper.getBBox().width},destroy:function(){var a=this.defs;this.box=null;this.boxWrapper=this.boxWrapper.destroy();Ma(this.gradients||{});this.gradients=null;a&&(this.defs=a.destroy());this.subPixelFix&&L(I,"resize",this.subPixelFix);return this.alignedObjects=null},createElement:function(a){var b=new this.Element;b.init(this,a);return b},draw:function(){},
buildText:function(a){for(var b=a.element,c=this,d=c.forExport,e=n(a.textStr,"").toString(),f=-1!==e.indexOf("<"),g=b.childNodes,h,k,l=N(b,"x"),m=a.styles,q=a.textWidth,r=m&&m.lineHeight,p=m&&m.HcTextStroke,Q=g.length,D=function(a){return r?E(r):c.fontMetrics(/(px|em)$/.test(a&&a.style.fontSize)?a.style.fontSize:m&&m.fontSize||c.style.fontSize||12,a).h};Q--;)b.removeChild(g[Q]);f||p||-1!==e.indexOf(" ")?(h=/<.*style="([^"]+)".*>/,k=/<.*href="(http[^"]+)".*>/,q&&!a.added&&this.box.appendChild(b),e=
f?e.replace(/<(b|strong)>/g,'<span style="font-weight:bold">').replace(/<(i|em)>/g,'<span style="font-style:italic">').replace(/<a/g,"<span").replace(/<\/(b|strong|i|em|a)>/g,"</span>").split(/<br.*?>/g):[e],""===e[e.length-1]&&e.pop(),t(e,function(e,f){var g,r=0;e=e.replace(/<span/g,"|||<span").replace(/<\/span>/g,"</span>|||");g=e.split("|||");t(g,function(e){if(""!==e||1===g.length){var p={},n=y.createElementNS("http://www.w3.org/2000/svg","tspan"),Q;h.test(e)&&(Q=e.match(h)[1].replace(/(;| |^)color([ :])/,
"$1fill$2"),N(n,"style",Q));k.test(e)&&!d&&(N(n,"onclick",'location.href="'+e.match(k)[1]+'"'),J(n,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/&lt;/g,"<").replace(/&gt;/g,">");if(" "!==e){n.appendChild(y.createTextNode(e));r?p.dx=0:f&&null!==l&&(p.x=l);N(n,p);b.appendChild(n);!r&&f&&(!Z&&d&&J(n,{display:"block"}),N(n,"dy",D(n)));if(q){e=e.replace(/([^\^])-/g,"$1- ").split(" ");for(var p=1<g.length||1<e.length&&"nowrap"!==m.whiteSpace,t,C,H=m.HcHeight,u=[],w=D(n),yb=1;p&&(e.length||
u.length);)delete a.bBox,t=a.getBBox(),C=t.width,!Z&&c.forExport&&(C=c.measureSpanWidth(n.firstChild.data,a.styles)),(t=C>q)&&1!==e.length?(n.removeChild(n.firstChild),u.unshift(e.pop())):(e=u,u=[],e.length&&(yb++,H&&yb*w>H?(e=["..."],a.attr("title",a.textStr)):(n=y.createElementNS("http://www.w3.org/2000/svg","tspan"),N(n,{dy:w,x:l}),Q&&N(n,"style",Q),b.appendChild(n))),C>q&&(q=C)),e.length&&n.appendChild(y.createTextNode(e.join(" ").replace(/- /g,"-")))}r++}}})})):b.appendChild(y.createTextNode(e))},
button:function(a,b,c,d,e,f,g,h,k){var l=this.label(a,b,c,k,null,null,null,null,"button"),m=0,q,r,p,n,D,t;a={x1:0,y1:0,x2:0,y2:1};e=G({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);p=e.style;delete e.style;f=G(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);n=f.style;delete f.style;g=G(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);D=g.style;delete g.style;
h=G(e,{style:{color:"#CCC"}},h);t=h.style;delete h.style;F(l.element,ra?"mouseover":"mouseenter",function(){3!==m&&l.attr(f).css(n)});F(l.element,ra?"mouseout":"mouseleave",function(){3!==m&&(q=[e,f,g][m],r=[p,n,D][m],l.attr(q).css(r))});l.setState=function(a){(l.state=m=a)?2===a?l.attr(g).css(D):3===a&&l.attr(h).css(t):l.attr(e).css(p)};return l.on("click",function(){3!==m&&d.call(l)}).attr(e).css(v({cursor:"default"},p))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=A(a[1])-b%2/2);a[2]===a[5]&&
(a[2]=a[5]=A(a[2])+b%2/2);return a},path:function(a){var b={fill:"none"};ta(a)?b.d=a:da(a)&&v(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=da(a)?a:{x:a,y:b,r:c};b=this.createElement("circle");b.xSetter=function(a){this.element.setAttribute("cx",a)};b.ySetter=function(a){this.element.setAttribute("cy",a)};return b.attr(a)},arc:function(a,b,c,d,e,f){da(a)&&(b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x);a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||
0});a.r=c;return a},rect:function(a,b,c,d,e,f){e=da(a)?a.r:e;var g=this.createElement("rect");a=da(a)?a:a===u?{}:{x:a,y:b,width:x(c,0),height:x(d,0)};f!==u&&(a.strokeWidth=f,a=g.crisp(a));e&&(a.r=e);g.rSetter=function(a){N(this.element,{rx:a,ry:a})};return g.attr(a)},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[n(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return w(a)?b.attr({"class":"highcharts-"+
a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:"none"};1<arguments.length&&v(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(A(b),A(c),d,e,f),k=/^url\((.*?)\)$/,l,m;h?(g=this.path(h),v(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&v(g,f)):k.test(a)&&(m=function(a,b){a.element&&
(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(A((d-b[0])/2),A((e-b[1])/2)))},l=a.match(k)[1],a=xb[l]||f&&f.width&&f.height&&[f.width,f.height],g=this.image(l).attr({x:b,y:c}),g.isImg=!0,a?m(g,a):(g.attr({width:0,height:0}),ja("img",{onload:function(){m(g,xb[l]=[this.width,this.height])},src:l})));return g},symbols:{circle:function(a,b,c,d){var e=.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",
a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start;c=e.r||c||d;var g=e.end-.001;d=e.innerR;var h=e.open,k=Fa(f),l=Sa(f),m=Fa(g),g=Sa(g);e=e.end-f<ub?0:1;return["M",a+c*k,b+c*l,"A",c,c,0,e,1,a+c*m,b+c*g,h?"M":"L",a+d*m,b+d*g,"A",d,d,0,e,0,a+d*k,b+d*l,h?"":"Z"]},
callout:function(a,b,c,d,e){var f=T(e&&e.r||0,c,d),g=f+6,h=e&&e.anchorX,k=e&&e.anchorY;e=A(e.strokeWidth||0)%2/2;a+=e;b+=e;e=["M",a+f,b,"L",a+c-f,b,"C",a+c,b,a+c,b,a+c,b+f,"L",a+c,b+d-f,"C",a+c,b+d,a+c,b+d,a+c-f,b+d,"L",a+f,b+d,"C",a,b+d,a,b+d,a,b+d-f,"L",a,b+f,"C",a,b,a,b,a+f,b];h&&h>c&&k>b+g&&k<b+d-g?e.splice(13,3,"L",a+c,k-6,a+c+6,k,a+c,k+6,a+c,b+d-f):h&&0>h&&k>b+g&&k<b+d-g?e.splice(33,3,"L",a,k+6,a-6,k,a,k-6,a,b+f):k&&k>d&&h>a+g&&h<a+c-g?e.splice(23,3,"L",h+6,b+d,h,b+d+6,h-6,b+d,a+f,b+d):k&&0>
k&&h>a+g&&h<a+c-g&&e.splice(3,3,"L",h-6,b,h,b-6,h+6,b,c-f,b);return e}},clipRect:function(a,b,c,d){var e="highcharts-"+hb++,f=this.createElement("clipPath").attr({id:e}).add(this.defs);a=this.rect(a,b,c,d,0).add(f);a.id=e;a.clipPath=f;return a},text:function(a,b,c,d){var e=ca||!Z&&this.forExport,f={};if(d&&!this.forExport)return this.html(a,b,c);f.x=Math.round(b||0);c&&(f.y=Math.round(c));if(a||0===a)f.text=a;a=this.createElement("text").attr(f);e&&a.css({position:"absolute"});d||(a.xSetter=function(a,
b,c){var d=c.getElementsByTagName("tspan"),e,f=c.getAttribute(b),r;for(r=0;r<d.length;r++)e=d[r],e.getAttribute(b)===f&&e.setAttribute(b,a);c.setAttribute(b,a)});return a},fontMetrics:function(a,b){a=a||this.style.fontSize;b&&I.getComputedStyle&&(b=b.element||b,a=I.getComputedStyle(b,"").fontSize);a=/px/.test(a)?E(a):/em/.test(a)?12*parseFloat(a):12;var c=24>a?a+4:A(1.2*a),d=A(.8*c);return{h:c,b:d,f:a}},label:function(a,b,c,d,e,f,g,h,k){function l(){var a,b;a=n.element.style;H=(void 0===Aa||void 0===
z||p.styles.textAlign)&&n.textStr&&n.getBBox();p.width=(Aa||H.width||0)+2*W+x;p.height=(z||H.height||0)+2*W;oa=W+r.fontMetrics(a&&a.fontSize,n).b;E&&(D||(a=A(-V*W),b=h?-oa:0,p.box=D=d?r.symbol(d,a,b,p.width,p.height,C):r.rect(a,b,p.width,p.height,0,C["stroke-width"]),D.attr("fill","none").add(p)),D.isImg||D.attr(v({width:A(p.width),height:A(p.height)},C)),C=null)}function m(){var a=p.styles,a=a&&a.textAlign,b=x+W*(1-V),c;c=h?0:oa;w(Aa)&&H&&("center"===a||"right"===a)&&(b+={center:.5,right:1}[a]*(Aa-
H.width));if(b!==n.x||c!==n.y)n.attr("x",b),c!==u&&n.attr("y",c);n.x=b;n.y=c}function q(a,b){D?D.attr(a,b):C[a]=b}var r=this,p=r.g(k),n=r.text("",0,0,g).attr({zIndex:1}),D,H,V=0,W=3,x=0,Aa,z,mb,y,B=0,C={},oa,E;p.onAdd=function(){n.add(p);p.attr({text:a||0===a?a:"",x:b,y:c});D&&w(e)&&p.attr({anchorX:e,anchorY:f})};p.widthSetter=function(a){Aa=a};p.heightSetter=function(a){z=a};p.paddingSetter=function(a){w(a)&&a!==W&&(W=a,m())};p.paddingLeftSetter=function(a){w(a)&&a!==x&&(x=a,m())};p.alignSetter=
function(a){V={left:0,center:.5,right:1}[a]};p.textSetter=function(a){a!==u&&n.textSetter(a);l();m()};p["stroke-widthSetter"]=function(a,b){a&&(E=!0);B=a%2/2;q(b,a)};p.strokeSetter=p.fillSetter=p.rSetter=function(a,b){"fill"===b&&a&&(E=!0);q(b,a)};p.anchorXSetter=function(a,b){e=a;q(b,a+B-mb)};p.anchorYSetter=function(a,b){f=a;q(b,a-y)};p.xSetter=function(a){p.x=a;V&&(a-=V*((Aa||H.width)+W));mb=A(a);p.attr("translateX",mb)};p.ySetter=function(a){y=p.y=A(a);p.attr("translateY",y)};var F=p.css;return v(p,
{css:function(a){if(a){var b={};a=G(a);t(p.textProps,function(c){a[c]!==u&&(b[c]=a[c],delete a[c])});n.css(b)}return F.call(p,a)},getBBox:function(){return{width:H.width+2*W,height:H.height+2*W,x:H.x-W,y:H.y-W}},shadow:function(a){D&&D.shadow(a);return p},destroy:function(){L(p.element,"mouseenter");L(p.element,"mouseleave");n&&(n=n.destroy());D&&(D=D.destroy());O.prototype.destroy.call(p);p=r=l=m=q=null}})}};xa.prototype={addLabel:function(){var a=this.axis,b=a.options,c=a.chart,d=a.horiz,e=a.categories,
f=a.names,g=this.pos,h=b.labels,k=h.rotation,l=a.tickPositions,d=d&&e&&!h.step&&!h.staggerLines&&!h.rotation&&c.plotWidth/l.length||!d&&(c.margin[3]||.33*c.chartWidth),m=g===l[0],q=g===l[l.length-1],r,f=e?n(e[g],f[g],g):g,e=this.label,p=l.info;a.isDatetimeAxis&&p&&(r=b.dateTimeLabelFormats[p.higherRanks[g]||p.unitName]);this.isFirst=m;this.isLast=q;b=a.labelFormatter.call({axis:a,chart:c,isFirst:m,isLast:q,dateTimeLabelFormat:r,value:a.isLog?ha(ia(f)):f});g=d&&{width:x(1,A(d-2*(h.padding||10)))+"px"};
w(e)?e&&e.attr({text:b}).css(g):(r={align:a.labelAlign},ga(k)&&(r.rotation=k),d&&h.ellipsis&&(g.HcHeight=a.len/l.length),this.label=e=w(b)&&h.enabled?c.renderer.text(b,0,0,h.useHTML).attr(r).css(v(g,h.style)).add(a.labelGroup):null,a.tickBaseline=c.renderer.fontMetrics(h.style.fontSize,e).b,k&&2===a.side&&(a.tickBaseline*=Fa(k*vb)));this.yOffset=e?n(h.y,a.tickBaseline+(2===a.side?8:-(e.getBBox().height/2))):0},getLabelSize:function(){var a=this.label,b=this.axis;return a?a.getBBox()[b.horiz?"height":
"width"]:0},getLabelSides:function(){var a=this.label.getBBox(),b=this.axis,c=b.horiz,d=b.options.labels,a=c?a.width:a.height,b=c?d.x-a*{left:0,center:.5,right:1}[b.labelAlign]:0;return[b,c?a+b:a]},handleOverflow:function(a,b){var c=!0,d=this.axis,e=this.isFirst,f=this.isLast,g=d.horiz?b.x:b.y,h=d.reversed,k=d.tickPositions,l=this.getLabelSides(),m=l[0],l=l[1],q,r,p,n=this.label.line;q=n||0;r=d.labelEdge;p=d.justifyLabels&&(e||f);r[q]===u||g+m>r[q]?r[q]=g+l:p||(c=!1);if(p){q=(r=d.justifyToPlot)?d.pos:
0;r=r?q+d.len:d.chart.chartWidth;do a+=e?1:-1,p=d.ticks[k[a]];while(k[a]&&(!p||!p.label||p.label.line!==n));d=p&&p.label.xy&&p.label.xy.x+p.getLabelSides()[e?0:1];e&&!h||f&&h?g+m<q&&(g=q-m,p&&g+l>d&&(c=!1)):g+l>r&&(g=r-l,p&&g+m<d&&(c=!1));b.x=g}return c},getPosition:function(a,b,c,d){var e=this.axis,f=e.chart,g=d&&f.oldChartHeight||f.chartHeight;return{x:a?e.translate(b+c,null,null,d)+e.transB:e.left+e.offset+(e.opposite?(d&&f.oldChartWidth||f.chartWidth)-e.right-e.left:0),y:a?g-e.bottom+e.offset-
(e.opposite?e.height:0):g-e.translate(b+c,null,null,d)-e.transB}},getLabelPosition:function(a,b,c,d,e,f,g,h){var k=this.axis,l=k.transA,m=k.reversed,q=k.staggerLines;a=a+e.x-(f&&d?f*l*(m?-1:1):0);b=b+this.yOffset-(f&&!d?f*l*(m?1:-1):0);q&&(c.line=g/(h||1)%q,b+=k.labelOffset/q*c.line);return{x:a,y:b}},getMarkPath:function(a,b,c,d,e,f){return f.crispLine(["M",a,b,"L",a+(e?0:-c),b+(e?c:0)],d)},render:function(a,b,c){var d=this.axis,e=d.options,f=d.chart.renderer,g=d.horiz,h=this.type,k=this.label,l=
this.pos,m=e.labels,q=this.gridLine,r=h?h+"Grid":"grid",p=h?h+"Tick":"tick",t=e[r+"LineWidth"],D=e[r+"LineColor"],H=e[r+"LineDashStyle"],V=e[p+"Length"],r=e[p+"Width"]||0,w=e[p+"Color"],x=e[p+"Position"],p=this.mark,v=m.step,A=!0,z=d.tickmarkOffset,y=this.getPosition(g,l,z,b),B=y.x,y=y.y,C=g&&B===d.pos+d.len||!g&&y===d.pos?-1:1;c=n(c,1);this.isActive=!0;if(t&&(l=d.getPlotLinePath(l+z,t*C,b,!0),q===u&&(q={stroke:D,"stroke-width":t},H&&(q.dashstyle=H),h||(q.zIndex=1),b&&(q.opacity=0),this.gridLine=
q=t?f.path(l).attr(q).add(d.gridGroup):null),!b&&q&&l))q[this.isNew?"attr":"animate"]({d:l,opacity:c});r&&V&&("inside"===x&&(V=-V),d.opposite&&(V=-V),h=this.getMarkPath(B,y,V,r*C,g,f),p?p.animate({d:h,opacity:c}):this.mark=f.path(h).attr({stroke:w,"stroke-width":r,opacity:c}).add(d.axisGroup));k&&!isNaN(B)&&(k.xy=y=this.getLabelPosition(B,y,k,g,m,z,a,v),this.isFirst&&!this.isLast&&!n(e.showFirstLabel,1)||this.isLast&&!this.isFirst&&!n(e.showLastLabel,1)?A=!1:d.isRadial||m.step||m.rotation||b||0===
c||(A=this.handleOverflow(a,y)),v&&a%v&&(A=!1),A&&!isNaN(y.y)?(y.opacity=c,k[this.isNew?"attr":"animate"](y),this.isNew=!1):k.attr("y",-9999))},destroy:function(){Ma(this,this.axis)}};ma.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:lb,lineColor:"#C0D0E0",lineWidth:1,minPadding:.01,maxPadding:.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,
minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:10,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#707070"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8,y:3},lineWidth:0,maxPadding:.05,minPadding:.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,
formatter:function(){return ka(this.total,-1)},style:lb.style}},defaultLeftAxisOptions:{labels:{x:-15,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:15,y:null},title:{rotation:90}},defaultBottomAxisOptions:{labels:{x:0,y:null},title:{rotation:0}},defaultTopAxisOptions:{labels:{x:0,y:-15},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.coll=(this.isXAxis=c)?"xAxis":"yAxis";this.opposite=b.opposite;this.side=b.side||(this.horiz?this.opposite?0:2:
this.opposite?1:3);this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=!1!==d.zoomEnabled;this.categories=d.categories||"category"===e;this.names=[];this.isLog="logarithmic"===e;this.isDatetimeAxis="datetime"===e;this.isLinked=w(d.linkedTo);this.tickmarkOffset=this.categories&&"between"===d.tickmarkPlacement&&1===n(d.tickInterval,1)?.5:0;this.ticks=
{};this.labelEdge=[];this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.min=this.max=null;this.crosshair=n(d.crosshair,ea(a.options.tooltip.crosshairs)[c?0:1],!1);var f,d=this.options.events;-1===Ia(this,a.axes)&&(c&&!this.isColorAxis?a.axes.splice(a.xAxis.length,0,this):a.axes.push(this),a[this.coll].push(this));this.series=this.series||
[];a.inverted&&c&&this.reversed===u&&(this.reversed=!0);this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)F(this,f,d[f]);this.isLog&&(this.val2lin=ab,this.lin2val=ia)},setOptions:function(a){this.options=G(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],G(M[this.coll],a))},defaultLabelFormatter:function(){var a=this.axis,b=this.value,
c=a.categories,d=this.dateTimeLabelFormat,e=M.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=ua(h,this);else if(c)g=b;else if(d)g=Ka(d,b);else if(f&&1E3<=a)for(;f--&&g===u;)c=Math.pow(1E3,f+1),a>=c&&null!==e[f]&&(g=ka(b/c,-1)+e[f]);g===u&&(g=1E4<=X(b)?ka(b,0):ka(b,-1,u,""));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=a.ignoreMinPadding=a.ignoreMaxPadding=null;a.buildStacks&&a.buildStacks();t(a.series,
function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d;d=c.options.threshold;var e;a.hasVisibleSeries=!0;a.isLog&&0>=d&&(d=null);a.isXAxis?(d=c.xData,d.length&&(a.dataMin=T(n(a.dataMin,d[0]),La(d)),a.dataMax=x(n(a.dataMax,d[0]),va(d)))):(c.getExtremes(),e=c.dataMax,c=c.dataMin,w(c)&&w(e)&&(a.dataMin=T(n(a.dataMin,c),c),a.dataMax=x(n(a.dataMax,e),e)),w(d)&&(a.dataMin>=d?(a.dataMin=d,a.ignoreMinPadding=!0):a.dataMax<d&&(a.dataMax=d,a.ignoreMaxPadding=!0)))}})},translate:function(a,b,c,
d,e,f){var g=1,h=0,k=d?this.oldTransA:this.transA;d=d?this.oldMin:this.min;var l=this.minPixelPadding;e=(this.options.ordinal||this.isLog&&e)&&this.lin2val;k||(k=this.transA);c&&(g*=-1,h=this.len);this.reversed&&(g*=-1,h-=g*(this.sector||this.len));b?(a=a*g+h-l,a=a/k+d,e&&(a=this.lin2val(a))):(e&&(a=this.val2lin(a)),"between"===f&&(f=.5),a=g*(a-d)*k+h+g*l+(ga(f)?k*f*this.pointRange:0));return a},toPixels:function(a,b){return this.translate(a,!1,!this.horiz,null,!0)+(b?0:this.pos)},toValue:function(a,
b){return this.translate(a-(b?0:this.pos),!0,!this.horiz,null,!0)},getPlotLinePath:function(a,b,c,d,e){var f=this.chart,g=this.left,h=this.top,k,l,m=c&&f.oldChartHeight||f.chartHeight,q=c&&f.oldChartWidth||f.chartWidth,r;k=this.transB;e=n(e,this.translate(a,null,null,c));a=c=A(e+k);k=l=A(m-e-k);if(isNaN(e))r=!0;else if(this.horiz){if(k=h,l=m-this.bottom,a<g||a>g+this.width)r=!0}else if(a=g,c=q-this.right,k<h||k>h+this.height)r=!0;return r&&!d?null:f.renderer.crispLine(["M",a,k,"L",c,l],b||1)},getLinearTickPositions:function(a,
b,c){var d,e=ha(K(b/a)*a),f=ha(ya(c/a)*a),g=[];if(b===c&&ga(b))return[b];for(b=e;b<=f;){g.push(b);b=ha(b+a);if(b===d)break;d=b}return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog)for(e=b.length,a=1;a<e;a++)d=d.concat(this.getLogTickPositions(c,b[a-1],b[a],!0));else if(this.isDatetimeAxis&&"auto"===a.minorTickInterval)d=d.concat(this.getTimeTicks(this.normalizeTimeTickInterval(c),this.min,this.max,a.startOfWeek)),d[0]<this.min&&
d.shift();else for(b=this.min+(b[0]-this.min)%c;b<=this.max;b+=c)d.push(b);return d},adjustForMinRange:function(){var a=this.options,b=this.min,c=this.max,d,e=this.dataMax-this.dataMin>=this.minRange,f,g,h,k,l;this.isXAxis&&this.minRange===u&&!this.isLog&&(w(a.min)||w(a.max)?this.minRange=null:(t(this.series,function(a){k=a.xData;for(g=l=a.xIncrement?1:k.length-1;0<g;g--)if(h=k[g]-k[g-1],f===u||h<f)f=h}),this.minRange=T(5*f,this.dataMax-this.dataMin)));if(c-b<this.minRange){var m=this.minRange;d=
(m-c+b)/2;d=[b-d,n(a.min,b-d)];e&&(d[2]=this.dataMin);b=va(d);c=[b+m,n(a.max,b+m)];e&&(c[2]=this.dataMax);c=La(c);c-b<m&&(d[0]=c-m,d[1]=n(a.min,c-m),b=va(d))}this.min=b;this.max=c},setAxisTranslation:function(a){var b=this,c=b.max-b.min,d=b.axisPointRange||0,e,f=0,g=0,h=b.linkedParent,k=!!b.categories,l=b.transA;if(b.isXAxis||k||d)h?(f=h.minPointOffset,g=h.pointRangePadding):t(b.series,function(a){var h=k?1:b.isXAxis?a.pointRange:b.axisPointRange||0,l=a.options.pointPlacement,p=a.closestPointRange;
h>c&&(h=0);d=x(d,h);f=x(f,pa(l)?0:h/2);g=x(g,"on"===l?0:h);!a.noSharedTooltip&&w(p)&&(e=w(e)?T(e,p):p)}),h=b.ordinalSlope&&e?b.ordinalSlope/e:1,b.minPointOffset=f*=h,b.pointRangePadding=g*=h,b.pointRange=T(d,c),b.closestPointRange=e;a&&(b.oldTransA=l);b.translationSlope=b.transA=l=b.len/(c+g||1);b.transB=b.horiz?b.left:b.bottom;b.minPixelPadding=l*f},setTickPositions:function(a){var b=this,c=b.chart,d=b.options,e=d.startOnTick,f=d.endOnTick,g=b.isLog,h=b.isDatetimeAxis,k=b.isXAxis,l=b.isLinked,m=
b.options.tickPositioner,q=d.maxPadding,r=d.minPadding,p=d.tickInterval,Q=d.minTickInterval,D=d.tickPixelInterval,H,u=b.categories;l?(b.linkedParent=c[b.coll][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=n(c.min,c.dataMin),b.max=n(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&aa(11,1)):(b.min=n(b.userMin,d.min,b.dataMin),b.max=n(b.userMax,d.max,b.dataMax));g&&(!a&&0>=T(b.min,n(b.dataMin,b.min))&&aa(10,1),b.min=ha(ab(b.min)),b.max=ha(ab(b.max)));b.range&&w(b.max)&&(b.userMin=b.min=x(b.min,
b.max-b.range),b.userMax=b.max,b.range=null);b.beforePadding&&b.beforePadding();b.adjustForMinRange();!(u||b.axisPointRange||b.usePercentage||l)&&w(b.min)&&w(b.max)&&(c=b.max-b.min)&&(w(d.min)||w(b.userMin)||!r||!(0>b.dataMin)&&b.ignoreMinPadding||(b.min-=c*r),w(d.max)||w(b.userMax)||!q||!(0<b.dataMax)&&b.ignoreMaxPadding||(b.max+=c*q));ga(d.floor)&&(b.min=x(b.min,d.floor));ga(d.ceiling)&&(b.max=T(b.max,d.ceiling));b.min===b.max||void 0===b.min||void 0===b.max?b.tickInterval=1:l&&!p&&D===b.linkedParent.options.tickPixelInterval?
b.tickInterval=b.linkedParent.tickInterval:(b.tickInterval=n(p,u?1:(b.max-b.min)*D/x(b.len,D)),!w(p)&&b.len<D&&!this.isRadial&&!this.isLog&&!u&&e&&f&&(H=!0,b.tickInterval/=4));k&&!a&&t(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)});b.setAxisTranslation(!0);b.beforeSetTickPositions&&b.beforeSetTickPositions();b.postProcessTickInterval&&(b.tickInterval=b.postProcessTickInterval(b.tickInterval));b.pointRange&&(b.tickInterval=x(b.pointRange,b.tickInterval));!p&&b.tickInterval<
Q&&(b.tickInterval=Q);h||g||p||(b.tickInterval=ob(b.tickInterval,null,R.pow(10,K(R.log(b.tickInterval)/R.LN10)),n(d.allowDecimals,!(1<b.tickInterval&&5>b.tickInterval&&1E3<b.max&&9999>b.max))));b.minorTickInterval="auto"===d.minorTickInterval&&b.tickInterval?b.tickInterval/5:d.minorTickInterval;b.tickPositions=a=d.tickPositions?[].concat(d.tickPositions):m&&m.apply(b,[b.min,b.max]);a||(!b.ordinalPositions&&(b.max-b.min)/b.tickInterval>x(2*b.len,200)&&aa(19,!0),a=h?b.getTimeTicks(b.normalizeTimeTickInterval(b.tickInterval,
d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):g?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),H&&a.splice(1,a.length-2),b.tickPositions=a);l||(d=a[0],g=a[a.length-1],h=b.minPointOffset||0,e?b.min=d:b.min-h>d&&a.shift(),f?b.max=g:b.max+h<g&&a.pop(),0===a.length&&w(d)&&a.push((g+d)/2),1===a.length&&(e=1E13<X(b.max)?1:.001,b.min-=e,b.max+=e))},setMaxTicks:function(){var a=this.chart,b=a.maxTicks||{},c=this.tickPositions,
d=this._maxTicksKey=[this.coll,this.pos,this.len].join("-");!this.isLinked&&!this.isDatetimeAxis&&c&&c.length>(b[d]||0)&&!1!==this.options.alignTicks&&(b[d]=c.length);a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&!1!==this.options.alignTicks&&this.min!==u){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e<a){for(;b.length<a;)b.push(ha(b[b.length-1]+this.tickInterval));
this.transA*=(e-1)/(a-1);this.max=b[b.length-1]}w(d)&&a!==d&&(this.isDirty=!0)}},setScale:function(){var a=this.stacks,b,c,d,e;this.oldMin=this.min;this.oldMax=this.max;this.oldAxisLength=this.len;this.setAxisSize();e=this.len!==this.oldAxisLength;t(this.series,function(a){if(a.isDirtyData||a.isDirty||a.xAxis.isDirty)d=!0});if(e||d||this.isLinked||this.forceRedraw||this.userMin!==this.oldUserMin||this.userMax!==this.oldUserMax){if(!this.isXAxis)for(b in a)for(c in a[b])a[b][c].total=null,a[b][c].cum=
0;this.forceRedraw=!1;this.getSeriesExtremes();this.setTickPositions();this.oldUserMin=this.userMin;this.oldUserMax=this.userMax;this.isDirty||(this.isDirty=e||this.min!==this.oldMin||this.max!==this.oldMax)}else if(!this.isXAxis)for(b in this.oldStacks&&(a=this.stacks=this.oldStacks),a)for(c in a[b])a[b][c].cum=a[b][c].total;this.setMaxTicks()},setExtremes:function(a,b,c,d,e){var f=this,g=f.chart;c=n(c,!0);e=v(e,{min:a,max:b});B(f,"setExtremes",e,function(){f.userMin=a;f.userMax=b;f.eventArgs=e;
f.isDirtyExtremes=!0;c&&g.redraw(d)})},zoom:function(a,b){var c=this.dataMin,d=this.dataMax,e=this.options;this.allowZoomOutside||(w(c)&&a<=T(c,n(e.min,c))&&(a=u),w(d)&&b>=x(d,n(e.max,d))&&(b=u));this.displayBtn=a!==u||b!==u;this.setExtremes(a,b,!1,u,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=this.horiz,e=n(b.width,a.plotWidth-c+(b.offsetRight||0)),f=n(b.height,a.plotHeight),g=n(b.top,a.plotTop),b=n(b.left,a.plotLeft+c),c=/%$/;c.test(f)&&
(f=parseInt(f,10)/100*a.plotHeight);c.test(g)&&(g=parseInt(g,10)/100*a.plotHeight+a.plotTop);this.left=b;this.top=g;this.width=e;this.height=f;this.bottom=a.chartHeight-f-g;this.right=a.chartWidth-e-b;this.len=x(d?e:f,0);this.pos=d?b:g},getExtremes:function(){var a=this.isLog;return{min:a?ha(ia(this.min)):this.min,max:a?ha(ia(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?ia(this.min):this.min,
b=b?ia(this.max):this.max;c>a||null===a?a=c:b<a&&(a=b);return this.translate(a,0,1,0,1)},autoLabelAlign:function(a){a=(n(a,0)-90*this.side+720)%360;return 15<a&&165>a?"right":195<a&&345>a?"left":"center"},getOffset:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,k=b.inverted?[1,0,3,2][h]:h,l,m,q=0,r,p=0,Q=d.title,D=d.labels,H=0,V=b.axisOffset,b=b.clipOffset,W=[-1,1,1,-1][h],v,A=1,y=n(D.maxStaggerLines,5),z,B,G,C,oa;a.hasData=l=a.hasVisibleSeries||
w(a.min)&&w(a.max)&&!!e;a.showAxis=m=l||n(d.showEmpty,!0);a.staggerLines=a.horiz&&D.staggerLines;a.axisGroup||(a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(),a.labelGroup=c.g("axis-labels").attr({zIndex:D.zIndex||7}).addClass("highcharts-"+a.coll.toLowerCase()+"-labels").add());if(l||a.isLinked){a.labelAlign=n(D.align||a.autoLabelAlign(D.rotation));t(e,function(b){f[b]?f[b].addLabel():f[b]=new xa(a,b)});if(a.horiz&&!a.staggerLines&&
y&&!D.rotation){for(l=a.reversed?[].concat(e).reverse():e;A<y;){z=[];B=!1;for(v=0;v<l.length;v++)G=l[v],C=(C=f[G].label&&f[G].label.getBBox())?C.width:0,oa=v%A,C&&(G=a.translate(G),z[oa]!==u&&G<z[oa]&&(B=!0),z[oa]=G+C);if(B)A++;else break}1<A&&(a.staggerLines=A)}t(e,function(b){if(0===h||2===h||{1:"left",3:"right"}[h]===a.labelAlign)H=x(f[b].getLabelSize(),H)});a.staggerLines&&(H*=a.staggerLines,a.labelOffset=H)}else for(v in f)f[v].destroy(),delete f[v];Q&&Q.text&&!1!==Q.enabled&&(a.axisTitle||(a.axisTitle=
c.text(Q.text,0,0,Q.useHTML).attr({zIndex:7,rotation:Q.rotation||0,align:Q.textAlign||{low:"left",middle:"center",high:"right"}[Q.align]}).addClass("highcharts-"+this.coll.toLowerCase()+"-title").css(Q.style).add(a.axisGroup),a.axisTitle.isNew=!0),m&&(q=a.axisTitle.getBBox()[g?"height":"width"],r=Q.offset,p=w(r)?0:n(Q.margin,g?5:10)),a.axisTitle[m?"show":"hide"]());a.offset=W*n(d.offset,V[h]);c=2===h?a.tickBaseline:0;g=H+p+(H&&W*(g?n(D.y,a.tickBaseline+8):D.x)-c);a.axisTitleMargin=n(r,g);V[h]=x(V[h],
a.axisTitleMargin+q+W*a.offset,g);b[k]=x(b[k],2*K(d.lineWidth/2))},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M",e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,
k=E(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(2===this.side?k:0);return{x:a?d:b+(g?this.width:0)+h+(e.x||0),y:a?b-(g?this.height:0)+h:d+(e.y||0)}},render:function(){var a=this,b=a.horiz,c=a.reversed,d=a.chart,e=d.renderer,f=a.options,g=a.isLog,h=a.isLinked,k=a.tickPositions,l,m=a.axisTitle,q=a.ticks,r=a.minorTicks,p=a.alternateBands,n=f.stackLabels,D=f.alternateGridColor,H=a.tickmarkOffset,v=f.lineWidth,
x=d.hasRendered&&w(a.oldMin)&&!isNaN(a.oldMin),A=a.hasData,y=a.showAxis,z,B=f.labels.overflow,G=a.justifyLabels=b&&!1!==B,E;a.labelEdge.length=0;a.justifyToPlot="justify"===B;t([q,r,p],function(a){for(var b in a)a[b].isActive=!1});if(A||h)a.minorTickInterval&&!a.categories&&t(a.getMinorTickPositions(),function(b){r[b]||(r[b]=new xa(a,b,"minor"));x&&r[b].isNew&&r[b].render(null,!0);r[b].render(null,!1,1)}),k.length&&(l=k.slice(),(b&&c||!b&&!c)&&l.reverse(),G&&(l=l.slice(1).concat([l[0]])),t(l,function(b,
c){G&&(c=c===l.length-1?0:c+1);if(!h||b>=a.min&&b<=a.max)q[b]||(q[b]=new xa(a,b)),x&&q[b].isNew&&q[b].render(c,!0,.1),q[b].render(c)}),H&&0===a.min&&(q[-1]||(q[-1]=new xa(a,-1,null,!0)),q[-1].render(-1))),D&&t(k,function(b,c){0===c%2&&b<a.max&&(p[b]||(p[b]=new U.PlotLineOrBand(a)),z=b+H,E=k[c+1]!==u?k[c+1]+H:a.max,p[b].options={from:g?ia(z):z,to:g?ia(E):E,color:D},p[b].render(),p[b].isActive=!0)}),a._addedPlotLB||(t((f.plotLines||[]).concat(f.plotBands||[]),function(b){a.addPlotBandOrLine(b)}),a._addedPlotLB=
!0);t([q,r,p],function(a){var b,c,e=[],f=S?S.duration||500:0,g=function(){for(c=e.length;c--;)a[e[c]]&&!a[e[c]].isActive&&(a[e[c]].destroy(),delete a[e[c]])};for(b in a)a[b].isActive||(a[b].render(b,!1,0),a[b].isActive=!1,e.push(b));a!==p&&d.hasRendered&&f?f&&setTimeout(g,f):g()});v&&(b=a.getLinePath(v),a.axisLine?a.axisLine.animate({d:b}):a.axisLine=e.path(b).attr({stroke:f.lineColor,"stroke-width":v,zIndex:7}).add(a.axisGroup),a.axisLine[y?"show":"hide"]());m&&y&&(m[m.isNew?"attr":"animate"](a.getTitlePosition()),
m.isNew=!1);n&&n.enabled&&a.renderStackTotals();a.isDirty=!1},redraw:function(){this.render();t(this.plotLinesAndBands,function(a){a.render()});t(this.series,function(a){a.isDirty=!0})},destroy:function(a){var b=this,c=b.stacks,d,e=b.plotLinesAndBands;a||L(b);for(d in c)Ma(c[d]),c[d]=null;t([b.ticks,b.minorTicks,b.alternateBands],function(a){Ma(a)});for(a=e.length;a--;)e[a].destroy();t("stackTotalGroup axisLine axisTitle axisGroup cross gridGroup labelGroup".split(" "),function(a){b[a]&&(b[a]=b[a].destroy())});
this.cross&&this.cross.destroy()},drawCrosshair:function(a,b){if(this.crosshair)if(!1===(w(b)||!n(this.crosshair.snap,!0)))this.hideCrosshair();else{var c,d=this.crosshair,e=d.animation;n(d.snap,!0)?w(b)&&(c=this.chart.inverted!=this.horiz?b.plotX:this.len-b.plotY):c=this.horiz?a.chartX-this.pos:this.len-a.chartY+this.pos;c=this.isRadial?this.getPlotLinePath(this.isXAxis?b.x:n(b.stackY,b.y)):this.getPlotLinePath(null,null,null,null,c);if(null===c)this.hideCrosshair();else if(this.cross)this.cross.attr({visibility:"visible"})[e?
"animate":"attr"]({d:c},e);else e={"stroke-width":d.width||1,stroke:d.color||"#C0C0C0",zIndex:d.zIndex||2},d.dashStyle&&(e.dashstyle=d.dashStyle),this.cross=this.chart.renderer.path(c).attr(e).add()}},hideCrosshair:function(){this.cross&&this.cross.hide()}};v(ma.prototype,void 0);ma.prototype.getTimeTicks=function(a,b,c,d){var e=[],f={},g=M.global.useUTC,h,k=new la(b-wa),l=a.unitRange,m=a.count;if(w(b)){l>=z.second&&(k.setMilliseconds(0),k.setSeconds(l>=z.minute?0:m*K(k.getSeconds()/m)));if(l>=z.minute)k[qb](l>=
z.hour?0:m*K(k[db]()/m));if(l>=z.hour)k[rb](l>=z.day?0:m*K(k[eb]()/m));if(l>=z.day)k[gb](l>=z.month?1:m*K(k[Da]()/m));l>=z.month&&(k[sb](l>=z.year?0:m*K(k[Qa]()/m)),h=k[Ra]());if(l>=z.year)k[tb](h-h%m);if(l===z.week)k[gb](k[Da]()-k[fb]()+n(d,1));b=1;wa&&(k=new la(k.getTime()+wa));h=k[Ra]();d=k.getTime();for(var q=k[Qa](),r=k[Da](),p=(z.day+(g?wa:6E4*k.getTimezoneOffset()))%z.day;d<c;)e.push(d),d=l===z.year?Pa(h+b*m,0):l===z.month?Pa(h,q+b*m):g||l!==z.day&&l!==z.week?d+l*m:Pa(h,q,r+b*m*(l===z.day?
1:7)),b++;e.push(d);t(jb(e,function(a){return l<=z.hour&&a%z.day===p}),function(a){f[a]="day"})}e.info=v(a,{higherRanks:f,totalRange:l*m});return e};ma.prototype.normalizeTimeTickInterval=function(a,b){var c=b||[["millisecond",[1,2,5,10,20,25,50,100,200,500]],["second",[1,2,5,10,15,30]],["minute",[1,2,5,10,15,30]],["hour",[1,2,3,4,6,8,12]],["day",[1,2]],["week",[1,2]],["month",[1,2,3,4,6]],["year",null]],d=c[c.length-1],e=z[d[0]],f=d[1],g;for(g=0;g<c.length&&!(d=c[g],e=z[d[0]],f=d[1],c[g+1]&&a<=(e*
f[f.length-1]+z[c[g+1][0]])/2);g++);e===z.year&&a<5*e&&(f=[1,2,5]);c=ob(a/e,f,"year"===d[0]?x(R.pow(10,K(R.log(a/e)/R.LN10)),1):1);return{unitRange:e,count:c,unitName:d[0]}};var zb=U.Tooltip=function(){this.init.apply(this,arguments)};zb.prototype={init:function(a,b){var c=b.borderWidth,d=b.style,e=E(d.padding);this.chart=a;this.options=b;this.crosshairs=[];this.now={x:0,y:0};this.isHidden=!0;this.label=a.renderer.label("",0,0,b.shape||"callout",null,null,b.useHTML,null,"tooltip").attr({padding:e,
fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).css({padding:0}).add().attr({y:-9999});ca||this.label.shadow(b.shadow);this.shared=b.shared},destroy:function(){this.label&&(this.label=this.label.destroy());clearTimeout(this.hideTimer);clearTimeout(this.tooltipTimeout)},move:function(a,b,c,d){var e=this,f=e.now,g=!1!==e.options.animation&&!e.isHidden&&(1<X(a-f.x)||1<X(b-f.y)),h=e.followPointer||1<e.len;v(f,{x:g?(2*f.x+a)/3:a,y:g?(f.y+b)/2:b,anchorX:h?u:g?(2*f.anchorX+c)/
3:c,anchorY:h?u:g?(f.anchorY+d)/2:d});e.label.attr(f);g&&(clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32))},hide:function(a){var b=this,c;clearTimeout(this.hideTimer);this.isHidden||(c=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){b.label.fadeOut();b.isHidden=!0},n(a,this.options.hideDelay,500)),c&&t(c,function(a){a.setState()}),this.chart.hoverPoints=null)},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=
0,k;a=ea(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===u&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(t(a,function(a){k=a.series.yAxis;g+=a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&k?k.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&1<a.length&&b?b.chartY-f:e?d.plotHeight-g:h]);return kb(c,A)},getPosition:function(a,b,c){var d=this.chart,e=this.distance,f={},g,h=["y",d.chartHeight,b,c.plotY+d.plotTop],k=["x",d.chartWidth,
a,c.plotX+d.plotLeft],l=c.ttBelow||d.inverted&&!c.negative||!d.inverted&&c.negative,m=function(a,b,c,d){var g=c<d-e;b=d+e+c<b;c=d-e-c;d+=e;if(l&&b)f[a]=d;else if(!l&&g)f[a]=c;else if(g)f[a]=c;else if(b)f[a]=d;else return!1},q=function(a,b,c,d){if(d<e||d>b-e)return!1;f[a]=d<c/2?1:d>b-c/2?b-c-2:d-c/2},r=function(a){var b=h;h=k;k=b;g=a},p=function(){!1!==m.apply(0,h)?!1!==q.apply(0,k)||g||(r(!0),p()):g?f.x=f.y=0:(r(!0),p())};(d.inverted||1<this.len)&&r();p();return f},defaultFormatter:function(a){var b=
this.points||ea(this),c=b[0].series,d;d=[a.tooltipHeaderFormatter(b[0])];t(b,function(a){c=a.series;d.push(c.tooltipFormatter&&c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h={},k,l=[];k=e.formatter||this.defaultFormatter;var h=c.hoverPoints,m,q=this.shared;clearTimeout(this.hideTimer);this.followPointer=ea(a)[0].series.tooltipOptions.followPointer;
g=this.getAnchor(a,b);f=g[0];g=g[1];!q||a.series&&a.series.noSharedTooltip?h=a.getLabelConfig():(c.hoverPoints=a,h&&t(h,function(a){a.setState()}),t(a,function(a){a.setState("hover");l.push(a.getLabelConfig())}),h={x:a[0].category,y:a[0].y},h.points=l,this.len=l.length,a=a[0]);k=k.call(h,this);h=a.series;this.distance=n(h.tooltipOptions.distance,16);!1===k?this.hide():(this.isHidden&&(Xa(d),d.attr("opacity",1).show()),d.attr({text:k}),m=e.borderColor||a.color||h.color||"#606060",d.attr({stroke:m}),
this.updatePosition({plotX:f,plotY:g,negative:a.negative,ttBelow:a.ttBelow}),this.isHidden=!1);B(c,"tooltipRefresh",{text:k,x:f+c.plotLeft,y:g+c.plotTop,borderColor:m})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(A(c.x),A(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)},tooltipHeaderFormatter:function(a){var b=a.series,c=b.tooltipOptions,d=c.dateTimeLabelFormats,e=c.xDateFormat,f=b.xAxis,g=f&&"datetime"===
f.options.type&&ga(a.key),c=c.headerFormat,f=f&&f.closestPointRange,h;if(g&&!e){if(f)for(h in z){if(z[h]>=f||z[h]<=z.day&&0<a.key%z[h]){e=d[h];break}}else e=d.day;e=e||d.year}g&&e&&(c=c.replace("{point.key}","{point.key:"+e+"}"));return ua(c,{point:a,series:b})}};var Ba;Ga=y.documentElement.ontouchstart!==u;var Ab=U.Pointer=function(a,b){this.init(a,b)};Ab.prototype={init:function(a,b){var c=b.chart,d=c.events,e=ca?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);
this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.hasZoom=f||e;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};U.Tooltip&&b.tooltip.enabled&&(a.tooltip=new zb(a,b.tooltip),this.followTouchMove=b.tooltip.followTouchMove);this.setDOMEvents()},normalize:function(a,b){var c,d;a=a||window.event;a=Hb(a);a.target||(a.target=a.srcElement);d=a.touches?a.touches.length?a.touches.item(0):a.changedTouches[0]:a;b||(this.chartPosition=b=Gb(this.chart.container));
d.pageX===u?(c=x(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return v(a,{chartX:A(c),chartY:A(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};t(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f,g=b.hoverPoint,
h=b.hoverSeries,k,l,m=b.chartWidth,q=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!h||!h.noSharedTooltip)){f=[];k=c.length;for(l=0;l<k;l++)c[l].visible&&!1!==c[l].options.enableMouseTracking&&!c[l].noSharedTooltip&&!0!==c[l].singularTooltips&&c[l].tooltipPoints.length&&(e=c[l].tooltipPoints[q])&&e.series&&(e._dist=X(q-e.clientX),m=T(m,e._dist),f.push(e));for(k=f.length;k--;)f[k]._dist>m&&f.splice(k,1);f.length&&f[0].clientX!==this.hoverX&&(d.refresh(f,a),this.hoverX=f[0].clientX)}c=h&&h.tooltipOptions.followPointer;
if(h&&h.tracker&&!c){if((e=h.tooltipPoints[q])&&e!==g)e.onMouseOver(a)}else d&&c&&!d.isHidden&&(h=d.getAnchor([{}],a),d.updatePosition({plotX:h[0],plotY:h[1]}));d&&!this._onDocumentMouseMove&&(this._onDocumentMouseMove=function(a){if(Y[Ba])Y[Ba].pointer.onDocumentMouseMove(a)},F(y,"mousemove",this._onDocumentMouseMove));t(b.axes,function(b){b.drawCrosshair(a,n(e,g))})},reset:function(a,b){var c=this.chart,d=c.hoverSeries,e=c.hoverPoint,f=c.tooltip,g=f&&f.shared?c.hoverPoints:e;(a=a&&f&&g)&&ea(g)[0].plotX===
u&&(a=!1);if(a)f.refresh(g),e&&e.setState(e.state,!0);else{if(e)e.onMouseOut();if(d)d.onMouseOut();f&&f.hide(b);this._onDocumentMouseMove&&(L(y,"mousemove",this._onDocumentMouseMove),this._onDocumentMouseMove=null);t(c.axes,function(a){a.hideCrosshair()});this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart,d;t(c.series,function(e){d=a||e.getPlotBox();e.xAxis&&e.xAxis.zoomEnabled&&(e.group.attr(d),e.markerGroup&&(e.markerGroup.attr(d),e.markerGroup.clip(b?c.clipRect:null)),e.dataLabelsGroup&&
e.dataLabelsGroup.attr(d))});c.clipRect.attr(b||c.clipBox)},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;b.mouseDownY=this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,e=a.chartY,f=this.zoomHor,g=this.zoomVert,h=b.plotLeft,k=b.plotTop,l=b.plotWidth,m=b.plotHeight,q,r=this.mouseDownX,p=this.mouseDownY,n=c.panKey&&a[c.panKey+"Key"];d<h?d=h:d>h+l&&(d=h+l);e<k?e=k:e>k+m&&(e=k+m);this.hasDragged=
Math.sqrt(Math.pow(r-d,2)+Math.pow(p-e,2));10<this.hasDragged&&(q=b.isInsidePlot(r-h,p-k),b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&q&&!n&&!this.selectionMarker&&(this.selectionMarker=b.renderer.rect(h,k,f?1:l,g?1:m,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add()),this.selectionMarker&&f&&(d-=r,this.selectionMarker.attr({width:X(d),x:(0<d?0:d)+r})),this.selectionMarker&&g&&(d=e-p,this.selectionMarker.attr({height:X(d),y:(0<d?0:d)+p})),q&&!this.selectionMarker&&
c.panning&&b.pan(a,c.panning))},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.attr?e.attr("x"):e.x,g=e.attr?e.attr("y"):e.y,h=e.attr?e.attr("width"):e.width,k=e.attr?e.attr("height"):e.height,l;if(this.hasDragged||c)t(b.axes,function(b){if(b.zoomEnabled){var c=b.horiz,e="touchend"===a.type?b.minPixelPadding:0,p=b.toValue((c?f:g)+e),c=b.toValue((c?f+h:g+k)-e);isNaN(p)||isNaN(c)||(d[b.coll].push({axis:b,
min:T(p,c),max:x(p,c)}),l=!0)}}),l&&B(b,"selection",d,function(a){b.zoom(v(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups()}b&&(J(b.container,{cursor:b._cursor}),b.cancelClick=10<this.hasDragged,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[])},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){Y[Ba]&&Y[Ba].pointer.drop(a)},onDocumentMouseMove:function(a){var b=
this.chart,c=this.chartPosition,d=b.hoverSeries;a=this.normalize(a,c);c&&d&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(){var a=Y[Ba];a&&(a.pointer.reset(),a.pointer.chartPosition=null)},onContainerMouseMove:function(a){var b=this.chart;Ba=b.index;a=this.normalize(a);a.returnValue=!1;"mousedown"===b.mouseIsDown&&this.drag(a);!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-
b.plotLeft,a.chartY-b.plotTop)||b.openMenu||this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=N(a,"class")){if(-1!==c.indexOf(b))return!0;if(-1!==c.indexOf("highcharts-container"))return!1}a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries,c=(a=a.relatedTarget||a.toElement)&&a.point&&a.point.series;if(b&&!b.options.stickyTracking&&!this.inClass(a,"highcharts-tooltip")&&c!==b)b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,
e=b.plotTop;a=this.normalize(a);a.cancelBubble=!0;b.cancelClick||(c&&this.inClass(a.target,"highcharts-tracker")?(B(c.series,"click",v(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(v(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&B(b,"click",a)))},setDOMEvents:function(){var a=this,b=a.chart.container;b.onmousedown=function(b){a.onContainerMouseDown(b)};b.onmousemove=function(b){a.onContainerMouseMove(b)};b.onclick=function(b){a.onContainerClick(b)};F(b,"mouseleave",
a.onContainerMouseLeave);1===Ha&&F(y,"mouseup",a.onDocumentMouseUp);Ga&&(b.ontouchstart=function(b){a.onContainerTouchStart(b)},b.ontouchmove=function(b){a.onContainerTouchMove(b)},1===Ha&&F(y,"touchend",a.onDocumentTouchEnd))},destroy:function(){var a;L(this.chart.container,"mouseleave",this.onContainerMouseLeave);Ha||(L(y,"mouseup",this.onDocumentMouseUp),L(y,"touchend",this.onDocumentTouchEnd));clearInterval(this.tooltipTimeout);for(a in this)this[a]=null}};var Za=U.Legend=function(a,b){this.init(a,
b)};Za.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=n(b.padding,8),f=b.itemMarginTop||0;this.options=b;b.enabled&&(c.itemStyle=d,c.itemHiddenStyle=G(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.symbolWidth=n(b.symbolWidth,16),c.pages=[],c.render(),F(c.chart,"endResize",function(){c.positionCheckboxes()}))},colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,
f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.legendColor||a.color||"#CCC":g,g=a.options&&a.options.marker,k={fill:h},l;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(l in k.stroke=h,g=a.convertAttribs(g),g)d=g[l],d!==u&&(k[l]=d);f.attr(k)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);f&&(f.x=e,f.y=
d)},destroyItem:function(a){var b=a.checkbox;t(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())});b&&Na(a.checkbox)},destroy:function(){var a=this.group,b=this.box;b&&(this.box=b.destroy());a&&(this.group=a.destroy())},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;b&&(c=b.translateY,t(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,J(f,{left:b.translateX+e.checkboxOffset+f.x-20+"px",
top:g+"px",display:g>c-6&&g<c+d-6?"":"none"}))}))},renderTitle:function(){var a=this.padding,b=this.options.title,c=0;b.text&&(this.title||(this.title=this.chart.renderer.label(b.text,a-3,a-4,null,null,null,null,null,"legend-title").attr({zIndex:1}).css(b.style).add(this.group)),a=this.title.getBBox(),c=a.height,this.offsetWidth=a.width,this.contentGroup.attr({translateY:c}));this.titleHeight=c},renderItem:function(a){var b=this.chart,c=b.renderer,d=this.options,e="horizontal"===d.layout,f=this.symbolWidth,
g=d.symbolPadding,h=this.itemStyle,k=this.itemHiddenStyle,l=this.padding,m=e?n(d.itemDistance,20):0,q=!d.rtl,r=d.width,p=d.itemMarginBottom||0,t=this.itemMarginTop,D=this.initialItemX,u=a.legendItem,v=a.series&&a.series.drawLegendSymbol?a.series:a,w=v.options,w=this.createCheckboxForItem&&w&&w.showCheckbox,y=d.useHTML;u||(a.legendGroup=c.g("legend-item").attr({zIndex:1}).add(this.scrollGroup),a.legendItem=u=c.text(d.labelFormat?ua(d.labelFormat,a):d.labelFormatter.call(a),q?f+g:-g,this.baseline||
0,y).css(G(a.visible?h:k)).attr({align:q?"left":"right",zIndex:2}).add(a.legendGroup),this.baseline||(this.baseline=c.fontMetrics(h.fontSize,u).f+3+t,u.attr("y",this.baseline)),v.drawLegendSymbol(this,a),this.setItemEvents&&this.setItemEvents(a,u,y,h,k),this.colorizeItem(a,a.visible),w&&this.createCheckboxForItem(a));c=u.getBBox();f=a.checkboxOffset=d.itemWidth||a.legendItemWidth||f+g+c.width+m+(w?20:0);this.itemHeight=g=A(a.legendItemHeight||c.height);e&&this.itemX-D+f>(r||b.chartWidth-2*l-D-d.x)&&
(this.itemX=D,this.itemY+=t+this.lastLineHeight+p,this.lastLineHeight=0);this.maxItemWidth=x(this.maxItemWidth,f);this.lastItemY=t+this.itemY+p;this.lastLineHeight=x(g,this.lastLineHeight);a._legendItemPos=[this.itemX,this.itemY];e?this.itemX+=f:(this.itemY+=t+g+p,this.lastLineHeight=g);this.offsetWidth=r||x((e?this.itemX-D-m:f)+l,this.offsetWidth)},getAllItems:function(){var a=[];t(this.chart.series,function(b){var c=b.options;n(c.showInLegend,w(c.linkedTo)?!1:u,!0)&&(a=a.concat(b.legendItems||("point"===
c.legendType?b.data:b)))});return a},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,k=a.box,l=a.options,m=a.padding,q=l.borderWidth,r=l.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;d||(a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup));a.renderTitle();e=a.getAllItems();cb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||
0)});l.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;t(e,function(b){a.renderItem(b)});g=l.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(q||r)g+=m,h+=m,k?0<g&&0<h&&(k[k.isNew?"attr":"animate"](k.crisp({width:g,height:h})),k.isNew=!1):(a.box=k=c.rect(0,0,g,h,l.borderRadius,q||0).attr({stroke:l.borderColor,"stroke-width":q||0,fill:r||"none"}).add(d).shadow(l.shadow),k.isNew=!0),k[f?"show":"hide"]();a.legendWidth=g;a.legendHeight=h;t(e,function(b){a.positionItem(b)});
f&&d.align(v({width:g,height:h},l),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+("top"===e.verticalAlign?-f:f)-this.padding,g=e.maxHeight,h,k=this.clipRect,l=e.navigation,m=n(l.animation,!0),q=l.arrowSize||12,r=this.nav,p=this.pages,u,D=this.allItems;"horizontal"===e.layout&&(f/=2);g&&(f=T(f,g));p.length=0;a>f&&!e.useHTML?(this.clipHeight=h=x(f-20-this.titleHeight-this.padding,0),
this.currentPage=n(this.currentPage,1),this.fullHeight=a,t(D,function(a,b){var c=a._legendItemPos[1],d=A(a.legendItem.getBBox().height),e=p.length;if(!e||c-p[e-1]>h&&(u||c)!==p[e-1])p.push(u||c),e++;b===D.length-1&&c+d-p[e-1]>h&&p.push(c);c!==u&&(u=c)}),k||(k=b.clipRect=d.clipRect(0,this.padding,9999,0),b.contentGroup.clip(k)),k.attr({height:h}),r||(this.nav=r=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,q,q).on("click",function(){b.scroll(-1,m)}).add(r),this.pager=d.text("",
15,10).css(l.style).add(r),this.down=d.symbol("triangle-down",0,0,q,q).on("click",function(){b.scroll(1,m)}).add(r)),b.scroll(0),a=f):r&&(k.attr({height:c.chartHeight}),r.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0);return a},scroll:function(a,b){var c=this.pages,d=c.length,e=this.currentPage+a,f=this.clipHeight,g=this.options.navigation,h=g.activeColor,g=g.inactiveColor,k=this.pager,l=this.padding;e>d&&(e=d);0<e&&(b!==u&&(S=n(b,this.chart.animation)),this.nav.attr({translateX:l,
translateY:f+this.padding+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:1===e?g:h}).css({cursor:1===e?"default":"pointer"}),k.attr({text:e+"/"+d}),this.down.attr({x:18+this.pager.getBBox().width,fill:e===d?g:h}).css({cursor:e===d?"default":"pointer"}),c=-c[e-1]+this.initialItemY,this.scrollGroup.animate({translateY:c}),this.currentPage=e,this.positionCheckboxes(c))}};var Lb=U.LegendSymbolMixin={drawRectangle:function(a,b){var c=a.options.symbolHeight||12;b.legendSymbol=this.chart.renderer.rect(0,
a.baseline-5-c/2,a.symbolWidth,c,a.options.symbolRadius||0).attr({zIndex:3}).add(b.legendGroup)},drawLineMarker:function(a){var b=this.options,c=b.marker,d;d=a.symbolWidth;var e=this.chart.renderer,f=this.legendGroup;a=a.baseline-A(.3*e.fontMetrics(a.options.itemStyle.fontSize,this.legendItem).b);var g;b.lineWidth&&(g={"stroke-width":b.lineWidth},b.dashStyle&&(g.dashstyle=b.dashStyle),this.legendLine=e.path(["M",0,a,"L",d,a]).attr(g).add(f));c&&!1!==c.enabled&&(b=c.radius,this.legendSymbol=d=e.symbol(this.symbol,
d/2-b,a-b,2*b,2*b).add(f),d.isMarker=!0)}};(/Trident\/7\.0/.test(na)||Ta)&&bb(Za.prototype,"positionItem",function(a,b){var c=this,d=function(){b._legendItemPos&&a.call(c,b)};d();setTimeout(d)});Ea.prototype={init:function(a,b){var c,d=a.series;a.series=null;c=G(M,a);c.series=a.series=d;this.userOptions=a;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];
this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=Y.length;Y.push(f);Ha++;!1!==d.reflow&&F(f,"load",function(){f.initReflow()});if(e)for(g in e)F(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=ca?!1:n(d.animation,!0);f.pointCount=f.colorCounter=f.symbolCounter=0;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=P[a.type||b.type||b.defaultSeriesType])||aa(17,!0);b=new b;b.init(this,a);return b},isInsidePlot:function(a,b,c){var d=c?b:a;a=c?a:b;return 0<=d&&d<=this.plotWidth&&
0<=a&&a<=this.plotHeight},adjustTickAmounts:function(){!1!==this.options.chart.alignTicks&&t(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,k=this.hasCartesianSeries,l=this.isDirtyBox,m=c.length,q=m,r=this.renderer,p=r.isHidden(),u=[];S=n(a,this.animation);p&&this.cloneRenderTo();for(this.layOutTitles();q--;)if(a=c[q],a.options.stacking&&(g=!0,a.isDirty)){h=!0;break}if(h)for(q=
m;q--;)a=c[q],a.options.stacking&&(a.isDirty=!0);t(c,function(a){a.isDirty&&"point"===a.options.legendType&&(f=!0)});f&&e.options.enabled&&(e.render(),this.isDirtyLegend=!1);g&&this.getStacks();k&&(this.isResizing||(this.maxTicks=null,t(b,function(a){a.setScale()})),this.adjustTickAmounts());this.getMargins();k&&(t(b,function(a){a.isDirty&&(l=!0)}),t(b,function(a){a.isDirtyExtremes&&(a.isDirtyExtremes=!1,u.push(function(){B(a,"afterSetExtremes",v(a.eventArgs,a.getExtremes()));delete a.eventArgs}));
(l||g)&&a.redraw()}));l&&this.drawChartBox();t(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset(!0);r.draw();B(this,"redraw");p&&this.cloneRenderTo(!0);t(u,function(a){a.call()})},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;d<b.length;d++)if(b[d].options.id===a)return b[d];for(d=0;d<c.length;d++)if(c[d].options.id===a)return c[d];for(d=0;d<c.length;d++)for(e=c[d].points||[],b=0;b<e.length;b++)if(e[b].id===a)return e[b];return null},getAxes:function(){var a=
this,b=this.options,c=b.xAxis=ea(b.xAxis||{}),b=b.yAxis=ea(b.yAxis||{});t(c,function(a,b){a.index=b;a.isX=!0});t(b,function(a,b){a.index=b});c=c.concat(b);t(c,function(b){new ma(a,b)});a.adjustTickAmounts()},getSelectedPoints:function(){var a=[];t(this.series,function(b){a=a.concat(jb(b.points||[],function(a){return a.selected}))});return a},getSelectedSeries:function(){return jb(this.series,function(a){return a.selected})},getStacks:function(){var a=this;t(a.yAxis,function(a){a.stacks&&a.hasVisibleSeries&&
(a.oldStacks=a.stacks)});t(a.series,function(b){!b.options.stacking||!0!==b.visible&&!1!==a.options.chart.ignoreHiddenSeries||(b.stackKey=b.type+n(b.options.stack,""))})},setTitle:function(a,b,c){var d=this,e=d.options,f;f=e.title=G(e.title,a);e=e.subtitle=G(e.subtitle,b);t([["title",a,f],["subtitle",b,e]],function(a){var b=a[0],c=d[b],e=a[1];a=a[2];c&&e&&(d[b]=c=c.destroy());a&&a.text&&!c&&(d[b]=d.renderer.text(a.text,0,0,a.useHTML).attr({align:a.align,"class":"highcharts-"+b,zIndex:a.zIndex||4}).css(a.style).add())});
d.layOutTitles(c)},layOutTitles:function(a){var b=0,c=this.title,d=this.subtitle,e=this.options,f=e.title,e=e.subtitle,g=this.renderer,h=this.spacingBox.width-44;c&&(c.css({width:(f.width||h)+"px"}).align(v({y:g.fontMetrics(f.style.fontSize,c).b-3},f),!1,"spacingBox"),f.floating||f.verticalAlign||(b=c.getBBox().height));d&&(d.css({width:(e.width||h)+"px"}).align(v({y:b+(f.margin-13)+g.fontMetrics(f.style.fontSize,d).b},e),!1,"spacingBox"),e.floating||e.verticalAlign||(b=ya(b+d.getBBox().height)));
c=this.titleOffset!==b;this.titleOffset=b;!this.isDirtyBox&&c&&(this.isDirtyBox=c,this.hasRendered&&n(a,!0)&&this.isDirtyBox&&this.redraw())},getChartSize:function(){var a=this.options.chart,b=a.width,a=a.height,c=this.renderToClone||this.renderTo;w(b)||(this.containerWidth=Va(c,"width"));w(a)||(this.containerHeight=Va(c,"height"));this.chartWidth=x(0,b||this.containerWidth||600);this.chartHeight=x(0,n(a,19<this.containerHeight?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,
c=this.container;a?b&&(this.renderTo.appendChild(c),Na(b),delete this.renderToClone):(c&&c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),J(b,{position:"absolute",top:"-9999px",display:"block"}),b.style.setProperty&&b.style.setProperty("display","block","important"),y.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+hb++;pa(a)&&(this.renderTo=a=y.getElementById(a));
a||aa(13,!0);c=E(N(a,"data-highcharts-chart"));!isNaN(c)&&Y[c]&&Y[c].hasRendered&&Y[c].destroy();N(a,"data-highcharts-chart",this.index);a.innerHTML="";b.skipClone||a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=ja("div",{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},v({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},
b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new Ja(a,c,d,b.style,!0):new Ja(a,c,d,b.style);ca&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.spacing,b,c=this.legend,d=this.margin,e=this.options.legend,f=n(e.margin,20),g=e.x,h=e.y,k=e.align,l=e.verticalAlign,m=this.titleOffset;this.resetMargins();b=this.axisOffset;m&&!w(d[0])&&(this.plotTop=x(this.plotTop,m+this.options.title.margin+a[0]));c.display&&!e.floating&&("right"===k?w(d[1])||
(this.marginRight=x(this.marginRight,c.legendWidth-g+f+a[1])):"left"===k?w(d[3])||(this.plotLeft=x(this.plotLeft,c.legendWidth+g+f+a[3])):"top"===l?w(d[0])||(this.plotTop=x(this.plotTop,c.legendHeight+h+f+a[0])):"bottom"!==l||w(d[2])||(this.marginBottom=x(this.marginBottom,c.legendHeight-h+f+a[2])));this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&t(this.axes,function(a){a.getOffset()});w(d[3])||(this.plotLeft+=
b[3]);w(d[0])||(this.plotTop+=b[0]);w(d[2])||(this.marginBottom+=b[2]);w(d[1])||(this.marginRight+=b[1]);this.setChartSize()},reflow:function(a){var b=this,c=b.options.chart,d=b.renderTo,e=c.width||Va(d,"width"),f=c.height||Va(d,"height"),c=a?a.target:I,d=function(){b.container&&(b.setSize(e,f,!1),b.hasUserSize=null)};if(!b.hasUserSize&&e&&f&&(c===I||c===y)){if(e!==b.containerWidth||f!==b.containerHeight)clearTimeout(b.reflowTimeout),a?b.reflowTimeout=setTimeout(d,100):d();b.containerWidth=e;b.containerHeight=
f}},initReflow:function(){var a=this,b=function(b){a.reflow(b)};F(I,"resize",b);F(a,"destroy",function(){L(I,"resize",b)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&B(d,"endResize",null,function(){--d.isResizing})};S=n(c,d.animation);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;w(a)&&(d.chartWidth=e=x(0,A(a)),d.hasUserSize=!!e);w(b)&&(d.chartHeight=f=x(0,A(b)));(S?Wa:J)(d.container,{width:e+"px",height:f+"px"},S);d.setChartSize(!0);d.renderer.setSize(e,
f,c);d.maxTicks=null;t(d.axes,function(a){a.isDirty=!0;a.setScale()});t(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.layOutTitles();d.getMargins();d.redraw(c);d.oldChartHeight=null;B(d,"resize");!1===S?g():setTimeout(g,S&&S.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset,k,l,m,q;this.plotLeft=k=A(this.plotLeft);this.plotTop=l=A(this.plotTop);this.plotWidth=
m=x(0,A(d-k-this.marginRight));this.plotHeight=q=x(0,A(e-l-this.marginBottom));this.plotSizeX=b?q:m;this.plotSizeY=b?m:q;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:k,y:l,width:m,height:q};d=2*K(this.plotBorderWidth/2);b=ya(x(d,h[3])/2);c=ya(x(d,h[0])/2);this.clipBox={x:b,y:c,width:K(this.plotSizeX-x(d,h[1])/2-b),height:x(0,K(this.plotSizeY-x(d,h[2])/2-c))};a||t(this.axes,function(a){a.setAxisSize();
a.setAxisTranslation()})},resetMargins:function(){var a=this.spacing,b=this.margin;this.plotTop=n(b[0],a[0]);this.marginRight=n(b[1],a[1]);this.marginBottom=n(b[2],a[2]);this.plotLeft=n(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,k=a.borderWidth||0,l=a.backgroundColor,m=a.plotBackgroundColor,q=a.plotBackgroundImage,
r=a.plotBorderWidth||0,p,n=this.plotLeft,t=this.plotTop,u=this.plotWidth,v=this.plotHeight,w=this.plotBox,x=this.clipRect,A=this.clipBox;p=k+(a.shadow?8:0);if(k||l)e?e.animate(e.crisp({width:c-p,height:d-p})):(e={fill:l||"none"},k&&(e.stroke=a.borderColor,e["stroke-width"]=k),this.chartBackground=b.rect(p/2,p/2,c-p,d-p,a.borderRadius,k).attr(e).addClass("highcharts-background").add().shadow(a.shadow));m&&(f?f.animate(w):this.plotBackground=b.rect(n,t,u,v,0).attr({fill:m}).add().shadow(a.plotShadow));
q&&(h?h.animate(w):this.plotBGImage=b.image(q,n,t,u,v).add());x?x.animate({width:A.width,height:A.height}):this.clipRect=b.clipRect(A);r&&(g?g.animate(g.crisp({x:n,y:t,width:u,height:v,strokeWidth:-r})):this.plotBorder=b.rect(n,t,u,v,0,-r).attr({stroke:a.plotBorderColor,"stroke-width":r,fill:"none",zIndex:1}).add());this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;t(["inverted","angular","polar"],function(g){c=P[b.type||b.defaultSeriesType];f=a[g]||
b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=P[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;t(b,function(a){a.linkedSeries.length=0});t(b,function(b){var d=b.options.linkedTo;pa(d)&&(d=":previous"===d?a.series[b.index-1]:a.get(d))&&(d.linkedSeries.push(b),b.linkedParent=d)})},renderSeries:function(){t(this.series,function(a){a.translate();a.setTooltipPoints&&a.setTooltipPoints();a.render()})},renderLabels:function(){var a=this,b=a.options.labels;
b.items&&t(b.items,function(c){var d=v(b.style,c.style),e=E(d.left)+a.plotLeft,f=E(d.top)+a.plotTop+12;delete d.left;delete d.top;a.renderer.text(c.html,e,f).attr({zIndex:2}).css(d).add()})},render:function(){var a=this.axes,b=this.renderer,c=this.options;this.setTitle();this.legend=new Za(this,c.legend);this.getStacks();t(a,function(a){a.setScale()});this.getMargins();this.maxTicks=null;t(a,function(a){a.setTickPositions(!0);a.setMaxTicks()});this.adjustTickAmounts();this.getMargins();this.drawChartBox();
this.hasCartesianSeries&&t(a,function(a){a.render()});this.seriesGroup||(this.seriesGroup=b.g("series-group").attr({zIndex:3}).add());this.renderSeries();this.renderLabels();this.showCredits(c.credits);this.hasRendered=!0},showCredits:function(a){a.enabled&&!this.credits&&(this.credits=this.renderer.text(a.text,0,0).on("click",function(){a.href&&(location.href=a.href)}).attr({align:a.position.align,zIndex:8}).css(a.style).add().align(a.position))},destroy:function(){var a=this,b=a.axes,c=a.series,
d=a.container,e,f=d&&d.parentNode;B(a,"destroy");Y[a.index]=u;Ha--;a.renderTo.removeAttribute("data-highcharts-chart");L(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();t("title subtitle chartBackground plotBackground plotBGImage plotBorder seriesGroup clipRect credits pointer scroller rangeSelector legend resetZoomButton tooltip renderer".split(" "),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});d&&(d.innerHTML="",L(d),f&&Na(d));for(e in a)delete a[e]},
isReadyToRender:function(){var a=this;return!Z&&I==I.top&&"complete"!==y.readyState||ca&&!I.canvg?(ca?CanVGController.push(function(){a.firstRender()},a.options.global.canvasToolsURL):y.attachEvent("onreadystatechange",function(){y.detachEvent("onreadystatechange",a.firstRender);"complete"===y.readyState&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;a.isReadyToRender()&&(a.getContainer(),B(a,"init"),a.resetMargins(),a.setChartSize(),a.propFromSeries(),a.getAxes(),
t(b.series||[],function(b){a.initSeries(b)}),a.linkSeries(),B(a,"beforeRender"),U.Pointer&&(a.pointer=new Ab(a,b)),a.render(),a.renderer.draw(),c&&c.apply(a,[a]),t(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),B(a,"load"))},splashArray:function(a,b){var c=b[a],c=da(c)?c:[c,c,c,c];return[n(b[a+"Top"],c[0]),n(b[a+"Right"],c[1]),n(b[a+"Bottom"],c[2]),n(b[a+"Left"],c[3])]}};Ea.prototype.callbacks=[];var Ca=function(){};Ca.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,
c);this.pointAttr={};a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length&&(a.colorCounter=0));a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.options.pointValKey||c.pointValKey;a=Ca.prototype.optionsToObject.call(this,a);v(this,a);this.options=this.options?v(this.options,a):a;d&&(this.y=this[d]);this.x===u&&c&&(this.x=b===u?c.autoIncrement():b);return this},optionsToObject:function(a){var b=
{},c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if("number"===typeof a||null===a)b[d[0]]=a;else if(ta(a))for(a.length>e&&(c=typeof a[0],"string"===c?b.name=a[0]:"number"===c&&(b.x=a[0]),f++);g<e;)b[d[g++]]=a[f++];else"object"===typeof a&&(b=a,a.dataLabels&&(c._hasPointLabels=!0),a.marker&&(c._hasPointMarkers=!0));return b},destroy:function(){var a=this.series.chart,b=a.hoverPoints,c;a.pointCount--;b&&(this.setState(),qa(b,this),b.length||(a.hoverPoints=null));if(this===a.hoverPoint)this.onMouseOut();
if(this.graphic||this.dataLabel)L(this),this.destroyElements();this.legendItem&&a.legend.destroyItem(this);for(c in this)this[c]=null},destroyElements:function(){for(var a="graphic dataLabel dataLabelUpper group connector shadowGroup".split(" "),b,c=6;c--;)b=a[c],this[b]&&(this[b]=this[b].destroy())},getLabelConfig:function(){return{x:this.category,y:this.y,key:this.name||this.category,series:this.series,point:this,percentage:this.percentage,total:this.total||this.stackTotal}},tooltipFormatter:function(a){var b=
this.series,c=b.tooltipOptions,d=n(c.valueDecimals,""),e=c.valuePrefix||"",f=c.valueSuffix||"";t(b.pointArrayMap||["y"],function(b){b="{point."+b;if(e||f)a=a.replace(b+"}",e+b+"}"+f);a=a.replace(b+"}",b+":,."+d+"f}")});return ua(a,{point:this,series:this.series})},firePointEvent:function(a,b,c){var d=this,e=this.series.options;(e.point.events[a]||d.options&&d.options.events&&d.options.events[a])&&this.importEvents();"click"===a&&e.allowPointSelect&&(c=function(a){d.select(null,a.ctrlKey||a.metaKey||
a.shiftKey)});B(this,a,b,c)}};var fa=function(){};fa.prototype={isCartesian:!0,type:"line",pointClass:Ca,sorted:!0,requireSorting:!0,pointAttrToOptions:{stroke:"lineColor","stroke-width":"lineWidth",fill:"fillColor",r:"radius"},axisTypes:["xAxis","yAxis"],colorCounter:0,parallelArrays:["x","y"],init:function(a,b){var c=this,d,e,f=a.series,g=function(a,b){return n(a.options.index,a._i)-n(b.options.index,b._i)};c.chart=a;c.options=b=c.setOptions(b);c.linkedSeries=[];c.bindAxes();v(c,{name:b.name,state:"",
pointAttr:{},visible:!1!==b.visible,selected:!0===b.selected});ca&&(b.animation=!1);e=b.events;for(d in e)F(c,d,e[d]);if(e&&e.click||b.point&&b.point.events&&b.point.events.click||b.allowPointSelect)a.runTrackerClick=!0;c.getColor();c.getSymbol();t(c.parallelArrays,function(a){c[a+"Data"]=[]});c.setData(b.data,!1);c.isCartesian&&(a.hasCartesianSeries=!0);f.push(c);c._i=f.length-1;cb(f,g);this.yAxis&&cb(this.yAxis.series,g);t(f,function(a,b){a.index=b;a.name=a.name||"Series "+(b+1)})},bindAxes:function(){var a=
this,b=a.options,c=a.chart,d;t(a.axisTypes||[],function(e){t(c[e],function(c){d=c.options;if(b[e]===d.index||b[e]!==u&&b[e]===d.id||b[e]===u&&0===d.index)c.series.push(a),a[e]=c,c.isDirty=!0});a[e]||a.optionalAxis===e||aa(18,!0)})},updateParallelArrays:function(a,b){var c=a.series,d=arguments;t(c.parallelArrays,"number"===typeof b?function(d){var f="y"===d&&c.toYData?c.toYData(a):a[d];c[d+"Data"][b]=f}:function(a){Array.prototype[b].apply(c[a+"Data"],Array.prototype.slice.call(d,2))})},autoIncrement:function(){var a=
this.options,b=this.xIncrement,b=n(b,a.pointStart,0);this.pointInterval=n(this.pointInterval,a.pointInterval,1);this.xIncrement=b+this.pointInterval;return b},getSegments:function(){var a=-1,b=[],c,d=this.points,e=d.length;if(e)if(this.options.connectNulls){for(c=e;c--;)null===d[c].y&&d.splice(c,1);d.length&&(b=[d])}else t(d,function(c,g){null===c.y?(g>a+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart,c=b.options.plotOptions,
b=b.userOptions||{},d=b.plotOptions||{},e=c[this.type];this.userOptions=a;c=G(e,c.series,a);this.tooltipOptions=G(M.tooltip,M.plotOptions[this.type].tooltip,b.tooltip,d.series&&d.series.tooltip,d[this.type]&&d[this.type].tooltip,a.tooltip);null===e.marker&&delete c.marker;return c},getCyclic:function(a,b,c){var d=this.userOptions,e="_"+a+"Index",f=a+"Counter";b||(w(d[e])?b=d[e]:(d[e]=b=this.chart[f]%c.length,this.chart[f]+=1),b=c[b]);this[a]=b},getColor:function(){this.options.colorByPoint||this.getCyclic("color",
this.options.color||Ya[this.type].color,this.chart.options.colors)},getSymbol:function(){var a=this.options.marker;this.getCyclic("symbol",a.symbol,this.chart.options.symbols);/^url/.test(this.symbol)&&(a.radius=0)},drawLegendSymbol:Lb.drawLineMarker,setData:function(a,b,c,d){var e=this,f=e.points,g=f&&f.length||0,h,k=e.options,l=e.chart,m=null,q=e.xAxis,r=q&&!!q.categories,p=e.tooltipPoints,v=k.turboThreshold,D=this.xData,w=this.yData,x=(h=e.pointArrayMap)&&h.length;a=a||[];h=a.length;b=n(b,!0);
if(!1===d||!h||g!==h||e.cropped||e.hasGroupedData){e.xIncrement=null;e.pointRange=r?1:k.pointRange;e.colorCounter=0;t(this.parallelArrays,function(a){e[a+"Data"].length=0});if(v&&h>v){for(c=0;null===m&&c<h;)m=a[c],c++;if(ga(m)){r=n(k.pointStart,0);k=n(k.pointInterval,1);for(c=0;c<h;c++)D[c]=r,w[c]=a[c],r+=k;e.xIncrement=r}else if(ta(m))if(x)for(c=0;c<h;c++)k=a[c],D[c]=k[0],w[c]=k.slice(1,x+1);else for(c=0;c<h;c++)k=a[c],D[c]=k[0],w[c]=k[1];else aa(12)}else for(c=0;c<h;c++)a[c]!==u&&(k={series:e},
e.pointClass.prototype.applyOptions.apply(k,[a[c]]),e.updateParallelArrays(k,c),r&&k.name&&(q.names[k.x]=k.name));pa(w[0])&&aa(14,!0);e.data=[];e.options.data=a;for(c=g;c--;)f[c]&&f[c].destroy&&f[c].destroy();p&&(p.length=0);q&&(q.minRange=q.userMinRange);e.isDirty=e.isDirtyData=l.isDirtyBox=!0;c=!1}else t(a,function(a,b){f[b].update(a,!1,null,!1)});b&&l.redraw(c)},processData:function(a){var b=this.xData,c=this.yData,d=b.length,e;e=0;var f,g,h=this.xAxis,k,l=this.options;k=l.cropThreshold;var m=
0,q=this.isCartesian,r,p;if(q&&!this.isDirty&&!h.isDirty&&!this.yAxis.isDirty&&!a)return!1;h&&(r=h.getExtremes(),p=r.min,r=r.max);if(q&&this.sorted&&(!k||d>k||this.forceCrop))if(b[d-1]<p||b[0]>r)b=[],c=[];else if(b[0]<p||b[d-1]>r)e=this.cropData(this.xData,this.yData,p,r),b=e.xData,c=e.yData,e=e.start,f=!0,m=b.length;for(k=b.length-1;0<=k;k--)d=b[k]-b[k-1],!f&&b[k]>p&&b[k]<r&&m++,0<d&&(g===u||d<g)?g=d:0>d&&this.requireSorting&&aa(15);this.cropped=f;this.cropStart=e;this.processedXData=b;this.processedYData=
c;this.activePointCount=m;null===l.pointRange&&(this.pointRange=g||1);this.closestPointRange=g},cropData:function(a,b,c,d){var e=a.length,f=0,g=e,h=n(this.cropShoulder,1),k;for(k=0;k<e;k++)if(a[k]>=c){f=x(0,k-h);break}for(;k<e;k++)if(a[k]>d){g=k+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data,b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,k,l=this.hasGroupedData,m,q=[],
r;b||l||(b=[],b.length=a.length,b=this.data=b);for(r=0;r<g;r++)k=h+r,l?q[r]=(new f).init(this,[d[r]].concat(ea(e[r]))):(b[k]?m=b[k]:a[k]!==u&&(b[k]=m=(new f).init(this,a[k],d[r])),q[r]=m),q[r].index=k;if(b&&(g!==(c=b.length)||l))for(r=0;r<c;r++)r!==h||l||(r+=g),b[r]&&(b[r].destroyElements(),b[r].plotX=u);this.data=b;this.points=q},getExtremes:function(a){var b=this.yAxis,c=this.processedXData,d,e=[],f=0;d=this.xAxis.getExtremes();var g=d.min,h=d.max,k,l,m,q;a=a||this.stackedYData||this.processedYData;
d=a.length;for(q=0;q<d;q++)if(l=c[q],m=a[q],k=null!==m&&m!==u&&(!b.isLog||m.length||0<m),l=this.getExtremesFromAll||this.cropped||(c[q+1]||l)>=g&&(c[q-1]||l)<=h,k&&l)if(k=m.length)for(;k--;)null!==m[k]&&(e[f++]=m[k]);else e[f++]=m;this.dataMin=n(void 0,La(e));this.dataMax=n(void 0,va(e))},translate:function(){this.processedXData||this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,k=a.pointPlacement,
l="between"===k||ga(k),m=a.threshold,a=0;a<g;a++){var q=f[a],r=q.x,p=q.y,t=q.low,v=b&&e.stacks[(this.negStacks&&p<m?"-":"")+this.stackKey];e.isLog&&0>=p&&(q.y=p=null,aa(10));q.plotX=c.translate(r,0,0,0,1,k,"flags"===this.type);b&&this.visible&&v&&v[r]&&(v=v[r],p=v.points[this.index+","+a],t=p[0],p=p[1],0===t&&(t=n(m,e.min)),e.isLog&&0>=t&&(t=null),q.total=q.stackTotal=v.total,q.percentage=v.total&&q.y/v.total*100,q.stackY=p,v.setOffset(this.pointXOffset||0,this.barW||0));q.yBottom=w(t)?e.translate(t,
0,1,0,1):null;h&&(p=this.modifyValue(p,q));q.plotY="number"===typeof p&&Infinity!==p?e.translate(p,0,1,0,1):u;q.clientX=l?c.translate(r,0,0,0,1):q.plotX;q.negative=q.y<(m||0);q.category=d&&d[q.x]!==u?d[q.x]:q.x}this.getSegments()},animate:function(a){var b=this.chart,c=b.renderer,d;d=this.options.animation;var e=this.clipBox||b.clipBox,f=b.inverted,g;d&&!da(d)&&(d=Ya[this.type].animation);g=["_sharedClip",d.duration,d.easing,e.height].join();a?(a=b[g],d=b[g+"m"],a||(b[g]=a=c.clipRect(v(e,{width:0})),
b[g+"m"]=d=c.clipRect(-99,f?-b.plotLeft:-b.plotTop,99,f?b.chartWidth:b.chartHeight)),this.group.clip(a),this.markerGroup.clip(d),this.sharedClipKey=g):((a=b[g])&&a.animate({width:b.plotSizeX},d),b[g+"m"]&&b[g+"m"].animate({width:b.plotSizeX+99},d),this.animate=null)},afterAnimate:function(){var a=this.chart,b=this.sharedClipKey,c=this.group,d=this.clipBox;c&&!1!==this.options.clip&&(b&&d||c.clip(d?a.renderer.clipRect(d):a.clipRect),this.markerGroup.clip());B(this,"afterAnimate");setTimeout(function(){b&&
a[b]&&(d||(a[b]=a[b].destroy()),a[b+"m"]&&(a[b+"m"]=a[b+"m"].destroy()))},100)},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,k,l,m,q=this.options.marker,r=this.pointAttr[""],p,t,w,x=this.markerGroup,y=n(q.enabled,!this.requireSorting||this.activePointCount<.5*this.xAxis.len/q.radius);if(!1!==q.enabled||this._hasPointMarkers)for(f=b.length;f--;)g=b[f],d=K(g.plotX),e=g.plotY,m=g.graphic,p=g.marker||{},t=!!g.marker,a=y&&p.enabled===u||p.enabled,w=c.isInsidePlot(A(d),e,c.inverted),
a&&e!==u&&!isNaN(e)&&null!==g.y?(a=g.pointAttr[g.selected?"select":""]||r,h=a.r,k=n(p.symbol,this.symbol),l=0===k.indexOf("url"),m?m[w?"show":"hide"](!0).animate(v({x:d-h,y:e-h},m.symbolName?{width:2*h,height:2*h}:{})):w&&(0<h||l)&&(g.graphic=c.renderer.symbol(k,d-h,e-h,2*h,2*h,t?p:q).attr(a).add(x))):m&&(g.graphic=m.destroy())},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={};a=a||{};b=b||{};c=c||{};d=d||{};for(f in e)g=e[f],h[f]=n(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=
this,b=a.options,c=Ya[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color;f={stroke:g,fill:g};var h=a.points||[],k,l=[],m,q=a.pointAttrToOptions;m=a.hasPointSpecificOptions;var r=b.negativeColor,p=c.lineColor,n=c.fillColor;k=b.turboThreshold;var u;b.marker?(e.radius=e.radius||c.radius+e.radiusPlus,e.lineWidth=e.lineWidth||c.lineWidth+e.lineWidthPlus):e.color=e.color||za(e.color||g).brighten(e.brightness).get();l[""]=a.convertAttribs(c,f);t(["hover","select"],function(b){l[b]=a.convertAttribs(d[b],
l[""])});a.pointAttr=l;g=h.length;if(!k||g<k||m)for(;g--;){k=h[g];(c=k.options&&k.options.marker||k.options)&&!1===c.enabled&&(c.radius=0);k.negative&&r&&(k.color=k.fillColor=r);m=b.colorByPoint||k.color;if(k.options)for(u in q)w(c[q[u]])&&(m=!0);m?(c=c||{},m=[],d=c.states||{},f=d.hover=d.hover||{},b.marker||(f.color=f.color||!k.options.color&&e.color||za(k.color).brighten(f.brightness||e.brightness).get()),f={color:k.color},n||(f.fillColor=k.color),p||(f.lineColor=k.color),m[""]=a.convertAttribs(v(f,
c),l[""]),m.hover=a.convertAttribs(d.hover,l.hover,m[""]),m.select=a.convertAttribs(d.select,l.select,m[""])):m=l;k.pointAttr=m}},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(na),d,e,f=a.data||[],g,h,k;B(a,"destroy");L(a);t(a.axisTypes||[],function(b){if(k=a[b])qa(k.series,a),k.isDirty=k.forceRedraw=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(e=f.length;e--;)(g=f[e])&&g.destroy&&g.destroy();a.points=null;clearTimeout(a.animationTimeout);t("area graph dataLabelsGroup group markerGroup tracker graphNeg areaNeg posClip negClip".split(" "),
function(b){a[b]&&(d=c&&"group"===b?"hide":"destroy",a[b][d]())});b.hoverSeries===a&&(b.hoverSeries=null);qa(b.series,a);for(h in a)delete a[h]},getSegmentPath:function(a){var b=this,c=[],d=b.options.step;t(a,function(e,f){var g=e.plotX,h=e.plotY,k;b.getPointSpline?c.push.apply(c,b.getPointSpline(a,e,f)):(c.push(f?"L":"M"),d&&f&&(k=a[f-1],"right"===d?c.push(k.plotX,h):"center"===d?c.push((k.plotX+g)/2,k.plotY,(k.plotX+g)/2,h):c.push(g,k.plotY)),c.push(e.plotX,e.plotY))});return c},getGraphPath:function(){var a=
this,b=[],c,d=[];t(a.segments,function(e){c=a.getSegmentPath(e);1<e.length?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f="square"!==b.linecap,g=this.getGraphPath(),h=b.negativeColor;h&&c.push(["graphNeg",h]);t(c,function(c,h){var m=c[0],q=a[m];q?(Xa(q),q.animate({d:g})):d&&g.length&&(q={stroke:c[1],"stroke-width":d,fill:"none",zIndex:1},e?q.dashstyle=e:f&&(q["stroke-linecap"]=
q["stroke-linejoin"]="round"),a[m]=a.chart.renderer.path(g).attr(q).add(a.group).shadow(!h&&b.shadow))})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor||a.negativeFillColor,e,f=this.graph,g=this.area,h=this.posClip,k=this.negClip;e=b.chartWidth;var l=b.chartHeight,m=x(e,l),q=this.yAxis;d&&(f||g)&&(d=A(q.toPixels(a.threshold||0,!0)),0>d&&(m-=d),a={x:0,y:0,width:m,height:d},m={x:0,y:d,width:m,height:m},b.inverted&&(a.height=m.y=b.plotWidth-d,c.isVML&&(a={x:b.plotWidth-
d-b.plotLeft,y:0,width:e,height:l},m={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e})),q.reversed?(b=m,e=a):(b=a,e=m),h?(h.animate(b),k.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=k=c.clipRect(e),f&&this.graphNeg&&(f.clip(h),this.graphNeg.clip(k)),g&&(g.clip(h),this.areaNeg.clip(k))))},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};t(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;b.xAxis&&(F(c,"resize",a),F(b,
"destroy",function(){L(c,"resize",a)}),a(),b.invertGroups=a)},plotGroup:function(a,b,c,d,e){var f=this[a],g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({visibility:c,zIndex:d||.1}).add(e));f[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){var a=this.chart,b=this.xAxis,c=this.yAxis;a.inverted&&(b=c,c=this.xAxis);return{translateX:b?b.left:a.plotLeft,translateY:c?c.top:a.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this,b=a.chart,c,d=a.options,e=(c=d.animation)&&!!a.animate&&
b.renderer.isSVG&&n(c.duration,500)||0,f=a.visible?"visible":"hidden",g=d.zIndex,h=a.hasRendered,k=b.seriesGroup;c=a.plotGroup("group","series",f,g,k);a.markerGroup=a.plotGroup("markerGroup","markers",f,g,k);e&&a.animate(!0);a.getAttribs();c.inverted=a.isCartesian?b.inverted:!1;a.drawGraph&&(a.drawGraph(),a.clipNeg());t(a.points,function(a){a.redraw&&a.redraw()});a.drawDataLabels&&a.drawDataLabels();a.visible&&a.drawPoints();a.drawTracker&&!1!==a.options.enableMouseTracking&&a.drawTracker();b.inverted&&
a.invertGroups();!1===d.clip||a.sharedClipKey||h||c.clip(b.clipRect);e&&a.animate();h||(e?a.animationTimeout=setTimeout(function(){a.afterAnimate()},e):a.afterAnimate());a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:n(d&&d.left,a.plotLeft),translateY:n(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints&&this.setTooltipPoints(!0);
this.render();b&&B(this,"updatedData")}};v(Ea.prototype,{addSeries:function(a,b,c){var d,e=this;a&&(b=n(b,!0),B(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;e.linkSeries();b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var e=b?"xAxis":"yAxis",f=this.options;new ma(this,G(a,{index:this[e].length,isX:b}));f[e]=ea(f[e]||{});f[e].push(a);n(c,!0)&&this.redraw(d)},showLoading:function(a){var b=this,c=b.options,d=b.loadingDiv,e=c.loading,f=function(){d&&J(d,{left:b.plotLeft+
"px",top:b.plotTop+"px",width:b.plotWidth+"px",height:b.plotHeight+"px"})};d||(b.loadingDiv=d=ja("div",{className:"highcharts-loading"},v(e.style,{zIndex:10,display:"none"}),b.container),b.loadingSpan=ja("span",null,e.labelStyle,d),F(b,"redraw",f));b.loadingSpan.innerHTML=a||c.lang.loading;b.loadingShown||(J(d,{opacity:0,display:""}),Wa(d,{opacity:e.style.opacity},{duration:e.showDuration||0}),b.loadingShown=!0);f()},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&Wa(b,{opacity:0},
{duration:a.loading.hideDuration||100,complete:function(){J(b,{display:"none"})}});this.loadingShown=!1}});v(Ca.prototype,{update:function(a,b,c,d){function e(){f.applyOptions(a);da(a)&&!ta(a)&&(f.redraw=function(){h&&(a&&a.marker&&a.marker.symbol?f.graphic=h.destroy():h.attr(f.pointAttr[f.state||""]));a&&a.dataLabels&&f.dataLabel&&(f.dataLabel=f.dataLabel.destroy());f.redraw=null});k=f.index;g.updateParallelArrays(f,k);m.data[k]=f.options;g.isDirty=g.isDirtyData=!0;!g.fixedBox&&g.hasCartesianSeries&&
(l.isDirtyBox=!0);"point"===m.legendType&&l.legend.destroyItem(f);b&&l.redraw(c)}var f=this,g=f.series,h=f.graphic,k,l=g.chart,m=g.options;b=n(b,!0);!1===d?e():f.firePointEvent("update",{options:a},e)},remove:function(a,b){var c=this,d=c.series,e=d.points,f=d.chart,g,h=d.data;S=n(b,f.animation);a=n(a,!0);c.firePointEvent("remove",null,function(){g=Ia(c,h);h.length===e.length&&e.splice(g,1);h.splice(g,1);d.options.data.splice(g,1);d.updateParallelArrays(c,"splice",g,1);c.destroy();d.isDirty=!0;d.isDirtyData=
!0;a&&f.redraw()})}});v(fa.prototype,{addPoint:function(a,b,c,d){var e=this.options,f=this.data,g=this.graph,h=this.area,k=this.chart,l=this.xAxis&&this.xAxis.names,m=g&&g.shift||0,q=e.data,r,p=this.xData;S=n(d,k.animation);c&&t([g,h,this.graphNeg,this.areaNeg],function(a){a&&(a.shift=m+1)});h&&(h.isArea=!0);b=n(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);g=d.x;h=p.length;if(this.requireSorting&&g<p[h-1])for(r=!0;h&&p[h-1]>g;)h--;this.updateParallelArrays(d,"splice",
h,0,0);this.updateParallelArrays(d,h);l&&d.name&&(l[g]=d.name);q.splice(h,0,a);r&&(this.data.splice(h,0,null),this.processData());"point"===e.legendType&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),this.updateParallelArrays(d,"shift"),q.shift()));this.isDirtyData=this.isDirty=!0;b&&(this.getAttribs(),k.redraw())},remove:function(a,b){var c=this,d=c.chart;a=n(a,!0);c.isRemoving||(c.isRemoving=!0,B(c,"remove",null,function(){c.destroy();d.isDirtyLegend=d.isDirtyBox=!0;d.linkSeries();
a&&d.redraw(b)}));c.isRemoving=!1},update:function(a,b){var c=this,d=this.chart,e=this.userOptions,f=this.type,g=P[f].prototype,h=["group","markerGroup","dataLabelsGroup"],k;t(h,function(a){h[a]=c[a];delete c[a]});a=G(e,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1);for(k in g)g.hasOwnProperty(k)&&(this[k]=u);v(this,P[a.type||f].prototype);t(h,function(a){c[a]=h[a]});this.init(d,a);d.linkSeries();n(b,!0)&&d.redraw(!1)}});v(ma.prototype,{update:function(a,
b){var c=this.chart;a=c.options[this.coll][this.options.index]=G(this.userOptions,a);this.destroy(!0);this._addedPlotLB=u;this.init(c,v(a,{events:u}));c.isDirtyBox=!0;n(b,!0)&&c.redraw()},remove:function(a){for(var b=this.chart,c=this.coll,d=this.series,e=d.length;e--;)d[e]&&d[e].remove(!1);qa(b.axes,this);qa(b[c],this);b.options[c].splice(this.options.index,1);t(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;n(a,!0)&&b.redraw()},setTitle:function(a,b){this.update({title:a},
b)},setCategories:function(a,b){this.update({categories:a},b)}});var Mb=nb(fa);P.line=Mb;fa.prototype.drawDataLabels=function(){var a=this,b=a.options,c=b.cursor,d=b.dataLabels,e=a.points,f,g,h=a.hasRendered||0,k,l;if(d.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(d),l=a.plotGroup("dataLabelsGroup","data-labels",d.defer?"hidden":"visible",d.zIndex||6),n(d.defer,!0)&&(l.attr({opacity:+h}),h||F(a,"afterAnimate",function(){a.visible&&l.show();l[b.animation?"animate":"attr"]({opacity:1},
{duration:200})})),g=d,t(e,function(b){var e,h=b.dataLabel,p,t,x=b.connector,A=!0;f=b.options&&b.options.dataLabels;e=n(f&&f.enabled,g.enabled);if(h&&!e)b.dataLabel=h.destroy();else if(e){d=G(g,f);e=d.rotation;p=b.getLabelConfig();k=d.format?ua(d.format,p):d.formatter.call(p,d);d.style.color=n(d.color,d.style.color,a.color,"black");if(h)w(k)?(h.attr({text:k}),A=!1):(b.dataLabel=h=h.destroy(),x&&(b.connector=x.destroy()));else if(w(k)){h={fill:d.backgroundColor,stroke:d.borderColor,"stroke-width":d.borderWidth,
r:d.borderRadius||0,rotation:e,padding:d.padding,zIndex:1};for(t in h)h[t]===u&&delete h[t];h=b.dataLabel=a.chart.renderer[e?"text":"label"](k,0,-999,null,null,null,d.useHTML).attr(h).css(v(d.style,c&&{cursor:c})).add(l).shadow(d.shadow)}h&&a.alignDataLabel(b,h,d,null,A)}})};fa.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=n(a.plotX,-999),k=n(a.plotY,-999),l=b.getBBox();if(a=this.visible&&(a.series.forceDL||f.isInsidePlot(h,A(k),g)||d&&f.isInsidePlot(h,g?d.x+1:d.y+d.height-
1,g)))d=v({x:g?f.plotWidth-k:h,y:A(g?f.plotHeight-h:k),width:0,height:0},d),v(c,{width:l.width,height:l.height}),c.rotation?b[e?"attr":"animate"]({x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2}).attr({align:c.align}):(b.align(c,null,d),g=b.alignAttr,"justify"===n(c.overflow,"justify")?this.justifyDataLabel(b,c,g,l,d,e):n(c.crop,!0)&&(a=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+l.width,g.y+l.height)));a||(b.attr({y:-999}),b.placed=!1)};fa.prototype.justifyDataLabel=function(a,b,c,d,e,f){var g=this.chart,
h=b.align,k=b.verticalAlign,l,m;l=c.x;0>l&&("right"===h?b.align="left":b.x=-l,m=!0);l=c.x+d.width;l>g.plotWidth&&("left"===h?b.align="right":b.x=g.plotWidth-l,m=!0);l=c.y;0>l&&("bottom"===k?b.verticalAlign="top":b.y=-l,m=!0);l=c.y+d.height;l>g.plotHeight&&("top"===k?b.verticalAlign="bottom":b.y=g.plotHeight-l,m=!0);m&&(a.placed=!f,a.align(b,null,e))};P.pie&&(P.pie.prototype.drawDataLabels=function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=n(e.connectorPadding,10),g=n(e.connectorWidth,
1),h=d.plotWidth,k=d.plotHeight,l,m,q=n(e.softConnector,!0),r=e.distance,p=a.center,u=p[2]/2,v=p[1],w=0<r,y,z,B,G=[[],[]],E,F,N,M,C,I=[0,0,0,0],R=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){fa.prototype.drawDataLabels.apply(a);t(b,function(a){a.dataLabel&&a.visible&&G[a.half].push(a)});for(M=2;M--;){var K=[],P=[],J=G[M],O=J.length,L;if(O){a.sortByAngle(J,M-.5);for(C=b=0;!b&&J[C];)b=J[C]&&J[C].dataLabel&&(J[C].dataLabel.getBBox().height||21),C++;if(0<r){z=T(v+u+r,d.plotHeight);
for(C=x(0,v-u-r);C<=z;C+=b)K.push(C);z=K.length;if(O>z){c=[].concat(J);c.sort(R);for(C=O;C--;)c[C].rank=C;for(C=O;C--;)J[C].rank>=z&&J.splice(C,1);O=J.length}for(C=0;C<O;C++){c=J[C];B=c.labelPos;c=9999;var U,S;for(S=0;S<z;S++)U=X(K[S]-B[1]),U<c&&(c=U,L=S);if(L<C&&null!==K[C])L=C;else for(z<O-C+L&&null!==K[C]&&(L=z-O+C);null===K[L];)L++;P.push({i:L,y:K[L]});K[L]=null}P.sort(R)}for(C=0;C<O;C++){c=J[C];B=c.labelPos;y=c.dataLabel;N=!1===c.visible?"hidden":"visible";c=B[1];if(0<r){if(z=P.pop(),L=z.i,F=
z.y,c>F&&null!==K[L+1]||c<F&&null!==K[L-1])F=T(x(0,c),d.plotHeight)}else F=c;E=e.justify?p[0]+(M?-1:1)*(u+r):a.getX(F===v-u-r||F===v+u+r?c:F,M);y._attr={visibility:N,align:B[6]};y._pos={x:E+e.x+({left:f,right:-f}[B[6]]||0),y:F+e.y-10};y.connX=E;y.connY=F;null===this.options.size&&(z=y.width,E-z<f?I[3]=x(A(z-E+f),I[3]):E+z>h-f&&(I[1]=x(A(E+z-h+f),I[1])),0>F-b/2?I[0]=x(A(-F+b/2),I[0]):F+b/2>k&&(I[2]=x(A(F+b/2-k),I[2])))}}}if(0===va(I)||this.verifyDataLabelOverflow(I))this.placeDataLabels(),w&&g&&t(this.points,
function(b){l=b.connector;B=b.labelPos;(y=b.dataLabel)&&y._pos?(N=y._attr.visibility,E=y.connX,F=y.connY,m=q?["M",E+("left"===B[6]?5:-5),F,"C",E,F,2*B[2]-B[4],2*B[3]-B[5],B[2],B[3],"L",B[4],B[5]]:["M",E+("left"===B[6]?5:-5),F,"L",B[2],B[3],"L",B[4],B[5]],l?(l.animate({d:m}),l.attr("visibility",N)):b.connector=l=a.chart.renderer.path(m).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:N}).add(a.dataLabelsGroup)):l&&(b.connector=l.destroy())})}},P.pie.prototype.placeDataLabels=
function(){t(this.points,function(a){a=a.dataLabel;var b;a&&((b=a._pos)?(a.attr(a._attr),a[a.moved?"animate":"attr"](b),a.moved=!0):a&&a.attr({y:-999}))})},P.pie.prototype.alignDataLabel=Eb,P.pie.prototype.verifyDataLabelOverflow=function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize||80,f;null!==d[0]?e=x(b[2]-x(a[1],a[3]),c):(e=x(b[2]-a[1]-a[3],c),b[0]+=(a[3]-a[1])/2);null!==d[1]?e=x(T(e,b[2]-x(a[0],a[2])),c):(e=x(T(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);e<b[2]?(b[2]=e,this.translate(b),
t(this.points,function(a){a.dataLabel&&(a.dataLabel._pos=null)}),this.drawDataLabels&&this.drawDataLabels()):f=!0;return f});P.column&&(P.column.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=a.dlBox||a.shapeArgs,k=a.below||a.plotY>n(this.translatedThreshold,f.plotSizeY),l=n(c.inside,!!this.options.stacking);h&&(d=G(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),l||(g?(d.x+=k?0:d.width,d.width=0):(d.y+=k?d.height:0,d.height=
0)));c.align=n(c.align,!g||l?"center":k?"right":"left");c.verticalAlign=n(c.verticalAlign,g||l?"middle":k?"top":"bottom");fa.prototype.alignDataLabel.call(this,a,b,c,d,e)});var $a=U.TrackerMixin={drawTrackerPoint:function(){var a=this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(c){var d=c.target,e;if(b.hoverSeries!==a)a.onMouseOver();for(;d&&!e;)e=d.point,d=d.parentNode;if(e!==u&&e!==b.hoverPoint)e.onMouseOver(c)};t(a.points,function(a){a.graphic&&(a.graphic.element.point=
a);a.dataLabel&&(a.dataLabel.element.point=a)});a._hasTracking||(t(a.trackerGroups,function(b){if(a[b]&&(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),Ga))a[b].on("touchstart",f)}),a._hasTracking=!0)},drawTrackerGraph:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,k=f.options.tooltip.snap,l=a.tracker,m=b.cursor,q=m&&{cursor:m},m=a.singlePoints,n,
p=function(){if(f.hoverSeries!==a)a.onMouseOver()},u="rgba(192,192,192,"+(Z?1E-4:.002)+")";if(e&&!c)for(n=e+1;n--;)"M"===d[n]&&d.splice(n+1,0,d[n+1]-k,d[n+2],"L"),(n&&"M"===d[n]||n===e)&&d.splice(n,0,"L",d[n-2]+k,d[n-1]);for(n=0;n<m.length;n++)e=m[n],d.push("M",e.plotX-k,e.plotY,"L",e.plotX+k,e.plotY);l?l.attr({d:d}):(a.tracker=h.path(d).attr({"stroke-linejoin":"round",visibility:a.visible?"visible":"hidden",stroke:u,fill:c?u:"none","stroke-width":b.lineWidth+(c?0:2*k),zIndex:2}).add(a.group),t([a.tracker,
a.markerGroup],function(a){a.addClass("highcharts-tracker").on("mouseover",p).on("mouseout",function(a){g.onTrackerMouseOut(a)}).css(q);if(Ga)a.on("touchstart",p)}))}};P.column&&(ColumnSeries.prototype.drawTracker=$a.drawTrackerPoint);P.pie&&(P.pie.prototype.drawTracker=$a.drawTrackerPoint);P.scatter&&(ScatterSeries.prototype.drawTracker=$a.drawTrackerPoint);v(Za.prototype,{setItemEvents:function(a,b,c,d,e){var f=this;(c?b:a.legendGroup).on("mouseover",function(){a.setState("hover");b.css(f.options.itemHoverStyle)}).on("mouseout",
function(){b.css(a.visible?d:e);a.setState()}).on("click",function(b){var c=function(){a.setVisible()};b={browserEvent:b};a.firePointEvent?a.firePointEvent("legendItemClick",b,c):B(a,"legendItemClick",b,c)})},createCheckboxForItem:function(a){a.checkbox=ja("input",{type:"checkbox",checked:a.selected,defaultChecked:a.selected},this.options.itemCheckboxStyle,this.chart.container);F(a.checkbox,"click",function(b){B(a,"checkboxClick",{checked:b.target.checked},function(){a.select()})})}});M.legend.itemStyle.cursor=
"pointer";v(Ea.prototype,{showResetZoom:function(){var a=this,b=M.lang,c=a.options.chart.resetZoomButton,d=c.theme,e=d.states,f="chart"===c.relativeTo?null:"plotBox";this.resetZoomButton=a.renderer.button(b.resetZoom,null,null,function(){a.zoomOut()},d,e&&e.hover).attr({align:c.position.align,title:b.resetZoomTitle}).add().align(c.position,!1,f)},zoomOut:function(){var a=this;B(a,"selection",{resetSelection:!0},function(){a.zoom()})},zoom:function(a){var b,c=this.pointer,d=!1,e;!a||a.resetSelection?
t(this.axes,function(a){b=a.zoom()}):t(a.xAxis.concat(a.yAxis),function(a){var e=a.axis,h=e.isXAxis;if(c[h?"zoomX":"zoomY"]||c[h?"pinchX":"pinchY"])b=e.zoom(a.min,a.max),e.displayBtn&&(d=!0)});e=this.resetZoomButton;d&&!e?this.showResetZoom():!d&&da(e)&&(this.resetZoomButton=e.destroy());b&&this.redraw(n(this.options.chart.animation,a&&a.animation,100>this.pointCount))},pan:function(a,b){var c=this,d=c.hoverPoints,e;d&&t(d,function(a){a.setState()});t("xy"===b?[1,0]:[1],function(b){var d=a[b?"chartX":
"chartY"],h=c[b?"xAxis":"yAxis"][0],k=c[b?"mouseDownX":"mouseDownY"],l=(h.pointRange||0)/2,m=h.getExtremes(),n=h.toValue(k-d,!0)+l,k=h.toValue(k+c[b?"plotWidth":"plotHeight"]-d,!0)-l;h.series.length&&n>T(m.dataMin,m.min)&&k<x(m.dataMax,m.max)&&(h.setExtremes(n,k,!1,!1,{trigger:"pan"}),e=!0);c[b?"mouseDownX":"mouseDownY"]=d});e&&c.redraw(!1);J(c.container,{cursor:"move"})}});v(Ca.prototype,{select:function(a,b){var c=this,d=c.series,e=d.chart;a=n(a,!c.selected);c.firePointEvent(a?"select":"unselect",
{accumulate:b},function(){c.selected=c.options.selected=a;d.options.data[Ia(c,d.data)]=c.options;c.setState(a&&"select");b||t(e.getSelectedPoints(),function(a){a.selected&&a!==c&&(a.selected=a.options.selected=!1,d.options.data[Ia(a,d.data)]=a.options,a.setState(""),a.firePointEvent("unselect"))})})},onMouseOver:function(a){var b=this.series,c=b.chart,d=c.tooltip,e=c.hoverPoint;if(e&&e!==this)e.onMouseOut();this.firePointEvent("mouseOver");!d||d.shared&&!b.noSharedTooltip||d.refresh(this,a);this.setState("hover");
c.hoverPoint=this},onMouseOut:function(){var a=this.series.chart,b=a.hoverPoints;this.firePointEvent("mouseOut");b&&-1!==Ia(this,b)||(this.setState(),a.hoverPoint=null)},importEvents:function(){if(!this.hasImportedEvents){var a=G(this.series.options.point,this.options).events,b;this.events=a;for(b in a)F(this,b,a[b]);this.hasImportedEvents=!0}},setState:function(a,b){var c=this.plotX,d=this.plotY,e=this.series,f=e.options.states,g=Ya[e.type].marker&&e.options.marker,h=g&&!g.enabled,k=g&&g.states[a],
l=k&&!1===k.enabled,m=e.stateMarkerGraphic,n=this.marker||{},r=e.chart,p=e.halo,t;a=a||"";t=this.pointAttr[a]||e.pointAttr[a];if(!(a===this.state&&!b||this.selected&&"select"!==a||f[a]&&!1===f[a].enabled||a&&(l||h&&!1===k.enabled)||a&&n.states&&n.states[a]&&!1===n.states[a].enabled)){if(this.graphic)g=g&&this.graphic.symbolName&&t.r,this.graphic.attr(G(t,g?{x:c-g,y:d-g,width:2*g,height:2*g}:{})),m&&m.hide();else{if(a&&k)if(g=k.radius,n=n.symbol||e.symbol,m&&m.currentSymbol!==n&&(m=m.destroy()),m)m[b?
"animate":"attr"]({x:c-g,y:d-g});else n&&(e.stateMarkerGraphic=m=r.renderer.symbol(n,c-g,d-g,2*g,2*g).attr(t).add(e.markerGroup),m.currentSymbol=n);if(m)m[a&&r.isInsidePlot(c,d,r.inverted)?"show":"hide"]()}(c=f[a]&&f[a].halo)&&c.size?(p||(e.halo=p=r.renderer.path().add(e.seriesGroup)),p.attr(v({fill:za(this.color||e.color).setOpacity(c.opacity).get()},c.attributes))[b?"animate":"attr"]({d:this.haloPath(c.size)})):p&&p.attr({d:[]});this.state=a}},haloPath:function(a){var b=this.series,c=b.chart,d=
b.getPlotBox(),e=c.inverted;return c.renderer.symbols.circle(d.translateX+(e?b.yAxis.len-this.plotY:this.plotX)-a,d.translateY+(e?b.xAxis.len-this.plotX:this.plotY)-a,2*a,2*a)}});v(fa.prototype,{onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&B(this,"mouseOver");this.setState("hover");a.hoverSeries=this},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&
B(this,"mouseOut");!c||a.stickyTracking||c.shared&&!this.noSharedTooltip||c.hide();this.setState();b.hoverSeries=null},setState:function(a){var b=this.options,c=this.graph,d=this.graphNeg,e=b.states,b=b.lineWidth;a=a||"";this.state!==a&&(this.state=a,e[a]&&!1===e[a].enabled||(a&&(b=e[a].lineWidth||b+(e[a].lineWidthPlus||0)),c&&!c.dashstyle&&(a={"stroke-width":b},c.attr(a),d&&d.attr(a))))},setVisible:function(a,b){var c=this,d=c.chart,e=c.legendItem,f,g=d.options.chart.ignoreHiddenSeries,h=c.visible;
f=(c.visible=a=c.userOptions.visible=a===u?!h:a)?"show":"hide";t(["group","dataLabelsGroup","markerGroup","tracker"],function(a){if(c[a])c[a][f]()});if(d.hoverSeries===c)c.onMouseOut();e&&d.legend.colorizeItem(c,a);c.isDirty=!0;c.options.stacking&&t(d.series,function(a){a.options.stacking&&a.visible&&(a.isDirty=!0)});t(c.linkedSeries,function(b){b.setVisible(a,!1)});g&&(d.isDirtyBox=!0);!1!==b&&d.redraw();B(c,f)},setTooltipPoints:function(a){var b=[],c,d,e=this.xAxis,f=e&&e.getExtremes(),g=e?e.tooltipLen||
e.len:this.chart.plotSizeX,h,k,l=[];if(!1!==this.options.enableMouseTracking&&!this.singularTooltips){a&&(this.tooltipPoints=null);t(this.segments||this.points,function(a){b=b.concat(a)});e&&e.reversed&&(b=b.reverse());this.orderTooltipPoints&&this.orderTooltipPoints(b);a=b.length;for(k=0;k<a;k++)if(e=b[k],c=e.x,c>=f.min&&c<=f.max)for(h=b[k+1],c=d===u?0:d+1,d=b[k+1]?T(x(0,K((e.clientX+(h?h.wrappedClientX||h.clientX:g))/2)),g):g;0<=c&&c<=d;)l[c++]=e;this.tooltipPoints=l}},show:function(){this.setVisible(!0)},
hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===u?!this.selected:a;this.checkbox&&(this.checkbox.checked=a);B(this,a?"select":"unselect")},drawTracker:$a.drawTrackerGraph});v(U,{Axis:ma,Chart:Ea,Color:za,Point:Ca,Tick:xa,Renderer:Ja,Series:fa,SVGElement:O,SVGRenderer:Ja,arrayMin:La,arrayMax:va,charts:Y,dateFormat:Ka,format:ua,pathAnim:ib,getOptions:function(){return M},hasBidiBug:Db,isTouchDevice:wb,numberFormat:ka,seriesTypes:P,setOptions:function(a){M=G(!0,M,a);pb();
return M},addEvent:F,removeEvent:L,createElement:ja,discardElement:Na,css:J,each:t,extend:v,map:kb,merge:G,pick:n,splat:ea,extendClass:nb,pInt:E,wrap:bb,svg:Z,canvas:ca,vml:!Z&&!ca,product:"Highcharts 4.0.4",version:"/Highstock 2.0.4"})})();

View file

@ -0,0 +1,381 @@
/*globals define, module, require, document*/
(function (root, factory) {
"use strict";
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = factory();
} else {
root.JsonHuman = factory();
}
}(this, function () {
"use strict";
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
function makePrefixer(prefix) {
return function (name) {
return prefix + "-" + name;
};
}
function isArray(obj) {
return toString.call(obj) === '[object Array]';
}
function sn(tagName, className, data, keypath) {
var result = document.createElement(tagName);
result.className = className;
if (keypath) {
result.setAttribute('data-keypath', keypath);
}
result.appendChild(document.createTextNode("" + data));
return result;
}
function scn(tagName, className, child) {
var result = document.createElement(tagName),
i, len;
result.className = className;
if (isArray(child)) {
for (i = 0, len = child.length; i < len; i += 1) {
result.appendChild(child[i]);
}
} else {
result.appendChild(child);
}
return result;
}
function linkNode(child, href, target){
var a = scn("a", HYPERLINK_CLASS_NAME, child);
a.setAttribute('href', href);
a.setAttribute('target', target);
return a;
}
var toString = Object.prototype.toString,
prefixer = makePrefixer("jh"),
p = prefixer,
ARRAY = 1,
BOOL = 2,
INT = 3,
FLOAT = 4,
STRING = 5,
OBJECT = 6,
FUNCTION = 7,
UNK = 99,
STRING_CLASS_NAME = p("type-string"),
STRING_EMPTY_CLASS_NAME = p("type-string") + " " + p("empty"),
BOOL_TRUE_CLASS_NAME = p("type-bool-true"),
BOOL_FALSE_CLASS_NAME = p("type-bool-false"),
BOOL_IMAGE = p("type-bool-image"),
INT_CLASS_NAME = p("type-int") + " " + p("type-number"),
FLOAT_CLASS_NAME = p("type-float") + " " + p("type-number"),
OBJECT_CLASS_NAME = p("type-object"),
OBJ_KEY_CLASS_NAME = p("key") + " " + p("object-key"),
OBJ_VAL_CLASS_NAME = p("value") + " " + p("object-value"),
OBJ_EMPTY_CLASS_NAME = p("type-object") + " " + p("empty"),
FUNCTION_CLASS_NAME = p("type-function"),
ARRAY_KEY_CLASS_NAME = p("key") + " " + p("array-key"),
ARRAY_VAL_CLASS_NAME = p("value") + " " + p("array-value"),
ARRAY_CLASS_NAME = p("type-array"),
ARRAY_EMPTY_CLASS_NAME = p("type-array") + " " + p("empty"),
HYPERLINK_CLASS_NAME = p('a'),
UNKNOWN_CLASS_NAME = p("type-unk");
function getType(obj) {
var type = typeof obj;
switch (type) {
case "boolean":
return BOOL;
case "string":
return STRING;
case "number":
return (obj % 1 === 0) ? INT : FLOAT;
case "function":
return FUNCTION;
default:
if (isArray(obj)) {
return ARRAY;
} else if (obj === Object(obj)) {
return OBJECT;
} else {
return UNK;
}
}
}
function _format(data, options, parentKey, keypath) {
var result, container, key, keyNode, valNode, len, childs, tr, value,
isEmpty = true,
accum = [],
type = getType(data);
// Initialized & used only in case of objects & arrays
var hyperlinksEnabled, aTarget, hyperlinkKeys ;
switch (type) {
case BOOL:
var boolOpt = options.bool;
container = document.createElement('div');
if(boolOpt.showImage) {
var img = document.createElement('img');
img.setAttribute('class', BOOL_IMAGE);
img.setAttribute('src',
'' + (data ? boolOpt.img.true : boolOpt.img.false));
container.appendChild(img);
}
if(boolOpt.showText){
container.appendChild(data ?
sn("span", BOOL_TRUE_CLASS_NAME, boolOpt.text.true, keypath) :
sn("span", BOOL_FALSE_CLASS_NAME, boolOpt.text.false, keypath));
}
result = container;
break;
case STRING:
if (data === "") {
result = sn("span", STRING_EMPTY_CLASS_NAME, "(Empty Text)", keypath);
} else {
result = sn("span", STRING_CLASS_NAME, data, keypath);
}
break;
case INT:
result = sn("span", INT_CLASS_NAME, data, keypath);
break;
case FLOAT:
result = sn("span", FLOAT_CLASS_NAME, data, keypath);
break;
case OBJECT:
childs = [];
aTarget = options.hyperlinks.target;
hyperlinkKeys = options.hyperlinks.keys;
// Is Hyperlink Key
hyperlinksEnabled =
options.hyperlinks.enable &&
hyperlinkKeys &&
hyperlinkKeys.length > 0;
for (key in data) {
isEmpty = false;
value = data[key];
var objectKeypath = keypath ? keypath + "." + key : key;
valNode = _format(value, options, key, objectKeypath);
keyNode = sn("th", OBJ_KEY_CLASS_NAME, key);
if( hyperlinksEnabled &&
typeof(value) === 'string' &&
indexOf.call(hyperlinkKeys, key) >= 0){
valNode = scn("td", OBJ_VAL_CLASS_NAME, linkNode(valNode, value, aTarget));
} else {
valNode = scn("td", OBJ_VAL_CLASS_NAME, valNode);
}
tr = document.createElement("tr");
tr.appendChild(keyNode);
tr.appendChild(valNode);
childs.push(tr);
}
if (isEmpty) {
result = sn("span", OBJ_EMPTY_CLASS_NAME, "(Empty Object)", keypath);
} else {
result = scn("table", OBJECT_CLASS_NAME, scn("tbody", '', childs));
}
break;
case FUNCTION:
result = sn("span", FUNCTION_CLASS_NAME, data, keypath);
break;
case ARRAY:
if (data.length > 0) {
childs = [];
var showArrayIndices = options.showArrayIndex;
aTarget = options.hyperlinks.target;
hyperlinkKeys = options.hyperlinks.keys;
// Hyperlink of arrays?
hyperlinksEnabled = parentKey && options.hyperlinks.enable &&
hyperlinkKeys &&
hyperlinkKeys.length > 0 &&
indexOf.call(hyperlinkKeys, parentKey) >= 0;
for (key = 0, len = data.length; key < len; key += 1) {
keyNode = sn("th", ARRAY_KEY_CLASS_NAME, key);
value = data[key];
var arrayKeypath = keypath + "[" + key + "]";
if(hyperlinksEnabled && typeof(value) === "string") {
valNode = _format(value, options, key, arrayKeypath);
valNode = scn("td", ARRAY_VAL_CLASS_NAME, linkNode(valNode, value, aTarget));
} else {
valNode = scn("td", ARRAY_VAL_CLASS_NAME, _format(value, options, key, arrayKeypath));
}
tr = document.createElement("tr");
if(showArrayIndices) {
tr.appendChild(keyNode);
}
tr.appendChild(valNode);
childs.push(tr);
}
result = scn("table", ARRAY_CLASS_NAME, scn("tbody", '', childs));
} else {
result = sn("span", ARRAY_EMPTY_CLASS_NAME, "(Empty List)", keypath);
}
break;
default:
result = sn("span", UNKNOWN_CLASS_NAME, data, keypath);
break;
}
return result;
}
function format(data, options) {
options = validateOptions(options || {});
var result;
result = _format(data, options);
result.className = result.className + " " + prefixer("root");
return result;
}
function validateOptions(options){
options = validateArrayIndexOption(options);
options = validateHyperlinkOptions(options);
options = validateBoolOptions(options);
// Add any more option validators here
return options;
}
function validateArrayIndexOption(options) {
if(options.showArrayIndex === undefined){
options.showArrayIndex = true;
} else {
// Force to boolean just in case
options.showArrayIndex = options.showArrayIndex ? true: false;
}
return options;
}
function validateHyperlinkOptions(options){
var hyperlinks = {
enable : false,
keys : null,
target : ''
};
if(options.hyperlinks && options.hyperlinks.enable) {
hyperlinks.enable = true;
hyperlinks.keys = isArray(options.hyperlinks.keys) ? options.hyperlinks.keys : [];
if(options.hyperlinks.target) {
hyperlinks.target = '' + options.hyperlinks.target;
} else {
hyperlinks.target = '_blank';
}
}
options.hyperlinks = hyperlinks;
return options;
}
function validateBoolOptions(options){
if(!options.bool){
options.bool = {
text: {
true : "true",
false : "false"
},
img : {
true: "",
false: ""
},
showImage : false,
showText : true
};
} else {
var boolOptions = options.bool;
// Show text if no option
if(!boolOptions.showText && !boolOptions.showImage){
boolOptions.showImage = false;
boolOptions.showText = true;
}
if(boolOptions.showText){
if(!boolOptions.text){
boolOptions.text = {
true : "true",
false : "false"
};
} else {
var t = boolOptions.text.true, f = boolOptions.text.false;
if(getType(t) != STRING || t === ''){
boolOptions.text.true = 'true';
}
if(getType(f) != STRING || f === ''){
boolOptions.text.false = 'false';
}
}
}
if(boolOptions.showImage){
if(!boolOptions.img.true && !boolOptions.img.false){
boolOptions.showImage = false;
}
}
}
return options;
}
return {
format: format
};
}));

View file

@ -1,42 +1,99 @@
function qs(key) {
key = key.replace(/[*+?^$.\[\]{}()|\\\/]/g, "\\$&"); // escape RegEx meta chars
var match = location.search.match(new RegExp("[?&]"+key+"=([^&]+)(&|$)"));
return match && decodeURIComponent(match[1].replace(/\+/g, " "));
}
$(document).ready(function(){
var currentHighchart;
// setup a function to grab the nodeStats
function getNodeStats() {
var uuid = qs("uuid");
var statsTableBody = "";
$.getJSON("/nodes/" + uuid + ".json", function(json){
// update the table header with the right node type
$('#stats-lead h3').html(json.node_type + " stats (" + uuid + ")");
delete json.node_type;
$.each(json, function(key, value) {
statsTableBody += "<tr>";
statsTableBody += "<td class='stats-key'>" + key + "</td>";
var formattedValue = (typeof value == 'number' ? value.toLocaleString() : value);
statsTableBody += "<td>" + formattedValue + "</td>";
statsTableBody += "</tr>";
});
$('#stats-table tbody').html(statsTableBody);
var stats = JsonHuman.format(json);
$('#stats-container').html(stats);
if (currentHighchart) {
// get the current time to set with the point
var x = (new Date()).getTime();
// get the last value using underscore-keypath
var y = _(json).valueForKeyPath(graphKeypath);
// start shifting the chart once we hit 20 data points
var shift = currentHighchart.series[0].data.length > 20;
currentHighchart.series[0].addPoint([x, y], true, shift);
}
}).fail(function(data) {
$('#stats-table td').each(function(){
$('#stats-container th').each(function(){
$(this).addClass('stale');
});
});
});
}
// do the first GET on page load
getNodeStats();
// grab the new assignments JSON every second
var getNodeStatsInterval = setInterval(getNodeStats, 1000);
var graphKeypath = "";
// set the global Highcharts option
Highcharts.setOptions({
global: {
useUTC: false
}
});
// add a function to help create the graph modal
function createGraphModal() {
var chartModal = bootbox.dialog({
title: graphKeypath,
message: "<div id='highchart-container'></div>",
buttons: {},
className: 'highchart-modal'
});
chartModal.on('hidden.bs.modal', function(e) {
currentHighchart.destroy();
currentHighchart = null;
});
currentHighchart = new Highcharts.Chart({
chart: {
renderTo: 'highchart-container',
defaultSeriesType: 'line'
},
title: {
text: ''
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150
},
yAxis: {
title: {
text: 'Value'
}
},
legend: {
enabled: false
},
series: [{
name: graphKeypath,
data: []
}]
});
}
// handle clicks on numerical values - this lets the user show a line graph in a modal
$('#stats-container').on('click', '.jh-type-number', function(){
graphKeypath = $(this).data('keypath');
// setup the new graph modal
createGraphModal();
});
});

File diff suppressed because it is too large Load diff

View file

@ -39,121 +39,128 @@ class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
public:
DomainServer(int argc, char* argv[]);
static int const EXIT_CODE_REBOOT;
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false);
bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false);
public slots:
/// Called by NodeList to inform us a node has been added
void nodeAdded(SharedNodePointer node);
/// Called by NodeList to inform us a node has been killed
void nodeKilled(SharedNodePointer node);
void publicKeyJSONCallback(QNetworkReply& requestReply);
void transactionJSONCallback(const QJsonObject& data);
void restart();
private slots:
void aboutToQuit();
void loginFailed();
void readAvailableDatagrams();
void setupPendingAssignmentCredits();
void sendPendingTransactionsToServer();
void requestCurrentPublicSocketViaSTUN();
void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr);
void performICEUpdates();
void sendHeartbeatToDataServer() { sendHeartbeatToDataServer(QString()); }
void sendHeartbeatToIceServer();
void sendICEPingPackets();
void handlePeerPingTimeout();
private:
void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid());
bool optionallySetupOAuth();
bool optionallyReadX509KeyAndCertificate();
bool didSetupAccountManagerWithAccessToken();
bool optionallySetupAssignmentPayment();
bool didSetupAccountManagerWithAccessToken();
bool resetAccountManagerAccessToken();
void setupAutomaticNetworking();
void sendHeartbeatToDataServer(const QString& networkAddress);
void processICEPingReply(const QByteArray& packet, const HifiSockAddr& senderSockAddr);
void processICEHeartbeatResponse(const QByteArray& packet);
void processICEPeerInformation(const QByteArray& packet);
void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);
void processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
void handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr);
unsigned int countConnectedUsers();
bool verifyUsersKey (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn);
bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr, QString& reasonReturn);
void preloadAllowedUserPublicKeys();
void requestUserPublicKey(const QString& username);
int parseNodeDataFromByteArray(QDataStream& packetStream,
NodeType_t& nodeType,
HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr,
const HifiSockAddr& senderSockAddr);
NodeSet nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes);
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr,
const NodeSet& nodeInterestList);
const NodeSet& nodeInterestSet);
QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
void broadcastNewNode(const SharedNodePointer& node);
void parseAssignmentConfigs(QSet<Assignment::Type>& excludedTypes);
void addStaticAssignmentToAssignmentHash(Assignment* newAssignment);
void createStaticAssignmentsForType(Assignment::Type type, const QVariantList& configList);
void populateDefaultStaticAssignmentsExcludingTypes(const QSet<Assignment::Type>& excludedTypes);
void populateStaticScriptedAssignmentsFromSettings();
SharedAssignmentPointer matchingQueuedAssignmentForCheckIn(const QUuid& checkInUUID, NodeType_t nodeType);
SharedAssignmentPointer deployableAssignmentForRequest(const Assignment& requestAssignment);
void removeMatchingAssignmentFromQueue(const SharedAssignmentPointer& removableAssignment);
void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment);
void addStaticAssignmentsToQueue();
void respondToPathQuery(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
QUrl oauthRedirectURL();
QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid());
bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url);
void handleTokenRequestFinished();
QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply);
void handleProfileRequestFinished();
Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply);
void loadExistingSessionsFromSettings();
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
HTTPManager _httpManager;
HTTPSManager* _httpsManager;
QHash<QUuid, SharedAssignmentPointer> _allAssignments;
QQueue<SharedAssignmentPointer> _unfulfilledAssignments;
QHash<QUuid, PendingAssignedNodeData*> _pendingAssignedNodes;
TransactionHash _pendingAssignmentCredits;
bool _isUsingDTLS;
QUrl _oauthProviderURL;
QString _oauthClientID;
QString _oauthClientSecret;
QString _hostname;
QSet<QUuid> _webAuthenticationStateSet;
QHash<QUuid, DomainServerWebSessionData> _cookieSessionHash;
QHash<QString, QByteArray> _userPublicKeys;
QHash<QUuid, NetworkPeer> _connectingICEPeers;
QHash<QUuid, HifiSockAddr> _connectedICEPeers;
QHash<QUuid, SharedNetworkPeer> _icePeers;
QString _automaticNetworkingSetting;
DomainServerSettingsManager _settingsManager;
HifiSockAddr _iceServerSocket;
};

View file

@ -13,6 +13,7 @@
#include <QtCore/QJsonObject>
#include <QtCore/QVariant>
#include <JSONBreakableMarshal.h>
#include <PacketHeaders.h>
#include "DomainServerNodeData.h"
@ -31,16 +32,8 @@ DomainServerNodeData::DomainServerNodeData() :
}
void DomainServerNodeData::parseJSONStatsPacket(const QByteArray& statsPacket) {
// push past the packet header
QDataStream packetStream(statsPacket);
packetStream.skipRawData(numBytesForPacketHeader(statsPacket));
QVariantMap unpackedVariantMap;
packetStream >> unpackedVariantMap;
QJsonObject unpackedStatsJSON = QJsonObject::fromVariantMap(unpackedVariantMap);
_statsJSONObject = mergeJSONStatsFromNewObject(unpackedStatsJSON, _statsJSONObject);
QVariantMap packetVariantMap = JSONBreakableMarshal::fromStringBuffer(statsPacket.mid(numBytesForPacketHeader(statsPacket)));
_statsJSONObject = mergeJSONStatsFromNewObject(QJsonObject::fromVariantMap(packetVariantMap), _statsJSONObject);
}
QJsonObject DomainServerNodeData::mergeJSONStatsFromNewObject(const QJsonObject& newObject, QJsonObject destinationObject) {

View file

@ -12,44 +12,47 @@
#ifndef hifi_DomainServerNodeData_h
#define hifi_DomainServerNodeData_h
#include <QtCore/QElapsedTimer>
#include <QtCore/QHash>
#include <QtCore/QUuid>
#include <HifiSockAddr.h>
#include <NodeData.h>
#include <NodeType.h>
class DomainServerNodeData : public NodeData {
public:
DomainServerNodeData();
int parseData(const QByteArray& packet) { return 0; }
const QJsonObject& getStatsJSONObject() const { return _statsJSONObject; }
void parseJSONStatsPacket(const QByteArray& statsPacket);
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
void setWalletUUID(const QUuid& walletUUID) { _walletUUID = walletUUID; }
const QUuid& getWalletUUID() const { return _walletUUID; }
void setUsername(const QString& username) { _username = username; }
const QString& getUsername() const { return _username; }
QElapsedTimer& getPaymentIntervalTimer() { return _paymentIntervalTimer; }
void setSendingSockAddr(const HifiSockAddr& sendingSockAddr) { _sendingSockAddr = sendingSockAddr; }
const HifiSockAddr& getSendingSockAddr() { return _sendingSockAddr; }
void setIsAuthenticated(bool isAuthenticated) { _isAuthenticated = isAuthenticated; }
bool isAuthenticated() const { return _isAuthenticated; }
QHash<QUuid, QUuid>& getSessionSecretHash() { return _sessionSecretHash; }
const NodeSet& getNodeInterestSet() const { return _nodeInterestSet; }
void setNodeInterestSet(const NodeSet& nodeInterestSet) { _nodeInterestSet = nodeInterestSet; }
private:
QJsonObject mergeJSONStatsFromNewObject(const QJsonObject& newObject, QJsonObject destinationObject);
QHash<QUuid, QUuid> _sessionSecretHash;
QUuid _assignmentUUID;
QUuid _walletUUID;
@ -58,6 +61,7 @@ private:
QJsonObject _statsJSONObject;
HifiSockAddr _sendingSockAddr;
bool _isAuthenticated;
NodeSet _nodeInterestSet;
};
#endif // hifi_DomainServerNodeData_h

View file

@ -14,6 +14,7 @@
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QSettings>
#include <QtCore/QStandardPaths>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
@ -30,6 +31,9 @@ const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString DESCRIPTION_NAME_KEY = "name";
const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
const QString DESCRIPTION_COLUMNS_KEY = "columns";
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
DomainServerSettingsManager::DomainServerSettingsManager() :
_descriptionArray(),
@ -38,50 +42,93 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
// load the description object from the settings description
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
descriptionFile.open(QIODevice::ReadOnly);
_descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array();
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll());
if (descriptionDocument.isObject()) {
QJsonObject descriptionObject = descriptionDocument.object();
const QString DESCRIPTION_VERSION_KEY = "version";
if (descriptionObject.contains(DESCRIPTION_VERSION_KEY)) {
// read the version from the settings description
_descriptionVersion = descriptionObject[DESCRIPTION_VERSION_KEY].toDouble();
if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
_descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray();
return;
}
}
}
qCritical() << "Did not find settings decription in JSON at" << SETTINGS_DESCRIPTION_RELATIVE_PATH
<< "- Unable to continue. domain-server will quit.";
QMetaObject::invokeMethod(QCoreApplication::instance(), "quit", Qt::QueuedConnection);
}
void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
_configMap.loadMasterAndUserConfig(argumentList);
// for now we perform a temporary transition from http-username and http-password to http_username and http_password
const QVariant* oldUsername = valueForKeyPath(_configMap.getUserConfig(), "security.http-username");
const QVariant* oldPassword = valueForKeyPath(_configMap.getUserConfig(), "security.http-password");
if (oldUsername || oldPassword) {
QVariantMap& settingsMap = *reinterpret_cast<QVariantMap*>(_configMap.getUserConfig()["security"].data());
// remove old keys, move to new format
if (oldUsername) {
settingsMap["http_username"] = oldUsername->toString();
settingsMap.remove("http-username");
// What settings version were we before and what are we using now?
// Do we need to do any re-mapping?
QSettings appSettings;
const QString JSON_SETTINGS_VERSION_KEY = "json-settings/version";
double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble();
if (oldVersion != _descriptionVersion) {
qDebug() << "Previous domain-server settings version was"
<< QString::number(oldVersion, 'g', 8) << "and the new version is"
<< QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required";
// we have a version mismatch - for now handle custom behaviour here since there are not many remappings
if (oldVersion < 1.0) {
// This was prior to the introduction of security.restricted_access
// If the user has a list of allowed users then set their value for security.restricted_access to true
QVariant* allowedUsers = valueForKeyPath(_configMap.getMergedConfig(), ALLOWED_USERS_SETTINGS_KEYPATH);
if (allowedUsers
&& allowedUsers->canConvert(QMetaType::QVariantList)
&& reinterpret_cast<QVariantList*>(allowedUsers)->size() > 0) {
qDebug() << "Forcing security.restricted_access to TRUE since there was an"
<< "existing list of allowed users.";
// In the pre-toggle system the user had a list of allowed users, so
// we need to set security.restricted_access to true
QVariant* restrictedAccess = valueForKeyPath(_configMap.getUserConfig(),
RESTRICTED_ACCESS_SETTINGS_KEYPATH,
true);
*restrictedAccess = QVariant(true);
// write the new settings to the json file
persistToFile();
// reload the master and user config so that the merged config is right
_configMap.loadMasterAndUserConfig(argumentList);
}
}
if (oldPassword) {
settingsMap["http_password"] = oldPassword->toString();
settingsMap.remove("http-password");
}
// save the updated settings
persistToFile();
}
// write the current description version to our settings
appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion);
}
QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString &keyPath) {
const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath);
if (foundValue) {
return *foundValue;
} else {
int dotIndex = keyPath.indexOf('.');
QString groupKey = keyPath.mid(0, dotIndex);
QString settingKey = keyPath.mid(dotIndex + 1);
foreach(const QVariant& group, _descriptionArray.toVariantList()) {
QVariantMap groupMap = group.toMap();
if (groupMap[DESCRIPTION_NAME_KEY].toString() == groupKey) {
foreach(const QVariant& setting, groupMap[DESCRIPTION_SETTINGS_KEY].toList()) {
QVariantMap settingMap = setting.toMap();
@ -89,187 +136,190 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin
return settingMap[SETTING_DEFAULT_KEY];
}
}
return QVariant();
}
}
}
return QVariant();
}
const QString SETTINGS_PATH = "/settings.json";
bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a GET operation for our settings
// check if there is a query parameter for settings affecting a particular type of assignment
const QString SETTINGS_TYPE_QUERY_KEY = "type";
QUrlQuery settingsQuery(url);
QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY);
if (!typeValue.isEmpty()) {
QJsonObject responseObject = responseObjectForType(typeValue);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json");
return true;
} else {
return false;
}
}
return false;
}
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
qDebug() << "DomainServerSettingsManager postedObject -" << postedObject;
// we recurse one level deep below each group for the appropriate setting
recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig(), _descriptionArray);
recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig());
// store whatever the current _settingsMap is to file
persistToFile();
// return success to the caller
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) {
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
const QString SETTINGS_RESPONSE_LOCKED_VALUES_KEY = "locked";
QJsonObject rootObject;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray;
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object();
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
}
return false;
}
QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) {
QJsonObject responseObject;
if (!typeValue.isEmpty() || isAuthenticated) {
// convert the string type value to a QJsonValue
QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt());
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QJsonValue& groupValue, _descriptionArray) {
QJsonObject groupObject = groupValue.toObject();
QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray();
QJsonObject groupResponseObject;
foreach(const QJsonValue& settingValue, groupSettingsArray) {
const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden";
QJsonObject settingObject = settingValue.toObject();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) {
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
if (affectedTypesArray.isEmpty()) {
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
}
if (affectedTypesArray.contains(queryType) ||
(queryType.isNull() && isAuthenticated)) {
// this is a setting we should include in the responseObject
QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString();
// we need to check if the settings map has a value for this setting
QVariant variantValue;
QVariant settingsMapGroupValue = _configMap.getMergedConfig()
.value(groupObject[DESCRIPTION_NAME_KEY].toString());
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingName);
if (!groupKey.isEmpty()) {
QVariant settingsMapGroupValue = _configMap.getMergedConfig().value(groupKey);
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingName);
}
} else {
variantValue = _configMap.getMergedConfig().value(settingName);
}
QJsonValue result;
if (variantValue.isNull()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
groupResponseObject[settingName] = settingObject[SETTING_DEFAULT_KEY];
result = settingObject[SETTING_DEFAULT_KEY];
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
groupResponseObject[settingName] = QString("");
result = QString("");
}
} else {
groupResponseObject[settingName] = QJsonValue::fromVariant(variantValue);
result = QJsonValue::fromVariant(variantValue);
}
if (!groupKey.isEmpty()) {
// this belongs in the group object
groupResponseObject[settingName] = result;
} else {
// this is a value that should be at the root
responseObject[settingName] = result;
}
}
}
}
if (!groupResponseObject.isEmpty()) {
if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) {
// set this group's object to the constructed object
responseObject[groupKey] = groupResponseObject;
}
}
}
return responseObject;
}
bool DomainServerSettingsManager::settingExists(const QString& groupName, const QString& settingName,
const QJsonArray& descriptionArray, QJsonValue& settingDescription) {
foreach(const QJsonValue& groupValue, descriptionArray) {
QJsonObject groupObject = groupValue.toObject();
if (groupObject[DESCRIPTION_NAME_KEY].toString() == groupName) {
foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
QJsonObject settingObject = settingValue.toObject();
if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) {
settingDescription = settingObject[SETTING_DEFAULT_KEY];
return true;
}
}
}
}
settingDescription = QJsonValue::Undefined;
return false;
}
void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonValue& settingDescription) {
const QJsonObject& settingDescription) {
if (newValue.isString()) {
if (newValue.toString().isEmpty()) {
// this is an empty value, clear it in settings variant so the default is sent
settingMap.remove(key);
} else {
// make sure the resulting json value has the right type
const QString settingType = settingDescription.toObject()[SETTING_DESCRIPTION_TYPE_KEY].toString();
QString settingType = settingDescription[SETTING_DESCRIPTION_TYPE_KEY].toString();
const QString INPUT_DOUBLE_TYPE = "double";
const QString INPUT_INTEGER_TYPE = "int";
if (settingType == INPUT_DOUBLE_TYPE) {
settingMap[key] = newValue.toString().toDouble();
} else if (settingType == INPUT_INTEGER_TYPE) {
settingMap[key] = newValue.toString().toInt();
} else {
settingMap[key] = newValue.toString();
QString sanitizedValue = newValue.toString();
// we perform special handling for viewpoints here
// we do not want them to be prepended with a slash
if (key == SETTINGS_VIEWPOINT_KEY && !sanitizedValue.startsWith('/')) {
sanitizedValue.prepend('/');
}
settingMap[key] = sanitizedValue;
}
}
} else if (newValue.isBool()) {
@ -279,66 +329,158 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
// we don't have a map below this key yet, so set it up now
settingMap[key] = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingMap[key].data());
foreach(const QString childKey, newValue.toObject().keys()) {
updateSetting(childKey, newValue.toObject()[childKey], thisMap, settingDescription.toObject()[key]);
QVariant& possibleMap = settingMap[key];
if (!possibleMap.canConvert(QMetaType::QVariantMap)) {
// if this isn't a map then we need to make it one, otherwise we're about to crash
qDebug() << "Value at" << key << "was not the expected QVariantMap while updating DS settings"
<< "- removing existing value and making it a QVariantMap";
possibleMap = QVariantMap();
}
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(possibleMap.data());
foreach(const QString childKey, newValue.toObject().keys()) {
QJsonObject childDescriptionObject = settingDescription;
// is this the key? if so we have the description already
if (key != settingDescription[DESCRIPTION_NAME_KEY].toString()) {
// otherwise find the description object for this childKey under columns
foreach(const QJsonValue& column, settingDescription[DESCRIPTION_COLUMNS_KEY].toArray()) {
if (column.isObject()) {
QJsonObject thisDescription = column.toObject();
if (thisDescription[DESCRIPTION_NAME_KEY] == childKey) {
childDescriptionObject = column.toObject();
break;
}
}
}
}
QString sanitizedKey = childKey;
if (key == SETTINGS_PATHS_KEY && !sanitizedKey.startsWith('/')) {
// We perform special handling for paths here.
// If we got sent a path without a leading slash then we add it.
sanitizedKey.prepend("/");
}
updateSetting(sanitizedKey, newValue.toObject()[childKey], thisMap, childDescriptionObject);
}
if (settingMap[key].toMap().isEmpty()) {
// we've cleared all of the settings below this value, so remove this one too
settingMap.remove(key);
}
} else if (newValue.isArray()) {
// we just assume array is replacement
// TODO: we still need to recurse here with the description in case values in the array have special types
settingMap[key] = newValue.toArray().toVariantList();
}
}
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
QVariantMap& settingsVariant,
const QJsonArray& descriptionArray) {
// Iterate on the setting groups
foreach(const QString& groupKey, postedObject.keys()) {
QJsonValue groupValue = postedObject[groupKey];
if (!settingsVariant.contains(groupKey)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[groupKey] = QVariantMap();
QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) {
foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
QJsonObject settingObject = settingValue.toObject();
if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) {
return settingObject;
}
// Iterate on the settings
foreach(const QString& settingKey, groupValue.toObject().keys()) {
QJsonValue settingValue = groupValue.toObject()[settingKey];
QJsonValue thisDescription;
if (settingExists(groupKey, settingKey, descriptionArray, thisDescription)) {
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(settingsVariant[groupKey].data());
updateSetting(settingKey, settingValue, thisMap, thisDescription);
}
return QJsonObject();
}
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
QVariantMap& settingsVariant) {
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
QJsonValue rootValue = postedObject[rootKey];
if (!settingsVariant.contains(rootKey)) {
// we don't have a map below this key yet, so set it up now
settingsVariant[rootKey] = QVariantMap();
}
QVariantMap* thisMap = &settingsVariant;
QJsonObject groupDescriptionObject;
// we need to check the description array to see if this is a root setting or a group setting
foreach(const QJsonValue& groupValue, _descriptionArray) {
if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) {
// we matched a group - keep this since we'll use it below to update the settings
groupDescriptionObject = groupValue.toObject();
// change the map we will update to be the map for this group
thisMap = reinterpret_cast<QVariantMap*>(settingsVariant[rootKey].data());
break;
}
}
if (settingsVariant[groupKey].toMap().empty()) {
if (groupDescriptionObject.isEmpty()) {
// this is a root value, so we can call updateSetting for it directly
// first we need to find our description value for it
QJsonObject matchingDescriptionObject;
foreach(const QJsonValue& groupValue, _descriptionArray) {
// find groups with root values (they don't have a group name)
QJsonObject groupObject = groupValue.toObject();
if (!groupObject.contains(DESCRIPTION_NAME_KEY)) {
// this is a group with root values - check if our setting is in here
matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey);
if (!matchingDescriptionObject.isEmpty()) {
break;
}
}
}
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
} else {
qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting.";
}
} else {
// this is a group - iterate on the settings in the group
foreach(const QString& settingKey, rootValue.toObject().keys()) {
// make sure this particular setting exists and we have a description object for it
QJsonObject matchingDescriptionObject = settingDescriptionFromGroup(groupDescriptionObject, settingKey);
// if we matched the setting then update the value
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
} else {
qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey <<
"- cannot update setting.";
}
}
}
if (settingsVariant[rootKey].toMap().empty()) {
// we've cleared all of the settings below this value, so remove this one too
settingsVariant.remove(groupKey);
settingsVariant.remove(rootKey);
}
}
}
void DomainServerSettingsManager::persistToFile() {
// make sure we have the dir the settings file is supposed to live in
QFileInfo settingsFileInfo(_configMap.getUserConfigFilename());
if (!settingsFileInfo.dir().exists()) {
settingsFileInfo.dir().mkpath(".");
}
QFile settingsFile(_configMap.getUserConfigFilename());
if (settingsFile.open(QIODevice::WriteOnly)) {
settingsFile.write(QJsonDocument::fromVariant(_configMap.getUserConfig()).toJson());
} else {
qCritical("Could not write to JSON settings file. Unable to persist settings.");
}
}
}

View file

@ -18,29 +18,38 @@
#include <HifiConfigVariantMap.h>
#include <HTTPManager.h>
const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
class DomainServerSettingsManager : public QObject {
Q_OBJECT
public:
DomainServerSettingsManager();
bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url);
bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url);
void setupConfigMap(const QStringList& argumentList);
QVariant valueOrDefaultValueForKeyPath(const QString& keyPath);
QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); }
QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); }
private:
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant,
const QJsonArray& descriptionArray);
bool settingExists(const QString& groupName, const QString& settingName,
const QJsonArray& descriptionArray, QJsonValue& settingDescription);
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant);
void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonValue& settingDescription);
const QJsonObject& settingDescription);
QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName);
void persistToFile();
double _descriptionVersion;
QJsonArray _descriptionArray;
HifiConfigVariantMap _configMap;
};
#endif // hifi_DomainServerSettingsManager_h
#endif // hifi_DomainServerSettingsManager_h

View file

@ -26,18 +26,16 @@ int main(int argc, char* argv[]) {
#ifndef WIN32
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
qInstallMessageHandler(LogHandler::verboseMessageHandler);
int currentExitCode = 0;
// use a do-while to handle domain-server restart
do {
DomainServer domainServer(argc, argv);
currentExitCode = domainServer.exec();
} while (currentExitCode == DomainServer::EXIT_CODE_REBOOT);
return currentExitCode;
}

View file

@ -12,7 +12,7 @@
//For procedural walk animation
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
Script.include("proceduralAnimationAPI.js");
Script.include(HIFI_PUBLIC_BUCKET + "scripts/acScripts/proceduralAnimationAPI.js");
var procAnimAPI = new ProcAnimAPI();

416
examples/avatarSelector.js Normal file
View file

@ -0,0 +1,416 @@
//
// avatarSelector.js
// examples
//
// Created by David Rowe on 21 Apr 2015.
// Copyright 2015 High Fidelity, Inc.
//
// Based on lobby.js created by Stephen Birarda on 17 Oct 2014.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
var panelWall = false;
var orbShell = false;
var descriptionText = false;
var showText = false;
// used for formating the description text, in meters
var textWidth = 4;
var textHeight = .5;
var numberOfLines = 2;
var textMargin = 0.0625;
var lineHeight = (textHeight - (2 * textMargin)) / numberOfLines;
var avatarStickPosition = {};
var orbNaturalExtentsMin = { x: -1.230354, y: -1.22077, z: -1.210487 };
var orbNaturalExtentsMax = { x: 1.230353, y: 1.229819, z: 1.210487 };
var panelsNaturalExtentsMin = { x: -1.223182, y: -0.348487, z: 0.0451369 };
var panelsNaturalExtentsMax = { x: 1.223039, y: 0.602978, z: 1.224298 };
var orbNaturalDimensions = Vec3.subtract(orbNaturalExtentsMax, orbNaturalExtentsMin);
var panelsNaturalDimensions = Vec3.subtract(panelsNaturalExtentsMax, panelsNaturalExtentsMin);
var SCALING_FACTOR = 10;
var orbDimensions = Vec3.multiply(orbNaturalDimensions, SCALING_FACTOR);
var panelsDimensions = Vec3.multiply(panelsNaturalDimensions, SCALING_FACTOR);
var orbNaturalCenter = Vec3.sum(orbNaturalExtentsMin, Vec3.multiply(orbNaturalDimensions, 0.5));
var panelsNaturalCenter = Vec3.sum(panelsNaturalExtentsMin, Vec3.multiply(panelsNaturalDimensions, 0.5));
var orbCenter = Vec3.multiply(orbNaturalCenter, SCALING_FACTOR);
var panelsCenter = Vec3.multiply(panelsNaturalCenter, SCALING_FACTOR);
var panelsCenterShift = Vec3.subtract(panelsCenter, orbCenter);
var ORB_SHIFT = { x: 0, y: -1.4, z: -0.8 };
var LOBBY_PANEL_WALL_URL = HIFI_PUBLIC_BUCKET + "models/sets/Lobby/PanelWallForInterface.fbx";
var LOBBY_BLANK_PANEL_TEXTURE_URL = HIFI_PUBLIC_BUCKET + "models/sets/Lobby/Texture.jpg";
var LOBBY_SHELL_URL = HIFI_PUBLIC_BUCKET + "models/sets/Lobby/LobbyShellForInterface.fbx";
var droneSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/drone.stereo.raw")
var currentDrone = null;
var latinSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/latin.stereo.raw")
var latinInjector = null;
var elevatorSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/Lobby/elevator.stereo.raw")
var elevatorInjector = null;
var currentMuzakInjector = null;
var currentSound = null;
function textOverlayPosition() {
var TEXT_DISTANCE_OUT = 6;
var TEXT_DISTANCE_DOWN = -2;
return Vec3.sum(Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), TEXT_DISTANCE_OUT)),
Vec3.multiply(Quat.getUp(Camera.orientation), TEXT_DISTANCE_DOWN));
}
var panelPlaceOrder = [
7, 8, 9, 10, 11, 12, 13,
0, 1, 2, 3, 4, 5, 6,
14, 15, 16, 17, 18, 19, 20
];
// Avatar index is 0-based
function avatarIndexToPanelIndex(avatarIndex) {
return panelPlaceOrder.indexOf(avatarIndex) + 1;
}
// Panel index is 1-based
function panelIndexToPlaceIndex(panelIndex) {
return panelPlaceOrder[panelIndex - 1];
}
var MAX_NUM_PANELS = 21;
var DRONE_VOLUME = 0.3;
function drawLobby() {
if (!panelWall) {
print("Adding overlays for the avatar selector panel wall and orb shell.");
var cameraEuler = Quat.safeEulerAngles(Camera.orientation);
var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, y: 1, z: 0 });
var orbPosition = Vec3.sum(Camera.position, Vec3.multiplyQbyV(towardsMe, ORB_SHIFT));
var panelWallProps = {
url: LOBBY_PANEL_WALL_URL,
position: Vec3.sum(orbPosition, Vec3.multiplyQbyV(towardsMe, panelsCenterShift)),
rotation: towardsMe,
dimensions: panelsDimensions
};
var orbShellProps = {
url: LOBBY_SHELL_URL,
position: orbPosition,
rotation: towardsMe,
dimensions: orbDimensions,
ignoreRayIntersection: true
};
var windowDimensions = Controller.getViewportDimensions();
var descriptionTextProps = {
position: textOverlayPosition(),
dimensions: { x: textWidth, y: textHeight },
backgroundColor: { red: 0, green: 0, blue: 0 },
color: { red: 255, green: 255, blue: 255 },
topMargin: textMargin,
leftMargin: textMargin,
bottomMargin: textMargin,
rightMargin: textMargin,
text: "",
lineHeight: lineHeight,
alpha: 0.9,
backgroundAlpha: 0.9,
ignoreRayIntersection: true,
visible: false,
isFacingAvatar: true
};
avatarStickPosition = MyAvatar.position;
panelWall = Overlays.addOverlay("model", panelWallProps);
orbShell = Overlays.addOverlay("model", orbShellProps);
descriptionText = Overlays.addOverlay("text3d", descriptionTextProps);
if (droneSound.downloaded) {
// start the drone sound
if (!currentDrone) {
currentDrone = Audio.playSound(droneSound, { stereo: true, loop: true, localOnly: true, volume: DRONE_VOLUME });
} else {
currentDrone.restart();
}
}
// start one of our muzak sounds
playRandomMuzak();
}
}
var avatars = {};
function changeLobbyTextures() {
var req = new XMLHttpRequest();
req.open("GET", "https://metaverse.highfidelity.com/api/v1/marketplace?category=head+%26+body&limit=21", false);
req.send(); // Data returned is randomized.
avatars = JSON.parse(req.responseText).data.items;
var NUM_PANELS = avatars.length;
var textureProp = {
textures: {}
};
for (var j = 0; j < NUM_PANELS; j++) {
var panelIndex = avatarIndexToPanelIndex(j);
textureProp["textures"]["file" + panelIndex] = avatars[j].preview_url;
};
Overlays.editOverlay(panelWall, textureProp);
}
var MUZAK_VOLUME = 0.1;
function playCurrentSound(secondOffset) {
if (currentSound == latinSound) {
if (!latinInjector) {
latinInjector = Audio.playSound(latinSound, { localOnly: true, secondOffset: secondOffset, volume: MUZAK_VOLUME });
} else {
latinInjector.restart();
}
currentMuzakInjector = latinInjector;
} else if (currentSound == elevatorSound) {
if (!elevatorInjector) {
elevatorInjector = Audio.playSound(elevatorSound, { localOnly: true, secondOffset: secondOffset, volume: MUZAK_VOLUME });
} else {
elevatorInjector.restart();
}
currentMuzakInjector = elevatorInjector;
}
}
function playNextMuzak() {
if (panelWall) {
if (currentSound == latinSound) {
if (elevatorSound.downloaded) {
currentSound = elevatorSound;
}
} else if (currentSound == elevatorSound) {
if (latinSound.downloaded) {
currentSound = latinSound;
}
}
playCurrentSound(0);
}
}
function playRandomMuzak() {
currentSound = null;
if (latinSound.downloaded && elevatorSound.downloaded) {
currentSound = Math.random() < 0.5 ? latinSound : elevatorSound;
} else if (latinSound.downloaded) {
currentSound = latinSound;
} else if (elevatorSound.downloaded) {
currentSound = elevatorSound;
}
if (currentSound) {
// pick a random number of seconds from 0-10 to offset the muzak
var secondOffset = Math.random() * 10;
playCurrentSound(secondOffset);
} else {
currentMuzakInjector = null;
}
}
function cleanupLobby() {
toggleEnvironmentRendering(true);
// for each of the 21 placeholder textures, set them back to default so the cached model doesn't have changed textures
var panelTexturesReset = {};
panelTexturesReset["textures"] = {};
for (var j = 0; j < MAX_NUM_PANELS; j++) {
panelTexturesReset["textures"]["file" + (j + 1)] = LOBBY_BLANK_PANEL_TEXTURE_URL;
};
Overlays.editOverlay(panelWall, panelTexturesReset);
Overlays.deleteOverlay(panelWall);
Overlays.deleteOverlay(orbShell);
Overlays.deleteOverlay(descriptionText);
panelWall = false;
orbShell = false;
if (currentDrone) {
currentDrone.stop();
currentDrone = null
}
if (currentMuzakInjector) {
currentMuzakInjector.stop();
currentMuzakInjector = null;
}
avatars = {};
}
function actionStartEvent(event) {
if (panelWall) {
// we've got an action event and our panel wall is up
// check if we hit a panel and if we should jump there
var result = Overlays.findRayIntersection(event.actionRay);
if (result.intersects && result.overlayID == panelWall) {
var panelName = result.extraInfo;
var panelStringIndex = panelName.indexOf("Panel");
if (panelStringIndex != -1) {
var panelIndex = parseInt(panelName.slice(5));
var avatarIndex = panelIndexToPlaceIndex(panelIndex);
if (avatarIndex < avatars.length) {
var actionPlace = avatars[avatarIndex];
print("Changing avatar to " + actionPlace.name
+ " after click on panel " + panelIndex + " with avatar index " + avatarIndex);
MyAvatar.useFullAvatarURL(actionPlace.content_url);
maybeCleanupLobby();
}
}
}
}
}
var control = false;
function keyPressEvent(event) {
if (event.text === "CONTROL") {
control = true;
}
if (control && event.text === "a") {
if (!panelWall) {
toggleEnvironmentRendering(false);
drawLobby();
changeLobbyTextures();
} else {
cleanupLobby();
}
}
}
function keyReleaseEvent(event) {
if (event.text === "CONTROL") {
control = false;
}
}
var CLEANUP_EPSILON_DISTANCE = 0.05;
function maybeCleanupLobby() {
if (panelWall && Vec3.length(Vec3.subtract(avatarStickPosition, MyAvatar.position)) > CLEANUP_EPSILON_DISTANCE) {
cleanupLobby();
}
}
function toggleEnvironmentRendering(shouldRender) {
Scene.shouldRenderAvatars = shouldRender;
Scene.shouldRenderEntities = shouldRender;
}
function handleLookAt(pickRay) {
if (panelWall && descriptionText) {
// we've got an action event and our panel wall is up
// check if we hit a panel and if we should jump there
var result = Overlays.findRayIntersection(pickRay);
if (result.intersects && result.overlayID == panelWall) {
var panelName = result.extraInfo;
var panelStringIndex = panelName.indexOf("Panel");
if (panelStringIndex != -1) {
var panelIndex = parseInt(panelName.slice(5));
var avatarIndex = panelIndexToPlaceIndex(panelIndex);
if (avatarIndex < avatars.length) {
var actionPlace = avatars[avatarIndex];
if (actionPlace.description == "") {
Overlays.editOverlay(descriptionText, { text: actionPlace.name, visible: showText });
} else {
// handle line wrapping
var allWords = actionPlace.description.split(" ");
var currentGoodLine = "";
var currentTestLine = "";
var formatedDescription = "";
var wordsFormated = 0;
var currentTestWord = 0;
var wordsOnLine = 0;
while (wordsFormated < allWords.length) {
// first add the "next word" to the line and test it.
currentTestLine = currentGoodLine;
if (wordsOnLine > 0) {
currentTestLine += " " + allWords[currentTestWord];
} else {
currentTestLine = allWords[currentTestWord];
}
var lineLength = Overlays.textSize(descriptionText, currentTestLine).width;
if (lineLength < textWidth || wordsOnLine == 0) {
wordsFormated++;
currentTestWord++;
wordsOnLine++;
currentGoodLine = currentTestLine;
} else {
formatedDescription += currentGoodLine + "\n";
wordsOnLine = 0;
currentGoodLine = "";
currentTestLine = "";
}
}
formatedDescription += currentGoodLine;
Overlays.editOverlay(descriptionText, { text: formatedDescription, visible: showText });
}
} else {
Overlays.editOverlay(descriptionText, { text: "", visible: false });
}
}
}
}
}
function update(deltaTime) {
maybeCleanupLobby();
if (panelWall) {
Overlays.editOverlay(descriptionText, { position: textOverlayPosition() });
// if the reticle is up then we may need to play the next muzak
if (currentMuzakInjector && !currentMuzakInjector.isPlaying) {
playNextMuzak();
}
}
}
function mouseMoveEvent(event) {
if (panelWall) {
var pickRay = Camera.computePickRay(event.x, event.y);
handleLookAt(pickRay);
}
}
Controller.actionStartEvent.connect(actionStartEvent);
Controller.keyPressEvent.connect(keyPressEvent);
Controller.keyReleaseEvent.connect(keyReleaseEvent);
Script.update.connect(update);
Script.scriptEnding.connect(maybeCleanupLobby);
Controller.mouseMoveEvent.connect(mouseMoveEvent);

201
examples/blockWorld.js Normal file
View file

@ -0,0 +1,201 @@
// blockWorld.js
// examples
//
// Created by Eric Levin on May 26, 2015
// Copyright 2015 High Fidelity, Inc.
//
// Creates a floor of tiles and then drops planky blocks at random points above the tile floor
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var TILE_SIZE = 7
var GENERATE_INTERVAL = 50;
var NUM_ROWS = 10;
var angVelRange = 4;
var floorTiles = [];
var blocks = [];
var blockSpawner;
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
var floorPos = Vec3.sum(MyAvatar.position, {
x: 0,
y: -2,
z: 0
});
var x = floorPos.x;
var currentRowIndex = 0;
var currentColumnIndex = 0;
var DROP_HEIGHT = floorPos.y + 5;
var BLOCK_GRAVITY = {
x: 0,
y: -9,
z: 0
};
var BLOCK_SIZE = {
x: 0.2,
y: 0.1,
z: 0.8
};
var bounds = {
xMin: floorPos.x,
xMax: floorPos.x + (TILE_SIZE * NUM_ROWS) - TILE_SIZE,
zMin: floorPos.z,
zMax: floorPos.z + (TILE_SIZE * NUM_ROWS) - TILE_SIZE
};
var screenSize = Controller.getViewportDimensions();
var BUTTON_SIZE = 32;
var PADDING = 3;
var offButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 - BUTTON_SIZE * 2 + PADDING,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/close.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
var deleteButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 - BUTTON_SIZE,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/delete.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
function generateFloor() {
for (var z = floorPos.z; currentColumnIndex < NUM_ROWS; z += TILE_SIZE, currentColumnIndex++) {
floorTiles.push(Entities.addEntity({
type: 'Box',
position: {
x: x,
y: floorPos.y,
z: z
},
dimensions: {
x: TILE_SIZE,
y: 2,
z: TILE_SIZE
},
color: {
red: randFloat(70, 120),
green: randFloat(70, 71),
blue: randFloat(70, 80)
},
// collisionsWillMove: true
}));
}
currentRowIndex++;
if (currentRowIndex < NUM_ROWS) {
currentColumnIndex = 0;
x += TILE_SIZE;
Script.setTimeout(generateFloor, GENERATE_INTERVAL);
} else {
//Once we're done generating floor, drop planky blocks at random points on floor
blockSpawner = Script.setInterval(function() {
dropBlock();
}, GENERATE_INTERVAL)
}
}
function dropBlock() {
var dropPos = floorPos;
dropPos.y = DROP_HEIGHT;
dropPos.x = randFloat(bounds.xMin, bounds.xMax);
dropPos.z = randFloat(bounds.zMin, bounds.zMax);
blocks.push(Entities.addEntity({
type: "Model",
modelURL: 'http://s3.amazonaws.com/hifi-public/marketplace/hificontent/Games/blocks/block.fbx',
shapeType: 'box',
position: dropPos,
dimensions: BLOCK_SIZE,
collisionsWillMove: true,
gravity: {
x: 0,
y: -9,
z: 0
},
velocity: {
x: 0,
y: .1,
z: 0
},
angularVelocity: {
x: randFloat(-angVelRange, angVelRange),
y: randFloat(-angVelRange, angVelRange),
z: randFloat(-angVelRange, angVelRange),
}
}));
}
function mousePressEvent(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({
x: event.x,
y: event.y
});
if (clickedOverlay == offButton) {
Script.clearInterval(blockSpawner);
}
if(clickedOverlay == deleteButton){
destroyStuff();
}
}
generateFloor();
function cleanup() {
// for (var i = 0; i < floorTiles.length; i++) {
// Entities.deleteEntity(floorTiles[i]);
// }
// for (var i = 0; i < blocks.length; i++) {
// Entities.deleteEntity(blocks[i]);
// }
Overlays.deleteOverlay(offButton);
Overlays.deleteOverlay(deleteButton)
Script.clearInterval(blockSpawner);
}
function destroyStuff() {
for (var i = 0; i < floorTiles.length; i++) {
Entities.deleteEntity(floorTiles[i]);
}
for (var i = 0; i < blocks.length; i++) {
Entities.deleteEntity(blocks[i]);
}
Script.clearInterval(blockSpawner);
}
function randFloat(low, high) {
return Math.floor(low + Math.random() * (high - low));
}
function map(value, min1, max1, min2, max2) {
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
}
Script.scriptEnding.connect(cleanup);
Controller.mousePressEvent.connect(mousePressEvent);

View file

@ -198,7 +198,7 @@ function checkControllerSide(hand) {
var closestEntity = Entities.findClosestEntity(hand.palmPosition(), CATCH_RADIUS);
var modelUrl = Entities.getEntityProperties(closestEntity).modelURL;
print("lol2"+closestEntity.isKnownID);
if (closestEntity.isKnownID && validFrisbeeURL(Entities.getEntityProperties(closestEntity).modelURL)) {
if (closestEntity && validFrisbeeURL(Entities.getEntityProperties(closestEntity).modelURL)) {
print("lol");
Entities.editEntity(closestEntity, {modelScale: 1, inHand: true, position: hand.holdPosition(), shouldDie: true});
Entities.deleteEntity(closestEntity);
@ -448,4 +448,4 @@ Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
Menu.menuItemEvent.connect(menuItemEvent);
Script.scriptEnding.connect(scriptEnding);
Script.update.connect(checkController);
Script.update.connect(controlFrisbees);
Script.update.connect(controlFrisbees);

View file

@ -153,6 +153,26 @@ if (showScore) {
var BULLET_VELOCITY = 10.0;
function entityCollisionWithEntity(entity1, entity2, collision) {
if (entity2 === targetID) {
score++;
if (showScore) {
Overlays.editOverlay(text, { text: "Score: " + score } );
}
// We will delete the bullet and target in 1/2 sec, but for now we can see them bounce!
Script.setTimeout(deleteBulletAndTarget, 500);
// Turn the target and the bullet white
Entities.editEntity(entity1, { color: { red: 255, green: 255, blue: 255 }});
Entities.editEntity(entity2, { color: { red: 255, green: 255, blue: 255 }});
// play the sound near the camera so the shooter can hear it
audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation()));
Audio.playSound(targetHitSound, audioOptions);
}
}
function shootBullet(position, velocity, grenade) {
var BULLET_SIZE = 0.10;
var BULLET_LIFETIME = 10.0;
@ -178,6 +198,7 @@ function shootBullet(position, velocity, grenade) {
ignoreCollisions: false,
collisionsWillMove: true
});
Script.addEventHandler(bulletID, "collisionWithEntity", entityCollisionWithEntity);
// Play firing sounds
audioOptions.position = position;
@ -310,27 +331,6 @@ function makePlatform(gravity, scale, size) {
}
function entityCollisionWithEntity(entity1, entity2, collision) {
if (((entity1.id == bulletID.id) || (entity1.id == targetID.id)) &&
((entity2.id == bulletID.id) || (entity2.id == targetID.id))) {
score++;
if (showScore) {
Overlays.editOverlay(text, { text: "Score: " + score } );
}
// We will delete the bullet and target in 1/2 sec, but for now we can see them bounce!
Script.setTimeout(deleteBulletAndTarget, 500);
// Turn the target and the bullet white
Entities.editEntity(entity1, { color: { red: 255, green: 255, blue: 255 }});
Entities.editEntity(entity2, { color: { red: 255, green: 255, blue: 255 }});
// play the sound near the camera so the shooter can hear it
audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation()));
Audio.playSound(targetHitSound, audioOptions);
}
}
function keyPressEvent(event) {
// if our tools are off, then don't do anything
if (event.text == "t") {
@ -386,13 +386,6 @@ MyAvatar.attach(gunModel, "LeftHand", {x:-0.04, y: 0.22, z: 0.02}, Quat.fromPitc
Script.setTimeout(playLoadSound, 2000);
function update(deltaTime) {
if (bulletID && !bulletID.isKnownID) {
bulletID = Entities.identifyEntity(bulletID);
}
if (targetID && !targetID.isKnownID) {
targetID = Entities.identifyEntity(targetID);
}
if (activeControllers == 0) {
if (Controller.getNumberOfSpatialControls() > 0) {
activeControllers = Controller.getNumberOfSpatialControls();
@ -512,7 +505,6 @@ function scriptEnding() {
clearPose();
}
Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity);
Script.scriptEnding.connect(scriptEnding);
Script.update.connect(update);
Controller.mouseReleaseEvent.connect(mouseReleaseEvent);

File diff suppressed because it is too large Load diff

View file

@ -132,12 +132,6 @@ function update(deltaTime) {
return;
}
if (!paddle.isKnownID) {
paddle = Entities.identifyEntity(paddle);
}
if (!ball.isKnownID) {
ball = Entities.identifyEntity(ball);
} else {
var paddleOrientation = leftHanded ? PADDLE_ORIENTATION : Quat.multiply(PADDLE_ORIENTATION, Quat.fromPitchYawRollDegrees(0, 180, 0));
var paddleWorldOrientation = Quat.multiply(Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(controllerID)), paddleOrientation);
var holdPosition = Vec3.sum(leftHanded ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(),
@ -157,7 +151,7 @@ function update(deltaTime) {
Entities.editEntity(paddleModel, { position: Vec3.sum(holdPosition, Vec3.multiplyQbyV(paddleWorldOrientation, PADDLE_BOX_OFFSET)),
velocity: Controller.getSpatialControlVelocity(controllerID),
rotation: paddleWorldOrientation });
}
}
function entityCollisionWithEntity(entity1, entity2, collision) {

View file

@ -109,17 +109,10 @@ function checkControllerSide(whichSide) {
var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3) || (Controller.getTriggerValue(TRIGGER) > 0.5));
// If I don't currently have a ball in my hand, then try to catch closest one
if (leftHandEntity && !leftHandEntity.isKnownID) {
leftHandEntity = Entities.identifyEntity(leftHandEntity);
}
if (rightHandEntity && !rightHandEntity.isKnownID) {
rightHandEntity = Entities.identifyEntity(rightHandEntity);
}
if (!ballAlreadyInHand && grabButtonPressed) {
var closestEntity = Entities.findClosestEntity(palmPosition, targetRadius);
if (closestEntity.isKnownID) {
if (closestEntity) {
var foundProperties = Entities.getEntityProperties(closestEntity);
if (Vec3.length(foundProperties.velocity) > 0.0) {

View file

@ -12,10 +12,9 @@ Script.load("progress.js");
Script.load("edit.js");
Script.load("selectAudioDevice.js");
Script.load("controllers/hydra/hydraMove.js");
Script.load("headMove.js");
Script.load("inspect.js");
Script.load("lobby.js");
Script.load("notifications.js");
Script.load("look.js");
Script.load("users.js");
Script.load("grab.js");
Script.load("pointer.js");

23
examples/dialTone.js Normal file
View file

@ -0,0 +1,23 @@
//
// dialTone.js
// examples
//
// Created by Stephen Birarda on 06/08/15.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// setup the local sound we're going to use
var connectSound = SoundCache.getSound("file://" + Paths.resources + "sounds/short1.wav");
// setup the options needed for that sound
var connectSoundOptions = {
localOnly: true
}
// play the sound locally once we get the first audio packet from a mixer
Audio.receivedFirstPacket.connect(function(){
Audio.playSound(connectSound, connectSoundOptions);
});

View file

@ -12,16 +12,17 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var isDice = false;
var NUMBER_OF_DICE = 2;
var isDice = false;
var NUMBER_OF_DICE = 4;
var LIFETIME = 10000; // Dice will live for about 3 hours
var dice = [];
var DIE_SIZE = 0.20;
var madeSound = true; // Set false at start of throw to look for collision
var madeSound = true; // Set false at start of throw to look for collision
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
SoundCache.getSound("http://s3.amazonaws.com/hifi-public/sounds/dice/diceCollide.wav");
var rollSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "sounds/dice/diceRoll.wav");
var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissions to create new objects."
@ -31,97 +32,118 @@ var BUTTON_SIZE = 32;
var PADDING = 3;
var offButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 - BUTTON_SIZE,
y: screenSize.y- (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/close.png",
color: { red: 255, green: 255, blue: 255},
alpha: 1
});
x: screenSize.x / 2 - BUTTON_SIZE * 2 + PADDING,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/close.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
var deleteButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 - BUTTON_SIZE,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/delete.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
var diceButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 + PADDING,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/die.png",
color: { red: 255, green: 255, blue: 255},
alpha: 1
});
x: screenSize.x / 2 + PADDING,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/die.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
var GRAVITY = -3.5;
var LIFETIME = 300;
// NOTE: angularVelocity is in radians/sec
var MAX_ANGULAR_SPEED = Math.PI;
function shootDice(position, velocity) {
if (!Entities.canRez()) {
Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG);
} else {
for (var i = 0; i < NUMBER_OF_DICE; i++) {
dice.push(Entities.addEntity(
{ type: "Model",
modelURL: HIFI_PUBLIC_BUCKET + "models/props/Dice/goldDie.fbx",
position: position,
velocity: velocity,
rotation: Quat.fromPitchYawRollDegrees(Math.random() * 360, Math.random() * 360, Math.random() * 360),
angularVelocity: { x: Math.random() * MAX_ANGULAR_SPEED,
y: Math.random() * MAX_ANGULAR_SPEED,
z: Math.random() * MAX_ANGULAR_SPEED },
lifetime: LIFETIME,
gravity: { x: 0, y: GRAVITY, z: 0 },
shapeType: "box",
collisionsWillMove: true
}));
position = Vec3.sum(position, Vec3.multiply(DIE_SIZE, Vec3.normalize(Quat.getRight(Camera.getOrientation()))));
}
if (!Entities.canRez()) {
Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG);
} else {
for (var i = 0; i < NUMBER_OF_DICE; i++) {
dice.push(Entities.addEntity(
{
type: "Model",
modelURL: HIFI_PUBLIC_BUCKET + "models/props/Dice/goldDie.fbx",
position: position,
velocity: velocity,
rotation: Quat.fromPitchYawRollDegrees(Math.random() * 360, Math.random() * 360, Math.random() * 360),
angularVelocity: {
x: Math.random() * MAX_ANGULAR_SPEED,
y: Math.random() * MAX_ANGULAR_SPEED,
z: Math.random() * MAX_ANGULAR_SPEED
},
gravity: {
x: 0,
y: GRAVITY,
z: 0
},
lifetime: LIFETIME,
shapeType: "box",
collisionsWillMove: true,
collisionSoundURL: "http://s3.amazonaws.com/hifi-public/sounds/dice/diceCollide.wav"
}));
position = Vec3.sum(position, Vec3.multiply(DIE_SIZE, Vec3.normalize(Quat.getRight(Camera.getOrientation()))));
}
}
}
function deleteDice() {
while(dice.length > 0) {
Entities.deleteEntity(dice.pop());
}
while (dice.length > 0) {
Entities.deleteEntity(dice.pop());
}
}
function entityCollisionWithEntity(entity1, entity2, collision) {
if (!madeSound) {
// Is it one of our dice?
for (var i = 0; i < dice.length; i++) {
if (!dice[i].isKnownID) {
dice[i] = Entities.identifyEntity(dice[i]);
}
if ((entity1.id == dice[i].id) || (entity2.id == dice[i].id)) {
madeSound = true;
Audio.playSound(rollSound, { position: collision.contactPoint });
}
}
}
}
function mousePressEvent(event) {
var clickedText = false;
var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
if (clickedOverlay == offButton) {
Script.stop();
} else if (clickedOverlay == diceButton) {
var HOW_HARD = 2.0;
var position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation()));
var velocity = Vec3.multiply(HOW_HARD, Quat.getFront(Camera.getOrientation()));
shootDice(position, velocity);
madeSound = false;
}
var clickedText = false;
var clickedOverlay = Overlays.getOverlayAtPoint({
x: event.x,
y: event.y
});
if (clickedOverlay == offButton) {
deleteDice();
Script.stop();
} else if (clickedOverlay == deleteButton) {
deleteDice();
} else if (clickedOverlay == diceButton) {
var HOW_HARD = 2.0;
var position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation()));
var velocity = Vec3.multiply(HOW_HARD, Quat.getFront(Camera.getOrientation()));
shootDice(position, velocity);
madeSound = false;
}
}
function scriptEnding() {
deleteDice();
Overlays.deleteOverlay(offButton);
Overlays.deleteOverlay(diceButton);
Overlays.deleteOverlay(offButton);
Overlays.deleteOverlay(diceButton);
Overlays.deleteOverlay(deleteButton);
}
Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity);
Controller.mousePressEvent.connect(mousePressEvent);
Script.scriptEnding.connect(scriptEnding);
Script.scriptEnding.connect(scriptEnding);

View file

@ -1,4 +1,3 @@
// newEditEntities.js
// examples
//
@ -15,7 +14,7 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
Script.include([
"libraries/stringHelpers.js",
"libraries/dataviewHelpers.js",
"libraries/dataViewHelpers.js",
"libraries/toolBars.js",
"libraries/progressDialog.js",
@ -28,7 +27,6 @@ Script.include([
"libraries/gridTool.js",
"libraries/entityList.js",
"libraries/lightOverlayManager.js",
"libraries/zoneOverlayManager.js",
]);
var selectionDisplay = SelectionDisplay;
@ -36,7 +34,6 @@ var selectionManager = SelectionManager;
var entityPropertyDialogBox = EntityPropertyDialogBox;
var lightOverlayManager = new LightOverlayManager();
var zoneOverlayManager = new ZoneOverlayManager();
var cameraManager = new CameraManager();
@ -49,7 +46,6 @@ var entityListTool = EntityListTool();
selectionManager.addEventListener(function() {
selectionDisplay.updateHandles();
lightOverlayManager.updatePositions();
zoneOverlayManager.updatePositions();
});
var windowDimensions = Controller.getViewportDimensions();
@ -140,7 +136,9 @@ var toolBar = (function () {
newSphereButton,
newLightButton,
newTextButton,
newWebButton,
newZoneButton,
newPolyVoxButton,
browseMarketplaceButton;
function initialize() {
@ -207,9 +205,28 @@ var toolBar = (function () {
alpha: 0.9,
visible: false
});
newWebButton = toolBar.addTool({
imageURL: "https://hifi-public.s3.amazonaws.com/images/www.svg",
subImage: { x: 0, y: 0, width: 128, height: 128 },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: false
});
newZoneButton = toolBar.addTool({
imageURL: toolIconUrl + "zonecube3.svg",
subImage: { x: 0, y: Tool.IMAGE_WIDTH + 208, width: 256, height: 256 },
imageURL: toolIconUrl + "zonecube_text.svg",
subImage: { x: 0, y: 128, width: 128, height: 128 },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
visible: false
});
newPolyVoxButton = toolBar.addTool({
imageURL: toolIconUrl + "polyvox.svg",
subImage: { x: 0, y: 0, width: 256, height: 256 },
width: toolWidth,
height: toolHeight,
alpha: 0.9,
@ -246,7 +263,7 @@ var toolBar = (function () {
}
toolBar.selectTool(activeButton, isActive);
lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE));
zoneOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE));
Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE));
};
// Sets visibility of tool buttons, excluding the power button
@ -256,7 +273,9 @@ var toolBar = (function () {
toolBar.showTool(newSphereButton, doShow);
toolBar.showTool(newLightButton, doShow);
toolBar.showTool(newTextButton, doShow);
toolBar.showTool(newWebButton, doShow);
toolBar.showTool(newZoneButton, doShow);
toolBar.showTool(newPolyVoxButton, doShow);
};
var RESIZE_INTERVAL = 50;
@ -428,6 +447,22 @@ var toolBar = (function () {
return true;
}
if (newWebButton === toolBar.clicked(clickedOverlay)) {
var position = getPositionToCreateEntity();
if (position.x > 0 && position.y > 0 && position.z > 0) {
placingEntityID = Entities.addEntity({
type: "Web",
position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS),
dimensions: { x: 1.6, y: 0.9, z: 0.01 },
sourceUrl: "https://highfidelity.com/",
});
} else {
print("Can't create Web Entity: would be out of bounds.");
}
return true;
}
if (newZoneButton === toolBar.clicked(clickedOverlay)) {
var position = getPositionToCreateEntity();
@ -443,6 +478,24 @@ var toolBar = (function () {
return true;
}
if (newPolyVoxButton === toolBar.clicked(clickedOverlay)) {
var position = getPositionToCreateEntity();
if (position.x > 0 && position.y > 0 && position.z > 0) {
placingEntityID = Entities.addEntity({
type: "PolyVox",
position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS),
DEFAULT_DIMENSIONS),
dimensions: { x: 10, y: 10, z: 10 },
voxelVolumeSize: {x:16, y:16, z:16},
voxelSurfaceStyle: 1
});
} else {
print("Can't create PolyVox: would be out of bounds.");
}
return true;
}
return false;
};
@ -554,16 +607,6 @@ function findClickedEntity(event) {
}
var foundEntity = result.entityID;
if (!foundEntity.isKnownID) {
var identify = Entities.identifyEntity(foundEntity);
if (!identify.isKnownID) {
print("Unknown ID " + identify.id + " (update loop " + foundEntity.id + ")");
return null;
}
foundEntity = identify;
}
return { pickRay: pickRay, entityID: foundEntity };
}
@ -585,20 +628,21 @@ function mousePressEvent(event) {
}
}
var highlightedEntityID = { isKnownID: false };
var highlightedEntityID = null;
var mouseCapturedByTool = false;
var lastMousePosition = null;
var idleMouseTimerId = null;
var IDLE_MOUSE_TIMEOUT = 200;
var DEFAULT_ENTITY_DRAG_DROP_DISTANCE = 2.0;
function mouseMoveEvent(event) {
var lastMouseMoveEvent = null;
function mouseMoveEventBuffered(event) {
lastMouseMoveEvent = event;
}
function mouseMove(event) {
mouseHasMovedSincePress = true;
if (placingEntityID) {
if (!placingEntityID.isKnownID) {
placingEntityID = Entities.identifyEntity(placingEntityID);
}
var pickRay = Camera.computePickRay(event.x, event.y);
var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE;
var offset = Vec3.multiply(distance, pickRay.direction);
@ -635,9 +679,9 @@ function highlightEntityUnderCursor(position, accurateRay) {
var pickRay = Camera.computePickRay(position.x, position.y);
var entityIntersection = Entities.findRayIntersection(pickRay, accurateRay);
if (entityIntersection.accurate) {
if(highlightedEntityID.isKnownID && highlightedEntityID.id != entityIntersection.entityID.id) {
if(highlightedEntityID && highlightedEntityID != entityIntersection.entityID) {
selectionDisplay.unhighlightSelectable(highlightedEntityID);
highlightedEntityID = { id: -1, isKnownID: false };
highlightedEntityID = { id: -1 };
}
var halfDiagonal = Vec3.length(entityIntersection.properties.dimensions) / 2.0;
@ -648,7 +692,7 @@ function highlightEntityUnderCursor(position, accurateRay) {
var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE)
&& (allowSmallModels || angularSize > MIN_ANGULAR_SIZE);
if (entityIntersection.entityID.isKnownID && sizeOK) {
if (entityIntersection.entityID && sizeOK) {
if (wantEntityGlow) {
Entities.editEntity(entityIntersection.entityID, { glowLevel: 0.25 });
}
@ -661,6 +705,10 @@ function highlightEntityUnderCursor(position, accurateRay) {
function mouseReleaseEvent(event) {
if (lastMouseMoveEvent) {
mouseMove(lastMouseMoveEvent);
lastMouseMoveEvent = null;
}
if (propertyMenu.mouseReleaseEvent(event) || toolBar.mouseReleaseEvent(event)) {
return true;
}
@ -703,7 +751,7 @@ function mouseClickEvent(event) {
} else {
var halfDiagonal = Vec3.length(properties.dimensions) / 2.0;
print("Checking properties: " + properties.id + " " + properties.isKnownID + " - Half Diagonal:" + halfDiagonal);
print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal);
// P P - Model
// /| A - Palm
// / | d B - unit vector toward tip
@ -742,7 +790,7 @@ function mouseClickEvent(event) {
selectionManager.addEntity(foundEntity, true);
}
print("Model selected: " + foundEntity.id);
print("Model selected: " + foundEntity);
selectionDisplay.select(selectedEntityID, event);
if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) {
@ -772,7 +820,7 @@ function mouseClickEvent(event) {
}
Controller.mousePressEvent.connect(mousePressEvent);
Controller.mouseMoveEvent.connect(mouseMoveEvent);
Controller.mouseMoveEvent.connect(mouseMoveEventBuffered);
Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
@ -882,6 +930,10 @@ Script.update.connect(function (deltaTime) {
lastOrientation = Camera.orientation;
lastPosition = Camera.position;
}
if (lastMouseMoveEvent) {
mouseMove(lastMouseMoveEvent);
lastMouseMoveEvent = null;
}
});
function insideBox(center, dimensions, point) {
@ -896,7 +948,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) {
var boundingBoxCorner = Vec3.subtract(selectionManager.worldPosition,
Vec3.multiply(selectionManager.worldDimensions, 0.5));
var entities = Entities.findEntitiesInBox(boundingBoxCorner, selectionManager.worldDimensions);
if (!keepIfTouching) {
var isValid;
if (selectionManager.localPosition === null) {
@ -930,8 +982,8 @@ function deleteSelectedEntities() {
var savedProperties = [];
for (var i = 0; i < selectionManager.selections.length; i++) {
var entityID = SelectionManager.selections[i];
var initialProperties = SelectionManager.savedProperties[entityID.id];
SelectionManager.savedProperties[entityID.id];
var initialProperties = SelectionManager.savedProperties[entityID];
SelectionManager.savedProperties[entityID];
savedProperties.push({
entityID: entityID,
properties: initialProperties
@ -968,7 +1020,7 @@ function handeMenuEvent(menuItem) {
}
}
} else if (menuItem == "Import Entities" || menuItem == "Import Entities from URL") {
var importURL;
if (menuItem == "Import Entities") {
importURL = Window.browse("Select models to import", "", "*.json");
@ -988,7 +1040,7 @@ function handeMenuEvent(menuItem) {
} else if (menuItem == MENU_SHOW_LIGHTS_IN_EDIT_MODE) {
lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE));
} else if (menuItem == MENU_SHOW_ZONES_IN_EDIT_MODE) {
zoneOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE));
Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE));
}
tooltip.show(false);
}
@ -1018,8 +1070,11 @@ function importSVO(importURL) {
var success = Clipboard.importEntities(importURL);
if (success) {
var position = getPositionToCreateEntity();
var VERY_LARGE = 10000;
var position = { x: 0, y: 0, z: 0};
if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) {
position = getPositionToCreateEntity();
}
var pastedEntityIDs = Clipboard.pasteEntities(position);
if (isActive) {
@ -1087,8 +1142,8 @@ function applyEntityProperties(data) {
var selectedEntityIDs = [];
for (var i = 0; i < properties.length; i++) {
var entityID = properties[i].entityID;
if (DELETED_ENTITY_MAP[entityID.id] !== undefined) {
entityID = DELETED_ENTITY_MAP[entityID.id];
if (DELETED_ENTITY_MAP[entityID] !== undefined) {
entityID = DELETED_ENTITY_MAP[entityID];
}
Entities.editEntity(entityID, properties[i].properties);
selectedEntityIDs.push(entityID);
@ -1097,15 +1152,15 @@ function applyEntityProperties(data) {
var entityID = data.createEntities[i].entityID;
var properties = data.createEntities[i].properties;
var newEntityID = Entities.addEntity(properties);
DELETED_ENTITY_MAP[entityID.id] = newEntityID;
DELETED_ENTITY_MAP[entityID] = newEntityID;
if (data.selectCreated) {
selectedEntityIDs.push(newEntityID);
}
}
for (var i = 0; i < data.deleteEntities.length; i++) {
var entityID = data.deleteEntities[i].entityID;
if (DELETED_ENTITY_MAP[entityID.id] !== undefined) {
entityID = DELETED_ENTITY_MAP[entityID.id];
if (DELETED_ENTITY_MAP[entityID] !== undefined) {
entityID = DELETED_ENTITY_MAP[entityID];
}
Entities.deleteEntity(entityID);
}
@ -1130,7 +1185,7 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) {
};
for (var i = 0; i < SelectionManager.selections.length; i++) {
var entityID = SelectionManager.selections[i];
var initialProperties = SelectionManager.savedProperties[entityID.id];
var initialProperties = SelectionManager.savedProperties[entityID];
var currentProperties = Entities.getEntityProperties(entityID);
undoData.setProperties.push({
entityID: entityID,
@ -1174,7 +1229,7 @@ PropertiesTool = function(opts) {
var selections = [];
for (var i = 0; i < selectionManager.selections.length; i++) {
var entity = {};
entity.id = selectionManager.selections[i].id;
entity.id = selectionManager.selections[i];
entity.properties = Entities.getEntityProperties(selectionManager.selections[i]);
entity.properties.rotation = Quat.safeEulerAngles(entity.properties.rotation);
selections.push(entity);
@ -1185,7 +1240,11 @@ PropertiesTool = function(opts) {
webView.eventBridge.webEventReceived.connect(function(data) {
data = JSON.parse(data);
if (data.type == "update") {
if (data.type == "print") {
if (data.message) {
print(data.message);
}
} else if (data.type == "update") {
selectionManager.saveProperties();
if (selectionManager.selections.length > 1) {
properties = {
@ -1201,6 +1260,9 @@ PropertiesTool = function(opts) {
data.properties.rotation = Quat.fromPitchYawRollDegrees(rotation.x, rotation.y, rotation.z);
}
Entities.editEntity(selectionManager.selections[0], data.properties);
if (data.properties.name != undefined) {
entityListTool.sendUpdate();
}
}
pushCommandForSelections();
selectionManager._update();
@ -1217,7 +1279,7 @@ PropertiesTool = function(opts) {
var dY = grid.getOrigin().y - (selectionManager.worldPosition.y - selectionManager.worldDimensions.y / 2),
var diff = { x: 0, y: dY, z: 0 };
for (var i = 0; i < selectionManager.selections.length; i++) {
var properties = selectionManager.savedProperties[selectionManager.selections[i].id];
var properties = selectionManager.savedProperties[selectionManager.selections[i]];
var newPosition = Vec3.sum(properties.position, diff);
Entities.editEntity(selectionManager.selections[i], {
position: newPosition,
@ -1230,7 +1292,7 @@ PropertiesTool = function(opts) {
if (selectionManager.hasSelection()) {
selectionManager.saveProperties();
for (var i = 0; i < selectionManager.selections.length; i++) {
var properties = selectionManager.savedProperties[selectionManager.selections[i].id];
var properties = selectionManager.savedProperties[selectionManager.selections[i]];
var bottomY = properties.boundingBox.center.y - properties.boundingBox.dimensions.y / 2;
var dY = grid.getOrigin().y - bottomY;
var diff = { x: 0, y: dY, z: 0 };
@ -1246,7 +1308,7 @@ PropertiesTool = function(opts) {
if (selectionManager.hasSelection()) {
selectionManager.saveProperties();
for (var i = 0; i < selectionManager.selections.length; i++) {
var properties = selectionManager.savedProperties[selectionManager.selections[i].id];
var properties = selectionManager.savedProperties[selectionManager.selections[i]];
var naturalDimensions = properties.naturalDimensions;
// If any of the natural dimensions are not 0, resize
@ -1268,7 +1330,7 @@ PropertiesTool = function(opts) {
if (selectionManager.hasSelection()) {
selectionManager.saveProperties();
for (var i = 0; i < selectionManager.selections.length; i++) {
var properties = selectionManager.savedProperties[selectionManager.selections[i].id];
var properties = selectionManager.savedProperties[selectionManager.selections[i]];
Entities.editEntity(selectionManager.selections[i], {
dimensions: Vec3.multiply(multiplier, properties.dimensions),
});
@ -1276,6 +1338,25 @@ PropertiesTool = function(opts) {
pushCommandForSelections();
selectionManager._update();
}
} else if (data.action == "centerAtmosphereToZone") {
if (selectionManager.hasSelection()) {
selectionManager.saveProperties();
for (var i = 0; i < selectionManager.selections.length; i++) {
var properties = selectionManager.savedProperties[selectionManager.selections[i]];
if (properties.type == "Zone") {
var centerOfZone = properties.boundingBox.center;
var atmosphereCenter = { x: centerOfZone.x,
y: centerOfZone.y - properties.atmosphere.innerRadius,
z: centerOfZone.z };
Entities.editEntity(selectionManager.selections[i], {
atmosphere: { center: atmosphereCenter },
});
}
}
pushCommandForSelections();
selectionManager._update();
}
}
}
});

View file

@ -17,8 +17,8 @@
// All callbacks start by updating the properties
this.updateProperties = function(entityID) {
// Piece ID
if (this.entityID === null || !this.entityID.isKnownID) {
this.entityID = Entities.identifyEntity(entityID);
if (this.entityID === null) {
this.entityID = entityID;
}
// Piece Properties
this.properties = Entities.getEntityProperties(this.entityID);
@ -27,12 +27,7 @@
if (this.boardID === null) {
// Read user data string and update boardID
var userData = JSON.parse(this.properties.userData);
var boardID = Entities.identifyEntity(userData.boardID);
if (boardID.isKnownID) {
this.boardID = boardID;
} else {
return;
}
this.boardID = userData.boardID;
}
// Board User Data
@ -52,13 +47,13 @@
// Updates user data related objects
this.updateUserData = function() {
// Get board's user data
if (this.boardID !== null && this.boardID.isKnownID) {
if (this.boardID !== null) {
this.boardUserData = this.getEntityUserData(this.boardID);
if (!(this.boardUserData &&
this.boardUserData.firstTile &&
this.boardUserData.tileSize)) {
print("Incomplete boardUserData " + this.boardID.id);
print("Incomplete boardUserData " + this.boardID);
} else {
this.FIRST_TILE = this.boardUserData.firstTile;
this.TILE_SIZE = this.boardUserData.tileSize;
@ -137,7 +132,7 @@
for (var i = 0; i < others.length; i++) {
var piece = others[i];
if (piece.id != this.entityID.id) {
if (piece.id != this.entityID) {
var properties = Entities.getEntityProperties(piece);
var isWhite = properties.modelURL.search("White") !== -1;
@ -198,4 +193,4 @@
this.updateProperties(entityID); // All callbacks start by updating the properties
this.release(mouseEvent);
};
})
})

View file

@ -35,8 +35,8 @@
}
// All callbacks start by updating the properties
this.updateProperties = function(entityID) {
if (this.entityID === null || !this.entityID.isKnownID) {
this.entityID = Entities.identifyEntity(entityID);
if (this.entityID === null) {
this.entityID = entityID;
}
this.properties = Entities.getEntityProperties(this.entityID);
};

View file

@ -32,14 +32,14 @@
return JSON.parse(JSON.stringify(object));
}
function didEntityExist(entityID) {
return entityID && entityID.isKnownID;
return entityID;
}
function doesEntityExistNow(entityID) {
return entityID && getTrueID(entityID).isKnownID;
return entityID;
}
function getTrueID(entityID) {
var properties = Entities.getEntityProperties(entityID);
return { id: properties.id, creatorTokenID: properties.creatorTokenID, isKnownID: properties.isKnownID };
return { id: properties.id };
}
function getUserData(entityID) {
var properties = Entities.getEntityProperties(entityID);
@ -225,4 +225,4 @@
this.updateRelativeLightPosition();
}
};
})
})

View file

@ -251,8 +251,8 @@
};
// All callbacks start by updating the properties
this.updateProperties = function(entityID) {
if (this.entityID === null || !this.entityID.isKnownID) {
this.entityID = Entities.identifyEntity(entityID);
if (this.entityID === null) {
this.entityID = entityID;
}
this.properties = Entities.getEntityProperties(this.entityID);
};

View file

@ -15,7 +15,7 @@
var bird;
this.preload = function(entityID) {
print("preload("+entityID.id+")");
print("preload("+entityID+")");
bird = SoundCache.getSound("http://s3.amazonaws.com/hifi-public/sounds/Animals/bushtit_1.raw");
};

View file

@ -22,17 +22,17 @@
}
this.preload = function(entityID) {
print("preload("+entityID.id+")");
print("preload("+entityID+")");
bird = SoundCache.getSound("http://s3.amazonaws.com/hifi-public/sounds/Animals/bushtit_1.raw");
};
this.enterEntity = function(entityID) {
print("enterEntity("+entityID.id+")");
print("enterEntity("+entityID+")");
playSound();
};
this.leaveEntity = function(entityID) {
print("leaveEntity("+entityID.id+")");
print("leaveEntity("+entityID+")");
playSound();
};
})

View file

@ -312,7 +312,7 @@
this.indicator[i].position,
this.indicator[i].scale / 2)) {
clickedOnSeat = true;
seat.model = this.entityID; // ??
seat.model = this.entityID;
seat.position = this.indicator[i].position;
seat.rotation = this.indicator[i].orientation;
}
@ -333,8 +333,8 @@
// All callbacks start by updating the properties
this.updateProperties = function(entityID) {
if (this.entityID === null || !this.entityID.isKnownID) {
this.entityID = Entities.identifyEntity(entityID);
if (this.entityID === null) {
this.entityID = entityID;
}
this.properties = Entities.getEntityProperties(this.entityID);
};
@ -369,4 +369,4 @@
this.updateProperties(entityID); // All callbacks start by updating the properties
};
})
})

View file

@ -0,0 +1,247 @@
var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
var SOUND_PATH = HIFI_PUBLIC_BUCKET + "sounds/Collisions-hitsandslaps/";
var soundURLS = ["67LCollision01.wav", "67LCollision02.wav", "airhockey_hit1.wav"];
var FLOOR_SIZE = 10;
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(FLOOR_SIZE * 1.5, Quat.getFront(Camera.getOrientation())));
var WALL_WIDTH = .1;
var FLOOR_HEIGHT_OFFSET = -2;
var WALL_HEIGHT = FLOOR_SIZE / 4;
var BALL_DROP_HEIGHT = center.y + WALL_HEIGHT;
var NUM_BALLS = 50;
var BALL_RADIUS = 1;
var BROWNIAN_INTERVAL_TIME = 16;
var BROWNIAN_FORCE_RANGE = 5;
var SPAWN_TIME = 50;
var spawnCount = 0;
var brownianMotionActivated = false;
var buttonOffColor = {
red: 250,
green: 10,
blue: 10
};
var buttonOnColor = {
red: 10,
green: 200,
blue: 100
};
var bounds = {
xMin: center.x - FLOOR_SIZE / 2,
xMax: center.x + FLOOR_SIZE / 2,
zMin: center.z - FLOOR_SIZE / 2,
zMax: center.z + FLOOR_SIZE / 2
};
var balls = [];
var screenSize = Controller.getViewportDimensions();
var BUTTON_SIZE = 32;
var PADDING = 3;
var brownianButton = Overlays.addOverlay("image", {
x: screenSize.x / 2 - BUTTON_SIZE,
y: screenSize.y - (BUTTON_SIZE + PADDING),
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: HIFI_PUBLIC_BUCKET + "images/blocks.png",
color: buttonOffColor,
alpha: 1
});
var floor = Entities.addEntity({
type: 'Box',
position: Vec3.sum(center, {
x: 0,
y: FLOOR_HEIGHT_OFFSET,
z: 0
}),
dimensions: {
x: FLOOR_SIZE,
y: WALL_WIDTH,
z: FLOOR_SIZE
},
color: {
red: 100,
green: 100,
blue: 100
}
});
var rightWall = Entities.addEntity({
type: 'Box',
position: Vec3.sum(center, {
x: FLOOR_SIZE / 2,
y: FLOOR_HEIGHT_OFFSET / 2,
z: 0
}),
dimensions: {
x: WALL_WIDTH,
y: WALL_HEIGHT,
z: FLOOR_SIZE
},
color: {
red: 120,
green: 100,
blue: 120
}
});
var leftWall = Entities.addEntity({
type: 'Box',
position: Vec3.sum(center, {
x: -FLOOR_SIZE / 2,
y: FLOOR_HEIGHT_OFFSET / 2,
z: 0
}),
dimensions: {
x: WALL_WIDTH,
y: WALL_HEIGHT,
z: FLOOR_SIZE
},
color: {
red: 120,
green: 100,
blue: 120
}
});
var backWall = Entities.addEntity({
type: 'Box',
position: Vec3.sum(center, {
x: 0,
y: FLOOR_HEIGHT_OFFSET / 2,
z: -FLOOR_SIZE / 2,
}),
dimensions: {
x: FLOOR_SIZE,
y: WALL_HEIGHT,
z: WALL_WIDTH
},
color: {
red: 120,
green: 100,
blue: 120
}
});
var frontWall = Entities.addEntity({
type: 'Box',
position: Vec3.sum(center, {
x: 0,
y: FLOOR_HEIGHT_OFFSET / 2,
z: FLOOR_SIZE / 2,
}),
dimensions: {
x: FLOOR_SIZE,
y: WALL_HEIGHT,
z: WALL_WIDTH
},
color: {
red: 120,
green: 100,
blue: 120
}
});
var spawnInterval = Script.setInterval(spawnBalls, SPAWN_TIME);
function spawnBalls() {
balls.push(Entities.addEntity({
type: "Sphere",
shapeType: "sphere",
position: {
x: randFloat(bounds.xMin, bounds.xMax),
y: BALL_DROP_HEIGHT,
z: randFloat(bounds.zMin, bounds.zMax)
},
dimensions: {
x: BALL_RADIUS,
y: BALL_RADIUS,
z: BALL_RADIUS
},
color: {
red: randFloat(100, 150),
green: randFloat(20, 80),
blue: randFloat(10, 180)
},
ignoreCollisions: false,
collisionsWillMove: true,
gravity: {
x: 0,
y: -9.9,
z: 0
},
velocity: {
x: 0,
y: -.25,
z: 0
},
collisionSoundURL: SOUND_PATH + soundURLS[randInt(0, soundURLS.length - 1)]
}));
spawnCount++;
if (spawnCount === NUM_BALLS) {
Script.clearInterval(spawnInterval);
}
}
function mousePressEvent(event) {
var clickedOverlay = Overlays.getOverlayAtPoint({
x: event.x,
y: event.y
});
if (clickedOverlay == brownianButton) {
brownianMotionActivated = !brownianMotionActivated;
if (brownianMotionActivated) {
brownianInterval = Script.setInterval(bumpBalls, BROWNIAN_INTERVAL_TIME);
Overlays.editOverlay(brownianButton, {
color: buttonOnColor
})
} else {
Script.clearInterval(brownianInterval);
Overlays.editOverlay(brownianButton, {
color: buttonOffColor
})
}
}
}
function bumpBalls() {
balls.forEach(function(ball) {
var props = Entities.getEntityProperties(ball);
var newVelocity = Vec3.sum(props.velocity, {
x: (Math.random() - 0.5) * BROWNIAN_FORCE_RANGE,
y: 0,
z: (Math.random() - 0.5) * BROWNIAN_FORCE_RANGE
});
Entities.editEntity(ball, {
velocity: newVelocity
});
});
}
function cleanup() {
Entities.deleteEntity(floor);
Entities.deleteEntity(rightWall);
Entities.deleteEntity(leftWall);
Entities.deleteEntity(backWall);
Entities.deleteEntity(frontWall);
balls.forEach(function(ball) {
Entities.deleteEntity(ball);
});
Overlays.deleteOverlay(brownianButton);
}
function randFloat(low, high) {
return Math.floor(low + Math.random() * (high - low));
}
function randInt(low, high) {
return Math.floor(randFloat(low, high));
}
Script.scriptEnding.connect(cleanup);
Script.scriptEnding.connect(cleanup);
Controller.mousePressEvent.connect(mousePressEvent);

View file

@ -0,0 +1,89 @@
// dynamicLandscape.js
// examples
//
// Created by Eric Levin on June 8
// Copyright 2015 High Fidelity, Inc.
//
// Meditative ocean landscape
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
Script.include(HIFI_PUBLIC_BUCKET + 'scripts/utilities.js')
var NUM_ROWS = 10;
var CUBE_SIZE = 1;
var cubes = [];
var cubesSettings = [];
var time = 0;
var OMEGA = 2.0 * Math.PI/8;
var RANGE = CUBE_SIZE/2;
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(CUBE_SIZE* 10, Quat.getFront(Camera.getOrientation())));
for (var x = 0, rowIndex = 0; x < NUM_ROWS * CUBE_SIZE; x += CUBE_SIZE, rowIndex++) {
for (var z = 0, columnIndex = 0; z < NUM_ROWS * CUBE_SIZE; z += CUBE_SIZE, columnIndex++) {
var baseHeight = map( columnIndex + 1, 1, NUM_ROWS, -CUBE_SIZE * 2, -CUBE_SIZE);
var relativePosition = {
x: x,
y: baseHeight,
z: z
};
var position = Vec3.sum(center, relativePosition);
cubes.push(Entities.addEntity({
type: 'Box',
position: position,
dimensions: {
x: CUBE_SIZE,
y: CUBE_SIZE,
z: CUBE_SIZE
}
}));
var phase = map( (columnIndex + 1) * (rowIndex + 1), 2, NUM_ROWS * NUM_ROWS, Math.PI * 2, Math.PI * 4);
cubesSettings.push({
baseHeight: center.y + baseHeight,
phase: phase
})
}
}
function update(deleteTime) {
time += deleteTime;
for (var i = 0; i < cubes.length; i++) {
var phase = cubesSettings[i].phase;
var props = Entities.getEntityProperties(cubes[i]);
var newHeight = Math.sin(time * OMEGA + phase) / 2.0;
var hue = map(newHeight, -.5, .5, 0.5, 0.7);
var light = map(newHeight, -.5, .5, 0.4, 0.6)
newHeight = cubesSettings[i].baseHeight + (newHeight * RANGE);
var newVelocityY = Math.cos(time * OMEGA + phase) / 2.0 * RANGE * OMEGA;
var newPosition = props.position;
var newVelocity = props.velocity;
newPosition.y = newHeight;
newVelocity = newVelocityY;
Entities.editEntity( cubes[i], {
position: newPosition,
velocity: props.velocity,
color: hslToRgb({hue: hue, sat: 0.7, light: light})
});
}
}
function cleanup() {
cubes.forEach(function(cube) {
Entities.deleteEntity(cube);
})
}
Script.update.connect(update);
Script.scriptEnding.connect(cleanup)

View file

@ -42,7 +42,6 @@ var originalProperties = {
};
var modelID = Entities.addEntity(originalProperties);
print("Entities.addEntity()... modelID.creatorTokenID = " + modelID.creatorTokenID);
var isPlaying = true;
var playPauseEveryWhile = 360;
@ -98,8 +97,6 @@ function moveModel(deltaTime) {
count++;
//print("modelID.creatorTokenID = " + modelID.creatorTokenID);
if (somethingChanged) {
var newProperties = {
animationIsPlaying: isPlaying,
@ -123,7 +120,7 @@ Script.update.connect(moveModel);
Script.scriptEnding.connect(function () {
print("cleaning up...");
print("modelID="+ modelID.creatorTokenID + ", id:" + modelID.id);
print("modelID=" + modelID);
Models.deleteModel(modelID);
});

View file

@ -106,9 +106,6 @@ function updateButterflies(deltaTime) {
var CHANCE_OF_IMPULSE = 0.04;
for (var i = 0; i < numButterflies; i++) {
if (Math.random() < CHANCE_OF_IMPULSE) {
if (!butterflies[i].isKnownID) {
butterflies[i] = Entities.identifyEntity(butterflies[i]);
}
var properties = Entities.getEntityProperties(butterflies[i]);
if (Vec3.length(Vec3.subtract(properties.position, flockPosition)) > range) {
Entities.editEntity(butterflies[i], { position: flockPosition } );

View file

@ -0,0 +1,71 @@
//
// changingAtmosphereExample.js
// examples
//
// Created by Brad Hefta-Gaub on 4/16/15.
// Copyright 2015 High Fidelity, Inc.
//
// This is an example script that demonstrates creating a zone using the atmosphere features that changes scatter properties
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
var count = 0;
var stopAfter = 10000;
var zoneEntityA = Entities.addEntity({
type: "Zone",
position: { x: 1000, y: 1000, z: 1000},
dimensions: { x: 2000, y: 2000, z: 2000 },
keyLightColor: { red: 255, green: 0, blue: 0 },
stageSunModelEnabled: false,
shapeType: "sphere",
backgroundMode: "atmosphere",
atmosphere: {
center: { x: 1000, y: 0, z: 1000},
innerRadius: 1000.0,
outerRadius: 1025.0,
rayleighScattering: 0.0025, // Meaningful values 0 to ~0.01
mieScattering: 0.0010, // Meaningful values 0 to ~0.01
// First two, Meaningful values 0 to 1 each, blue, purple; third meaningful 0.3 to 1 - affects shape
scatteringWavelengths: { x: 0.650, y: 0.570, z: 0.475 },
hasStars: true
},
stage: {
latitude: 37.777,
longitude: 122.407,
altitude: 0.03,
day: 183,
hour: 5,
sunModelEnabled: true
}
});
// register the call back so it fires before each data send
Script.update.connect(function(deltaTime) {
// stop it...
if (count >= stopAfter) {
print("calling Script.stop()");
Script.stop();
}
count++;
var rayleighScattering = (count / 100000) % 0.01;
var mieScattering = (count / 100000) % 0.01;
var waveX = (count / 2000) % 1;
var waveZ = ((count / 2000) % 0.7) + 0.3;
Entities.editEntity(zoneEntityA, {
atmosphere: {
rayleighScattering: rayleighScattering,
mieScattering: mieScattering,
scatteringWavelengths: { x: waveX, y: waveX, z: waveZ }
},
});
});

Some files were not shown because too many files have changed in this diff Show more