Merge branch 'mgcore_T156_lintfix' into dev
This commit is contained in:
commit
92f6004507
@ -86,6 +86,7 @@ set(fmt_source_dir ${libs_dir}/fmt)
|
||||
set(fmt_static_lib ${fmt_source_dir}/fmt/libfmt.a)
|
||||
# yaml-cpp
|
||||
set(yaml_source_dir ${libs_dir}/yaml-cpp)
|
||||
set(yaml_include_dir ${yaml_source_dir}/include)
|
||||
set(yaml_static_lib ${yaml_source_dir}/libyaml-cpp.a)
|
||||
# Catch (C++ Automated Test Cases in Headers)
|
||||
set(catch_source_dir "${libs_dir}/Catch")
|
||||
@ -156,7 +157,7 @@ if(CLANG_TIDY)
|
||||
-config=''
|
||||
--
|
||||
-std=c++1y
|
||||
-I${CMAKE_SOURCE_DIR}/include -I${fmt_source_dir}
|
||||
-I${CMAKE_SOURCE_DIR}/include -I${fmt_source_dir} -I${yaml_include_dir}
|
||||
)
|
||||
endif()
|
||||
# linter setup
|
||||
@ -263,7 +264,7 @@ include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||
include_directories(${src_dir})
|
||||
include_directories(${build_include_dir})
|
||||
include_directories(${fmt_source_dir})
|
||||
include_directories(${yaml_source_dir}/include)
|
||||
include_directories(${yaml_include_dir})
|
||||
include_directories(${http_parser_source_dir})
|
||||
include_directories(${lexertl_dir})
|
||||
include_directories(${libuv_source_dir}/include)
|
||||
|
@ -9,7 +9,7 @@
|
||||
compile_cpu_path: "./compiled/cpu/"
|
||||
|
||||
# path to the template (cpp) for codes generation
|
||||
template_cpu_cpp_path: "./template/template_code_cpu.cpp"
|
||||
template_cpu_cpp_path: "./template/template_code_cpu_cpp"
|
||||
|
||||
# path to the template (hpp) for codes generation
|
||||
template_cpu_hpp_path: "./template/template_code_cpu.hpp"
|
||||
|
1
example/.gitignore
vendored
Normal file
1
example/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.out
|
0
src/examples/timestamp.cpp → example/timestamp.cpp
Executable file → Normal file
0
src/examples/timestamp.cpp → example/timestamp.cpp
Executable file → Normal file
@ -5,6 +5,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
@ -47,6 +48,7 @@ int main(void)
|
||||
|
||||
auto max = std::accumulate(buckets.begin(), buckets.end(), 0u,
|
||||
[](auto& acc, auto& x) { return std::max(acc, x.load()); });
|
||||
assert(max != 0u);
|
||||
|
||||
std::cout << std::fixed;
|
||||
|
@ -90,7 +90,7 @@ std::string BoltDecoder::read_string()
|
||||
size = marker & 0x0F;
|
||||
}
|
||||
// if the marker is 0xD0, size is an 8-bit unsigned integer
|
||||
if (marker == pack::String8) {
|
||||
else if (marker == pack::String8) {
|
||||
size = read_byte();
|
||||
}
|
||||
// if the marker is 0xD1, size is a 16-bit big-endian unsigned integer
|
||||
|
@ -1,131 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
|
||||
#include "data_structures/queue/mpsc_queue.hpp"
|
||||
#include "utils/bash_colors.hpp"
|
||||
|
||||
class Log
|
||||
{
|
||||
public:
|
||||
enum class Level : std::uint_fast8_t { Debug, Info, Warn, Error };
|
||||
|
||||
struct Message
|
||||
{
|
||||
std::chrono::system_clock::time_point time;
|
||||
Level level;
|
||||
std::string text;
|
||||
std::string file;
|
||||
std::string function;
|
||||
size_t line;
|
||||
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& stream, const Message& message)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
|
||||
auto t = system_clock::to_time_t(message.time);
|
||||
auto time_string = std::string(std::ctime(&t));
|
||||
|
||||
return stream << bash_color::green
|
||||
<< "[" << to_string(message.level) << "] " << bash_color::end
|
||||
<< message.text;
|
||||
/* << bash_color::yellow << " on " << bash_color::end */
|
||||
/* << time_string.substr(0, time_string.size() - 1) */
|
||||
/* << bash_color::yellow << " in file " << bash_color::end */
|
||||
/* << message.file */
|
||||
/* << bash_color::yellow << " in function " << bash_color::end */
|
||||
/* << message.function */
|
||||
/* << bash_color::yellow << " at line " << bash_color::end */
|
||||
/* << message.line; */
|
||||
}
|
||||
};
|
||||
|
||||
static std::array<std::string, 4> level_strings;
|
||||
|
||||
static std::string to_string(Log::Level level)
|
||||
{
|
||||
return level_strings[static_cast<std::size_t>(level)];
|
||||
}
|
||||
|
||||
Log() : alive(true), worker([this]() { work(); }) {}
|
||||
|
||||
~Log()
|
||||
{
|
||||
alive.store(false, std::memory_order_seq_cst);
|
||||
worker.join();
|
||||
}
|
||||
|
||||
static Log& instance()
|
||||
{
|
||||
static Log log;
|
||||
return log;
|
||||
}
|
||||
|
||||
public:
|
||||
Log(Log&) = delete;
|
||||
Log(Log&&) = delete;
|
||||
|
||||
static void log(Level level, const std::string& text,
|
||||
const char* file, const char* function, size_t line)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
|
||||
auto& log = Log::instance();
|
||||
|
||||
log.messages.push(std::unique_ptr<Message>(new Message {
|
||||
system_clock::now(), level, text, file, function, line
|
||||
}));
|
||||
}
|
||||
|
||||
private:
|
||||
lockfree::MpscQueue<Message> messages;
|
||||
std::atomic<bool> alive;
|
||||
std::thread worker;
|
||||
|
||||
void work()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
while(true)
|
||||
{
|
||||
auto message = messages.pop();
|
||||
|
||||
if(message != nullptr)
|
||||
{
|
||||
std::cerr << *message << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!alive)
|
||||
return;
|
||||
|
||||
std::this_thread::sleep_for(10ms);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::array<std::string, 4> Log::level_strings {{
|
||||
"DEBUG", "INFO", "WARN", "ERROR"
|
||||
}};
|
||||
|
||||
#define LOG(_LEVEL_, _MESSAGE_) \
|
||||
Log::log( \
|
||||
_LEVEL_, \
|
||||
static_cast<std::ostringstream&>( \
|
||||
std::ostringstream().flush() << _MESSAGE_ \
|
||||
).str(), \
|
||||
__FILE__, \
|
||||
__PRETTY_FUNCTION__, \
|
||||
__LINE__ \
|
||||
);
|
||||
|
||||
#ifdef NDEBUG
|
||||
# define LOG_DEBUG(_) do {} while(0);
|
||||
#else
|
||||
# define LOG_DEBUG(_MESSAGE_) LOG(Log::Level::Debug, _MESSAGE_)
|
||||
#endif
|
1
src/demo/.gitignore
vendored
1
src/demo/.gitignore
vendored
@ -1 +0,0 @@
|
||||
ve/
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"esnext": true,
|
||||
"browser": true,
|
||||
"globals": {
|
||||
"sigma": true,
|
||||
"console": true
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from util import get_env, set_modul_attrs
|
||||
|
||||
host = "0.0.0.0"
|
||||
|
||||
port = 5000
|
||||
|
||||
log_level = 'INFO'
|
||||
|
||||
try:
|
||||
set_modul_attrs(__name__, json.loads(get_env('CONFIG')))
|
||||
except:
|
||||
pass
|
@ -1,20 +0,0 @@
|
||||
FROM ubuntu:14.04
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y python3.4 python3.4-dev python3-pip python3-setuptools
|
||||
# RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN pip3 install flask
|
||||
|
||||
COPY simulation /app/simulation
|
||||
COPY demo_server_init.py /app/demo_server_init.py
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV MEMGRAPH_DEMO prod
|
||||
|
||||
# uwsgi --http-socket 0.0.0.0:8080 --module demo_server_init:app \
|
||||
# --master --enable-threads
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python3", "demo_server_init.py"]
|
@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
The demo server init script. Environment could be configured
|
||||
via the MEMGRAPH_DEMO environtment variable. Available environments
|
||||
are: debug, prod.
|
||||
'''
|
||||
|
||||
import logging
|
||||
|
||||
from util import get_env
|
||||
from simulation.web_server import SimulationWebServer
|
||||
|
||||
environment = get_env('MEMGRAPH_DEMO', 'debug')
|
||||
wsgi = get_env('MEMGRAPH_DEMO_WSGI', 'werkzeug')
|
||||
|
||||
|
||||
def _init():
|
||||
'''
|
||||
Initialzies logging level and server.
|
||||
'''
|
||||
if environment == 'prod':
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
elif environment == 'test':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
else:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
return SimulationWebServer().server
|
||||
|
||||
|
||||
app = _init()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if wsgi == 'gevent':
|
||||
from gevent.wsgi import WSGIServer
|
||||
http_server = WSGIServer(('', 8080), app)
|
||||
http_server.serve_forever()
|
||||
else:
|
||||
app.run(host="0.0.0.0", port=8080)
|
@ -1,10 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y git
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
RUN git clone --recursive https://${MEMGRAPH_PULL_USER}:${MEMGRAPH_PULL_PASS}@phabricator.tomicevic.com/diffusion/MG/memgraph.git
|
||||
|
||||
CMD ["/bin/bash"]
|
@ -1,19 +0,0 @@
|
||||
Cython==0.23.4
|
||||
Flask==0.10.1
|
||||
Jinja2==2.8
|
||||
MarkupSafe==0.23
|
||||
Werkzeug==0.11.4
|
||||
decorator==4.0.9
|
||||
gevent==1.1rc5
|
||||
greenlet==0.4.9
|
||||
ipython==4.1.1
|
||||
ipython-genutils==0.1.0
|
||||
itsdangerous==0.24
|
||||
path.py==8.1.2
|
||||
pexpect==4.0.1
|
||||
pickleshare==0.6
|
||||
ptyprocess==0.5.1
|
||||
requests==2.9.1
|
||||
simplegeneric==0.8.1
|
||||
traitlets==4.1.0
|
||||
uWSGI==2.0.12
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker stop memgraph_demo
|
||||
docker rm memgraph_demo
|
||||
docker run -it --rm memgraph_demo
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker stop neo4j_server
|
||||
docker rm neo4j_server
|
||||
docker run -d --name neo4j_server --net=host -p 7474:7474 neo4j
|
@ -1,7 +0,0 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
# TODO init object by module name
|
||||
|
||||
# something like
|
||||
# module_name, class_name = module_class_string.rsplit(".", 1)
|
||||
# return getattr(importlib.import_module(module_name), class_name)()
|
@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class SimulationEpochResult(object):
|
||||
'''
|
||||
Encapsulates single epoch result.
|
||||
'''
|
||||
|
||||
def __init__(self, per_query, for_all):
|
||||
'''
|
||||
Sets per_query and for_all results.
|
||||
|
||||
:param per_query: list of SimulationGroupResult objects
|
||||
:param for_all: float, queries per second
|
||||
'''
|
||||
self.per_query = per_query
|
||||
self.for_all = for_all
|
||||
|
||||
def json_data(self):
|
||||
'''
|
||||
:returns: dict, epoch results
|
||||
'''
|
||||
return {
|
||||
"per_query": [item.json_data() for item in self.per_query],
|
||||
"for_all": self.for_all
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import itertools
|
||||
import http
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
from .substitutor import substitute
|
||||
from .epoch_result import SimulationEpochResult
|
||||
from .group_result import SimulationGroupResult
|
||||
from .iteration_result import SimulationIterationResult
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate_qps(results, delta_t=None):
|
||||
'''
|
||||
Calculates queris per second for the results list. The main idea
|
||||
is to scale up results to the result with the biggest execution time.
|
||||
|
||||
Example:
|
||||
Let say that 2 workers execute the same query. First worker
|
||||
executes 100 queries in 1s, second worker executes 10 queries in 10s.
|
||||
In that case first query result has to be scaled up.
|
||||
Here we are goint to make aproximation, if first worker
|
||||
execution time was 10s, it would execute 1000 queries.
|
||||
So, the total speed is (1000q + 10q) / 10s = 101 qps. In that case
|
||||
101 would be returned from this function.
|
||||
|
||||
:param results: list of SimulationIterationResult objects
|
||||
:returns: queries per second result calculated on the input list
|
||||
'''
|
||||
min_start_time = min([result.start_time for result in results])
|
||||
max_end_time = max([result.end_time for result in results])
|
||||
delta_t = max_end_time - min_start_time
|
||||
qps = sum([result.count for result in results]) / delta_t
|
||||
return qps
|
||||
|
||||
|
||||
class SimulationExecutor(object):
|
||||
'''
|
||||
The main executor object. Every period the instance of this class
|
||||
will execute all queries, collect the results and calculate speed of
|
||||
queries execution.
|
||||
'''
|
||||
|
||||
def setup(self, params):
|
||||
'''
|
||||
Setup params and initialize the workers pool.
|
||||
|
||||
:param params: SimulationParams object
|
||||
'''
|
||||
self.params = params
|
||||
self.pool = ProcessPoolExecutor
|
||||
return self
|
||||
|
||||
def send(self, connection, query):
|
||||
'''
|
||||
Sends the query to the graph database.
|
||||
|
||||
:param connection: http.client.HTTPConnection
|
||||
:param query: str, query string
|
||||
'''
|
||||
body = json.dumps({'statements': [{'statement': substitute(query)}]})
|
||||
headers = {
|
||||
'Authorization': self.params.authorization,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
connection.request('POST', '/db/data/transaction/commit',
|
||||
body=body, headers=headers)
|
||||
response = connection.getresponse()
|
||||
log.debug('New response: %s' % response.read())
|
||||
|
||||
def iteration(self, task):
|
||||
'''
|
||||
Executes the task. Task encapsulates the informations about query.
|
||||
The task is smallest piece of work and this method will try to execute
|
||||
queries (one query, more times) from the task as fastest as possible.
|
||||
Execution time of this method is constrained with the period_time time.
|
||||
|
||||
:param task: instance of SimulationTask class.
|
||||
:returns: SimulationIterationResult
|
||||
'''
|
||||
count = 0
|
||||
delta_t = 0
|
||||
|
||||
log.debug("New worker with PID: %s" % os.getpid())
|
||||
|
||||
connection = http.client.HTTPConnection(
|
||||
self.params.host, self.params.port)
|
||||
connection.connect()
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(self.params.queries_per_period):
|
||||
|
||||
# send the query on execution
|
||||
self.send(connection, task.query)
|
||||
|
||||
# calculate delta time
|
||||
end_time = time.time()
|
||||
delta_t = end_time - start_time
|
||||
|
||||
count = count + 1
|
||||
|
||||
if delta_t > self.params.period_time:
|
||||
break
|
||||
|
||||
connection.close()
|
||||
|
||||
return SimulationIterationResult(task.id, count, start_time, end_time)
|
||||
|
||||
def epoch(self):
|
||||
'''
|
||||
Single simulation epoc. All workers are going to execute
|
||||
their queries in the period that is period_time seconds length.
|
||||
'''
|
||||
log.debug('epoch')
|
||||
|
||||
max_workers = self.params.workers_per_query * len(self.params.tasks)
|
||||
|
||||
with self.pool(max_workers=max_workers) as executor:
|
||||
|
||||
log.debug('pool iter')
|
||||
|
||||
# execute all tasks
|
||||
start_time = time.time()
|
||||
futures = [executor.submit(self.iteration, task)
|
||||
for task in self.params.tasks
|
||||
for i in range(self.params.workers_per_query)]
|
||||
results = [future.result() for future in futures]
|
||||
end_time = time.time()
|
||||
epoch_time = end_time - start_time
|
||||
log.info("Total epoch time: %s" % epoch_time)
|
||||
|
||||
# per query calculation
|
||||
grouped = itertools.groupby(results, lambda x: x.id)
|
||||
per_query = [SimulationGroupResult(id, calculate_qps(list(tasks)))
|
||||
for id, tasks in grouped]
|
||||
|
||||
# for all calculation
|
||||
for_all = calculate_qps(results)
|
||||
|
||||
log.info('Queries per period: %s' % sum([r.count
|
||||
for r in results]))
|
||||
|
||||
return SimulationEpochResult(per_query, for_all)
|
@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class SimulationGroupResult(object):
|
||||
'''
|
||||
Encapsulates query per seconds information for qroup of workers
|
||||
(all workers that execute the same query).
|
||||
'''
|
||||
|
||||
def __init__(self, id, queries_per_second):
|
||||
'''
|
||||
:param id: str, query id
|
||||
:param queries_per_second: float, queries per second
|
||||
'''
|
||||
self.id = id
|
||||
self.queries_per_second = queries_per_second
|
||||
|
||||
def json_data(self):
|
||||
'''
|
||||
:returns: dict, {query_id(str):queries_per_second(float)}
|
||||
'''
|
||||
return {
|
||||
'id': self.id,
|
||||
'queries_per_second': self.queries_per_second
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class SimulationIterationResult(object):
|
||||
'''
|
||||
Encapsulates single worker result.
|
||||
'''
|
||||
|
||||
def __init__(self, id, count, start_time, end_time):
|
||||
'''
|
||||
:param id: str, query id
|
||||
:param count: int, number of the query exection
|
||||
:param delta_t: time of execution
|
||||
'''
|
||||
self.id = id
|
||||
self.count = count
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
self.delta_t = end_time - start_time
|
||||
self.queries_per_second = self.count / self.delta_t
|
||||
|
||||
def json_data(self):
|
||||
'''
|
||||
:returns: dict {query_id(str):queries_per_second(float)}
|
||||
'''
|
||||
return {
|
||||
"id": self.id,
|
||||
"queries_per_second": self.queries_per_second
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimulationParams(object):
|
||||
'''
|
||||
Encapsulates the simulation params.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Setup default params values.
|
||||
'''
|
||||
self.protocol = 'http'
|
||||
self.host = 'localhost'
|
||||
self.port = 7474
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
|
||||
self.period_time = 0.5
|
||||
self.workers_per_query = 1
|
||||
self.queries_per_second = 15000
|
||||
self.recalculate_qpp()
|
||||
|
||||
self.tasks = []
|
||||
|
||||
def json_data(self):
|
||||
'''
|
||||
:returns: dict with all param values
|
||||
'''
|
||||
return {
|
||||
"protocol": self.protocol,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"period_time": self.period_time,
|
||||
"workers_per_query": self.workers_per_query,
|
||||
"queries_per_period": self.queries_per_period,
|
||||
"queries_per_second": self.queries_per_second
|
||||
}
|
||||
|
||||
# protocol
|
||||
@property
|
||||
def protocol(self):
|
||||
return self._protocol
|
||||
|
||||
@protocol.setter
|
||||
def protocol(self, value):
|
||||
self._protocol = value
|
||||
|
||||
# host
|
||||
@property
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
@host.setter
|
||||
def host(self, value):
|
||||
self._host = value
|
||||
|
||||
# port
|
||||
@property
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
@port.setter
|
||||
def port(self, value):
|
||||
self._port = value
|
||||
|
||||
# username
|
||||
@property
|
||||
def username(self):
|
||||
return self._username
|
||||
|
||||
@username.setter
|
||||
def username(self, value):
|
||||
self._username = value
|
||||
log.info("Username is now: %s" % self._username)
|
||||
self.http_basic()
|
||||
|
||||
# password
|
||||
@property
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
@password.setter
|
||||
def password(self, value):
|
||||
self._password = value
|
||||
log.info("Password is now: %s" % self._password)
|
||||
self.http_basic()
|
||||
|
||||
def http_basic(self):
|
||||
'''
|
||||
Recalculates http authorization header.
|
||||
'''
|
||||
try:
|
||||
encoded = base64.b64encode(
|
||||
str.encode(self.username + ":" + self.password))
|
||||
self.authorization = "Basic " + encoded.decode()
|
||||
log.info("Authorization is now: %s" % self.authorization)
|
||||
except AttributeError:
|
||||
log.debug("Username or password isn't defined.")
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
# authorization
|
||||
@property
|
||||
def authorization(self):
|
||||
return self._authorization
|
||||
|
||||
@authorization.setter
|
||||
def authorization(self, value):
|
||||
self._authorization = value
|
||||
|
||||
# workers per query
|
||||
@property
|
||||
def workers_per_query(self):
|
||||
return self._workers_per_query
|
||||
|
||||
@workers_per_query.setter
|
||||
def workers_per_query(self, value):
|
||||
self._workers_per_query = value
|
||||
|
||||
# queries per second
|
||||
@property
|
||||
def queries_per_second(self):
|
||||
return self._queries_per_second
|
||||
|
||||
@queries_per_second.setter
|
||||
def queries_per_second(self, value):
|
||||
self._queries_per_second = value
|
||||
self.recalculate_qpp()
|
||||
|
||||
def recalculate_qpp(self):
|
||||
try:
|
||||
self.queries_per_period = \
|
||||
int(self.queries_per_second * self.period_time)
|
||||
except:
|
||||
pass
|
||||
|
||||
# queries per period
|
||||
@property
|
||||
def queries_per_period(self):
|
||||
return self._queries_per_period
|
||||
|
||||
@queries_per_period.setter
|
||||
def queries_per_period(self, value):
|
||||
self._queries_per_period = value
|
||||
|
||||
# period time
|
||||
@property
|
||||
def period_time(self):
|
||||
return self._period_time
|
||||
|
||||
@period_time.setter
|
||||
def period_time(self, value):
|
||||
self._period_time = value
|
||||
self.recalculate_qpp()
|
@ -1,77 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
char_options = string.ascii_lowercase + string.digits
|
||||
|
||||
|
||||
def random_integer(start=0, end=sys.maxsize):
|
||||
'''
|
||||
:returns: random integer between start and end
|
||||
'''
|
||||
return random.randint(start, end)
|
||||
|
||||
|
||||
def random_float(start=0, end=1):
|
||||
'''
|
||||
:returns: random float between start and end (uniform distribution)
|
||||
'''
|
||||
return random.uniform(start, end)
|
||||
|
||||
|
||||
def random_bool():
|
||||
'''
|
||||
:returns: random bool value
|
||||
'''
|
||||
return bool(random.getrandbits(1))
|
||||
|
||||
|
||||
def random_string(size=5):
|
||||
'''
|
||||
:param size: int, string size
|
||||
|
||||
:returns: random string of specific size build from ascii_lowercase chars
|
||||
and digits
|
||||
'''
|
||||
return ''.join([random.choice(char_options)
|
||||
for _ in range(size)])
|
||||
|
||||
|
||||
placeholders = {
|
||||
'#': random_integer,
|
||||
'@': random_float,
|
||||
'*': random_bool,
|
||||
'^': random_string
|
||||
}
|
||||
|
||||
|
||||
def substitute(text='', placeholders=placeholders):
|
||||
'''
|
||||
Substitutes chars in text with values generated from functions placed
|
||||
in placeholders dict.
|
||||
|
||||
:param text: str, substitutable text
|
||||
:param placeholders: dict, key is char that will be substituted, value
|
||||
is function that is going to be used to generate
|
||||
a new value
|
||||
'''
|
||||
return ''.join((c if c not in placeholders else str(placeholders[c]())
|
||||
for c in iter(text)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def test_1():
|
||||
print([f() for f in [random_integer, random_float,
|
||||
random_bool, random_string]])
|
||||
|
||||
def test_2():
|
||||
return substitute('int # float @ bool * string ^')
|
||||
|
||||
def test_3():
|
||||
print(test_2())
|
||||
|
||||
test_3()
|
@ -1,24 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class SimulationTask(object):
|
||||
'''
|
||||
Encapsulates query data.
|
||||
'''
|
||||
|
||||
def __init__(self, id, query):
|
||||
'''
|
||||
:param id: query id
|
||||
:param query: str, query string
|
||||
'''
|
||||
self.id = id
|
||||
self.query = query
|
||||
|
||||
def json_data(self):
|
||||
'''
|
||||
:returns: dict with all elements
|
||||
'''
|
||||
return {
|
||||
"id": self.id,
|
||||
"query": self.query
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
from .executor import SimulationExecutor
|
||||
from .params import SimulationParams
|
||||
from .task import SimulationTask
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimulationWebServer(object):
|
||||
'''
|
||||
Memgraph demo fontend server. For now it wraps the flask server.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Instantiates the flask web server.
|
||||
'''
|
||||
self.is_simulation_running = False
|
||||
self.simulation_stats = None
|
||||
self.simulation_params = SimulationParams()
|
||||
self.simulation_executor = SimulationExecutor()
|
||||
self.server = Flask(__name__)
|
||||
self.setup_routes()
|
||||
self.server.before_first_request(self.before_first_request)
|
||||
|
||||
def setup_routes(self):
|
||||
'''
|
||||
Setup all routes.
|
||||
'''
|
||||
self.add_route('/ping', self.ping, 'GET')
|
||||
self.add_route('/', self.index, 'GET')
|
||||
self.add_route('/<path:path>', self.static, 'GET')
|
||||
self.add_route('/tasks', self.tasks_get, 'GET')
|
||||
self.add_route('/tasks', self.tasks_set, 'POST')
|
||||
self.add_route('/start', self.start, 'POST')
|
||||
self.add_route('/stop', self.stop, 'POST')
|
||||
self.add_route('/stats', self.stats, 'GET')
|
||||
self.add_route('/params', self.params_get, 'GET')
|
||||
self.add_route('/params', self.params_set, 'POST')
|
||||
|
||||
def before_first_request(self):
|
||||
'''
|
||||
Initializes simulation executor before first request.
|
||||
'''
|
||||
log.info('before first request')
|
||||
self.simulation_executor.setup(self.simulation_params)
|
||||
|
||||
def add_route(self, route, code_method, http_method):
|
||||
'''
|
||||
Registers URL rule
|
||||
|
||||
:param route: str, route string
|
||||
:param object_method: object method responsible for the
|
||||
request handling
|
||||
:param http_method: name of http method
|
||||
'''
|
||||
self.server.add_url_rule(route, '%s_%s' % (route, http_method),
|
||||
code_method, methods=[http_method])
|
||||
|
||||
def ping(self):
|
||||
'''
|
||||
Ping endpoint. Returns 204 HTTP status code.
|
||||
'''
|
||||
return ('', 204)
|
||||
|
||||
def index(self):
|
||||
'''
|
||||
Serves demo.html on the index path.
|
||||
'''
|
||||
print('index')
|
||||
return self.server.send_static_file('demo.html')
|
||||
|
||||
def static(self, path):
|
||||
'''
|
||||
Serves other static files.
|
||||
'''
|
||||
return self.server.send_static_file(path)
|
||||
|
||||
def tasks_get(self):
|
||||
'''
|
||||
Retutns all defined tasks.
|
||||
'''
|
||||
return json.dumps(
|
||||
[task.json_data() for task in self.simulation_params.tasks]
|
||||
)
|
||||
|
||||
def tasks_set(self):
|
||||
'''
|
||||
Register tasks. Task is object that encapsulates single query data.
|
||||
'''
|
||||
data = request.get_json()['data']
|
||||
|
||||
self.simulation_params.tasks = \
|
||||
[SimulationTask(item['id'], item['query'])
|
||||
for item in data]
|
||||
|
||||
return ('', 200)
|
||||
|
||||
def run_simulation(self):
|
||||
'''
|
||||
If flag is_simulation_running flag is up (True) the executor
|
||||
epoch will be executed. Epochs will be executed until somebody
|
||||
set is_simulation_running flag to Flase.
|
||||
'''
|
||||
log.info('new simulation run')
|
||||
|
||||
while self.is_simulation_running:
|
||||
self.simulation_stats = self.simulation_executor.epoch()
|
||||
|
||||
def start(self):
|
||||
'''
|
||||
Starts new executor epoch in separate thread.
|
||||
'''
|
||||
self.is_simulation_running = True
|
||||
t = threading.Thread(target=self.run_simulation, daemon=True)
|
||||
t.start()
|
||||
return ('', 204)
|
||||
|
||||
def stop(self):
|
||||
'''
|
||||
On POST /stop, stops the executor. The is not immediately, first
|
||||
the is_simulation_running flag is set to False value, and next
|
||||
epoc of executor won't be executed.
|
||||
'''
|
||||
self.is_simulation_running = False
|
||||
return ('', 204)
|
||||
|
||||
def stats(self):
|
||||
'''
|
||||
Returns the simulation stats. Queries per second.
|
||||
'''
|
||||
if not self.simulation_stats:
|
||||
return ('', 204)
|
||||
|
||||
return jsonify(self.simulation_stats.json_data())
|
||||
|
||||
def params_get(self):
|
||||
'''
|
||||
Returns simulation parameters.
|
||||
'''
|
||||
return jsonify(self.simulation_params.json_data())
|
||||
|
||||
def params_set(self):
|
||||
'''
|
||||
Sets simulation parameters.
|
||||
'''
|
||||
data = request.get_json()
|
||||
|
||||
param_names = ['protocol', 'host', 'port', 'username', 'password',
|
||||
'period_time', 'queries_per_second',
|
||||
'workers_per_query']
|
||||
|
||||
for param in param_names:
|
||||
if param in data:
|
||||
setattr(self.simulation_params, param, data[param])
|
||||
|
||||
return self.params_get()
|
8
src/demo/static/cypher/.gitattributes
vendored
8
src/demo/static/cypher/.gitattributes
vendored
@ -1,8 +0,0 @@
|
||||
*.txt text
|
||||
*.js text
|
||||
*.html text
|
||||
*.md text
|
||||
*.json text
|
||||
*.yml text
|
||||
*.css text
|
||||
*.svg text
|
8
src/demo/static/cypher/.gitignore
vendored
8
src/demo/static/cypher/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/test*.html
|
||||
.tern-*
|
||||
*~
|
||||
*.swp
|
||||
.idea
|
||||
*.iml
|
@ -1,10 +0,0 @@
|
||||
/node_modules
|
||||
/demo
|
||||
/doc
|
||||
/test
|
||||
/test*.html
|
||||
/index.html
|
||||
/mode/*/*test.js
|
||||
/mode/*/*.html
|
||||
/mode/index.html
|
||||
.*
|
@ -1,4 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
sudo: false
|
@ -1,335 +0,0 @@
|
||||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CodeMirror-linenumbers {}
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0 3px 0 5px;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker { color: black; }
|
||||
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
background: #7e7;
|
||||
}
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
background-color: #7e7;
|
||||
}
|
||||
@-moz-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
|
||||
/* Can style cursor different in overwrite (non-insert) mode */
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||
|
||||
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
.cm-strikethrough {text-decoration: line-through;}
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3 {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -30px; margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actual scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
left: 0; bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -30px;
|
||||
/* Hack to make IE7 behave */
|
||||
*zoom:1;
|
||||
*display:inline;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Force content-box sizing for the elements where we expect it */
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-sizer,
|
||||
.CodeMirror-gutter,
|
||||
.CodeMirror-gutters,
|
||||
.CodeMirror-linenumber {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor { position: absolute; }
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
div.CodeMirror-dragcursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
.CodeMirror-crosshair { cursor: crosshair; }
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||
|
||||
.cm-searching {
|
||||
background: #ffa;
|
||||
background: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
|
||||
.CodeMirror span { *vertical-align: text-bottom; }
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border { padding-right: .1px; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* See issue #2901 */
|
||||
.cm-tab-wrap-hack:after { content: ''; }
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
File diff suppressed because it is too large
Load Diff
@ -1,146 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// By the Neo4j Team and contributors.
|
||||
// https://github.com/neo4j-contrib/CodeMirror
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var wordRegexp = function(words) {
|
||||
return new RegExp("^(?:" + words.join("|") + ")$", "i");
|
||||
};
|
||||
|
||||
CodeMirror.defineMode("cypher", function(config) {
|
||||
var tokenBase = function(stream/*, state*/) {
|
||||
var ch = stream.next();
|
||||
if (ch === "\"" || ch === "'") {
|
||||
stream.match(/.+?["']/);
|
||||
return "string";
|
||||
}
|
||||
if (/[{}\(\),\.;\[\]]/.test(ch)) {
|
||||
curPunc = ch;
|
||||
return "node";
|
||||
} else if (ch === "/" && stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
} else if (operatorChars.test(ch)) {
|
||||
stream.eatWhile(operatorChars);
|
||||
return null;
|
||||
} else {
|
||||
stream.eatWhile(/[_\w\d]/);
|
||||
if (stream.eat(":")) {
|
||||
stream.eatWhile(/[\w\d_\-]/);
|
||||
return "atom";
|
||||
}
|
||||
var word = stream.current();
|
||||
if (funcs.test(word)) return "builtin";
|
||||
if (preds.test(word)) return "def";
|
||||
if (keywords.test(word)) return "keyword";
|
||||
return "variable";
|
||||
}
|
||||
};
|
||||
var pushContext = function(state, type, col) {
|
||||
return state.context = {
|
||||
prev: state.context,
|
||||
indent: state.indent,
|
||||
col: col,
|
||||
type: type
|
||||
};
|
||||
};
|
||||
var popContext = function(state) {
|
||||
state.indent = state.context.indent;
|
||||
return state.context = state.context.prev;
|
||||
};
|
||||
var indentUnit = config.indentUnit;
|
||||
var curPunc;
|
||||
var funcs = wordRegexp(["abs", "acos", "allShortestPaths", "asin", "atan", "atan2", "avg", "ceil", "coalesce", "collect", "cos", "cot", "count", "degrees", "e", "endnode", "exp", "extract", "filter", "floor", "haversin", "head", "id", "keys", "labels", "last", "left", "length", "log", "log10", "lower", "ltrim", "max", "min", "node", "nodes", "percentileCont", "percentileDisc", "pi", "radians", "rand", "range", "reduce", "rel", "relationship", "relationships", "replace", "reverse", "right", "round", "rtrim", "shortestPath", "sign", "sin", "size", "split", "sqrt", "startnode", "stdev", "stdevp", "str", "substring", "sum", "tail", "tan", "timestamp", "toFloat", "toInt", "toString", "trim", "type", "upper"]);
|
||||
var preds = wordRegexp(["all", "and", "any", "contains", "exists", "has", "in", "none", "not", "or", "single", "xor"]);
|
||||
var keywords = wordRegexp(["as", "asc", "ascending", "assert", "by", "case", "commit", "constraint", "create", "csv", "cypher", "delete", "desc", "descending", "detach", "distinct", "drop", "else", "end", "ends", "explain", "false", "fieldterminator", "foreach", "from", "headers", "in", "index", "is", "join", "limit", "load", "match", "merge", "null", "on", "optional", "order", "periodic", "profile", "remove", "return", "scan", "set", "skip", "start", "starts", "then", "true", "union", "unique", "unwind", "using", "when", "where", "with"]);
|
||||
var operatorChars = /[*+\-<>=&|~%^]/;
|
||||
|
||||
return {
|
||||
startState: function(/*base*/) {
|
||||
return {
|
||||
tokenize: tokenBase,
|
||||
context: null,
|
||||
indent: 0,
|
||||
col: 0
|
||||
};
|
||||
},
|
||||
token: function(stream, state) {
|
||||
if (stream.sol()) {
|
||||
if (state.context && (state.context.align == null)) {
|
||||
state.context.align = false;
|
||||
}
|
||||
state.indent = stream.indentation();
|
||||
}
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
var style = state.tokenize(stream, state);
|
||||
if (style !== "comment" && state.context && (state.context.align == null) && state.context.type !== "pattern") {
|
||||
state.context.align = true;
|
||||
}
|
||||
if (curPunc === "(") {
|
||||
pushContext(state, ")", stream.column());
|
||||
} else if (curPunc === "[") {
|
||||
pushContext(state, "]", stream.column());
|
||||
} else if (curPunc === "{") {
|
||||
pushContext(state, "}", stream.column());
|
||||
} else if (/[\]\}\)]/.test(curPunc)) {
|
||||
while (state.context && state.context.type === "pattern") {
|
||||
popContext(state);
|
||||
}
|
||||
if (state.context && curPunc === state.context.type) {
|
||||
popContext(state);
|
||||
}
|
||||
} else if (curPunc === "." && state.context && state.context.type === "pattern") {
|
||||
popContext(state);
|
||||
} else if (/atom|string|variable/.test(style) && state.context) {
|
||||
if (/[\}\]]/.test(state.context.type)) {
|
||||
pushContext(state, "pattern", stream.column());
|
||||
} else if (state.context.type === "pattern" && !state.context.align) {
|
||||
state.context.align = true;
|
||||
state.context.col = stream.column();
|
||||
}
|
||||
}
|
||||
return style;
|
||||
},
|
||||
indent: function(state, textAfter) {
|
||||
var firstChar = textAfter && textAfter.charAt(0);
|
||||
var context = state.context;
|
||||
if (/[\]\}]/.test(firstChar)) {
|
||||
while (context && context.type === "pattern") {
|
||||
context = context.prev;
|
||||
}
|
||||
}
|
||||
var closing = context && firstChar === context.type;
|
||||
if (!context) return 0;
|
||||
if (context.type === "keywords") return CodeMirror.commands.newlineAndIndent;
|
||||
if (context.align) return context.col + (closing ? 0 : 1);
|
||||
return context.indent + (closing ? 0 : indentUnit);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.modeExtensions["cypher"] = {
|
||||
autoFormatLineBreaks: function(text) {
|
||||
var i, lines, reProcessedPortion;
|
||||
var lines = text.split("\n");
|
||||
var reProcessedPortion = /\s+\b(return|where|order by|match|with|skip|limit|create|delete|set)\b\s/g;
|
||||
for (var i = 0; i < lines.length; i++)
|
||||
lines[i] = lines[i].replace(reProcessedPortion, " \n$1 ").trim();
|
||||
return lines.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
CodeMirror.defineMIME("application/x-cypher-query", "cypher");
|
||||
|
||||
});
|
@ -1,43 +0,0 @@
|
||||
/* neo theme for codemirror */
|
||||
|
||||
/* Color scheme */
|
||||
|
||||
.cm-s-neo.CodeMirror {
|
||||
background-color:#ffffff;
|
||||
color:#2e383c;
|
||||
line-height:1.4375;
|
||||
}
|
||||
.cm-s-neo .cm-comment { color:#75787b; }
|
||||
.cm-s-neo .cm-keyword, .cm-s-neo .cm-property { color:#1d75b3; }
|
||||
.cm-s-neo .cm-atom,.cm-s-neo .cm-number { color:#75438a; }
|
||||
.cm-s-neo .cm-node,.cm-s-neo .cm-tag { color:#9c3328; }
|
||||
.cm-s-neo .cm-string { color:#b35e14; }
|
||||
.cm-s-neo .cm-variable,.cm-s-neo .cm-qualifier { color:#047d65; }
|
||||
|
||||
|
||||
/* Editor styling */
|
||||
|
||||
.cm-s-neo pre {
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.cm-s-neo .CodeMirror-gutters {
|
||||
border:none;
|
||||
border-right:10px solid transparent;
|
||||
background-color:transparent;
|
||||
}
|
||||
|
||||
.cm-s-neo .CodeMirror-linenumber {
|
||||
padding:0;
|
||||
color:#e0e2e5;
|
||||
}
|
||||
|
||||
.cm-s-neo .CodeMirror-guttermarker { color: #1d75b3; }
|
||||
.cm-s-neo .CodeMirror-guttermarker-subtle { color: #e0e2e5; }
|
||||
|
||||
.cm-s-neo .CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
background: rgba(155,157,162,0.37);
|
||||
z-index: 1;
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
body {
|
||||
font-family: GillSans, Calibri, Trebuchet, sans-serif;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.CodeMirror pre {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* label color inactive */
|
||||
.input-field label {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* label color active */
|
||||
.input-field input[type=text]:focus + label {
|
||||
color: #007FFF;
|
||||
}
|
||||
|
||||
/* label bottom border color */
|
||||
.input-field input[type=text]:focus {
|
||||
border-bottom: 1px solid #007FFF;
|
||||
box-shadow: 0 1px 0 0 #007FFF;
|
||||
}
|
||||
|
||||
.input-field input[type=text] {
|
||||
border-bottom: 1px solid #007FFF;
|
||||
box-shadow: 0 1px 0 0 #007FFF;
|
||||
}
|
||||
|
||||
label#hostname_port {
|
||||
font: normal 15px GillSans, Calibri, Trebuchet, sans-serif !important;
|
||||
}
|
||||
|
||||
#hostname_port {
|
||||
font-size: 2;
|
||||
}
|
||||
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 80%;
|
||||
color: #007FFF;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-top: 1px solid #eee;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
svg, text {
|
||||
font-size: 28;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #007FFF;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #007FFF;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
background-color: #007FFF;
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Memgraph Demo</title>
|
||||
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.min.css">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="cypher/codemirror.css">
|
||||
<link rel="stylesheet" href="cypher/neo.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- title row -->
|
||||
<div class="row">
|
||||
<div class="valign-wrapper">
|
||||
<div class="input-field col s2">
|
||||
<input value="localhost:7474" id="neo4j_url" type="text" class="validate">
|
||||
<label id="neo4j_url" for="neo4j_url">hostname:port</label>
|
||||
</div>
|
||||
<div class="col s2">
|
||||
<button id="run-neo4j"
|
||||
class="btn waves-effect waves-light"
|
||||
type="submit" name="action">
|
||||
Start Neo4j
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="valign center col s4">Graph Benchmark</h1>
|
||||
<div class="col s2">
|
||||
<button id="run-memgraph" style="float:right;"
|
||||
class="btn waves-effect waves-light right-align"
|
||||
type="submit" name="action">
|
||||
Start Memgraph
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-field col s2">
|
||||
<input value="localhost:7475" id="memgraph_url" type="text" class="validate">
|
||||
<label id="memgraph_url" for="memgraph_url">hostname:port</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- neo4j cards -->
|
||||
<div class="row">
|
||||
<div class="col s2">
|
||||
<div id="q-0-0" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-0-1" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-0-2" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-0-3" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-0-4" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the graph -->
|
||||
<div class="col s8">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div id="chart" style="height: 750px;">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- memgraph cards -->
|
||||
<div class="col s2">
|
||||
<div id="q-1-0" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-1-1" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-1-2" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-1-3" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
<div id="q-1-4" class="card">
|
||||
<div class="qps valign-wrapper">
|
||||
<div class="col s6">
|
||||
<h3 id="text" class="valign center w100">0</h3>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<canvas id="gauge" width="150" height="60"
|
||||
class="valign center"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="query">MATCH (n:Person) RETURN n</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/js/materialize.min.js"></script>
|
||||
<script type="text/javascript" src="http://bernii.github.io/gauge.js/dist/gauge.min.js"></script>
|
||||
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
|
||||
<script src="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.min.js"></script>
|
||||
<script src="cypher/codemirror.js"></script>
|
||||
<script src="cypher/cypher.js"></script>
|
||||
<script src="demo.js"></script>
|
||||
|
||||
</body>
|
@ -1,308 +0,0 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
class QpsGauge {
|
||||
constructor(canvas) {
|
||||
this.gauge = new Gauge(canvas).setOptions({
|
||||
lines: 12, // The number of lines to draw
|
||||
angle: 0.07, // The length of each line
|
||||
lineWidth: 0.27, // The line thickness
|
||||
pointer: {
|
||||
length: 0.82, // The radius of the inner circle
|
||||
strokeWidth: 0.073, // The rotation offset
|
||||
color: '#000' // Fill color
|
||||
},
|
||||
// If true, the pointer will not go past the end of the gauge
|
||||
limitMax: 'true',
|
||||
strokeColor: '#e0e0e0', // to see which ones work best for you
|
||||
generateGradient: true,
|
||||
percentColors: [[0.0, '#d32f2f'], [0.5, '#ffee58'], [1.0, '#388e3c']],
|
||||
});
|
||||
|
||||
this.gauge.animationSpeed = 1;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.gauge.set(value);
|
||||
}
|
||||
|
||||
setMax(value) {
|
||||
this.gauge.maxValue = this.maxValue = value;
|
||||
}
|
||||
|
||||
getMax() {
|
||||
return this.maxValue;
|
||||
}
|
||||
}
|
||||
|
||||
class CypherQuery {
|
||||
constructor(textarea) {
|
||||
this.textarea = textarea;
|
||||
this.editor = CodeMirror.fromTextArea(this.textarea, {
|
||||
height: 0,
|
||||
lineNumbers: true,
|
||||
mode: 'application/x-cypher-query',
|
||||
indentWithTabs: false,
|
||||
smartIndent: true,
|
||||
matchBrackets: true,
|
||||
theme: 'neo',
|
||||
viewportMargin: Infinity
|
||||
});
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.editor.getDoc().setValue(value);
|
||||
}
|
||||
|
||||
get(separator) {
|
||||
separator = separator ? separator : ' ';
|
||||
this.editor.getDoc().getValue(separator);
|
||||
}
|
||||
}
|
||||
|
||||
class QpsText {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
get() {
|
||||
$(this.element).text();
|
||||
}
|
||||
|
||||
set(text) {
|
||||
$(this.element).text(text);
|
||||
}
|
||||
}
|
||||
|
||||
class QueryCard {
|
||||
constructor(card, maxQps, value) {
|
||||
this.card = card;
|
||||
this.maxQps = maxQps;
|
||||
|
||||
value = value ? value : 1;
|
||||
|
||||
this.text = new QpsText($(card).find('#text')[0]);
|
||||
this.gauge = new QpsGauge($(card).find('#gauge')[0]);
|
||||
this.query = new CypherQuery($(card).find('#query')[0]);
|
||||
|
||||
this.gauge.setMax(maxQps + 1);
|
||||
this.gauge.animationSpeed = 1;
|
||||
this.gauge.set(value);
|
||||
}
|
||||
|
||||
set_value(value) {
|
||||
this.text.set(value);
|
||||
this.gauge.set(value);
|
||||
}
|
||||
|
||||
set_query(query) {
|
||||
this.query.set(query);
|
||||
}
|
||||
}
|
||||
|
||||
// put \n on module 2 space positions
|
||||
var put_new_line_mod_2 = function(array) {
|
||||
let join_array = array.map(function(o, i) {
|
||||
if (i % 2 === 0)
|
||||
return ' ';
|
||||
else
|
||||
return '\n';
|
||||
});
|
||||
join_array.pop();
|
||||
return array.map(function(v,i) {
|
||||
return [v, join_array[i]];
|
||||
}).reduce(function(a,b) {
|
||||
return a.concat(b);
|
||||
});
|
||||
};
|
||||
|
||||
// -- variable part ----
|
||||
let running = false;
|
||||
let value = 0;
|
||||
let maxQps = 10000;
|
||||
let memgraphCards = [];
|
||||
let neo4jCards = [];
|
||||
let memgraphSec = 0;
|
||||
let neo4jSec = 0;
|
||||
let memgraphLine = [];
|
||||
let neo4jLine = [];
|
||||
var queries = [
|
||||
"CREATE (n:Item{id:@}) RETURN n",
|
||||
"MATCH (n:Item{id:#}),(m:Item{id:#}) CREATE (n)-[r:test]->(m) RETURN r",
|
||||
"MATCH (n:Item{id:#}) SET n.prop=# RETURN n",
|
||||
"MATCH (n:Item{id:#}) RETURN n",
|
||||
"MATCH (n:Item{id:#})-[r]->(m) RETURN count(r)"
|
||||
];
|
||||
var params = {
|
||||
host: "localhost",
|
||||
port: "7474",
|
||||
connections: 2,
|
||||
duration: 1,
|
||||
queries: queries
|
||||
};
|
||||
// -----------------------
|
||||
|
||||
// server control functions
|
||||
var start = function() {
|
||||
$.ajax({url:'/start', type:"POST", success: function(data){}});
|
||||
};
|
||||
var stop = function() {
|
||||
$.ajax({url:'/stop', type:"POST", success: function(data){}});
|
||||
};
|
||||
var registerParams = function(f) {
|
||||
$.ajax({
|
||||
url:'/params', type:"POST", data:JSON.stringify(params),
|
||||
contentType:"application/json; charset=utf-8",
|
||||
success: function(data){
|
||||
f();
|
||||
}
|
||||
});
|
||||
};
|
||||
registerParams();
|
||||
|
||||
// setup cards
|
||||
queries.forEach(function(query, i) {
|
||||
query = put_new_line_mod_2(query.split(" ")).join('');
|
||||
neo4jCards.push(new QueryCard($('#q-0-' + i.toString())[0], maxQps));
|
||||
memgraphCards.push(new QueryCard($('#q-1-' + i.toString())[0], maxQps));
|
||||
neo4jCards[i].set_query(query);
|
||||
memgraphCards[i].set_query(query);
|
||||
});
|
||||
|
||||
// start stop button
|
||||
$("#run-neo4j").click(function() {
|
||||
running = !running;
|
||||
if (running) {
|
||||
$(this).text('Stop Neo4j');
|
||||
let hostname_port = $("#neo4j_url").val().split(":");
|
||||
params.host = hostname_port[0];
|
||||
params.port = hostname_port[1];
|
||||
params.connections = 2;
|
||||
registerParams(function() {
|
||||
start();
|
||||
updateNeo4j();
|
||||
});
|
||||
}
|
||||
if (!running) {
|
||||
$(this).text('Start Neo4j');
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
// start stop button
|
||||
$("#run-memgraph").click(function() {
|
||||
running = !running;
|
||||
if (running) {
|
||||
$(this).text('Stop Memgraph');
|
||||
let hostname_port = $("#memgraph_url").val().split(":");
|
||||
params.host = hostname_port[0];
|
||||
params.port = hostname_port[1];
|
||||
params.connections = 4;
|
||||
registerParams(function() {
|
||||
start();
|
||||
updateMemgraph();
|
||||
});
|
||||
}
|
||||
if (!running) {
|
||||
$(this).text('Start Memgraph');
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
// update only line on the graph
|
||||
var updateGraph = function() {
|
||||
let newData = [{
|
||||
values: memgraphLine,
|
||||
key: 'Memgraph',
|
||||
color: '#ff0000',
|
||||
strokeWidth: 3
|
||||
}, {
|
||||
values: neo4jLine,
|
||||
key: 'Neo4j',
|
||||
color: '#0000ff',
|
||||
strokeWidth: 3
|
||||
}];
|
||||
chartData.datum(newData).transition().duration(500).call(chart);
|
||||
};
|
||||
|
||||
// update
|
||||
function updateNeo4j() {
|
||||
if (!running) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
$.ajax({
|
||||
url:'/stats', type:"GET",
|
||||
success: function(data){
|
||||
if (!data || !data.total || !data.per_query)
|
||||
return;
|
||||
|
||||
neo4jSec = neo4jSec + 1;
|
||||
neo4jLine.push({x: neo4jSec, y: data.total});
|
||||
data.per_query.forEach(function(speed, i) {
|
||||
neo4jCards[i].set_value(Math.round(speed));
|
||||
});
|
||||
updateGraph();
|
||||
}
|
||||
});
|
||||
updateNeo4j();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// update
|
||||
function updateMemgraph() {
|
||||
if (!running) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
$.ajax({
|
||||
url:'/stats', type:"GET",
|
||||
success: function(data){
|
||||
if (!data || !data.total || !data.per_query)
|
||||
return;
|
||||
|
||||
memgraphSec = memgraphSec + 1;
|
||||
memgraphLine.push({x: memgraphSec, y: data.total});
|
||||
data.per_query.forEach(function(speed, i) {
|
||||
memgraphCards[i].set_value(Math.round(speed));
|
||||
});
|
||||
updateGraph();
|
||||
}
|
||||
});
|
||||
updateMemgraph();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// graph init
|
||||
var chart;
|
||||
var chartData;
|
||||
nv.addGraph(function() {
|
||||
chart = nv.models.lineChart()
|
||||
.interpolate('basis')
|
||||
.useInteractiveGuideline(true)
|
||||
.showLegend(true)
|
||||
.showYAxis(true)
|
||||
.showXAxis(true);
|
||||
|
||||
chart.xAxis
|
||||
.axisLabel('Time (s)')
|
||||
.tickFormat(d3.format(',r'));
|
||||
|
||||
chart.yAxis
|
||||
.axisLabel('QPS')
|
||||
.tickFormat(d3.format('f'));
|
||||
|
||||
chart.forceY([0, 50000]);
|
||||
|
||||
chartData = d3.select('#chart svg')
|
||||
.datum([]);
|
||||
chartData.call(chart);
|
||||
|
||||
chart.update();
|
||||
nv.utils.windowResize(function() { chart.update(); });
|
||||
return chart;
|
||||
});
|
||||
|
||||
})();
|
@ -1,395 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.captors');
|
||||
|
||||
/**
|
||||
* The user inputs default captor. It deals with mouse events, keyboards
|
||||
* events and touch events.
|
||||
*
|
||||
* @param {DOMElement} target The DOM element where the listeners will be
|
||||
* bound.
|
||||
* @param {camera} camera The camera related to the target.
|
||||
* @param {configurable} settings The settings function.
|
||||
* @return {sigma.captor} The fresh new captor instance.
|
||||
*/
|
||||
sigma.captors.mouse = function(target, camera, settings) {
|
||||
var _self = this,
|
||||
_target = target,
|
||||
_camera = camera,
|
||||
_settings = settings,
|
||||
|
||||
// CAMERA MANAGEMENT:
|
||||
// ******************
|
||||
// The camera position when the user starts dragging:
|
||||
_startCameraX,
|
||||
_startCameraY,
|
||||
_startCameraAngle,
|
||||
|
||||
// The latest stage position:
|
||||
_lastCameraX,
|
||||
_lastCameraY,
|
||||
_lastCameraAngle,
|
||||
_lastCameraRatio,
|
||||
|
||||
// MOUSE MANAGEMENT:
|
||||
// *****************
|
||||
// The mouse position when the user starts dragging:
|
||||
_startMouseX,
|
||||
_startMouseY,
|
||||
|
||||
_isMouseDown,
|
||||
_isMoving,
|
||||
_hasDragged,
|
||||
_downStartTime,
|
||||
_movingTimeoutId;
|
||||
|
||||
sigma.classes.dispatcher.extend(this);
|
||||
|
||||
sigma.utils.doubleClick(_target, 'click', _doubleClickHandler);
|
||||
_target.addEventListener('DOMMouseScroll', _wheelHandler, false);
|
||||
_target.addEventListener('mousewheel', _wheelHandler, false);
|
||||
_target.addEventListener('mousemove', _moveHandler, false);
|
||||
_target.addEventListener('mousedown', _downHandler, false);
|
||||
_target.addEventListener('click', _clickHandler, false);
|
||||
_target.addEventListener('mouseout', _outHandler, false);
|
||||
document.addEventListener('mouseup', _upHandler, false);
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This method unbinds every handlers that makes the captor work.
|
||||
*/
|
||||
this.kill = function() {
|
||||
sigma.utils.unbindDoubleClick(_target, 'click');
|
||||
_target.removeEventListener('DOMMouseScroll', _wheelHandler);
|
||||
_target.removeEventListener('mousewheel', _wheelHandler);
|
||||
_target.removeEventListener('mousemove', _moveHandler);
|
||||
_target.removeEventListener('mousedown', _downHandler);
|
||||
_target.removeEventListener('click', _clickHandler);
|
||||
_target.removeEventListener('mouseout', _outHandler);
|
||||
document.removeEventListener('mouseup', _upHandler);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// MOUSE EVENTS:
|
||||
// *************
|
||||
|
||||
/**
|
||||
* The handler listening to the 'move' mouse event. It will effectively
|
||||
* drag the graph.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _moveHandler(e) {
|
||||
var x,
|
||||
y,
|
||||
pos;
|
||||
|
||||
// Dispatch event:
|
||||
if (_settings('mouseEnabled'))
|
||||
_self.dispatchEvent('mousemove', {
|
||||
x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
|
||||
y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
|
||||
if (_settings('mouseEnabled') && _isMouseDown) {
|
||||
_isMoving = true;
|
||||
_hasDragged = true;
|
||||
|
||||
if (_movingTimeoutId)
|
||||
clearTimeout(_movingTimeoutId);
|
||||
|
||||
_movingTimeoutId = setTimeout(function() {
|
||||
_isMoving = false;
|
||||
}, _settings('dragTimeout'));
|
||||
|
||||
sigma.misc.animation.killAll(_camera);
|
||||
|
||||
_camera.isMoving = true;
|
||||
pos = _camera.cameraPosition(
|
||||
sigma.utils.getX(e) - _startMouseX,
|
||||
sigma.utils.getY(e) - _startMouseY,
|
||||
true
|
||||
);
|
||||
|
||||
x = _startCameraX - pos.x;
|
||||
y = _startCameraY - pos.y;
|
||||
|
||||
if (x !== _camera.x || y !== _camera.y) {
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
|
||||
_camera.goTo({
|
||||
x: x,
|
||||
y: y
|
||||
});
|
||||
}
|
||||
|
||||
if (e.preventDefault)
|
||||
e.preventDefault();
|
||||
else
|
||||
e.returnValue = false;
|
||||
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'up' mouse event. It will stop dragging the
|
||||
* graph.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _upHandler(e) {
|
||||
if (_settings('mouseEnabled') && _isMouseDown) {
|
||||
_isMouseDown = false;
|
||||
if (_movingTimeoutId)
|
||||
clearTimeout(_movingTimeoutId);
|
||||
|
||||
_camera.isMoving = false;
|
||||
|
||||
var x = sigma.utils.getX(e),
|
||||
y = sigma.utils.getY(e);
|
||||
|
||||
if (_isMoving) {
|
||||
sigma.misc.animation.killAll(_camera);
|
||||
sigma.misc.animation.camera(
|
||||
_camera,
|
||||
{
|
||||
x: _camera.x +
|
||||
_settings('mouseInertiaRatio') * (_camera.x - _lastCameraX),
|
||||
y: _camera.y +
|
||||
_settings('mouseInertiaRatio') * (_camera.y - _lastCameraY)
|
||||
},
|
||||
{
|
||||
easing: 'quadraticOut',
|
||||
duration: _settings('mouseInertiaDuration')
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
_startMouseX !== x ||
|
||||
_startMouseY !== y
|
||||
)
|
||||
_camera.goTo({
|
||||
x: _camera.x,
|
||||
y: _camera.y
|
||||
});
|
||||
|
||||
_self.dispatchEvent('mouseup', {
|
||||
x: x - sigma.utils.getWidth(e) / 2,
|
||||
y: y - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
|
||||
// Update _isMoving flag:
|
||||
_isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'down' mouse event. It will start observing
|
||||
* the mouse position for dragging the graph.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _downHandler(e) {
|
||||
if (_settings('mouseEnabled')) {
|
||||
_startCameraX = _camera.x;
|
||||
_startCameraY = _camera.y;
|
||||
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
|
||||
_startMouseX = sigma.utils.getX(e);
|
||||
_startMouseY = sigma.utils.getY(e);
|
||||
|
||||
_hasDragged = false;
|
||||
_downStartTime = (new Date()).getTime();
|
||||
|
||||
switch (e.which) {
|
||||
case 2:
|
||||
// Middle mouse button pressed
|
||||
// Do nothing.
|
||||
break;
|
||||
case 3:
|
||||
// Right mouse button pressed
|
||||
_self.dispatchEvent('rightclick', {
|
||||
x: _startMouseX - sigma.utils.getWidth(e) / 2,
|
||||
y: _startMouseY - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
break;
|
||||
// case 1:
|
||||
default:
|
||||
// Left mouse button pressed
|
||||
_isMouseDown = true;
|
||||
|
||||
_self.dispatchEvent('mousedown', {
|
||||
x: _startMouseX - sigma.utils.getWidth(e) / 2,
|
||||
y: _startMouseY - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'out' mouse event. It will just redispatch
|
||||
* the event.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _outHandler(e) {
|
||||
if (_settings('mouseEnabled'))
|
||||
_self.dispatchEvent('mouseout');
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'click' mouse event. It will redispatch the
|
||||
* click event, but with normalized X and Y coordinates.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _clickHandler(e) {
|
||||
if (_settings('mouseEnabled'))
|
||||
_self.dispatchEvent('click', {
|
||||
x: sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
|
||||
y: sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey,
|
||||
isDragging:
|
||||
(((new Date()).getTime() - _downStartTime) > 100) &&
|
||||
_hasDragged
|
||||
});
|
||||
|
||||
if (e.preventDefault)
|
||||
e.preventDefault();
|
||||
else
|
||||
e.returnValue = false;
|
||||
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the double click custom event. It will
|
||||
* basically zoom into the graph.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _doubleClickHandler(e) {
|
||||
var pos,
|
||||
ratio,
|
||||
animation;
|
||||
|
||||
if (_settings('mouseEnabled')) {
|
||||
ratio = 1 / _settings('doubleClickZoomingRatio');
|
||||
|
||||
_self.dispatchEvent('doubleclick', {
|
||||
x: _startMouseX - sigma.utils.getWidth(e) / 2,
|
||||
y: _startMouseY - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
|
||||
if (_settings('doubleClickEnabled')) {
|
||||
pos = _camera.cameraPosition(
|
||||
sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
|
||||
sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
|
||||
true
|
||||
);
|
||||
|
||||
animation = {
|
||||
duration: _settings('doubleClickZoomDuration')
|
||||
};
|
||||
|
||||
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
|
||||
}
|
||||
|
||||
if (e.preventDefault)
|
||||
e.preventDefault();
|
||||
else
|
||||
e.returnValue = false;
|
||||
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'wheel' mouse event. It will basically zoom
|
||||
* in or not into the graph.
|
||||
*
|
||||
* @param {event} e A mouse event.
|
||||
*/
|
||||
function _wheelHandler(e) {
|
||||
var pos,
|
||||
ratio,
|
||||
animation;
|
||||
|
||||
if (_settings('mouseEnabled') && _settings('mouseWheelEnabled')) {
|
||||
ratio = sigma.utils.getDelta(e) > 0 ?
|
||||
1 / _settings('zoomingRatio') :
|
||||
_settings('zoomingRatio');
|
||||
|
||||
pos = _camera.cameraPosition(
|
||||
sigma.utils.getX(e) - sigma.utils.getWidth(e) / 2,
|
||||
sigma.utils.getY(e) - sigma.utils.getHeight(e) / 2,
|
||||
true
|
||||
);
|
||||
|
||||
animation = {
|
||||
duration: _settings('mouseZoomDuration')
|
||||
};
|
||||
|
||||
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
|
||||
|
||||
if (e.preventDefault)
|
||||
e.preventDefault();
|
||||
else
|
||||
e.returnValue = false;
|
||||
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}).call(this);
|
@ -1,430 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.captors');
|
||||
|
||||
/**
|
||||
* The user inputs default captor. It deals with mouse events, keyboards
|
||||
* events and touch events.
|
||||
*
|
||||
* @param {DOMElement} target The DOM element where the listeners will be
|
||||
* bound.
|
||||
* @param {camera} camera The camera related to the target.
|
||||
* @param {configurable} settings The settings function.
|
||||
* @return {sigma.captor} The fresh new captor instance.
|
||||
*/
|
||||
sigma.captors.touch = function(target, camera, settings) {
|
||||
var _self = this,
|
||||
_target = target,
|
||||
_camera = camera,
|
||||
_settings = settings,
|
||||
|
||||
// CAMERA MANAGEMENT:
|
||||
// ******************
|
||||
// The camera position when the user starts dragging:
|
||||
_startCameraX,
|
||||
_startCameraY,
|
||||
_startCameraAngle,
|
||||
_startCameraRatio,
|
||||
|
||||
// The latest stage position:
|
||||
_lastCameraX,
|
||||
_lastCameraY,
|
||||
_lastCameraAngle,
|
||||
_lastCameraRatio,
|
||||
|
||||
// TOUCH MANAGEMENT:
|
||||
// *****************
|
||||
// Touches that are down:
|
||||
_downTouches = [],
|
||||
|
||||
_startTouchX0,
|
||||
_startTouchY0,
|
||||
_startTouchX1,
|
||||
_startTouchY1,
|
||||
_startTouchAngle,
|
||||
_startTouchDistance,
|
||||
|
||||
_touchMode,
|
||||
|
||||
_isMoving,
|
||||
_doubleTap,
|
||||
_movingTimeoutId;
|
||||
|
||||
sigma.classes.dispatcher.extend(this);
|
||||
|
||||
sigma.utils.doubleClick(_target, 'touchstart', _doubleTapHandler);
|
||||
_target.addEventListener('touchstart', _handleStart, false);
|
||||
_target.addEventListener('touchend', _handleLeave, false);
|
||||
_target.addEventListener('touchcancel', _handleLeave, false);
|
||||
_target.addEventListener('touchleave', _handleLeave, false);
|
||||
_target.addEventListener('touchmove', _handleMove, false);
|
||||
|
||||
function position(e) {
|
||||
var offset = sigma.utils.getOffset(_target);
|
||||
|
||||
return {
|
||||
x: e.pageX - offset.left,
|
||||
y: e.pageY - offset.top
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This method unbinds every handlers that makes the captor work.
|
||||
*/
|
||||
this.kill = function() {
|
||||
sigma.utils.unbindDoubleClick(_target, 'touchstart');
|
||||
_target.addEventListener('touchstart', _handleStart);
|
||||
_target.addEventListener('touchend', _handleLeave);
|
||||
_target.addEventListener('touchcancel', _handleLeave);
|
||||
_target.addEventListener('touchleave', _handleLeave);
|
||||
_target.addEventListener('touchmove', _handleMove);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// TOUCH EVENTS:
|
||||
// *************
|
||||
/**
|
||||
* The handler listening to the 'touchstart' event. It will set the touch
|
||||
* mode ("_touchMode") and start observing the user touch moves.
|
||||
*
|
||||
* @param {event} e A touch event.
|
||||
*/
|
||||
function _handleStart(e) {
|
||||
if (_settings('touchEnabled')) {
|
||||
var x0,
|
||||
x1,
|
||||
y0,
|
||||
y1,
|
||||
pos0,
|
||||
pos1;
|
||||
|
||||
_downTouches = e.touches;
|
||||
|
||||
switch (_downTouches.length) {
|
||||
case 1:
|
||||
_camera.isMoving = true;
|
||||
_touchMode = 1;
|
||||
|
||||
_startCameraX = _camera.x;
|
||||
_startCameraY = _camera.y;
|
||||
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
|
||||
pos0 = position(_downTouches[0]);
|
||||
_startTouchX0 = pos0.x;
|
||||
_startTouchY0 = pos0.y;
|
||||
|
||||
break;
|
||||
case 2:
|
||||
_camera.isMoving = true;
|
||||
_touchMode = 2;
|
||||
|
||||
pos0 = position(_downTouches[0]);
|
||||
pos1 = position(_downTouches[1]);
|
||||
x0 = pos0.x;
|
||||
y0 = pos0.y;
|
||||
x1 = pos1.x;
|
||||
y1 = pos1.y;
|
||||
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
|
||||
_startCameraAngle = _camera.angle;
|
||||
_startCameraRatio = _camera.ratio;
|
||||
|
||||
_startCameraX = _camera.x;
|
||||
_startCameraY = _camera.y;
|
||||
|
||||
_startTouchX0 = x0;
|
||||
_startTouchY0 = y0;
|
||||
_startTouchX1 = x1;
|
||||
_startTouchY1 = y1;
|
||||
|
||||
_startTouchAngle = Math.atan2(
|
||||
_startTouchY1 - _startTouchY0,
|
||||
_startTouchX1 - _startTouchX0
|
||||
);
|
||||
_startTouchDistance = Math.sqrt(
|
||||
Math.pow(_startTouchY1 - _startTouchY0, 2) +
|
||||
Math.pow(_startTouchX1 - _startTouchX0, 2)
|
||||
);
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'touchend', 'touchcancel' and 'touchleave'
|
||||
* event. It will update the touch mode if there are still at least one
|
||||
* finger, and stop dragging else.
|
||||
*
|
||||
* @param {event} e A touch event.
|
||||
*/
|
||||
function _handleLeave(e) {
|
||||
if (_settings('touchEnabled')) {
|
||||
_downTouches = e.touches;
|
||||
var inertiaRatio = _settings('touchInertiaRatio');
|
||||
|
||||
if (_movingTimeoutId) {
|
||||
_isMoving = false;
|
||||
clearTimeout(_movingTimeoutId);
|
||||
}
|
||||
|
||||
switch (_touchMode) {
|
||||
case 2:
|
||||
if (e.touches.length === 1) {
|
||||
_handleStart(e);
|
||||
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
/* falls through */
|
||||
case 1:
|
||||
_camera.isMoving = false;
|
||||
_self.dispatchEvent('stopDrag');
|
||||
|
||||
if (_isMoving) {
|
||||
_doubleTap = false;
|
||||
sigma.misc.animation.camera(
|
||||
_camera,
|
||||
{
|
||||
x: _camera.x +
|
||||
inertiaRatio * (_camera.x - _lastCameraX),
|
||||
y: _camera.y +
|
||||
inertiaRatio * (_camera.y - _lastCameraY)
|
||||
},
|
||||
{
|
||||
easing: 'quadraticOut',
|
||||
duration: _settings('touchInertiaDuration')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_isMoving = false;
|
||||
_touchMode = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the 'touchmove' event. It will effectively drag
|
||||
* the graph, and eventually zooms and turn it if the user is using two
|
||||
* fingers.
|
||||
*
|
||||
* @param {event} e A touch event.
|
||||
*/
|
||||
function _handleMove(e) {
|
||||
if (!_doubleTap && _settings('touchEnabled')) {
|
||||
var x0,
|
||||
x1,
|
||||
y0,
|
||||
y1,
|
||||
cos,
|
||||
sin,
|
||||
end,
|
||||
pos0,
|
||||
pos1,
|
||||
diff,
|
||||
start,
|
||||
dAngle,
|
||||
dRatio,
|
||||
newStageX,
|
||||
newStageY,
|
||||
newStageRatio,
|
||||
newStageAngle;
|
||||
|
||||
_downTouches = e.touches;
|
||||
_isMoving = true;
|
||||
|
||||
if (_movingTimeoutId)
|
||||
clearTimeout(_movingTimeoutId);
|
||||
|
||||
_movingTimeoutId = setTimeout(function() {
|
||||
_isMoving = false;
|
||||
}, _settings('dragTimeout'));
|
||||
|
||||
switch (_touchMode) {
|
||||
case 1:
|
||||
pos0 = position(_downTouches[0]);
|
||||
x0 = pos0.x;
|
||||
y0 = pos0.y;
|
||||
|
||||
diff = _camera.cameraPosition(
|
||||
x0 - _startTouchX0,
|
||||
y0 - _startTouchY0,
|
||||
true
|
||||
);
|
||||
|
||||
newStageX = _startCameraX - diff.x;
|
||||
newStageY = _startCameraY - diff.y;
|
||||
|
||||
if (newStageX !== _camera.x || newStageY !== _camera.y) {
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
|
||||
_camera.goTo({
|
||||
x: newStageX,
|
||||
y: newStageY
|
||||
});
|
||||
|
||||
_self.dispatchEvent('mousemove', {
|
||||
x: pos0.x - sigma.utils.getWidth(e) / 2,
|
||||
y: pos0.y - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
|
||||
_self.dispatchEvent('drag');
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
pos0 = position(_downTouches[0]);
|
||||
pos1 = position(_downTouches[1]);
|
||||
x0 = pos0.x;
|
||||
y0 = pos0.y;
|
||||
x1 = pos1.x;
|
||||
y1 = pos1.y;
|
||||
|
||||
start = _camera.cameraPosition(
|
||||
(_startTouchX0 + _startTouchX1) / 2 -
|
||||
sigma.utils.getWidth(e) / 2,
|
||||
(_startTouchY0 + _startTouchY1) / 2 -
|
||||
sigma.utils.getHeight(e) / 2,
|
||||
true
|
||||
);
|
||||
end = _camera.cameraPosition(
|
||||
(x0 + x1) / 2 - sigma.utils.getWidth(e) / 2,
|
||||
(y0 + y1) / 2 - sigma.utils.getHeight(e) / 2,
|
||||
true
|
||||
);
|
||||
|
||||
dAngle = Math.atan2(y1 - y0, x1 - x0) - _startTouchAngle;
|
||||
dRatio = Math.sqrt(
|
||||
Math.pow(y1 - y0, 2) + Math.pow(x1 - x0, 2)
|
||||
) / _startTouchDistance;
|
||||
|
||||
// Translation:
|
||||
x0 = start.x;
|
||||
y0 = start.y;
|
||||
|
||||
// Homothetic transformation:
|
||||
newStageRatio = _startCameraRatio / dRatio;
|
||||
x0 = x0 * dRatio;
|
||||
y0 = y0 * dRatio;
|
||||
|
||||
// Rotation:
|
||||
newStageAngle = _startCameraAngle - dAngle;
|
||||
cos = Math.cos(-dAngle);
|
||||
sin = Math.sin(-dAngle);
|
||||
x1 = x0 * cos + y0 * sin;
|
||||
y1 = y0 * cos - x0 * sin;
|
||||
x0 = x1;
|
||||
y0 = y1;
|
||||
|
||||
// Finalize:
|
||||
newStageX = x0 - end.x + _startCameraX;
|
||||
newStageY = y0 - end.y + _startCameraY;
|
||||
|
||||
if (
|
||||
newStageRatio !== _camera.ratio ||
|
||||
newStageAngle !== _camera.angle ||
|
||||
newStageX !== _camera.x ||
|
||||
newStageY !== _camera.y
|
||||
) {
|
||||
_lastCameraX = _camera.x;
|
||||
_lastCameraY = _camera.y;
|
||||
_lastCameraAngle = _camera.angle;
|
||||
_lastCameraRatio = _camera.ratio;
|
||||
|
||||
_camera.goTo({
|
||||
x: newStageX,
|
||||
y: newStageY,
|
||||
angle: newStageAngle,
|
||||
ratio: newStageRatio
|
||||
});
|
||||
|
||||
_self.dispatchEvent('drag');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler listening to the double tap custom event. It will
|
||||
* basically zoom into the graph.
|
||||
*
|
||||
* @param {event} e A touch event.
|
||||
*/
|
||||
function _doubleTapHandler(e) {
|
||||
var pos,
|
||||
ratio,
|
||||
animation;
|
||||
|
||||
if (e.touches && e.touches.length === 1 && _settings('touchEnabled')) {
|
||||
_doubleTap = true;
|
||||
|
||||
ratio = 1 / _settings('doubleClickZoomingRatio');
|
||||
|
||||
pos = position(e.touches[0]);
|
||||
_self.dispatchEvent('doubleclick', {
|
||||
x: pos.x - sigma.utils.getWidth(e) / 2,
|
||||
y: pos.y - sigma.utils.getHeight(e) / 2,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
ctrlKey: e.ctrlKey,
|
||||
metaKey: e.metaKey,
|
||||
altKey: e.altKey,
|
||||
shiftKey: e.shiftKey
|
||||
});
|
||||
|
||||
if (_settings('doubleClickEnabled')) {
|
||||
pos = _camera.cameraPosition(
|
||||
pos.x - sigma.utils.getWidth(e) / 2,
|
||||
pos.y - sigma.utils.getHeight(e) / 2,
|
||||
true
|
||||
);
|
||||
|
||||
animation = {
|
||||
duration: _settings('doubleClickZoomDuration'),
|
||||
onComplete: function() {
|
||||
_doubleTap = false;
|
||||
}
|
||||
};
|
||||
|
||||
sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation);
|
||||
}
|
||||
|
||||
if (e.preventDefault)
|
||||
e.preventDefault();
|
||||
else
|
||||
e.returnValue = false;
|
||||
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}).call(this);
|
@ -1,240 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
sigma.utils.pkg('sigma.classes');
|
||||
|
||||
/**
|
||||
* The camera constructor. It just initializes its attributes and methods.
|
||||
*
|
||||
* @param {string} id The id.
|
||||
* @param {sigma.classes.graph} graph The graph.
|
||||
* @param {configurable} settings The settings function.
|
||||
* @param {?object} options Eventually some overriding options.
|
||||
* @return {camera} Returns the fresh new camera instance.
|
||||
*/
|
||||
sigma.classes.camera = function(id, graph, settings, options) {
|
||||
sigma.classes.dispatcher.extend(this);
|
||||
|
||||
Object.defineProperty(this, 'graph', {
|
||||
value: graph
|
||||
});
|
||||
Object.defineProperty(this, 'id', {
|
||||
value: id
|
||||
});
|
||||
Object.defineProperty(this, 'readPrefix', {
|
||||
value: 'read_cam' + id + ':'
|
||||
});
|
||||
Object.defineProperty(this, 'prefix', {
|
||||
value: 'cam' + id + ':'
|
||||
});
|
||||
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.ratio = 1;
|
||||
this.angle = 0;
|
||||
this.isAnimated = false;
|
||||
this.settings = (typeof options === 'object' && options) ?
|
||||
settings.embedObject(options) :
|
||||
settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the camera position.
|
||||
*
|
||||
* @param {object} coordinates The new coordinates object.
|
||||
* @return {camera} Returns the camera.
|
||||
*/
|
||||
sigma.classes.camera.prototype.goTo = function(coordinates) {
|
||||
if (!this.settings('enableCamera'))
|
||||
return this;
|
||||
|
||||
var i,
|
||||
l,
|
||||
c = coordinates || {},
|
||||
keys = ['x', 'y', 'ratio', 'angle'];
|
||||
|
||||
for (i = 0, l = keys.length; i < l; i++)
|
||||
if (c[keys[i]] !== undefined) {
|
||||
if (typeof c[keys[i]] === 'number' && !isNaN(c[keys[i]]))
|
||||
this[keys[i]] = c[keys[i]];
|
||||
else
|
||||
throw 'Value for "' + keys[i] + '" is not a number.';
|
||||
}
|
||||
|
||||
this.dispatchEvent('coordinatesUpdated');
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method takes a graph and computes for each node and edges its
|
||||
* coordinates relatively to the center of the camera. Basically, it will
|
||||
* compute the coordinates that will be used by the graphic renderers.
|
||||
*
|
||||
* Since it should be possible to use different cameras and different
|
||||
* renderers, it is possible to specify a prefix to put before the new
|
||||
* coordinates (to get something like "node.camera1_x")
|
||||
*
|
||||
* @param {?string} read The prefix of the coordinates to read.
|
||||
* @param {?string} write The prefix of the coordinates to write.
|
||||
* @param {?object} options Eventually an object of options. Those can be:
|
||||
* - A restricted nodes array.
|
||||
* - A restricted edges array.
|
||||
* - A width.
|
||||
* - A height.
|
||||
* @return {camera} Returns the camera.
|
||||
*/
|
||||
sigma.classes.camera.prototype.applyView = function(read, write, options) {
|
||||
options = options || {};
|
||||
write = write !== undefined ? write : this.prefix;
|
||||
read = read !== undefined ? read : this.readPrefix;
|
||||
|
||||
var nodes = options.nodes || this.graph.nodes(),
|
||||
edges = options.edges || this.graph.edges();
|
||||
|
||||
var i,
|
||||
l,
|
||||
node,
|
||||
cos = Math.cos(this.angle),
|
||||
sin = Math.sin(this.angle),
|
||||
nodeRatio = Math.pow(this.ratio, this.settings('nodesPowRatio')),
|
||||
edgeRatio = Math.pow(this.ratio, this.settings('edgesPowRatio'));
|
||||
|
||||
for (i = 0, l = nodes.length; i < l; i++) {
|
||||
node = nodes[i];
|
||||
node[write + 'x'] =
|
||||
(
|
||||
((node[read + 'x'] || 0) - this.x) * cos +
|
||||
((node[read + 'y'] || 0) - this.y) * sin
|
||||
) / this.ratio + (options.width || 0) / 2;
|
||||
node[write + 'y'] =
|
||||
(
|
||||
((node[read + 'y'] || 0) - this.y) * cos -
|
||||
((node[read + 'x'] || 0) - this.x) * sin
|
||||
) / this.ratio + (options.height || 0) / 2;
|
||||
node[write + 'size'] =
|
||||
(node[read + 'size'] || 0) /
|
||||
nodeRatio;
|
||||
}
|
||||
|
||||
for (i = 0, l = edges.length; i < l; i++) {
|
||||
edges[i][write + 'size'] =
|
||||
(edges[i][read + 'size'] || 0) /
|
||||
edgeRatio;
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function converts the coordinates of a point from the frame of the
|
||||
* camera to the frame of the graph.
|
||||
*
|
||||
* @param {number} x The X coordinate of the point in the frame of the
|
||||
* camera.
|
||||
* @param {number} y The Y coordinate of the point in the frame of the
|
||||
* camera.
|
||||
* @return {object} The point coordinates in the frame of the graph.
|
||||
*/
|
||||
sigma.classes.camera.prototype.graphPosition = function(x, y, vector) {
|
||||
var X = 0,
|
||||
Y = 0,
|
||||
cos = Math.cos(this.angle),
|
||||
sin = Math.sin(this.angle);
|
||||
|
||||
// Revert the origin differential vector:
|
||||
if (!vector) {
|
||||
X = - (this.x * cos + this.y * sin) / this.ratio;
|
||||
Y = - (this.y * cos - this.x * sin) / this.ratio;
|
||||
}
|
||||
|
||||
return {
|
||||
x: (x * cos + y * sin) / this.ratio + X,
|
||||
y: (y * cos - x * sin) / this.ratio + Y
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This function converts the coordinates of a point from the frame of the
|
||||
* graph to the frame of the camera.
|
||||
*
|
||||
* @param {number} x The X coordinate of the point in the frame of the
|
||||
* graph.
|
||||
* @param {number} y The Y coordinate of the point in the frame of the
|
||||
* graph.
|
||||
* @return {object} The point coordinates in the frame of the camera.
|
||||
*/
|
||||
sigma.classes.camera.prototype.cameraPosition = function(x, y, vector) {
|
||||
var X = 0,
|
||||
Y = 0,
|
||||
cos = Math.cos(this.angle),
|
||||
sin = Math.sin(this.angle);
|
||||
|
||||
// Revert the origin differential vector:
|
||||
if (!vector) {
|
||||
X = - (this.x * cos + this.y * sin) / this.ratio;
|
||||
Y = - (this.y * cos - this.x * sin) / this.ratio;
|
||||
}
|
||||
|
||||
return {
|
||||
x: ((x - X) * cos - (y - Y) * sin) * this.ratio,
|
||||
y: ((y - Y) * cos + (x - X) * sin) * this.ratio
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This method returns the transformation matrix of the camera. This is
|
||||
* especially useful to apply the camera view directly in shaders, in case of
|
||||
* WebGL rendering.
|
||||
*
|
||||
* @return {array} The transformation matrix.
|
||||
*/
|
||||
sigma.classes.camera.prototype.getMatrix = function() {
|
||||
var scale = sigma.utils.matrices.scale(1 / this.ratio),
|
||||
rotation = sigma.utils.matrices.rotation(this.angle),
|
||||
translation = sigma.utils.matrices.translation(-this.x, -this.y),
|
||||
matrix = sigma.utils.matrices.multiply(
|
||||
translation,
|
||||
sigma.utils.matrices.multiply(
|
||||
rotation,
|
||||
scale
|
||||
)
|
||||
);
|
||||
|
||||
return matrix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Taking a width and a height as parameters, this method returns the
|
||||
* coordinates of the rectangle representing the camera on screen, in the
|
||||
* graph's referentiel.
|
||||
*
|
||||
* To keep displaying labels of nodes going out of the screen, the method
|
||||
* keeps a margin around the screen in the returned rectangle.
|
||||
*
|
||||
* @param {number} width The width of the screen.
|
||||
* @param {number} height The height of the screen.
|
||||
* @return {object} The rectangle as x1, y1, x2 and y2, representing
|
||||
* two opposite points.
|
||||
*/
|
||||
sigma.classes.camera.prototype.getRectangle = function(width, height) {
|
||||
var widthVect = this.cameraPosition(width, 0, true),
|
||||
heightVect = this.cameraPosition(0, height, true),
|
||||
centerVect = this.cameraPosition(width / 2, height / 2, true),
|
||||
marginX = this.cameraPosition(width / 4, 0, true).x,
|
||||
marginY = this.cameraPosition(0, height / 4, true).y;
|
||||
|
||||
return {
|
||||
x1: this.x - centerVect.x - marginX,
|
||||
y1: this.y - centerVect.y - marginY,
|
||||
x2: this.x - centerVect.x + marginX + widthVect.x,
|
||||
y2: this.y - centerVect.y - marginY + widthVect.y,
|
||||
height: Math.sqrt(
|
||||
Math.pow(heightVect.x, 2) +
|
||||
Math.pow(heightVect.y + 2 * marginY, 2)
|
||||
)
|
||||
};
|
||||
};
|
||||
}).call(this);
|
@ -1,116 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This utils aims to facilitate the manipulation of each instance setting.
|
||||
* Using a function instead of an object brings two main advantages: First,
|
||||
* it will be easier in the future to catch settings updates through a
|
||||
* function than an object. Second, giving it a full object will "merge" it
|
||||
* to the settings object properly, keeping us to have to always add a loop.
|
||||
*
|
||||
* @return {configurable} The "settings" function.
|
||||
*/
|
||||
var configurable = function() {
|
||||
var i,
|
||||
l,
|
||||
data = {},
|
||||
datas = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
/**
|
||||
* The method to use to set or get any property of this instance.
|
||||
*
|
||||
* @param {string|object} a1 If it is a string and if a2 is undefined,
|
||||
* then it will return the corresponding
|
||||
* property. If it is a string and if a2 is
|
||||
* set, then it will set a2 as the property
|
||||
* corresponding to a1, and return this. If
|
||||
* it is an object, then each pair string +
|
||||
* object(or any other type) will be set as a
|
||||
* property.
|
||||
* @param {*?} a2 The new property corresponding to a1 if a1
|
||||
* is a string.
|
||||
* @return {*|configurable} Returns itself or the corresponding
|
||||
* property.
|
||||
*
|
||||
* Polymorphism:
|
||||
* *************
|
||||
* Here are some basic use examples:
|
||||
*
|
||||
* > settings = new configurable();
|
||||
* > settings('mySetting', 42);
|
||||
* > settings('mySetting'); // Logs: 42
|
||||
* > settings('mySetting', 123);
|
||||
* > settings('mySetting'); // Logs: 123
|
||||
* > settings({mySetting: 456});
|
||||
* > settings('mySetting'); // Logs: 456
|
||||
*
|
||||
* Also, it is possible to use the function as a fallback:
|
||||
* > settings({mySetting: 'abc'}, 'mySetting'); // Logs: 'abc'
|
||||
* > settings({hisSetting: 'abc'}, 'mySetting'); // Logs: 456
|
||||
*/
|
||||
var settings = function(a1, a2) {
|
||||
var o,
|
||||
i,
|
||||
l,
|
||||
k;
|
||||
|
||||
if (arguments.length === 1 && typeof a1 === 'string') {
|
||||
if (data[a1] !== undefined)
|
||||
return data[a1];
|
||||
for (i = 0, l = datas.length; i < l; i++)
|
||||
if (datas[i][a1] !== undefined)
|
||||
return datas[i][a1];
|
||||
return undefined;
|
||||
} else if (typeof a1 === 'object' && typeof a2 === 'string') {
|
||||
return (a1 || {})[a2] !== undefined ? a1[a2] : settings(a2);
|
||||
} else {
|
||||
o = (typeof a1 === 'object' && a2 === undefined) ? a1 : {};
|
||||
|
||||
if (typeof a1 === 'string')
|
||||
o[a1] = a2;
|
||||
|
||||
for (i = 0, k = Object.keys(o), l = k.length; i < l; i++)
|
||||
data[k[i]] = o[k[i]];
|
||||
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method returns a new configurable function, with new objects
|
||||
*
|
||||
* @param {object*} Any number of objects to search in.
|
||||
* @return {function} Returns the function. Check its documentation to know
|
||||
* more about how it works.
|
||||
*/
|
||||
settings.embedObjects = function() {
|
||||
var args = datas.concat(
|
||||
data
|
||||
).concat(
|
||||
Array.prototype.splice.call(arguments, 0)
|
||||
);
|
||||
|
||||
return configurable.apply({}, args);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
for (i = 0, l = arguments.length; i < l; i++)
|
||||
settings(arguments[i]);
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* EXPORT:
|
||||
* *******
|
||||
*/
|
||||
if (typeof this.sigma !== 'undefined') {
|
||||
this.sigma.classes = this.sigma.classes || {};
|
||||
this.sigma.classes.configurable = configurable;
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = configurable;
|
||||
exports.configurable = configurable;
|
||||
} else
|
||||
this.configurable = configurable;
|
||||
}).call(this);
|
@ -1,204 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dispatcher constructor.
|
||||
*
|
||||
* @return {dispatcher} The new dispatcher instance.
|
||||
*/
|
||||
var dispatcher = function() {
|
||||
Object.defineProperty(this, '_handlers', {
|
||||
value: {}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Will execute the handler everytime that the indicated event (or the
|
||||
* indicated events) will be triggered.
|
||||
*
|
||||
* @param {string} events The name of the event (or the events
|
||||
* separated by spaces).
|
||||
* @param {function(Object)} handler The handler to bind.
|
||||
* @return {dispatcher} Returns the instance itself.
|
||||
*/
|
||||
dispatcher.prototype.bind = function(events, handler) {
|
||||
var i,
|
||||
l,
|
||||
event,
|
||||
eArray;
|
||||
|
||||
if (
|
||||
arguments.length === 1 &&
|
||||
typeof arguments[0] === 'object'
|
||||
)
|
||||
for (events in arguments[0])
|
||||
this.bind(events, arguments[0][events]);
|
||||
else if (
|
||||
arguments.length === 2 &&
|
||||
typeof arguments[1] === 'function'
|
||||
) {
|
||||
eArray = typeof events === 'string' ? events.split(' ') : events;
|
||||
|
||||
for (i = 0, l = eArray.length; i !== l; i += 1) {
|
||||
event = eArray[i];
|
||||
|
||||
// Check that event is not '':
|
||||
if (!event)
|
||||
continue;
|
||||
|
||||
if (!this._handlers[event])
|
||||
this._handlers[event] = [];
|
||||
|
||||
// Using an object instead of directly the handler will make possible
|
||||
// later to add flags
|
||||
this._handlers[event].push({
|
||||
handler: handler
|
||||
});
|
||||
}
|
||||
} else
|
||||
throw 'bind: Wrong arguments.';
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the handler from a specified event (or specified events).
|
||||
*
|
||||
* @param {?string} events The name of the event (or the events
|
||||
* separated by spaces). If undefined,
|
||||
* then all handlers are removed.
|
||||
* @param {?function(object)} handler The handler to unbind. If undefined,
|
||||
* each handler bound to the event or the
|
||||
* events will be removed.
|
||||
* @return {dispatcher} Returns the instance itself.
|
||||
*/
|
||||
dispatcher.prototype.unbind = function(events, handler) {
|
||||
var i,
|
||||
n,
|
||||
j,
|
||||
m,
|
||||
k,
|
||||
a,
|
||||
event,
|
||||
eArray = typeof events === 'string' ? events.split(' ') : events;
|
||||
|
||||
if (!arguments.length) {
|
||||
for (k in this._handlers)
|
||||
delete this._handlers[k];
|
||||
return this;
|
||||
}
|
||||
|
||||
if (handler) {
|
||||
for (i = 0, n = eArray.length; i !== n; i += 1) {
|
||||
event = eArray[i];
|
||||
if (this._handlers[event]) {
|
||||
a = [];
|
||||
for (j = 0, m = this._handlers[event].length; j !== m; j += 1)
|
||||
if (this._handlers[event][j].handler !== handler)
|
||||
a.push(this._handlers[event][j]);
|
||||
|
||||
this._handlers[event] = a;
|
||||
}
|
||||
|
||||
if (this._handlers[event] && this._handlers[event].length === 0)
|
||||
delete this._handlers[event];
|
||||
}
|
||||
} else
|
||||
for (i = 0, n = eArray.length; i !== n; i += 1)
|
||||
delete this._handlers[eArray[i]];
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes each handler bound to the event
|
||||
*
|
||||
* @param {string} events The name of the event (or the events separated
|
||||
* by spaces).
|
||||
* @param {?object} data The content of the event (optional).
|
||||
* @return {dispatcher} Returns the instance itself.
|
||||
*/
|
||||
dispatcher.prototype.dispatchEvent = function(events, data) {
|
||||
var i,
|
||||
n,
|
||||
j,
|
||||
m,
|
||||
a,
|
||||
event,
|
||||
eventName,
|
||||
self = this,
|
||||
eArray = typeof events === 'string' ? events.split(' ') : events;
|
||||
|
||||
data = data === undefined ? {} : data;
|
||||
|
||||
for (i = 0, n = eArray.length; i !== n; i += 1) {
|
||||
eventName = eArray[i];
|
||||
|
||||
if (this._handlers[eventName]) {
|
||||
event = self.getEvent(eventName, data);
|
||||
a = [];
|
||||
|
||||
for (j = 0, m = this._handlers[eventName].length; j !== m; j += 1) {
|
||||
this._handlers[eventName][j].handler(event);
|
||||
if (!this._handlers[eventName][j].one)
|
||||
a.push(this._handlers[eventName][j]);
|
||||
}
|
||||
|
||||
this._handlers[eventName] = a;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return an event object.
|
||||
*
|
||||
* @param {string} events The name of the event.
|
||||
* @param {?object} data The content of the event (optional).
|
||||
* @return {object} Returns the instance itself.
|
||||
*/
|
||||
dispatcher.prototype.getEvent = function(event, data) {
|
||||
return {
|
||||
type: event,
|
||||
data: data || {},
|
||||
target: this
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A useful function to deal with inheritance. It will make the target
|
||||
* inherit the prototype of the class dispatcher as well as its constructor.
|
||||
*
|
||||
* @param {object} target The target.
|
||||
*/
|
||||
dispatcher.extend = function(target, args) {
|
||||
var k;
|
||||
|
||||
for (k in dispatcher.prototype)
|
||||
if (dispatcher.prototype.hasOwnProperty(k))
|
||||
target[k] = dispatcher.prototype[k];
|
||||
|
||||
dispatcher.apply(target, args);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* EXPORT:
|
||||
* *******
|
||||
*/
|
||||
if (typeof this.sigma !== 'undefined') {
|
||||
this.sigma.classes = this.sigma.classes || {};
|
||||
this.sigma.classes.dispatcher = dispatcher;
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = dispatcher;
|
||||
exports.dispatcher = dispatcher;
|
||||
} else
|
||||
this.dispatcher = dispatcher;
|
||||
}).call(this);
|
@ -1,832 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Sigma Quadtree Module for edges
|
||||
* ===============================
|
||||
*
|
||||
* Author: Sébastien Heymann,
|
||||
* from the quad of Guillaume Plique (Yomguithereal)
|
||||
* Version: 0.2
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Quad Geometric Operations
|
||||
* -------------------------
|
||||
*
|
||||
* A useful batch of geometric operations used by the quadtree.
|
||||
*/
|
||||
|
||||
var _geom = {
|
||||
|
||||
/**
|
||||
* Transforms a graph node with x, y and size into an
|
||||
* axis-aligned square.
|
||||
*
|
||||
* @param {object} A graph node with at least a point (x, y) and a size.
|
||||
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
pointToSquare: function(n) {
|
||||
return {
|
||||
x1: n.x - n.size,
|
||||
y1: n.y - n.size,
|
||||
x2: n.x + n.size,
|
||||
y2: n.y - n.size,
|
||||
height: n.size * 2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms a graph edge with x1, y1, x2, y2 and size into an
|
||||
* axis-aligned square.
|
||||
*
|
||||
* @param {object} A graph edge with at least two points
|
||||
* (x1, y1), (x2, y2) and a size.
|
||||
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
lineToSquare: function(e) {
|
||||
if (e.y1 < e.y2) {
|
||||
// (e.x1, e.y1) on top
|
||||
if (e.x1 < e.x2) {
|
||||
// (e.x1, e.y1) on left
|
||||
return {
|
||||
x1: e.x1 - e.size,
|
||||
y1: e.y1 - e.size,
|
||||
x2: e.x2 + e.size,
|
||||
y2: e.y1 - e.size,
|
||||
height: e.y2 - e.y1 + e.size * 2
|
||||
};
|
||||
}
|
||||
// (e.x1, e.y1) on right
|
||||
return {
|
||||
x1: e.x2 - e.size,
|
||||
y1: e.y1 - e.size,
|
||||
x2: e.x1 + e.size,
|
||||
y2: e.y1 - e.size,
|
||||
height: e.y2 - e.y1 + e.size * 2
|
||||
};
|
||||
}
|
||||
|
||||
// (e.x2, e.y2) on top
|
||||
if (e.x1 < e.x2) {
|
||||
// (e.x1, e.y1) on left
|
||||
return {
|
||||
x1: e.x1 - e.size,
|
||||
y1: e.y2 - e.size,
|
||||
x2: e.x2 + e.size,
|
||||
y2: e.y2 - e.size,
|
||||
height: e.y1 - e.y2 + e.size * 2
|
||||
};
|
||||
}
|
||||
// (e.x2, e.y2) on right
|
||||
return {
|
||||
x1: e.x2 - e.size,
|
||||
y1: e.y2 - e.size,
|
||||
x2: e.x1 + e.size,
|
||||
y2: e.y2 - e.size,
|
||||
height: e.y1 - e.y2 + e.size * 2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms a graph edge of type 'curve' with x1, y1, x2, y2,
|
||||
* control point and size into an axis-aligned square.
|
||||
*
|
||||
* @param {object} e A graph edge with at least two points
|
||||
* (x1, y1), (x2, y2) and a size.
|
||||
* @param {object} cp A control point (x,y).
|
||||
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
quadraticCurveToSquare: function(e, cp) {
|
||||
var pt = sigma.utils.getPointOnQuadraticCurve(
|
||||
0.5,
|
||||
e.x1,
|
||||
e.y1,
|
||||
e.x2,
|
||||
e.y2,
|
||||
cp.x,
|
||||
cp.y
|
||||
);
|
||||
|
||||
// Bounding box of the two points and the point at the middle of the
|
||||
// curve:
|
||||
var minX = Math.min(e.x1, e.x2, pt.x),
|
||||
maxX = Math.max(e.x1, e.x2, pt.x),
|
||||
minY = Math.min(e.y1, e.y2, pt.y),
|
||||
maxY = Math.max(e.y1, e.y2, pt.y);
|
||||
|
||||
return {
|
||||
x1: minX - e.size,
|
||||
y1: minY - e.size,
|
||||
x2: maxX + e.size,
|
||||
y2: minY - e.size,
|
||||
height: maxY - minY + e.size * 2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms a graph self loop into an axis-aligned square.
|
||||
*
|
||||
* @param {object} n A graph node with a point (x, y) and a size.
|
||||
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
selfLoopToSquare: function(n) {
|
||||
// Fitting to the curve is too costly, we compute a larger bounding box
|
||||
// using the control points:
|
||||
var cp = sigma.utils.getSelfLoopControlPoints(n.x, n.y, n.size);
|
||||
|
||||
// Bounding box of the point and the two control points:
|
||||
var minX = Math.min(n.x, cp.x1, cp.x2),
|
||||
maxX = Math.max(n.x, cp.x1, cp.x2),
|
||||
minY = Math.min(n.y, cp.y1, cp.y2),
|
||||
maxY = Math.max(n.y, cp.y1, cp.y2);
|
||||
|
||||
return {
|
||||
x1: minX - n.size,
|
||||
y1: minY - n.size,
|
||||
x2: maxX + n.size,
|
||||
y2: minY - n.size,
|
||||
height: maxY - minY + n.size * 2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a rectangle is axis-aligned.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points
|
||||
* (x1, y1) and (x2, y2).
|
||||
* @return {boolean} True if the rectangle is axis-aligned.
|
||||
*/
|
||||
isAxisAligned: function(r) {
|
||||
return r.x1 === r.x2 || r.y1 === r.y2;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute top points of an axis-aligned rectangle. This is useful in
|
||||
* cases when the rectangle has been rotated (left, right or bottom up) and
|
||||
* later operations need to know the top points.
|
||||
*
|
||||
* @param {object} An axis-aligned rectangle defined by two points
|
||||
* (x1, y1), (x2, y2) and height.
|
||||
* @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
axisAlignedTopPoints: function(r) {
|
||||
|
||||
// Basic
|
||||
if (r.y1 === r.y2 && r.x1 < r.x2)
|
||||
return r;
|
||||
|
||||
// Rotated to right
|
||||
if (r.x1 === r.x2 && r.y2 > r.y1)
|
||||
return {
|
||||
x1: r.x1 - r.height, y1: r.y1,
|
||||
x2: r.x1, y2: r.y1,
|
||||
height: r.height
|
||||
};
|
||||
|
||||
// Rotated to left
|
||||
if (r.x1 === r.x2 && r.y2 < r.y1)
|
||||
return {
|
||||
x1: r.x1, y1: r.y2,
|
||||
x2: r.x2 + r.height, y2: r.y2,
|
||||
height: r.height
|
||||
};
|
||||
|
||||
// Bottom's up
|
||||
return {
|
||||
x1: r.x2, y1: r.y1 - r.height,
|
||||
x2: r.x1, y2: r.y1 - r.height,
|
||||
height: r.height
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get coordinates of a rectangle's lower left corner from its top points.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @return {object} Coordinates of the corner (x, y).
|
||||
*/
|
||||
lowerLeftCoor: function(r) {
|
||||
var width = (
|
||||
Math.sqrt(
|
||||
Math.pow(r.x2 - r.x1, 2) +
|
||||
Math.pow(r.y2 - r.y1, 2)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
x: r.x1 - (r.y2 - r.y1) * r.height / width,
|
||||
y: r.y1 + (r.x2 - r.x1) * r.height / width
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get coordinates of a rectangle's lower right corner from its top points
|
||||
* and its lower left corner.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @param {object} A corner's coordinates (x, y).
|
||||
* @return {object} Coordinates of the corner (x, y).
|
||||
*/
|
||||
lowerRightCoor: function(r, llc) {
|
||||
return {
|
||||
x: llc.x - r.x1 + r.x2,
|
||||
y: llc.y - r.y1 + r.y2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the coordinates of all the corners of a rectangle from its top point.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @return {array} An array of the four corners' coordinates (x, y).
|
||||
*/
|
||||
rectangleCorners: function(r) {
|
||||
var llc = this.lowerLeftCoor(r),
|
||||
lrc = this.lowerRightCoor(r, llc);
|
||||
|
||||
return [
|
||||
{x: r.x1, y: r.y1},
|
||||
{x: r.x2, y: r.y2},
|
||||
{x: llc.x, y: llc.y},
|
||||
{x: lrc.x, y: lrc.y}
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Split a square defined by its boundaries into four.
|
||||
*
|
||||
* @param {object} Boundaries of the square (x, y, width, height).
|
||||
* @return {array} An array containing the four new squares, themselves
|
||||
* defined by an array of their four corners (x, y).
|
||||
*/
|
||||
splitSquare: function(b) {
|
||||
return [
|
||||
[
|
||||
{x: b.x, y: b.y},
|
||||
{x: b.x + b.width / 2, y: b.y},
|
||||
{x: b.x, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2}
|
||||
],
|
||||
[
|
||||
{x: b.x + b.width / 2, y: b.y},
|
||||
{x: b.x + b.width, y: b.y},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width, y: b.y + b.height / 2}
|
||||
],
|
||||
[
|
||||
{x: b.x, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x, y: b.y + b.height},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height}
|
||||
],
|
||||
[
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height},
|
||||
{x: b.x + b.width, y: b.y + b.height}
|
||||
]
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the four axis between corners of rectangle A and corners of
|
||||
* rectangle B. This is needed later to check an eventual collision.
|
||||
*
|
||||
* @param {array} An array of rectangle A's four corners (x, y).
|
||||
* @param {array} An array of rectangle B's four corners (x, y).
|
||||
* @return {array} An array of four axis defined by their coordinates (x,y).
|
||||
*/
|
||||
axis: function(c1, c2) {
|
||||
return [
|
||||
{x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
|
||||
{x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
|
||||
{x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
|
||||
{x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Project a rectangle's corner on an axis.
|
||||
*
|
||||
* @param {object} Coordinates of a corner (x, y).
|
||||
* @param {object} Coordinates of an axis (x, y).
|
||||
* @return {object} The projection defined by coordinates (x, y).
|
||||
*/
|
||||
projection: function(c, a) {
|
||||
var l = (
|
||||
(c.x * a.x + c.y * a.y) /
|
||||
(Math.pow(a.x, 2) + Math.pow(a.y, 2))
|
||||
);
|
||||
|
||||
return {
|
||||
x: l * a.x,
|
||||
y: l * a.y
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether two rectangles collide on one particular axis.
|
||||
*
|
||||
* @param {object} An axis' coordinates (x, y).
|
||||
* @param {array} Rectangle A's corners.
|
||||
* @param {array} Rectangle B's corners.
|
||||
* @return {boolean} True if the rectangles collide on the axis.
|
||||
*/
|
||||
axisCollision: function(a, c1, c2) {
|
||||
var sc1 = [],
|
||||
sc2 = [];
|
||||
|
||||
for (var ci = 0; ci < 4; ci++) {
|
||||
var p1 = this.projection(c1[ci], a),
|
||||
p2 = this.projection(c2[ci], a);
|
||||
|
||||
sc1.push(p1.x * a.x + p1.y * a.y);
|
||||
sc2.push(p2.x * a.x + p2.y * a.y);
|
||||
}
|
||||
|
||||
var maxc1 = Math.max.apply(Math, sc1),
|
||||
maxc2 = Math.max.apply(Math, sc2),
|
||||
minc1 = Math.min.apply(Math, sc1),
|
||||
minc2 = Math.min.apply(Math, sc2);
|
||||
|
||||
return (minc2 <= maxc1 && maxc2 >= minc1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether two rectangles collide on each one of their four axis. If
|
||||
* all axis collide, then the two rectangles do collide on the plane.
|
||||
*
|
||||
* @param {array} Rectangle A's corners.
|
||||
* @param {array} Rectangle B's corners.
|
||||
* @return {boolean} True if the rectangles collide.
|
||||
*/
|
||||
collision: function(c1, c2) {
|
||||
var axis = this.axis(c1, c2),
|
||||
col = true;
|
||||
|
||||
for (var i = 0; i < 4; i++)
|
||||
col = col && this.axisCollision(axis[i], c1, c2);
|
||||
|
||||
return col;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Quad Functions
|
||||
* ------------
|
||||
*
|
||||
* The Quadtree functions themselves.
|
||||
* For each of those functions, we consider that in a splitted quad, the
|
||||
* index of each node is the following:
|
||||
* 0: top left
|
||||
* 1: top right
|
||||
* 2: bottom left
|
||||
* 3: bottom right
|
||||
*
|
||||
* Moreover, the hereafter quad's philosophy is to consider that if an element
|
||||
* collides with more than one nodes, this element belongs to each of the
|
||||
* nodes it collides with where other would let it lie on a higher node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the index of the node containing the point in the quad
|
||||
*
|
||||
* @param {object} point A point defined by coordinates (x, y).
|
||||
* @param {object} quadBounds Boundaries of the quad (x, y, width, heigth).
|
||||
* @return {integer} The index of the node containing the point.
|
||||
*/
|
||||
function _quadIndex(point, quadBounds) {
|
||||
var xmp = quadBounds.x + quadBounds.width / 2,
|
||||
ymp = quadBounds.y + quadBounds.height / 2,
|
||||
top = (point.y < ymp),
|
||||
left = (point.x < xmp);
|
||||
|
||||
if (top) {
|
||||
if (left)
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
if (left)
|
||||
return 2;
|
||||
else
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of indexes of nodes containing an axis-aligned rectangle
|
||||
*
|
||||
* @param {object} rectangle A rectangle defined by two points (x1, y1),
|
||||
* (x2, y2) and height.
|
||||
* @param {array} quadCorners An array of the quad nodes' corners.
|
||||
* @return {array} An array of indexes containing one to
|
||||
* four integers.
|
||||
*/
|
||||
function _quadIndexes(rectangle, quadCorners) {
|
||||
var indexes = [];
|
||||
|
||||
// Iterating through quads
|
||||
for (var i = 0; i < 4; i++)
|
||||
if ((rectangle.x2 >= quadCorners[i][0].x) &&
|
||||
(rectangle.x1 <= quadCorners[i][1].x) &&
|
||||
(rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
|
||||
(rectangle.y1 <= quadCorners[i][2].y))
|
||||
indexes.push(i);
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of indexes of nodes containing a non-axis-aligned rectangle
|
||||
*
|
||||
* @param {array} corners An array containing each corner of the
|
||||
* rectangle defined by its coordinates (x, y).
|
||||
* @param {array} quadCorners An array of the quad nodes' corners.
|
||||
* @return {array} An array of indexes containing one to
|
||||
* four integers.
|
||||
*/
|
||||
function _quadCollision(corners, quadCorners) {
|
||||
var indexes = [];
|
||||
|
||||
// Iterating through quads
|
||||
for (var i = 0; i < 4; i++)
|
||||
if (_geom.collision(corners, quadCorners[i]))
|
||||
indexes.push(i);
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subdivide a quad by creating a node at a precise index. The function does
|
||||
* not generate all four nodes not to potentially create unused nodes.
|
||||
*
|
||||
* @param {integer} index The index of the node to create.
|
||||
* @param {object} quad The quad object to subdivide.
|
||||
* @return {object} A new quad representing the node created.
|
||||
*/
|
||||
function _quadSubdivide(index, quad) {
|
||||
var next = quad.level + 1,
|
||||
subw = Math.round(quad.bounds.width / 2),
|
||||
subh = Math.round(quad.bounds.height / 2),
|
||||
qx = Math.round(quad.bounds.x),
|
||||
qy = Math.round(quad.bounds.y),
|
||||
x,
|
||||
y;
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
x = qx;
|
||||
y = qy;
|
||||
break;
|
||||
case 1:
|
||||
x = qx + subw;
|
||||
y = qy;
|
||||
break;
|
||||
case 2:
|
||||
x = qx;
|
||||
y = qy + subh;
|
||||
break;
|
||||
case 3:
|
||||
x = qx + subw;
|
||||
y = qy + subh;
|
||||
break;
|
||||
}
|
||||
|
||||
return _quadTree(
|
||||
{x: x, y: y, width: subw, height: subh},
|
||||
next,
|
||||
quad.maxElements,
|
||||
quad.maxLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively insert an element into the quadtree. Only points
|
||||
* with size, i.e. axis-aligned squares, may be inserted with this
|
||||
* method.
|
||||
*
|
||||
* @param {object} el The element to insert in the quadtree.
|
||||
* @param {object} sizedPoint A sized point defined by two top points
|
||||
* (x1, y1), (x2, y2) and height.
|
||||
* @param {object} quad The quad in which to insert the element.
|
||||
* @return {undefined} The function does not return anything.
|
||||
*/
|
||||
function _quadInsert(el, sizedPoint, quad) {
|
||||
if (quad.level < quad.maxLevel) {
|
||||
|
||||
// Searching appropriate quads
|
||||
var indexes = _quadIndexes(sizedPoint, quad.corners);
|
||||
|
||||
// Iterating
|
||||
for (var i = 0, l = indexes.length; i < l; i++) {
|
||||
|
||||
// Subdividing if necessary
|
||||
if (quad.nodes[indexes[i]] === undefined)
|
||||
quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
|
||||
|
||||
// Recursion
|
||||
_quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
// Pushing the element in a leaf node
|
||||
quad.elements.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieve every elements held by the node containing the
|
||||
* searched point.
|
||||
*
|
||||
* @param {object} point The searched point (x, y).
|
||||
* @param {object} quad The searched quad.
|
||||
* @return {array} An array of elements contained in the relevant
|
||||
* node.
|
||||
*/
|
||||
function _quadRetrievePoint(point, quad) {
|
||||
if (quad.level < quad.maxLevel) {
|
||||
var index = _quadIndex(point, quad.bounds);
|
||||
|
||||
// If node does not exist we return an empty list
|
||||
if (quad.nodes[index] !== undefined) {
|
||||
return _quadRetrievePoint(point, quad.nodes[index]);
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
else {
|
||||
return quad.elements;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieve every elements contained within an rectangular area
|
||||
* that may or may not be axis-aligned.
|
||||
*
|
||||
* @param {object|array} rectData The searched area defined either by
|
||||
* an array of four corners (x, y) in
|
||||
* the case of a non-axis-aligned
|
||||
* rectangle or an object with two top
|
||||
* points (x1, y1), (x2, y2) and height.
|
||||
* @param {object} quad The searched quad.
|
||||
* @param {function} collisionFunc The collision function used to search
|
||||
* for node indexes.
|
||||
* @param {array?} els The retrieved elements.
|
||||
* @return {array} An array of elements contained in the
|
||||
* area.
|
||||
*/
|
||||
function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
|
||||
els = els || {};
|
||||
|
||||
if (quad.level < quad.maxLevel) {
|
||||
var indexes = collisionFunc(rectData, quad.corners);
|
||||
|
||||
for (var i = 0, l = indexes.length; i < l; i++)
|
||||
if (quad.nodes[indexes[i]] !== undefined)
|
||||
_quadRetrieveArea(
|
||||
rectData,
|
||||
quad.nodes[indexes[i]],
|
||||
collisionFunc,
|
||||
els
|
||||
);
|
||||
} else
|
||||
for (var j = 0, m = quad.elements.length; j < m; j++)
|
||||
if (els[quad.elements[j].id] === undefined)
|
||||
els[quad.elements[j].id] = quad.elements[j];
|
||||
|
||||
return els;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the quadtree object itself.
|
||||
*
|
||||
* @param {object} bounds The boundaries of the quad defined by an
|
||||
* origin (x, y), width and heigth.
|
||||
* @param {integer} level The level of the quad in the tree.
|
||||
* @param {integer} maxElements The max number of element in a leaf node.
|
||||
* @param {integer} maxLevel The max recursion level of the tree.
|
||||
* @return {object} The quadtree object.
|
||||
*/
|
||||
function _quadTree(bounds, level, maxElements, maxLevel) {
|
||||
return {
|
||||
level: level || 0,
|
||||
bounds: bounds,
|
||||
corners: _geom.splitSquare(bounds),
|
||||
maxElements: maxElements || 40,
|
||||
maxLevel: maxLevel || 8,
|
||||
elements: [],
|
||||
nodes: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sigma Quad Constructor
|
||||
* ----------------------
|
||||
*
|
||||
* The edgequad API as exposed to sigma.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The edgequad core that will become the sigma interface with the quadtree.
|
||||
*
|
||||
* property {object} _tree Property holding the quadtree object.
|
||||
* property {object} _geom Exposition of the _geom namespace for testing.
|
||||
* property {object} _cache Cache for the area method.
|
||||
* property {boolean} _enabled Can index and retreive elements.
|
||||
*/
|
||||
var edgequad = function() {
|
||||
this._geom = _geom;
|
||||
this._tree = null;
|
||||
this._cache = {
|
||||
query: false,
|
||||
result: false
|
||||
};
|
||||
this._enabled = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Index a graph by inserting its edges into the quadtree.
|
||||
*
|
||||
* @param {object} graph A graph instance.
|
||||
* @param {object} params An object of parameters with at least the quad
|
||||
* bounds.
|
||||
* @return {object} The quadtree object.
|
||||
*
|
||||
* Parameters:
|
||||
* ----------
|
||||
* bounds: {object} boundaries of the quad defined by its origin (x, y)
|
||||
* width and heigth.
|
||||
* prefix: {string?} a prefix for edge geometric attributes.
|
||||
* maxElements: {integer?} the max number of elements in a leaf node.
|
||||
* maxLevel: {integer?} the max recursion level of the tree.
|
||||
*/
|
||||
edgequad.prototype.index = function(graph, params) {
|
||||
if (!this._enabled)
|
||||
return this._tree;
|
||||
|
||||
// Enforcing presence of boundaries
|
||||
if (!params.bounds)
|
||||
throw 'sigma.classes.edgequad.index: bounds information not given.';
|
||||
|
||||
// Prefix
|
||||
var prefix = params.prefix || '',
|
||||
cp,
|
||||
source,
|
||||
target,
|
||||
n,
|
||||
e;
|
||||
|
||||
// Building the tree
|
||||
this._tree = _quadTree(
|
||||
params.bounds,
|
||||
0,
|
||||
params.maxElements,
|
||||
params.maxLevel
|
||||
);
|
||||
|
||||
var edges = graph.edges();
|
||||
|
||||
// Inserting graph edges into the tree
|
||||
for (var i = 0, l = edges.length; i < l; i++) {
|
||||
source = graph.nodes(edges[i].source);
|
||||
target = graph.nodes(edges[i].target);
|
||||
e = {
|
||||
x1: source[prefix + 'x'],
|
||||
y1: source[prefix + 'y'],
|
||||
x2: target[prefix + 'x'],
|
||||
y2: target[prefix + 'y'],
|
||||
size: edges[i][prefix + 'size'] || 0
|
||||
};
|
||||
|
||||
// Inserting edge
|
||||
if (edges[i].type === 'curve' || edges[i].type === 'curvedArrow') {
|
||||
if (source.id === target.id) {
|
||||
n = {
|
||||
x: source[prefix + 'x'],
|
||||
y: source[prefix + 'y'],
|
||||
size: source[prefix + 'size'] || 0
|
||||
};
|
||||
_quadInsert(
|
||||
edges[i],
|
||||
_geom.selfLoopToSquare(n),
|
||||
this._tree);
|
||||
}
|
||||
else {
|
||||
cp = sigma.utils.getQuadraticControlPoint(e.x1, e.y1, e.x2, e.y2);
|
||||
_quadInsert(
|
||||
edges[i],
|
||||
_geom.quadraticCurveToSquare(e, cp),
|
||||
this._tree);
|
||||
}
|
||||
}
|
||||
else {
|
||||
_quadInsert(
|
||||
edges[i],
|
||||
_geom.lineToSquare(e),
|
||||
this._tree);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cache:
|
||||
this._cache = {
|
||||
query: false,
|
||||
result: false
|
||||
};
|
||||
|
||||
// remove?
|
||||
return this._tree;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve every graph edges held by the quadtree node containing the
|
||||
* searched point.
|
||||
*
|
||||
* @param {number} x of the point.
|
||||
* @param {number} y of the point.
|
||||
* @return {array} An array of edges retrieved.
|
||||
*/
|
||||
edgequad.prototype.point = function(x, y) {
|
||||
if (!this._enabled)
|
||||
return [];
|
||||
|
||||
return this._tree ?
|
||||
_quadRetrievePoint({x: x, y: y}, this._tree) || [] :
|
||||
[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve every graph edges within a rectangular area. The methods keep the
|
||||
* last area queried in cache for optimization reason and will act differently
|
||||
* for the same reason if the area is axis-aligned or not.
|
||||
*
|
||||
* @param {object} A rectangle defined by two top points (x1, y1), (x2, y2)
|
||||
* and height.
|
||||
* @return {array} An array of edges retrieved.
|
||||
*/
|
||||
edgequad.prototype.area = function(rect) {
|
||||
if (!this._enabled)
|
||||
return [];
|
||||
|
||||
var serialized = JSON.stringify(rect),
|
||||
collisionFunc,
|
||||
rectData;
|
||||
|
||||
// Returning cache?
|
||||
if (this._cache.query === serialized)
|
||||
return this._cache.result;
|
||||
|
||||
// Axis aligned ?
|
||||
if (_geom.isAxisAligned(rect)) {
|
||||
collisionFunc = _quadIndexes;
|
||||
rectData = _geom.axisAlignedTopPoints(rect);
|
||||
}
|
||||
else {
|
||||
collisionFunc = _quadCollision;
|
||||
rectData = _geom.rectangleCorners(rect);
|
||||
}
|
||||
|
||||
// Retrieving edges
|
||||
var edges = this._tree ?
|
||||
_quadRetrieveArea(
|
||||
rectData,
|
||||
this._tree,
|
||||
collisionFunc
|
||||
) :
|
||||
[];
|
||||
|
||||
// Object to array
|
||||
var edgesArray = [];
|
||||
for (var i in edges)
|
||||
edgesArray.push(edges[i]);
|
||||
|
||||
// Caching
|
||||
this._cache.query = serialized;
|
||||
this._cache.result = edgesArray;
|
||||
|
||||
return edgesArray;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EXPORT:
|
||||
* *******
|
||||
*/
|
||||
if (typeof this.sigma !== 'undefined') {
|
||||
this.sigma.classes = this.sigma.classes || {};
|
||||
this.sigma.classes.edgequad = edgequad;
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = edgequad;
|
||||
exports.edgequad = edgequad;
|
||||
} else
|
||||
this.edgequad = edgequad;
|
||||
}).call(this);
|
@ -1,859 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
var _methods = Object.create(null),
|
||||
_indexes = Object.create(null),
|
||||
_initBindings = Object.create(null),
|
||||
_methodBindings = Object.create(null),
|
||||
_methodBeforeBindings = Object.create(null),
|
||||
_defaultSettings = {
|
||||
immutable: true,
|
||||
clone: true
|
||||
},
|
||||
_defaultSettingsFunction = function(key) {
|
||||
return _defaultSettings[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* The graph constructor. It initializes the data and the indexes, and binds
|
||||
* the custom indexes and methods to its own scope.
|
||||
*
|
||||
* Recognized parameters:
|
||||
* **********************
|
||||
* Here is the exhaustive list of every accepted parameters in the settings
|
||||
* object:
|
||||
*
|
||||
* {boolean} clone Indicates if the data have to be cloned in methods
|
||||
* to add nodes or edges.
|
||||
* {boolean} immutable Indicates if nodes "id" values and edges "id",
|
||||
* "source" and "target" values must be set as
|
||||
* immutable.
|
||||
*
|
||||
* @param {?configurable} settings Eventually a settings function.
|
||||
* @return {graph} The new graph instance.
|
||||
*/
|
||||
var graph = function(settings) {
|
||||
var k,
|
||||
fn,
|
||||
data;
|
||||
|
||||
/**
|
||||
* DATA:
|
||||
* *****
|
||||
* Every data that is callable from graph methods are stored in this "data"
|
||||
* object. This object will be served as context for all these methods,
|
||||
* and it is possible to add other type of data in it.
|
||||
*/
|
||||
data = {
|
||||
/**
|
||||
* SETTINGS FUNCTION:
|
||||
* ******************
|
||||
*/
|
||||
settings: settings || _defaultSettingsFunction,
|
||||
|
||||
/**
|
||||
* MAIN DATA:
|
||||
* **********
|
||||
*/
|
||||
nodesArray: [],
|
||||
edgesArray: [],
|
||||
|
||||
/**
|
||||
* GLOBAL INDEXES:
|
||||
* ***************
|
||||
* These indexes just index data by ids.
|
||||
*/
|
||||
nodesIndex: Object.create(null),
|
||||
edgesIndex: Object.create(null),
|
||||
|
||||
/**
|
||||
* LOCAL INDEXES:
|
||||
* **************
|
||||
* These indexes refer from node to nodes. Each key is an id, and each
|
||||
* value is the array of the ids of related nodes.
|
||||
*/
|
||||
inNeighborsIndex: Object.create(null),
|
||||
outNeighborsIndex: Object.create(null),
|
||||
allNeighborsIndex: Object.create(null),
|
||||
|
||||
inNeighborsCount: Object.create(null),
|
||||
outNeighborsCount: Object.create(null),
|
||||
allNeighborsCount: Object.create(null)
|
||||
};
|
||||
|
||||
// Execute bindings:
|
||||
for (k in _initBindings)
|
||||
_initBindings[k].call(data);
|
||||
|
||||
// Add methods to both the scope and the data objects:
|
||||
for (k in _methods) {
|
||||
fn = __bindGraphMethod(k, data, _methods[k]);
|
||||
this[k] = fn;
|
||||
data[k] = fn;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A custom tool to bind methods such that function that are bound to it will
|
||||
* be executed anytime the method is called.
|
||||
*
|
||||
* @param {string} methodName The name of the method to bind.
|
||||
* @param {object} scope The scope where the method must be executed.
|
||||
* @param {function} fn The method itself.
|
||||
* @return {function} The new method.
|
||||
*/
|
||||
function __bindGraphMethod(methodName, scope, fn) {
|
||||
var result = function() {
|
||||
var k,
|
||||
res;
|
||||
|
||||
// Execute "before" bound functions:
|
||||
for (k in _methodBeforeBindings[methodName])
|
||||
_methodBeforeBindings[methodName][k].apply(scope, arguments);
|
||||
|
||||
// Apply the method:
|
||||
res = fn.apply(scope, arguments);
|
||||
|
||||
// Execute bound functions:
|
||||
for (k in _methodBindings[methodName])
|
||||
_methodBindings[methodName][k].apply(scope, arguments);
|
||||
|
||||
// Return res:
|
||||
return res;
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This custom tool function removes every pair key/value from an hash. The
|
||||
* goal is to avoid creating a new object while some other references are
|
||||
* still hanging in some scopes...
|
||||
*
|
||||
* @param {object} obj The object to empty.
|
||||
* @return {object} The empty object.
|
||||
*/
|
||||
function __emptyObject(obj) {
|
||||
var k;
|
||||
|
||||
for (k in obj)
|
||||
if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k))
|
||||
delete obj[k];
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This global method adds a method that will be bound to the futurly created
|
||||
* graph instances.
|
||||
*
|
||||
* Since these methods will be bound to their scope when the instances are
|
||||
* created, it does not use the prototype. Because of that, methods have to
|
||||
* be added before instances are created to make them available.
|
||||
*
|
||||
* Here is an example:
|
||||
*
|
||||
* > graph.addMethod('getNodesCount', function() {
|
||||
* > return this.nodesArray.length;
|
||||
* > });
|
||||
* >
|
||||
* > var myGraph = new graph();
|
||||
* > console.log(myGraph.getNodesCount()); // outputs 0
|
||||
*
|
||||
* @param {string} methodName The name of the method.
|
||||
* @param {function} fn The method itself.
|
||||
* @return {object} The global graph constructor.
|
||||
*/
|
||||
graph.addMethod = function(methodName, fn) {
|
||||
if (
|
||||
typeof methodName !== 'string' ||
|
||||
typeof fn !== 'function' ||
|
||||
arguments.length !== 2
|
||||
)
|
||||
throw 'addMethod: Wrong arguments.';
|
||||
|
||||
if (_methods[methodName] || graph[methodName])
|
||||
throw 'The method "' + methodName + '" already exists.';
|
||||
|
||||
_methods[methodName] = fn;
|
||||
_methodBindings[methodName] = Object.create(null);
|
||||
_methodBeforeBindings[methodName] = Object.create(null);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* This global method returns true if the method has already been added, and
|
||||
* false else.
|
||||
*
|
||||
* Here are some examples:
|
||||
*
|
||||
* > graph.hasMethod('addNode'); // returns true
|
||||
* > graph.hasMethod('hasMethod'); // returns true
|
||||
* > graph.hasMethod('unexistingMethod'); // returns false
|
||||
*
|
||||
* @param {string} methodName The name of the method.
|
||||
* @return {boolean} The result.
|
||||
*/
|
||||
graph.hasMethod = function(methodName) {
|
||||
return !!(_methods[methodName] || graph[methodName]);
|
||||
};
|
||||
|
||||
/**
|
||||
* This global methods attaches a function to a method. Anytime the specified
|
||||
* method is called, the attached function is called right after, with the
|
||||
* same arguments and in the same scope. The attached function is called
|
||||
* right before if the last argument is true, unless the method is the graph
|
||||
* constructor.
|
||||
*
|
||||
* To attach a function to the graph constructor, use 'constructor' as the
|
||||
* method name (first argument).
|
||||
*
|
||||
* The main idea is to have a clean way to keep custom indexes up to date,
|
||||
* for instance:
|
||||
*
|
||||
* > var timesAddNodeCalled = 0;
|
||||
* > graph.attach('addNode', 'timesAddNodeCalledInc', function() {
|
||||
* > timesAddNodeCalled++;
|
||||
* > });
|
||||
* >
|
||||
* > var myGraph = new graph();
|
||||
* > console.log(timesAddNodeCalled); // outputs 0
|
||||
* >
|
||||
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
|
||||
* > console.log(timesAddNodeCalled); // outputs 2
|
||||
*
|
||||
* The idea for calling a function before is to provide pre-processors, for
|
||||
* instance:
|
||||
*
|
||||
* > var colorPalette = { Person: '#C3CBE1', Place: '#9BDEBD' };
|
||||
* > graph.attach('addNode', 'applyNodeColorPalette', function(n) {
|
||||
* > n.color = colorPalette[n.category];
|
||||
* > }, true);
|
||||
* >
|
||||
* > var myGraph = new graph();
|
||||
* > myGraph.addNode({ id: 'n0', category: 'Person' });
|
||||
* > console.log(myGraph.nodes('n0').color); // outputs '#C3CBE1'
|
||||
*
|
||||
* @param {string} methodName The name of the related method or
|
||||
* "constructor".
|
||||
* @param {string} key The key to identify the function to attach.
|
||||
* @param {function} fn The function to bind.
|
||||
* @param {boolean} before If true the function is called right before.
|
||||
* @return {object} The global graph constructor.
|
||||
*/
|
||||
graph.attach = function(methodName, key, fn, before) {
|
||||
if (
|
||||
typeof methodName !== 'string' ||
|
||||
typeof key !== 'string' ||
|
||||
typeof fn !== 'function' ||
|
||||
arguments.length < 3 ||
|
||||
arguments.length > 4
|
||||
)
|
||||
throw 'attach: Wrong arguments.';
|
||||
|
||||
var bindings;
|
||||
|
||||
if (methodName === 'constructor')
|
||||
bindings = _initBindings;
|
||||
else {
|
||||
if (before) {
|
||||
if (!_methodBeforeBindings[methodName])
|
||||
throw 'The method "' + methodName + '" does not exist.';
|
||||
|
||||
bindings = _methodBeforeBindings[methodName];
|
||||
}
|
||||
else {
|
||||
if (!_methodBindings[methodName])
|
||||
throw 'The method "' + methodName + '" does not exist.';
|
||||
|
||||
bindings = _methodBindings[methodName];
|
||||
}
|
||||
}
|
||||
|
||||
if (bindings[key])
|
||||
throw 'A function "' + key + '" is already attached ' +
|
||||
'to the method "' + methodName + '".';
|
||||
|
||||
bindings[key] = fn;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias of attach(methodName, key, fn, true).
|
||||
*/
|
||||
graph.attachBefore = function(methodName, key, fn) {
|
||||
return this.attach(methodName, key, fn, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* This methods is just an helper to deal with custom indexes. It takes as
|
||||
* arguments the name of the index and an object containing all the different
|
||||
* functions to bind to the methods.
|
||||
*
|
||||
* Here is a basic example, that creates an index to keep the number of nodes
|
||||
* in the current graph. It also adds a method to provide a getter on that
|
||||
* new index:
|
||||
*
|
||||
* > sigma.classes.graph.addIndex('nodesCount', {
|
||||
* > constructor: function() {
|
||||
* > this.nodesCount = 0;
|
||||
* > },
|
||||
* > addNode: function() {
|
||||
* > this.nodesCount++;
|
||||
* > },
|
||||
* > dropNode: function() {
|
||||
* > this.nodesCount--;
|
||||
* > }
|
||||
* > });
|
||||
* >
|
||||
* > sigma.classes.graph.addMethod('getNodesCount', function() {
|
||||
* > return this.nodesCount;
|
||||
* > });
|
||||
* >
|
||||
* > var myGraph = new sigma.classes.graph();
|
||||
* > console.log(myGraph.getNodesCount()); // outputs 0
|
||||
* >
|
||||
* > myGraph.addNode({ id: '1' }).addNode({ id: '2' });
|
||||
* > console.log(myGraph.getNodesCount()); // outputs 2
|
||||
*
|
||||
* @param {string} name The name of the index.
|
||||
* @param {object} bindings The object containing the functions to bind.
|
||||
* @return {object} The global graph constructor.
|
||||
*/
|
||||
graph.addIndex = function(name, bindings) {
|
||||
if (
|
||||
typeof name !== 'string' ||
|
||||
Object(bindings) !== bindings ||
|
||||
arguments.length !== 2
|
||||
)
|
||||
throw 'addIndex: Wrong arguments.';
|
||||
|
||||
if (_indexes[name])
|
||||
throw 'The index "' + name + '" already exists.';
|
||||
|
||||
var k;
|
||||
|
||||
// Store the bindings:
|
||||
_indexes[name] = bindings;
|
||||
|
||||
// Attach the bindings:
|
||||
for (k in bindings)
|
||||
if (typeof bindings[k] !== 'function')
|
||||
throw 'The bindings must be functions.';
|
||||
else
|
||||
graph.attach(k, name, bindings[k]);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This method adds a node to the graph. The node must be an object, with a
|
||||
* string under the key "id". Except for this, it is possible to add any
|
||||
* other attribute, that will be preserved all along the manipulations.
|
||||
*
|
||||
* If the graph option "clone" has a truthy value, the node will be cloned
|
||||
* when added to the graph. Also, if the graph option "immutable" has a
|
||||
* truthy value, its id will be defined as immutable.
|
||||
*
|
||||
* @param {object} node The node to add.
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('addNode', function(node) {
|
||||
// Check that the node is an object and has an id:
|
||||
if (Object(node) !== node || arguments.length !== 1)
|
||||
throw 'addNode: Wrong arguments.';
|
||||
|
||||
if (typeof node.id !== 'string' && typeof node.id !== 'number')
|
||||
throw 'The node must have a string or number id.';
|
||||
|
||||
if (this.nodesIndex[node.id])
|
||||
throw 'The node "' + node.id + '" already exists.';
|
||||
|
||||
var k,
|
||||
id = node.id,
|
||||
validNode = Object.create(null);
|
||||
|
||||
// Check the "clone" option:
|
||||
if (this.settings('clone')) {
|
||||
for (k in node)
|
||||
if (k !== 'id')
|
||||
validNode[k] = node[k];
|
||||
} else
|
||||
validNode = node;
|
||||
|
||||
// Check the "immutable" option:
|
||||
if (this.settings('immutable'))
|
||||
Object.defineProperty(validNode, 'id', {
|
||||
value: id,
|
||||
enumerable: true
|
||||
});
|
||||
else
|
||||
validNode.id = id;
|
||||
|
||||
// Add empty containers for edges indexes:
|
||||
this.inNeighborsIndex[id] = Object.create(null);
|
||||
this.outNeighborsIndex[id] = Object.create(null);
|
||||
this.allNeighborsIndex[id] = Object.create(null);
|
||||
|
||||
this.inNeighborsCount[id] = 0;
|
||||
this.outNeighborsCount[id] = 0;
|
||||
this.allNeighborsCount[id] = 0;
|
||||
|
||||
// Add the node to indexes:
|
||||
this.nodesArray.push(validNode);
|
||||
this.nodesIndex[validNode.id] = validNode;
|
||||
|
||||
// Return the current instance:
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method adds an edge to the graph. The edge must be an object, with a
|
||||
* string under the key "id", and strings under the keys "source" and
|
||||
* "target" that design existing nodes. Except for this, it is possible to
|
||||
* add any other attribute, that will be preserved all along the
|
||||
* manipulations.
|
||||
*
|
||||
* If the graph option "clone" has a truthy value, the edge will be cloned
|
||||
* when added to the graph. Also, if the graph option "immutable" has a
|
||||
* truthy value, its id, source and target will be defined as immutable.
|
||||
*
|
||||
* @param {object} edge The edge to add.
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('addEdge', function(edge) {
|
||||
// Check that the edge is an object and has an id:
|
||||
if (Object(edge) !== edge || arguments.length !== 1)
|
||||
throw 'addEdge: Wrong arguments.';
|
||||
|
||||
if (typeof edge.id !== 'string' && typeof edge.id !== 'number')
|
||||
throw 'The edge must have a string or number id.';
|
||||
|
||||
if ((typeof edge.source !== 'string' && typeof edge.source !== 'number') ||
|
||||
!this.nodesIndex[edge.source])
|
||||
throw 'The edge source must have an existing node id.';
|
||||
|
||||
if ((typeof edge.target !== 'string' && typeof edge.target !== 'number') ||
|
||||
!this.nodesIndex[edge.target])
|
||||
throw 'The edge target must have an existing node id.';
|
||||
|
||||
if (this.edgesIndex[edge.id])
|
||||
throw 'The edge "' + edge.id + '" already exists.';
|
||||
|
||||
var k,
|
||||
validEdge = Object.create(null);
|
||||
|
||||
// Check the "clone" option:
|
||||
if (this.settings('clone')) {
|
||||
for (k in edge)
|
||||
if (k !== 'id' && k !== 'source' && k !== 'target')
|
||||
validEdge[k] = edge[k];
|
||||
} else
|
||||
validEdge = edge;
|
||||
|
||||
// Check the "immutable" option:
|
||||
if (this.settings('immutable')) {
|
||||
Object.defineProperty(validEdge, 'id', {
|
||||
value: edge.id,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(validEdge, 'source', {
|
||||
value: edge.source,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(validEdge, 'target', {
|
||||
value: edge.target,
|
||||
enumerable: true
|
||||
});
|
||||
} else {
|
||||
validEdge.id = edge.id;
|
||||
validEdge.source = edge.source;
|
||||
validEdge.target = edge.target;
|
||||
}
|
||||
|
||||
// Add the edge to indexes:
|
||||
this.edgesArray.push(validEdge);
|
||||
this.edgesIndex[validEdge.id] = validEdge;
|
||||
|
||||
if (!this.inNeighborsIndex[validEdge.target][validEdge.source])
|
||||
this.inNeighborsIndex[validEdge.target][validEdge.source] =
|
||||
Object.create(null);
|
||||
this.inNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
|
||||
validEdge;
|
||||
|
||||
if (!this.outNeighborsIndex[validEdge.source][validEdge.target])
|
||||
this.outNeighborsIndex[validEdge.source][validEdge.target] =
|
||||
Object.create(null);
|
||||
this.outNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
|
||||
validEdge;
|
||||
|
||||
if (!this.allNeighborsIndex[validEdge.source][validEdge.target])
|
||||
this.allNeighborsIndex[validEdge.source][validEdge.target] =
|
||||
Object.create(null);
|
||||
this.allNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] =
|
||||
validEdge;
|
||||
|
||||
if (validEdge.target !== validEdge.source) {
|
||||
if (!this.allNeighborsIndex[validEdge.target][validEdge.source])
|
||||
this.allNeighborsIndex[validEdge.target][validEdge.source] =
|
||||
Object.create(null);
|
||||
this.allNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] =
|
||||
validEdge;
|
||||
}
|
||||
|
||||
// Keep counts up to date:
|
||||
this.inNeighborsCount[validEdge.target]++;
|
||||
this.outNeighborsCount[validEdge.source]++;
|
||||
this.allNeighborsCount[validEdge.target]++;
|
||||
this.allNeighborsCount[validEdge.source]++;
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method drops a node from the graph. It also removes each edge that is
|
||||
* bound to it, through the dropEdge method. An error is thrown if the node
|
||||
* does not exist.
|
||||
*
|
||||
* @param {string} id The node id.
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('dropNode', function(id) {
|
||||
// Check that the arguments are valid:
|
||||
if ((typeof id !== 'string' && typeof id !== 'number') ||
|
||||
arguments.length !== 1)
|
||||
throw 'dropNode: Wrong arguments.';
|
||||
|
||||
if (!this.nodesIndex[id])
|
||||
throw 'The node "' + id + '" does not exist.';
|
||||
|
||||
var i, k, l;
|
||||
|
||||
// Remove the node from indexes:
|
||||
delete this.nodesIndex[id];
|
||||
for (i = 0, l = this.nodesArray.length; i < l; i++)
|
||||
if (this.nodesArray[i].id === id) {
|
||||
this.nodesArray.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove related edges:
|
||||
for (i = this.edgesArray.length - 1; i >= 0; i--)
|
||||
if (this.edgesArray[i].source === id || this.edgesArray[i].target === id)
|
||||
this.dropEdge(this.edgesArray[i].id);
|
||||
|
||||
// Remove related edge indexes:
|
||||
delete this.inNeighborsIndex[id];
|
||||
delete this.outNeighborsIndex[id];
|
||||
delete this.allNeighborsIndex[id];
|
||||
|
||||
delete this.inNeighborsCount[id];
|
||||
delete this.outNeighborsCount[id];
|
||||
delete this.allNeighborsCount[id];
|
||||
|
||||
for (k in this.nodesIndex) {
|
||||
delete this.inNeighborsIndex[k][id];
|
||||
delete this.outNeighborsIndex[k][id];
|
||||
delete this.allNeighborsIndex[k][id];
|
||||
}
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method drops an edge from the graph. An error is thrown if the edge
|
||||
* does not exist.
|
||||
*
|
||||
* @param {string} id The edge id.
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('dropEdge', function(id) {
|
||||
// Check that the arguments are valid:
|
||||
if ((typeof id !== 'string' && typeof id !== 'number') ||
|
||||
arguments.length !== 1)
|
||||
throw 'dropEdge: Wrong arguments.';
|
||||
|
||||
if (!this.edgesIndex[id])
|
||||
throw 'The edge "' + id + '" does not exist.';
|
||||
|
||||
var i, l, edge;
|
||||
|
||||
// Remove the edge from indexes:
|
||||
edge = this.edgesIndex[id];
|
||||
delete this.edgesIndex[id];
|
||||
for (i = 0, l = this.edgesArray.length; i < l; i++)
|
||||
if (this.edgesArray[i].id === id) {
|
||||
this.edgesArray.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
delete this.inNeighborsIndex[edge.target][edge.source][edge.id];
|
||||
if (!Object.keys(this.inNeighborsIndex[edge.target][edge.source]).length)
|
||||
delete this.inNeighborsIndex[edge.target][edge.source];
|
||||
|
||||
delete this.outNeighborsIndex[edge.source][edge.target][edge.id];
|
||||
if (!Object.keys(this.outNeighborsIndex[edge.source][edge.target]).length)
|
||||
delete this.outNeighborsIndex[edge.source][edge.target];
|
||||
|
||||
delete this.allNeighborsIndex[edge.source][edge.target][edge.id];
|
||||
if (!Object.keys(this.allNeighborsIndex[edge.source][edge.target]).length)
|
||||
delete this.allNeighborsIndex[edge.source][edge.target];
|
||||
|
||||
if (edge.target !== edge.source) {
|
||||
delete this.allNeighborsIndex[edge.target][edge.source][edge.id];
|
||||
if (!Object.keys(this.allNeighborsIndex[edge.target][edge.source]).length)
|
||||
delete this.allNeighborsIndex[edge.target][edge.source];
|
||||
}
|
||||
|
||||
this.inNeighborsCount[edge.target]--;
|
||||
this.outNeighborsCount[edge.source]--;
|
||||
this.allNeighborsCount[edge.source]--;
|
||||
this.allNeighborsCount[edge.target]--;
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method destroys the current instance. It basically empties each index
|
||||
* and methods attached to the graph.
|
||||
*/
|
||||
graph.addMethod('kill', function() {
|
||||
// Delete arrays:
|
||||
this.nodesArray.length = 0;
|
||||
this.edgesArray.length = 0;
|
||||
delete this.nodesArray;
|
||||
delete this.edgesArray;
|
||||
|
||||
// Delete indexes:
|
||||
delete this.nodesIndex;
|
||||
delete this.edgesIndex;
|
||||
delete this.inNeighborsIndex;
|
||||
delete this.outNeighborsIndex;
|
||||
delete this.allNeighborsIndex;
|
||||
delete this.inNeighborsCount;
|
||||
delete this.outNeighborsCount;
|
||||
delete this.allNeighborsCount;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method empties the nodes and edges arrays, as well as the different
|
||||
* indexes.
|
||||
*
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('clear', function() {
|
||||
this.nodesArray.length = 0;
|
||||
this.edgesArray.length = 0;
|
||||
|
||||
// Due to GC issues, I prefer not to create new object. These objects are
|
||||
// only available from the methods and attached functions, but still, it is
|
||||
// better to prevent ghost references to unrelevant data...
|
||||
__emptyObject(this.nodesIndex);
|
||||
__emptyObject(this.edgesIndex);
|
||||
__emptyObject(this.nodesIndex);
|
||||
__emptyObject(this.inNeighborsIndex);
|
||||
__emptyObject(this.outNeighborsIndex);
|
||||
__emptyObject(this.allNeighborsIndex);
|
||||
__emptyObject(this.inNeighborsCount);
|
||||
__emptyObject(this.outNeighborsCount);
|
||||
__emptyObject(this.allNeighborsCount);
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This method reads an object and adds the nodes and edges, through the
|
||||
* proper methods "addNode" and "addEdge".
|
||||
*
|
||||
* Here is an example:
|
||||
*
|
||||
* > var myGraph = new graph();
|
||||
* > myGraph.read({
|
||||
* > nodes: [
|
||||
* > { id: 'n0' },
|
||||
* > { id: 'n1' }
|
||||
* > ],
|
||||
* > edges: [
|
||||
* > {
|
||||
* > id: 'e0',
|
||||
* > source: 'n0',
|
||||
* > target: 'n1'
|
||||
* > }
|
||||
* > ]
|
||||
* > });
|
||||
* >
|
||||
* > console.log(
|
||||
* > myGraph.nodes().length,
|
||||
* > myGraph.edges().length
|
||||
* > ); // outputs 2 1
|
||||
*
|
||||
* @param {object} g The graph object.
|
||||
* @return {object} The graph instance.
|
||||
*/
|
||||
graph.addMethod('read', function(g) {
|
||||
var i,
|
||||
a,
|
||||
l;
|
||||
|
||||
a = g.nodes || [];
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
this.addNode(a[i]);
|
||||
|
||||
a = g.edges || [];
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
this.addEdge(a[i]);
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
/**
|
||||
* This methods returns one or several nodes, depending on how it is called.
|
||||
*
|
||||
* To get the array of nodes, call "nodes" without argument. To get a
|
||||
* specific node, call it with the id of the node. The get multiple node,
|
||||
* call it with an array of ids, and it will return the array of nodes, in
|
||||
* the same order.
|
||||
*
|
||||
* @param {?(string|array)} v Eventually one id, an array of ids.
|
||||
* @return {object|array} The related node or array of nodes.
|
||||
*/
|
||||
graph.addMethod('nodes', function(v) {
|
||||
// Clone the array of nodes and return it:
|
||||
if (!arguments.length)
|
||||
return this.nodesArray.slice(0);
|
||||
|
||||
// Return the related node:
|
||||
if (arguments.length === 1 &&
|
||||
(typeof v === 'string' || typeof v === 'number'))
|
||||
return this.nodesIndex[v];
|
||||
|
||||
// Return an array of the related node:
|
||||
if (
|
||||
arguments.length === 1 &&
|
||||
Object.prototype.toString.call(v) === '[object Array]'
|
||||
) {
|
||||
var i,
|
||||
l,
|
||||
a = [];
|
||||
|
||||
for (i = 0, l = v.length; i < l; i++)
|
||||
if (typeof v[i] === 'string' || typeof v[i] === 'number')
|
||||
a.push(this.nodesIndex[v[i]]);
|
||||
else
|
||||
throw 'nodes: Wrong arguments.';
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
throw 'nodes: Wrong arguments.';
|
||||
});
|
||||
|
||||
/**
|
||||
* This methods returns the degree of one or several nodes, depending on how
|
||||
* it is called. It is also possible to get incoming or outcoming degrees
|
||||
* instead by specifying 'in' or 'out' as a second argument.
|
||||
*
|
||||
* @param {string|array} v One id, an array of ids.
|
||||
* @param {?string} which Which degree is required. Values are 'in',
|
||||
* 'out', and by default the normal degree.
|
||||
* @return {number|array} The related degree or array of degrees.
|
||||
*/
|
||||
graph.addMethod('degree', function(v, which) {
|
||||
// Check which degree is required:
|
||||
which = {
|
||||
'in': this.inNeighborsCount,
|
||||
'out': this.outNeighborsCount
|
||||
}[which || ''] || this.allNeighborsCount;
|
||||
|
||||
// Return the related node:
|
||||
if (typeof v === 'string' || typeof v === 'number')
|
||||
return which[v];
|
||||
|
||||
// Return an array of the related node:
|
||||
if (Object.prototype.toString.call(v) === '[object Array]') {
|
||||
var i,
|
||||
l,
|
||||
a = [];
|
||||
|
||||
for (i = 0, l = v.length; i < l; i++)
|
||||
if (typeof v[i] === 'string' || typeof v[i] === 'number')
|
||||
a.push(which[v[i]]);
|
||||
else
|
||||
throw 'degree: Wrong arguments.';
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
throw 'degree: Wrong arguments.';
|
||||
});
|
||||
|
||||
/**
|
||||
* This methods returns one or several edges, depending on how it is called.
|
||||
*
|
||||
* To get the array of edges, call "edges" without argument. To get a
|
||||
* specific edge, call it with the id of the edge. The get multiple edge,
|
||||
* call it with an array of ids, and it will return the array of edges, in
|
||||
* the same order.
|
||||
*
|
||||
* @param {?(string|array)} v Eventually one id, an array of ids.
|
||||
* @return {object|array} The related edge or array of edges.
|
||||
*/
|
||||
graph.addMethod('edges', function(v) {
|
||||
// Clone the array of edges and return it:
|
||||
if (!arguments.length)
|
||||
return this.edgesArray.slice(0);
|
||||
|
||||
// Return the related edge:
|
||||
if (arguments.length === 1 &&
|
||||
(typeof v === 'string' || typeof v === 'number'))
|
||||
return this.edgesIndex[v];
|
||||
|
||||
// Return an array of the related edge:
|
||||
if (
|
||||
arguments.length === 1 &&
|
||||
Object.prototype.toString.call(v) === '[object Array]'
|
||||
) {
|
||||
var i,
|
||||
l,
|
||||
a = [];
|
||||
|
||||
for (i = 0, l = v.length; i < l; i++)
|
||||
if (typeof v[i] === 'string' || typeof v[i] === 'number')
|
||||
a.push(this.edgesIndex[v[i]]);
|
||||
else
|
||||
throw 'edges: Wrong arguments.';
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
throw 'edges: Wrong arguments.';
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* EXPORT:
|
||||
* *******
|
||||
*/
|
||||
if (typeof sigma !== 'undefined') {
|
||||
sigma.classes = sigma.classes || Object.create(null);
|
||||
sigma.classes.graph = graph;
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = graph;
|
||||
exports.graph = graph;
|
||||
} else
|
||||
this.graph = graph;
|
||||
}).call(this);
|
@ -1,674 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Sigma Quadtree Module
|
||||
* =====================
|
||||
*
|
||||
* Author: Guillaume Plique (Yomguithereal)
|
||||
* Version: 0.2
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Quad Geometric Operations
|
||||
* -------------------------
|
||||
*
|
||||
* A useful batch of geometric operations used by the quadtree.
|
||||
*/
|
||||
|
||||
var _geom = {
|
||||
|
||||
/**
|
||||
* Transforms a graph node with x, y and size into an
|
||||
* axis-aligned square.
|
||||
*
|
||||
* @param {object} A graph node with at least a point (x, y) and a size.
|
||||
* @return {object} A square: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
pointToSquare: function(n) {
|
||||
return {
|
||||
x1: n.x - n.size,
|
||||
y1: n.y - n.size,
|
||||
x2: n.x + n.size,
|
||||
y2: n.y - n.size,
|
||||
height: n.size * 2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a rectangle is axis-aligned.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points
|
||||
* (x1, y1) and (x2, y2).
|
||||
* @return {boolean} True if the rectangle is axis-aligned.
|
||||
*/
|
||||
isAxisAligned: function(r) {
|
||||
return r.x1 === r.x2 || r.y1 === r.y2;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute top points of an axis-aligned rectangle. This is useful in
|
||||
* cases when the rectangle has been rotated (left, right or bottom up) and
|
||||
* later operations need to know the top points.
|
||||
*
|
||||
* @param {object} An axis-aligned rectangle defined by two points
|
||||
* (x1, y1), (x2, y2) and height.
|
||||
* @return {object} A rectangle: two points (x1, y1), (x2, y2) and height.
|
||||
*/
|
||||
axisAlignedTopPoints: function(r) {
|
||||
|
||||
// Basic
|
||||
if (r.y1 === r.y2 && r.x1 < r.x2)
|
||||
return r;
|
||||
|
||||
// Rotated to right
|
||||
if (r.x1 === r.x2 && r.y2 > r.y1)
|
||||
return {
|
||||
x1: r.x1 - r.height, y1: r.y1,
|
||||
x2: r.x1, y2: r.y1,
|
||||
height: r.height
|
||||
};
|
||||
|
||||
// Rotated to left
|
||||
if (r.x1 === r.x2 && r.y2 < r.y1)
|
||||
return {
|
||||
x1: r.x1, y1: r.y2,
|
||||
x2: r.x2 + r.height, y2: r.y2,
|
||||
height: r.height
|
||||
};
|
||||
|
||||
// Bottom's up
|
||||
return {
|
||||
x1: r.x2, y1: r.y1 - r.height,
|
||||
x2: r.x1, y2: r.y1 - r.height,
|
||||
height: r.height
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get coordinates of a rectangle's lower left corner from its top points.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @return {object} Coordinates of the corner (x, y).
|
||||
*/
|
||||
lowerLeftCoor: function(r) {
|
||||
var width = (
|
||||
Math.sqrt(
|
||||
Math.pow(r.x2 - r.x1, 2) +
|
||||
Math.pow(r.y2 - r.y1, 2)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
x: r.x1 - (r.y2 - r.y1) * r.height / width,
|
||||
y: r.y1 + (r.x2 - r.x1) * r.height / width
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get coordinates of a rectangle's lower right corner from its top points
|
||||
* and its lower left corner.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @param {object} A corner's coordinates (x, y).
|
||||
* @return {object} Coordinates of the corner (x, y).
|
||||
*/
|
||||
lowerRightCoor: function(r, llc) {
|
||||
return {
|
||||
x: llc.x - r.x1 + r.x2,
|
||||
y: llc.y - r.y1 + r.y2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the coordinates of all the corners of a rectangle from its top point.
|
||||
*
|
||||
* @param {object} A rectangle defined by two points (x1, y1) and (x2, y2).
|
||||
* @return {array} An array of the four corners' coordinates (x, y).
|
||||
*/
|
||||
rectangleCorners: function(r) {
|
||||
var llc = this.lowerLeftCoor(r),
|
||||
lrc = this.lowerRightCoor(r, llc);
|
||||
|
||||
return [
|
||||
{x: r.x1, y: r.y1},
|
||||
{x: r.x2, y: r.y2},
|
||||
{x: llc.x, y: llc.y},
|
||||
{x: lrc.x, y: lrc.y}
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Split a square defined by its boundaries into four.
|
||||
*
|
||||
* @param {object} Boundaries of the square (x, y, width, height).
|
||||
* @return {array} An array containing the four new squares, themselves
|
||||
* defined by an array of their four corners (x, y).
|
||||
*/
|
||||
splitSquare: function(b) {
|
||||
return [
|
||||
[
|
||||
{x: b.x, y: b.y},
|
||||
{x: b.x + b.width / 2, y: b.y},
|
||||
{x: b.x, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2}
|
||||
],
|
||||
[
|
||||
{x: b.x + b.width / 2, y: b.y},
|
||||
{x: b.x + b.width, y: b.y},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width, y: b.y + b.height / 2}
|
||||
],
|
||||
[
|
||||
{x: b.x, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x, y: b.y + b.height},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height}
|
||||
],
|
||||
[
|
||||
{x: b.x + b.width / 2, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width, y: b.y + b.height / 2},
|
||||
{x: b.x + b.width / 2, y: b.y + b.height},
|
||||
{x: b.x + b.width, y: b.y + b.height}
|
||||
]
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the four axis between corners of rectangle A and corners of
|
||||
* rectangle B. This is needed later to check an eventual collision.
|
||||
*
|
||||
* @param {array} An array of rectangle A's four corners (x, y).
|
||||
* @param {array} An array of rectangle B's four corners (x, y).
|
||||
* @return {array} An array of four axis defined by their coordinates (x,y).
|
||||
*/
|
||||
axis: function(c1, c2) {
|
||||
return [
|
||||
{x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y},
|
||||
{x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y},
|
||||
{x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y},
|
||||
{x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y}
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Project a rectangle's corner on an axis.
|
||||
*
|
||||
* @param {object} Coordinates of a corner (x, y).
|
||||
* @param {object} Coordinates of an axis (x, y).
|
||||
* @return {object} The projection defined by coordinates (x, y).
|
||||
*/
|
||||
projection: function(c, a) {
|
||||
var l = (
|
||||
(c.x * a.x + c.y * a.y) /
|
||||
(Math.pow(a.x, 2) + Math.pow(a.y, 2))
|
||||
);
|
||||
|
||||
return {
|
||||
x: l * a.x,
|
||||
y: l * a.y
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether two rectangles collide on one particular axis.
|
||||
*
|
||||
* @param {object} An axis' coordinates (x, y).
|
||||
* @param {array} Rectangle A's corners.
|
||||
* @param {array} Rectangle B's corners.
|
||||
* @return {boolean} True if the rectangles collide on the axis.
|
||||
*/
|
||||
axisCollision: function(a, c1, c2) {
|
||||
var sc1 = [],
|
||||
sc2 = [];
|
||||
|
||||
for (var ci = 0; ci < 4; ci++) {
|
||||
var p1 = this.projection(c1[ci], a),
|
||||
p2 = this.projection(c2[ci], a);
|
||||
|
||||
sc1.push(p1.x * a.x + p1.y * a.y);
|
||||
sc2.push(p2.x * a.x + p2.y * a.y);
|
||||
}
|
||||
|
||||
var maxc1 = Math.max.apply(Math, sc1),
|
||||
maxc2 = Math.max.apply(Math, sc2),
|
||||
minc1 = Math.min.apply(Math, sc1),
|
||||
minc2 = Math.min.apply(Math, sc2);
|
||||
|
||||
return (minc2 <= maxc1 && maxc2 >= minc1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether two rectangles collide on each one of their four axis. If
|
||||
* all axis collide, then the two rectangles do collide on the plane.
|
||||
*
|
||||
* @param {array} Rectangle A's corners.
|
||||
* @param {array} Rectangle B's corners.
|
||||
* @return {boolean} True if the rectangles collide.
|
||||
*/
|
||||
collision: function(c1, c2) {
|
||||
var axis = this.axis(c1, c2),
|
||||
col = true;
|
||||
|
||||
for (var i = 0; i < 4; i++)
|
||||
col = col && this.axisCollision(axis[i], c1, c2);
|
||||
|
||||
return col;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Quad Functions
|
||||
* ------------
|
||||
*
|
||||
* The Quadtree functions themselves.
|
||||
* For each of those functions, we consider that in a splitted quad, the
|
||||
* index of each node is the following:
|
||||
* 0: top left
|
||||
* 1: top right
|
||||
* 2: bottom left
|
||||
* 3: bottom right
|
||||
*
|
||||
* Moreover, the hereafter quad's philosophy is to consider that if an element
|
||||
* collides with more than one nodes, this element belongs to each of the
|
||||
* nodes it collides with where other would let it lie on a higher node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the index of the node containing the point in the quad
|
||||
*
|
||||
* @param {object} point A point defined by coordinates (x, y).
|
||||
* @param {object} quadBounds Boundaries of the quad (x, y, width, heigth).
|
||||
* @return {integer} The index of the node containing the point.
|
||||
*/
|
||||
function _quadIndex(point, quadBounds) {
|
||||
var xmp = quadBounds.x + quadBounds.width / 2,
|
||||
ymp = quadBounds.y + quadBounds.height / 2,
|
||||
top = (point.y < ymp),
|
||||
left = (point.x < xmp);
|
||||
|
||||
if (top) {
|
||||
if (left)
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
if (left)
|
||||
return 2;
|
||||
else
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of indexes of nodes containing an axis-aligned rectangle
|
||||
*
|
||||
* @param {object} rectangle A rectangle defined by two points (x1, y1),
|
||||
* (x2, y2) and height.
|
||||
* @param {array} quadCorners An array of the quad nodes' corners.
|
||||
* @return {array} An array of indexes containing one to
|
||||
* four integers.
|
||||
*/
|
||||
function _quadIndexes(rectangle, quadCorners) {
|
||||
var indexes = [];
|
||||
|
||||
// Iterating through quads
|
||||
for (var i = 0; i < 4; i++)
|
||||
if ((rectangle.x2 >= quadCorners[i][0].x) &&
|
||||
(rectangle.x1 <= quadCorners[i][1].x) &&
|
||||
(rectangle.y1 + rectangle.height >= quadCorners[i][0].y) &&
|
||||
(rectangle.y1 <= quadCorners[i][2].y))
|
||||
indexes.push(i);
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of indexes of nodes containing a non-axis-aligned rectangle
|
||||
*
|
||||
* @param {array} corners An array containing each corner of the
|
||||
* rectangle defined by its coordinates (x, y).
|
||||
* @param {array} quadCorners An array of the quad nodes' corners.
|
||||
* @return {array} An array of indexes containing one to
|
||||
* four integers.
|
||||
*/
|
||||
function _quadCollision(corners, quadCorners) {
|
||||
var indexes = [];
|
||||
|
||||
// Iterating through quads
|
||||
for (var i = 0; i < 4; i++)
|
||||
if (_geom.collision(corners, quadCorners[i]))
|
||||
indexes.push(i);
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subdivide a quad by creating a node at a precise index. The function does
|
||||
* not generate all four nodes not to potentially create unused nodes.
|
||||
*
|
||||
* @param {integer} index The index of the node to create.
|
||||
* @param {object} quad The quad object to subdivide.
|
||||
* @return {object} A new quad representing the node created.
|
||||
*/
|
||||
function _quadSubdivide(index, quad) {
|
||||
var next = quad.level + 1,
|
||||
subw = Math.round(quad.bounds.width / 2),
|
||||
subh = Math.round(quad.bounds.height / 2),
|
||||
qx = Math.round(quad.bounds.x),
|
||||
qy = Math.round(quad.bounds.y),
|
||||
x,
|
||||
y;
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
x = qx;
|
||||
y = qy;
|
||||
break;
|
||||
case 1:
|
||||
x = qx + subw;
|
||||
y = qy;
|
||||
break;
|
||||
case 2:
|
||||
x = qx;
|
||||
y = qy + subh;
|
||||
break;
|
||||
case 3:
|
||||
x = qx + subw;
|
||||
y = qy + subh;
|
||||
break;
|
||||
}
|
||||
|
||||
return _quadTree(
|
||||
{x: x, y: y, width: subw, height: subh},
|
||||
next,
|
||||
quad.maxElements,
|
||||
quad.maxLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively insert an element into the quadtree. Only points
|
||||
* with size, i.e. axis-aligned squares, may be inserted with this
|
||||
* method.
|
||||
*
|
||||
* @param {object} el The element to insert in the quadtree.
|
||||
* @param {object} sizedPoint A sized point defined by two top points
|
||||
* (x1, y1), (x2, y2) and height.
|
||||
* @param {object} quad The quad in which to insert the element.
|
||||
* @return {undefined} The function does not return anything.
|
||||
*/
|
||||
function _quadInsert(el, sizedPoint, quad) {
|
||||
if (quad.level < quad.maxLevel) {
|
||||
|
||||
// Searching appropriate quads
|
||||
var indexes = _quadIndexes(sizedPoint, quad.corners);
|
||||
|
||||
// Iterating
|
||||
for (var i = 0, l = indexes.length; i < l; i++) {
|
||||
|
||||
// Subdividing if necessary
|
||||
if (quad.nodes[indexes[i]] === undefined)
|
||||
quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad);
|
||||
|
||||
// Recursion
|
||||
_quadInsert(el, sizedPoint, quad.nodes[indexes[i]]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
// Pushing the element in a leaf node
|
||||
quad.elements.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieve every elements held by the node containing the
|
||||
* searched point.
|
||||
*
|
||||
* @param {object} point The searched point (x, y).
|
||||
* @param {object} quad The searched quad.
|
||||
* @return {array} An array of elements contained in the relevant
|
||||
* node.
|
||||
*/
|
||||
function _quadRetrievePoint(point, quad) {
|
||||
if (quad.level < quad.maxLevel) {
|
||||
var index = _quadIndex(point, quad.bounds);
|
||||
|
||||
// If node does not exist we return an empty list
|
||||
if (quad.nodes[index] !== undefined) {
|
||||
return _quadRetrievePoint(point, quad.nodes[index]);
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
else {
|
||||
return quad.elements;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieve every elements contained within an rectangular area
|
||||
* that may or may not be axis-aligned.
|
||||
*
|
||||
* @param {object|array} rectData The searched area defined either by
|
||||
* an array of four corners (x, y) in
|
||||
* the case of a non-axis-aligned
|
||||
* rectangle or an object with two top
|
||||
* points (x1, y1), (x2, y2) and height.
|
||||
* @param {object} quad The searched quad.
|
||||
* @param {function} collisionFunc The collision function used to search
|
||||
* for node indexes.
|
||||
* @param {array?} els The retrieved elements.
|
||||
* @return {array} An array of elements contained in the
|
||||
* area.
|
||||
*/
|
||||
function _quadRetrieveArea(rectData, quad, collisionFunc, els) {
|
||||
els = els || {};
|
||||
|
||||
if (quad.level < quad.maxLevel) {
|
||||
var indexes = collisionFunc(rectData, quad.corners);
|
||||
|
||||
for (var i = 0, l = indexes.length; i < l; i++)
|
||||
if (quad.nodes[indexes[i]] !== undefined)
|
||||
_quadRetrieveArea(
|
||||
rectData,
|
||||
quad.nodes[indexes[i]],
|
||||
collisionFunc,
|
||||
els
|
||||
);
|
||||
} else
|
||||
for (var j = 0, m = quad.elements.length; j < m; j++)
|
||||
if (els[quad.elements[j].id] === undefined)
|
||||
els[quad.elements[j].id] = quad.elements[j];
|
||||
|
||||
return els;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the quadtree object itself.
|
||||
*
|
||||
* @param {object} bounds The boundaries of the quad defined by an
|
||||
* origin (x, y), width and heigth.
|
||||
* @param {integer} level The level of the quad in the tree.
|
||||
* @param {integer} maxElements The max number of element in a leaf node.
|
||||
* @param {integer} maxLevel The max recursion level of the tree.
|
||||
* @return {object} The quadtree object.
|
||||
*/
|
||||
function _quadTree(bounds, level, maxElements, maxLevel) {
|
||||
return {
|
||||
level: level || 0,
|
||||
bounds: bounds,
|
||||
corners: _geom.splitSquare(bounds),
|
||||
maxElements: maxElements || 20,
|
||||
maxLevel: maxLevel || 4,
|
||||
elements: [],
|
||||
nodes: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sigma Quad Constructor
|
||||
* ----------------------
|
||||
*
|
||||
* The quad API as exposed to sigma.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The quad core that will become the sigma interface with the quadtree.
|
||||
*
|
||||
* property {object} _tree Property holding the quadtree object.
|
||||
* property {object} _geom Exposition of the _geom namespace for testing.
|
||||
* property {object} _cache Cache for the area method.
|
||||
*/
|
||||
var quad = function() {
|
||||
this._geom = _geom;
|
||||
this._tree = null;
|
||||
this._cache = {
|
||||
query: false,
|
||||
result: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Index a graph by inserting its nodes into the quadtree.
|
||||
*
|
||||
* @param {array} nodes An array of nodes to index.
|
||||
* @param {object} params An object of parameters with at least the quad
|
||||
* bounds.
|
||||
* @return {object} The quadtree object.
|
||||
*
|
||||
* Parameters:
|
||||
* ----------
|
||||
* bounds: {object} boundaries of the quad defined by its origin (x, y)
|
||||
* width and heigth.
|
||||
* prefix: {string?} a prefix for node geometric attributes.
|
||||
* maxElements: {integer?} the max number of elements in a leaf node.
|
||||
* maxLevel: {integer?} the max recursion level of the tree.
|
||||
*/
|
||||
quad.prototype.index = function(nodes, params) {
|
||||
|
||||
// Enforcing presence of boundaries
|
||||
if (!params.bounds)
|
||||
throw 'sigma.classes.quad.index: bounds information not given.';
|
||||
|
||||
// Prefix
|
||||
var prefix = params.prefix || '';
|
||||
|
||||
// Building the tree
|
||||
this._tree = _quadTree(
|
||||
params.bounds,
|
||||
0,
|
||||
params.maxElements,
|
||||
params.maxLevel
|
||||
);
|
||||
|
||||
// Inserting graph nodes into the tree
|
||||
for (var i = 0, l = nodes.length; i < l; i++) {
|
||||
|
||||
// Inserting node
|
||||
_quadInsert(
|
||||
nodes[i],
|
||||
_geom.pointToSquare({
|
||||
x: nodes[i][prefix + 'x'],
|
||||
y: nodes[i][prefix + 'y'],
|
||||
size: nodes[i][prefix + 'size']
|
||||
}),
|
||||
this._tree
|
||||
);
|
||||
}
|
||||
|
||||
// Reset cache:
|
||||
this._cache = {
|
||||
query: false,
|
||||
result: false
|
||||
};
|
||||
|
||||
// remove?
|
||||
return this._tree;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve every graph nodes held by the quadtree node containing the
|
||||
* searched point.
|
||||
*
|
||||
* @param {number} x of the point.
|
||||
* @param {number} y of the point.
|
||||
* @return {array} An array of nodes retrieved.
|
||||
*/
|
||||
quad.prototype.point = function(x, y) {
|
||||
return this._tree ?
|
||||
_quadRetrievePoint({x: x, y: y}, this._tree) || [] :
|
||||
[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve every graph nodes within a rectangular area. The methods keep the
|
||||
* last area queried in cache for optimization reason and will act differently
|
||||
* for the same reason if the area is axis-aligned or not.
|
||||
*
|
||||
* @param {object} A rectangle defined by two top points (x1, y1), (x2, y2)
|
||||
* and height.
|
||||
* @return {array} An array of nodes retrieved.
|
||||
*/
|
||||
quad.prototype.area = function(rect) {
|
||||
var serialized = JSON.stringify(rect),
|
||||
collisionFunc,
|
||||
rectData;
|
||||
|
||||
// Returning cache?
|
||||
if (this._cache.query === serialized)
|
||||
return this._cache.result;
|
||||
|
||||
// Axis aligned ?
|
||||
if (_geom.isAxisAligned(rect)) {
|
||||
collisionFunc = _quadIndexes;
|
||||
rectData = _geom.axisAlignedTopPoints(rect);
|
||||
}
|
||||
else {
|
||||
collisionFunc = _quadCollision;
|
||||
rectData = _geom.rectangleCorners(rect);
|
||||
}
|
||||
|
||||
// Retrieving nodes
|
||||
var nodes = this._tree ?
|
||||
_quadRetrieveArea(
|
||||
rectData,
|
||||
this._tree,
|
||||
collisionFunc
|
||||
) :
|
||||
[];
|
||||
|
||||
// Object to array
|
||||
var nodesArray = [];
|
||||
for (var i in nodes)
|
||||
nodesArray.push(nodes[i]);
|
||||
|
||||
// Caching
|
||||
this._cache.query = serialized;
|
||||
this._cache.result = nodesArray;
|
||||
|
||||
return nodesArray;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EXPORT:
|
||||
* *******
|
||||
*/
|
||||
if (typeof this.sigma !== 'undefined') {
|
||||
this.sigma.classes = this.sigma.classes || {};
|
||||
this.sigma.classes.quad = quad;
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = quad;
|
||||
exports.quad = quad;
|
||||
} else
|
||||
this.quad = quad;
|
||||
}).call(this);
|
@ -1,984 +0,0 @@
|
||||
/**
|
||||
* conrad.js is a tiny JavaScript jobs scheduler,
|
||||
*
|
||||
* Version: 0.1.0
|
||||
* Sources: http://github.com/jacomyal/conrad.js
|
||||
* Doc: http://github.com/jacomyal/conrad.js#readme
|
||||
*
|
||||
* License:
|
||||
* --------
|
||||
* Copyright © 2013 Alexis Jacomy, Sciences-Po médialab
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* The Software is provided "as is", without warranty of any kind, express or
|
||||
* implied, including but not limited to the warranties of merchantability,
|
||||
* fitness for a particular purpose and noninfringement. In no event shall the
|
||||
* authors or copyright holders be liable for any claim, damages or other
|
||||
* liability, whether in an action of contract, tort or otherwise, arising
|
||||
* from, out of or in connection with the software or the use or other dealings
|
||||
* in the Software.
|
||||
*/
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
// Check that conrad.js has not been loaded yet:
|
||||
if (global.conrad)
|
||||
throw new Error('conrad already exists');
|
||||
|
||||
|
||||
/**
|
||||
* PRIVATE VARIABLES:
|
||||
* ******************
|
||||
*/
|
||||
|
||||
/**
|
||||
* A flag indicating whether conrad is running or not.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
var _lastFrameTime;
|
||||
|
||||
/**
|
||||
* A flag indicating whether conrad is running or not.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
var _isRunning = false;
|
||||
|
||||
/**
|
||||
* The hash of registered jobs. Each job must at least have a unique ID
|
||||
* under the key "id" and a function under the key "job". This hash
|
||||
* contains each running job and each waiting job.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var _jobs = {};
|
||||
|
||||
/**
|
||||
* The hash of currently running jobs.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var _runningJobs = {};
|
||||
|
||||
/**
|
||||
* The array of currently running jobs, sorted by priority.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
var _sortedByPriorityJobs = [];
|
||||
|
||||
/**
|
||||
* The array of currently waiting jobs.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var _waitingJobs = {};
|
||||
|
||||
/**
|
||||
* The array of finished jobs. They are stored in an array, since two jobs
|
||||
* with the same "id" can happen at two different times.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
var _doneJobs = [];
|
||||
|
||||
/**
|
||||
* A dirty flag to keep conrad from starting: Indeed, when addJob() is called
|
||||
* with several jobs, conrad must be started only at the end. This flag keeps
|
||||
* me from duplicating the code that effectively adds a job.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
var _noStart = false;
|
||||
|
||||
/**
|
||||
* An hash containing some global settings about how conrad.js should
|
||||
* behave.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var _parameters = {
|
||||
frameDuration: 20,
|
||||
history: true
|
||||
};
|
||||
|
||||
/**
|
||||
* This object contains every handlers bound to conrad events. It does not
|
||||
* requirea any DOM implementation, since the events are all JavaScript.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
var _handlers = Object.create(null);
|
||||
|
||||
|
||||
/**
|
||||
* PRIVATE FUNCTIONS:
|
||||
* ******************
|
||||
*/
|
||||
|
||||
/**
|
||||
* Will execute the handler everytime that the indicated event (or the
|
||||
* indicated events) will be triggered.
|
||||
*
|
||||
* @param {string|array|object} events The name of the event (or the events
|
||||
* separated by spaces).
|
||||
* @param {function(Object)} handler The handler to bind.
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _bind(events, handler) {
|
||||
var i,
|
||||
i_end,
|
||||
event,
|
||||
eArray;
|
||||
|
||||
if (!arguments.length)
|
||||
return;
|
||||
else if (
|
||||
arguments.length === 1 &&
|
||||
Object(arguments[0]) === arguments[0]
|
||||
)
|
||||
for (events in arguments[0])
|
||||
_bind(events, arguments[0][events]);
|
||||
else if (arguments.length > 1) {
|
||||
eArray =
|
||||
Array.isArray(events) ?
|
||||
events :
|
||||
events.split(/ /);
|
||||
|
||||
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
|
||||
event = eArray[i];
|
||||
|
||||
if (!_handlers[event])
|
||||
_handlers[event] = [];
|
||||
|
||||
// Using an object instead of directly the handler will make possible
|
||||
// later to add flags
|
||||
_handlers[event].push({
|
||||
handler: handler
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the handler from a specified event (or specified events).
|
||||
*
|
||||
* @param {?string} events The name of the event (or the events
|
||||
* separated by spaces). If undefined,
|
||||
* then all handlers are removed.
|
||||
* @param {?function(Object)} handler The handler to unbind. If undefined,
|
||||
* each handler bound to the event or the
|
||||
* events will be removed.
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _unbind(events, handler) {
|
||||
var i,
|
||||
i_end,
|
||||
j,
|
||||
j_end,
|
||||
a,
|
||||
event,
|
||||
eArray = Array.isArray(events) ?
|
||||
events :
|
||||
events.split(/ /);
|
||||
|
||||
if (!arguments.length)
|
||||
_handlers = Object.create(null);
|
||||
else if (handler) {
|
||||
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
|
||||
event = eArray[i];
|
||||
if (_handlers[event]) {
|
||||
a = [];
|
||||
for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1)
|
||||
if (_handlers[event][j].handler !== handler)
|
||||
a.push(_handlers[event][j]);
|
||||
|
||||
_handlers[event] = a;
|
||||
}
|
||||
|
||||
if (_handlers[event] && _handlers[event].length === 0)
|
||||
delete _handlers[event];
|
||||
}
|
||||
} else
|
||||
for (i = 0, i_end = eArray.length; i !== i_end; i += 1)
|
||||
delete _handlers[eArray[i]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes each handler bound to the event.
|
||||
*
|
||||
* @param {string} events The name of the event (or the events separated
|
||||
* by spaces).
|
||||
* @param {?Object} data The content of the event (optional).
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _dispatch(events, data) {
|
||||
var i,
|
||||
j,
|
||||
i_end,
|
||||
j_end,
|
||||
event,
|
||||
eventName,
|
||||
eArray = Array.isArray(events) ?
|
||||
events :
|
||||
events.split(/ /);
|
||||
|
||||
data = data === undefined ? {} : data;
|
||||
|
||||
for (i = 0, i_end = eArray.length; i !== i_end; i += 1) {
|
||||
eventName = eArray[i];
|
||||
|
||||
if (_handlers[eventName]) {
|
||||
event = {
|
||||
type: eventName,
|
||||
data: data || {}
|
||||
};
|
||||
|
||||
for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1)
|
||||
try {
|
||||
_handlers[eventName][j].handler(event);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the most prioritary job once, and deals with filling the stats
|
||||
* (done, time, averageTime, currentTime, etc...).
|
||||
*
|
||||
* @return {?Object} Returns the job object if it has to be killed, null else.
|
||||
*/
|
||||
function _executeFirstJob() {
|
||||
var i,
|
||||
l,
|
||||
test,
|
||||
kill,
|
||||
pushed = false,
|
||||
time = __dateNow(),
|
||||
job = _sortedByPriorityJobs.shift();
|
||||
|
||||
// Execute the job and look at the result:
|
||||
test = job.job();
|
||||
|
||||
// Deal with stats:
|
||||
time = __dateNow() - time;
|
||||
job.done++;
|
||||
job.time += time;
|
||||
job.currentTime += time;
|
||||
job.weightTime = job.currentTime / (job.weight || 1);
|
||||
job.averageTime = job.time / job.done;
|
||||
|
||||
// Check if the job has to be killed:
|
||||
kill = job.count ? (job.count <= job.done) : !test;
|
||||
|
||||
// Reset priorities:
|
||||
if (!kill) {
|
||||
for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++)
|
||||
if (_sortedByPriorityJobs[i].weightTime > job.weightTime) {
|
||||
_sortedByPriorityJobs.splice(i, 0, job);
|
||||
pushed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!pushed)
|
||||
_sortedByPriorityJobs.push(job);
|
||||
}
|
||||
|
||||
return kill ? job : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates a job, by adding it to the _runningJobs object and the
|
||||
* _sortedByPriorityJobs array. It also initializes its currentTime value.
|
||||
*
|
||||
* @param {Object} job The job to activate.
|
||||
*/
|
||||
function _activateJob(job) {
|
||||
var l = _sortedByPriorityJobs.length;
|
||||
|
||||
// Add the job to the running jobs:
|
||||
_runningJobs[job.id] = job;
|
||||
job.status = 'running';
|
||||
|
||||
// Add the job to the priorities:
|
||||
if (l) {
|
||||
job.weightTime = _sortedByPriorityJobs[l - 1].weightTime;
|
||||
job.currentTime = job.weightTime * (job.weight || 1);
|
||||
}
|
||||
|
||||
// Initialize the job and dispatch:
|
||||
job.startTime = __dateNow();
|
||||
_dispatch('jobStarted', __clone(job));
|
||||
|
||||
_sortedByPriorityJobs.push(job);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main loop of conrad.js:
|
||||
* . It executes job such that they all occupate the same processing time.
|
||||
* . It stops jobs that do not need to be executed anymore.
|
||||
* . It triggers callbacks when it is relevant.
|
||||
* . It starts waiting jobs when they need to be started.
|
||||
* . It injects frames to keep a constant frapes per second ratio.
|
||||
* . It stops itself when there are no more jobs to execute.
|
||||
*/
|
||||
function _loop() {
|
||||
var k,
|
||||
o,
|
||||
l,
|
||||
job,
|
||||
time,
|
||||
deadJob;
|
||||
|
||||
// Deal with the newly added jobs (the _jobs object):
|
||||
for (k in _jobs) {
|
||||
job = _jobs[k];
|
||||
|
||||
if (job.after)
|
||||
_waitingJobs[k] = job;
|
||||
else
|
||||
_activateJob(job);
|
||||
|
||||
delete _jobs[k];
|
||||
}
|
||||
|
||||
// Set the _isRunning flag to false if there are no running job:
|
||||
_isRunning = !!_sortedByPriorityJobs.length;
|
||||
|
||||
// Deal with the running jobs (the _runningJobs object):
|
||||
while (
|
||||
_sortedByPriorityJobs.length &&
|
||||
__dateNow() - _lastFrameTime < _parameters.frameDuration
|
||||
) {
|
||||
deadJob = _executeFirstJob();
|
||||
|
||||
// Deal with the case where the job has ended:
|
||||
if (deadJob) {
|
||||
_killJob(deadJob.id);
|
||||
|
||||
// Check for waiting jobs:
|
||||
for (k in _waitingJobs)
|
||||
if (_waitingJobs[k].after === deadJob.id) {
|
||||
_activateJob(_waitingJobs[k]);
|
||||
delete _waitingJobs[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if conrad still has jobs to deal with, and kill it if not:
|
||||
if (_isRunning) {
|
||||
// Update the _lastFrameTime:
|
||||
_lastFrameTime = __dateNow();
|
||||
|
||||
_dispatch('enterFrame');
|
||||
setTimeout(_loop, 0);
|
||||
} else
|
||||
_dispatch('stop');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more jobs, and starts the loop if no job was running before. A
|
||||
* job is at least a unique string "id" and a function, and there are some
|
||||
* parameters that you can specify for each job to modify the way conrad will
|
||||
* execute it. If a job is added with the "id" of another job that is waiting
|
||||
* or still running, an error will be thrown.
|
||||
*
|
||||
* When a job is added, it is referenced in the _jobs object, by its id.
|
||||
* Then, if it has to be executed right now, it will be also referenced in
|
||||
* the _runningJobs object. If it has to wait, then it will be added into the
|
||||
* _waitingJobs object, until it can start.
|
||||
*
|
||||
* Keep reading this documentation to see how to call this method.
|
||||
*
|
||||
* @return {Object} Returns conrad.
|
||||
*
|
||||
* Adding one job:
|
||||
* ***************
|
||||
* Basically, a job is defined by its string id and a function (the job). It
|
||||
* is also possible to add some parameters:
|
||||
*
|
||||
* > conrad.addJob('myJobId', myJobFunction);
|
||||
* > conrad.addJob('myJobId', {
|
||||
* > job: myJobFunction,
|
||||
* > someParameter: someValue
|
||||
* > });
|
||||
* > conrad.addJob({
|
||||
* > id: 'myJobId',
|
||||
* > job: myJobFunction,
|
||||
* > someParameter: someValue
|
||||
* > });
|
||||
*
|
||||
* Adding several jobs:
|
||||
* ********************
|
||||
* When adding several jobs at the same time, it is possible to specify
|
||||
* parameters for each one individually or for all:
|
||||
*
|
||||
* > conrad.addJob([
|
||||
* > {
|
||||
* > id: 'myJobId1',
|
||||
* > job: myJobFunction1,
|
||||
* > someParameter1: someValue1
|
||||
* > },
|
||||
* > {
|
||||
* > id: 'myJobId2',
|
||||
* > job: myJobFunction2,
|
||||
* > someParameter2: someValue2
|
||||
* > }
|
||||
* > ], {
|
||||
* > someCommonParameter: someCommonValue
|
||||
* > });
|
||||
* > conrad.addJob({
|
||||
* > myJobId1: {,
|
||||
* > job: myJobFunction1,
|
||||
* > someParameter1: someValue1
|
||||
* > },
|
||||
* > myJobId2: {,
|
||||
* > job: myJobFunction2,
|
||||
* > someParameter2: someValue2
|
||||
* > }
|
||||
* > }, {
|
||||
* > someCommonParameter: someCommonValue
|
||||
* > });
|
||||
* > conrad.addJob({
|
||||
* > myJobId1: myJobFunction1,
|
||||
* > myJobId2: myJobFunction2
|
||||
* > }, {
|
||||
* > someCommonParameter: someCommonValue
|
||||
* > });
|
||||
*
|
||||
* Recognized parameters:
|
||||
* **********************
|
||||
* Here is the exhaustive list of every accepted parameters:
|
||||
*
|
||||
* {?Function} end A callback to execute when the job is ended. It is
|
||||
* not executed if the job is killed instead of ended
|
||||
* "naturally".
|
||||
* {?Integer} count The number of time the job has to be executed.
|
||||
* {?Number} weight If specified, the job will be executed as it was
|
||||
* added "weight" times.
|
||||
* {?String} after The id of another job (eventually not added yet).
|
||||
* If specified, this job will start only when the
|
||||
* specified "after" job is ended.
|
||||
*/
|
||||
function _addJob(v1, v2) {
|
||||
var i,
|
||||
l,
|
||||
o;
|
||||
|
||||
// Array of jobs:
|
||||
if (Array.isArray(v1)) {
|
||||
// Keep conrad to start until the last job is added:
|
||||
_noStart = true;
|
||||
|
||||
for (i = 0, l = v1.length; i < l; i++)
|
||||
_addJob(v1[i].id, __extend(v1[i], v2));
|
||||
|
||||
_noStart = false;
|
||||
if (!_isRunning) {
|
||||
// Update the _lastFrameTime:
|
||||
_lastFrameTime = __dateNow();
|
||||
|
||||
_dispatch('start');
|
||||
_loop();
|
||||
}
|
||||
} else if (typeof v1 === 'object') {
|
||||
// One job (object):
|
||||
if (typeof v1.id === 'string')
|
||||
_addJob(v1.id, v1);
|
||||
|
||||
// Hash of jobs:
|
||||
else {
|
||||
// Keep conrad to start until the last job is added:
|
||||
_noStart = true;
|
||||
|
||||
for (i in v1)
|
||||
if (typeof v1[i] === 'function')
|
||||
_addJob(i, __extend({
|
||||
job: v1[i]
|
||||
}, v2));
|
||||
else
|
||||
_addJob(i, __extend(v1[i], v2));
|
||||
|
||||
_noStart = false;
|
||||
if (!_isRunning) {
|
||||
// Update the _lastFrameTime:
|
||||
_lastFrameTime = __dateNow();
|
||||
|
||||
_dispatch('start');
|
||||
_loop();
|
||||
}
|
||||
}
|
||||
|
||||
// One job (string, *):
|
||||
} else if (typeof v1 === 'string') {
|
||||
if (_hasJob(v1))
|
||||
throw new Error(
|
||||
'[conrad.addJob] Job with id "' + v1 + '" already exists.'
|
||||
);
|
||||
|
||||
// One job (string, function):
|
||||
if (typeof v2 === 'function') {
|
||||
o = {
|
||||
id: v1,
|
||||
done: 0,
|
||||
time: 0,
|
||||
status: 'waiting',
|
||||
currentTime: 0,
|
||||
averageTime: 0,
|
||||
weightTime: 0,
|
||||
job: v2
|
||||
};
|
||||
|
||||
// One job (string, object):
|
||||
} else if (typeof v2 === 'object') {
|
||||
o = __extend(
|
||||
{
|
||||
id: v1,
|
||||
done: 0,
|
||||
time: 0,
|
||||
status: 'waiting',
|
||||
currentTime: 0,
|
||||
averageTime: 0,
|
||||
weightTime: 0
|
||||
},
|
||||
v2
|
||||
);
|
||||
|
||||
// If none of those cases, throw an error:
|
||||
} else
|
||||
throw new Error('[conrad.addJob] Wrong arguments.');
|
||||
|
||||
// Effectively add the job:
|
||||
_jobs[v1] = o;
|
||||
_dispatch('jobAdded', __clone(o));
|
||||
|
||||
// Check if the loop has to be started:
|
||||
if (!_isRunning && !_noStart) {
|
||||
// Update the _lastFrameTime:
|
||||
_lastFrameTime = __dateNow();
|
||||
|
||||
_dispatch('start');
|
||||
_loop();
|
||||
}
|
||||
|
||||
// If none of those cases, throw an error:
|
||||
} else
|
||||
throw new Error('[conrad.addJob] Wrong arguments.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills one or more jobs, indicated by their ids. It is only possible to
|
||||
* kill running jobs or waiting jobs. If you try to kill a job that does not
|
||||
* exists or that is already killed, a warning will be thrown.
|
||||
*
|
||||
* @param {Array|String} v1 A string job id or an array of job ids.
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _killJob(v1) {
|
||||
var i,
|
||||
l,
|
||||
k,
|
||||
a,
|
||||
job,
|
||||
found = false;
|
||||
|
||||
// Array of job ids:
|
||||
if (Array.isArray(v1))
|
||||
for (i = 0, l = v1.length; i < l; i++)
|
||||
_killJob(v1[i]);
|
||||
|
||||
// One job's id:
|
||||
else if (typeof v1 === 'string') {
|
||||
a = [_runningJobs, _waitingJobs, _jobs];
|
||||
|
||||
// Remove the job from the hashes:
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
if (v1 in a[i]) {
|
||||
job = a[i][v1];
|
||||
|
||||
if (_parameters.history) {
|
||||
job.status = 'done';
|
||||
_doneJobs.push(job);
|
||||
}
|
||||
|
||||
_dispatch('jobEnded', __clone(job));
|
||||
delete a[i][v1];
|
||||
|
||||
if (typeof job.end === 'function')
|
||||
job.end();
|
||||
|
||||
found = true;
|
||||
}
|
||||
|
||||
// Remove the priorities array:
|
||||
a = _sortedByPriorityJobs;
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
if (a[i].id === v1) {
|
||||
a.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
throw new Error('[conrad.killJob] Job "' + v1 + '" not found.');
|
||||
|
||||
// If none of those cases, throw an error:
|
||||
} else
|
||||
throw new Error('[conrad.killJob] Wrong arguments.');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills every running, waiting, and just added jobs.
|
||||
*
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _killAll() {
|
||||
var k,
|
||||
jobs = __extend(_jobs, _runningJobs, _waitingJobs);
|
||||
|
||||
// Take every jobs and push them into the _doneJobs object:
|
||||
if (_parameters.history)
|
||||
for (k in jobs) {
|
||||
jobs[k].status = 'done';
|
||||
_doneJobs.push(jobs[k]);
|
||||
|
||||
if (typeof jobs[k].end === 'function')
|
||||
jobs[k].end();
|
||||
}
|
||||
|
||||
// Reinitialize the different jobs lists:
|
||||
_jobs = {};
|
||||
_waitingJobs = {};
|
||||
_runningJobs = {};
|
||||
_sortedByPriorityJobs = [];
|
||||
|
||||
// In case some jobs are added right after the kill:
|
||||
_isRunning = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a job with the specified id is currently running or
|
||||
* waiting, and false else.
|
||||
*
|
||||
* @param {String} id The id of the job.
|
||||
* @return {?Object} Returns the job object if it exists.
|
||||
*/
|
||||
function _hasJob(id) {
|
||||
var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id];
|
||||
return job ? __extend(job) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will set the setting specified by "v1" to the value specified
|
||||
* by "v2" if both are given, and else return the current value of the
|
||||
* settings "v1".
|
||||
*
|
||||
* @param {String} v1 The name of the property.
|
||||
* @param {?*} v2 Eventually, a value to set to the specified
|
||||
* property.
|
||||
* @return {Object|*} Returns the specified settings value if "v2" is not
|
||||
* given, and conrad else.
|
||||
*/
|
||||
function _settings(v1, v2) {
|
||||
var o;
|
||||
|
||||
if (typeof a1 === 'string' && arguments.length === 1)
|
||||
return _parameters[a1];
|
||||
else {
|
||||
o = (typeof a1 === 'object' && arguments.length === 1) ?
|
||||
a1 || {} :
|
||||
{};
|
||||
if (typeof a1 === 'string')
|
||||
o[a1] = a2;
|
||||
|
||||
for (var k in o)
|
||||
if (o[k] !== undefined)
|
||||
_parameters[k] = o[k];
|
||||
else
|
||||
delete _parameters[k];
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if conrad is currently running, and false else.
|
||||
*
|
||||
* @return {Boolean} Returns _isRunning.
|
||||
*/
|
||||
function _getIsRunning() {
|
||||
return _isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unreference every jobs that are stored in the _doneJobs object. It will
|
||||
* not be possible anymore to get stats about these jobs, but it will release
|
||||
* the memory.
|
||||
*
|
||||
* @return {Object} Returns conrad.
|
||||
*/
|
||||
function _clearHistory() {
|
||||
_doneJobs = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of every data about jobs that wait to be started, are
|
||||
* currently running or are done.
|
||||
*
|
||||
* It is possible to get only running, waiting or done jobs by giving
|
||||
* "running", "waiting" or "done" as fist argument.
|
||||
*
|
||||
* It is also possible to get every job with a specified id by giving it as
|
||||
* first argument. Also, using a RegExp instead of an id will return every
|
||||
* jobs whose ids match the RegExp. And these two last use cases work as well
|
||||
* by giving before "running", "waiting" or "done".
|
||||
*
|
||||
* @return {Array} The array of the matching jobs.
|
||||
*
|
||||
* Some call examples:
|
||||
* *******************
|
||||
* > conrad.getStats('running')
|
||||
* > conrad.getStats('waiting')
|
||||
* > conrad.getStats('done')
|
||||
* > conrad.getStats('myJob')
|
||||
* > conrad.getStats(/test/)
|
||||
* > conrad.getStats('running', 'myRunningJob')
|
||||
* > conrad.getStats('running', /test/)
|
||||
*/
|
||||
function _getStats(v1, v2) {
|
||||
var a,
|
||||
k,
|
||||
i,
|
||||
l,
|
||||
stats,
|
||||
pattern,
|
||||
isPatternString;
|
||||
|
||||
if (!arguments.length) {
|
||||
stats = [];
|
||||
|
||||
for (k in _jobs)
|
||||
stats.push(_jobs[k]);
|
||||
|
||||
for (k in _waitingJobs)
|
||||
stats.push(_waitingJobs[k]);
|
||||
|
||||
for (k in _runningJobs)
|
||||
stats.push(_runningJobs[k]);
|
||||
|
||||
stats = stats.concat(_doneJobs);
|
||||
}
|
||||
|
||||
if (typeof v1 === 'string')
|
||||
switch (v1) {
|
||||
case 'waiting':
|
||||
stats = __objectValues(_waitingJobs);
|
||||
break;
|
||||
case 'running':
|
||||
stats = __objectValues(_runningJobs);
|
||||
break;
|
||||
case 'done':
|
||||
stats = _doneJobs;
|
||||
break;
|
||||
default:
|
||||
pattern = v1;
|
||||
}
|
||||
|
||||
if (v1 instanceof RegExp)
|
||||
pattern = v1;
|
||||
|
||||
if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp))
|
||||
pattern = v2;
|
||||
|
||||
// Filter jobs if a pattern is given:
|
||||
if (pattern) {
|
||||
isPatternString = typeof pattern === 'string';
|
||||
|
||||
if (stats instanceof Array) {
|
||||
a = stats;
|
||||
} else if (typeof stats === 'object') {
|
||||
a = [];
|
||||
|
||||
for (k in stats)
|
||||
a = a.concat(stats[k]);
|
||||
} else {
|
||||
a = [];
|
||||
|
||||
for (k in _jobs)
|
||||
a.push(_jobs[k]);
|
||||
|
||||
for (k in _waitingJobs)
|
||||
a.push(_waitingJobs[k]);
|
||||
|
||||
for (k in _runningJobs)
|
||||
a.push(_runningJobs[k]);
|
||||
|
||||
a = a.concat(_doneJobs);
|
||||
}
|
||||
|
||||
stats = [];
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern))
|
||||
stats.push(a[i]);
|
||||
}
|
||||
|
||||
return __clone(stats);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TOOLS FUNCTIONS:
|
||||
* ****************
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function takes any number of objects as arguments, copies from each
|
||||
* of these objects each pair key/value into a new object, and finally
|
||||
* returns this object.
|
||||
*
|
||||
* The arguments are parsed from the last one to the first one, such that
|
||||
* when two objects have keys in common, the "earliest" object wins.
|
||||
*
|
||||
* Example:
|
||||
* ********
|
||||
* > var o1 = {
|
||||
* > a: 1,
|
||||
* > b: 2,
|
||||
* > c: '3'
|
||||
* > },
|
||||
* > o2 = {
|
||||
* > c: '4',
|
||||
* > d: [ 5 ]
|
||||
* > };
|
||||
* > __extend(o1, o2);
|
||||
* > // Returns: {
|
||||
* > // a: 1,
|
||||
* > // b: 2,
|
||||
* > // c: '3',
|
||||
* > // d: [ 5 ]
|
||||
* > // };
|
||||
*
|
||||
* @param {Object+} Any number of objects.
|
||||
* @return {Object} The merged object.
|
||||
*/
|
||||
function __extend() {
|
||||
var i,
|
||||
k,
|
||||
res = {},
|
||||
l = arguments.length;
|
||||
|
||||
for (i = l - 1; i >= 0; i--)
|
||||
for (k in arguments[i])
|
||||
res[k] = arguments[i][k];
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function simply clones an object. This object must contain only
|
||||
* objects, arrays and immutable values. Since it is not public, it does not
|
||||
* deal with cyclic references, DOM elements and instantiated objects - so
|
||||
* use it carefully.
|
||||
*
|
||||
* @param {Object} The object to clone.
|
||||
* @return {Object} The clone.
|
||||
*/
|
||||
function __clone(item) {
|
||||
var result, i, k, l;
|
||||
|
||||
if (!item)
|
||||
return item;
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
result = [];
|
||||
for (i = 0, l = item.length; i < l; i++)
|
||||
result.push(__clone(item[i]));
|
||||
} else if (typeof item === 'object') {
|
||||
result = {};
|
||||
for (i in item)
|
||||
result[i] = __clone(item[i]);
|
||||
} else
|
||||
result = item;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the values of an object.
|
||||
*
|
||||
* @param {Object} The object.
|
||||
* @return {Array} The array of values.
|
||||
*/
|
||||
function __objectValues(o) {
|
||||
var k,
|
||||
a = [];
|
||||
|
||||
for (k in o)
|
||||
a.push(o[k]);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* A short "Date.now()" polyfill.
|
||||
*
|
||||
* @return {Number} The current time (in ms).
|
||||
*/
|
||||
function __dateNow() {
|
||||
return Date.now ? Date.now() : new Date().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for the Array.isArray function:
|
||||
*/
|
||||
if (!Array.isArray)
|
||||
Array.isArray = function(v) {
|
||||
return Object.prototype.toString.call(v) === '[object Array]';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* EXPORT PUBLIC API:
|
||||
* ******************
|
||||
*/
|
||||
var conrad = {
|
||||
hasJob: _hasJob,
|
||||
addJob: _addJob,
|
||||
killJob: _killJob,
|
||||
killAll: _killAll,
|
||||
settings: _settings,
|
||||
getStats: _getStats,
|
||||
isRunning: _getIsRunning,
|
||||
clearHistory: _clearHistory,
|
||||
|
||||
// Events management:
|
||||
bind: _bind,
|
||||
unbind: _unbind,
|
||||
|
||||
// Version:
|
||||
version: '0.1.0'
|
||||
};
|
||||
|
||||
if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
exports = module.exports = conrad;
|
||||
exports.conrad = conrad;
|
||||
}
|
||||
global.conrad = conrad;
|
||||
})(this);
|
@ -1,35 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.middlewares');
|
||||
|
||||
/**
|
||||
* This middleware will just copy the graphic properties.
|
||||
*
|
||||
* @param {?string} readPrefix The read prefix.
|
||||
* @param {?string} writePrefix The write prefix.
|
||||
*/
|
||||
sigma.middlewares.copy = function(readPrefix, writePrefix) {
|
||||
var i,
|
||||
l,
|
||||
a;
|
||||
|
||||
if (writePrefix + '' === readPrefix + '')
|
||||
return;
|
||||
|
||||
a = this.graph.nodes();
|
||||
for (i = 0, l = a.length; i < l; i++) {
|
||||
a[i][writePrefix + 'x'] = a[i][readPrefix + 'x'];
|
||||
a[i][writePrefix + 'y'] = a[i][readPrefix + 'y'];
|
||||
a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
|
||||
}
|
||||
|
||||
a = this.graph.edges();
|
||||
for (i = 0, l = a.length; i < l; i++)
|
||||
a[i][writePrefix + 'size'] = a[i][readPrefix + 'size'];
|
||||
};
|
||||
}).call(this);
|
@ -1,189 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.middlewares');
|
||||
sigma.utils.pkg('sigma.utils');
|
||||
|
||||
/**
|
||||
* This middleware will rescale the graph such that it takes an optimal space
|
||||
* on the renderer.
|
||||
*
|
||||
* As each middleware, this function is executed in the scope of the sigma
|
||||
* instance.
|
||||
*
|
||||
* @param {?string} readPrefix The read prefix.
|
||||
* @param {?string} writePrefix The write prefix.
|
||||
* @param {object} options The parameters.
|
||||
*/
|
||||
sigma.middlewares.rescale = function(readPrefix, writePrefix, options) {
|
||||
var i,
|
||||
l,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
scale,
|
||||
margin,
|
||||
n = this.graph.nodes(),
|
||||
e = this.graph.edges(),
|
||||
settings = this.settings.embedObjects(options || {}),
|
||||
bounds = settings('bounds') || sigma.utils.getBoundaries(
|
||||
this.graph,
|
||||
readPrefix,
|
||||
true
|
||||
),
|
||||
minX = bounds.minX,
|
||||
minY = bounds.minY,
|
||||
maxX = bounds.maxX,
|
||||
maxY = bounds.maxY,
|
||||
sizeMax = bounds.sizeMax,
|
||||
weightMax = bounds.weightMax,
|
||||
w = settings('width') || 1,
|
||||
h = settings('height') || 1,
|
||||
rescaleSettings = settings('autoRescale'),
|
||||
validSettings = {
|
||||
nodePosition: 1,
|
||||
nodeSize: 1,
|
||||
edgeSize: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* What elements should we rescale?
|
||||
*/
|
||||
if (!(rescaleSettings instanceof Array))
|
||||
rescaleSettings = ['nodePosition', 'nodeSize', 'edgeSize'];
|
||||
|
||||
for (i = 0, l = rescaleSettings.length; i < l; i++)
|
||||
if (!validSettings[rescaleSettings[i]])
|
||||
throw new Error(
|
||||
'The rescale setting "' + rescaleSettings[i] + '" is not recognized.'
|
||||
);
|
||||
|
||||
var np = ~rescaleSettings.indexOf('nodePosition'),
|
||||
ns = ~rescaleSettings.indexOf('nodeSize'),
|
||||
es = ~rescaleSettings.indexOf('edgeSize');
|
||||
|
||||
/**
|
||||
* First, we compute the scaling ratio, without considering the sizes
|
||||
* of the nodes : Each node will have its center in the canvas, but might
|
||||
* be partially out of it.
|
||||
*/
|
||||
scale = settings('scalingMode') === 'outside' ?
|
||||
Math.max(
|
||||
w / Math.max(maxX - minX, 1),
|
||||
h / Math.max(maxY - minY, 1)
|
||||
) :
|
||||
Math.min(
|
||||
w / Math.max(maxX - minX, 1),
|
||||
h / Math.max(maxY - minY, 1)
|
||||
);
|
||||
|
||||
/**
|
||||
* Then, we correct that scaling ratio considering a margin, which is
|
||||
* basically the size of the biggest node.
|
||||
* This has to be done as a correction since to compare the size of the
|
||||
* biggest node to the X and Y values, we have to first get an
|
||||
* approximation of the scaling ratio.
|
||||
**/
|
||||
margin =
|
||||
(
|
||||
settings('rescaleIgnoreSize') ?
|
||||
0 :
|
||||
(settings('maxNodeSize') || sizeMax) / scale
|
||||
) +
|
||||
(settings('sideMargin') || 0);
|
||||
maxX += margin;
|
||||
minX -= margin;
|
||||
maxY += margin;
|
||||
minY -= margin;
|
||||
|
||||
// Fix the scaling with the new extrema:
|
||||
scale = settings('scalingMode') === 'outside' ?
|
||||
Math.max(
|
||||
w / Math.max(maxX - minX, 1),
|
||||
h / Math.max(maxY - minY, 1)
|
||||
) :
|
||||
Math.min(
|
||||
w / Math.max(maxX - minX, 1),
|
||||
h / Math.max(maxY - minY, 1)
|
||||
);
|
||||
|
||||
// Size homothetic parameters:
|
||||
if (!settings('maxNodeSize') && !settings('minNodeSize')) {
|
||||
a = 1;
|
||||
b = 0;
|
||||
} else if (settings('maxNodeSize') === settings('minNodeSize')) {
|
||||
a = 0;
|
||||
b = +settings('maxNodeSize');
|
||||
} else {
|
||||
a = (settings('maxNodeSize') - settings('minNodeSize')) / sizeMax;
|
||||
b = +settings('minNodeSize');
|
||||
}
|
||||
|
||||
if (!settings('maxEdgeSize') && !settings('minEdgeSize')) {
|
||||
c = 1;
|
||||
d = 0;
|
||||
} else if (settings('maxEdgeSize') === settings('minEdgeSize')) {
|
||||
c = 0;
|
||||
d = +settings('minEdgeSize');
|
||||
} else {
|
||||
c = (settings('maxEdgeSize') - settings('minEdgeSize')) / weightMax;
|
||||
d = +settings('minEdgeSize');
|
||||
}
|
||||
|
||||
// Rescale the nodes and edges:
|
||||
for (i = 0, l = e.length; i < l; i++)
|
||||
e[i][writePrefix + 'size'] =
|
||||
e[i][readPrefix + 'size'] * (es ? c : 1) + (es ? d : 0);
|
||||
|
||||
for (i = 0, l = n.length; i < l; i++) {
|
||||
n[i][writePrefix + 'size'] =
|
||||
n[i][readPrefix + 'size'] * (ns ? a : 1) + (ns ? b : 0);
|
||||
n[i][writePrefix + 'x'] =
|
||||
(n[i][readPrefix + 'x'] - (maxX + minX) / 2) * (np ? scale : 1);
|
||||
n[i][writePrefix + 'y'] =
|
||||
(n[i][readPrefix + 'y'] - (maxY + minY) / 2) * (np ? scale : 1);
|
||||
}
|
||||
};
|
||||
|
||||
sigma.utils.getBoundaries = function(graph, prefix, doEdges) {
|
||||
var i,
|
||||
l,
|
||||
e = graph.edges(),
|
||||
n = graph.nodes(),
|
||||
weightMax = -Infinity,
|
||||
sizeMax = -Infinity,
|
||||
minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
if (doEdges)
|
||||
for (i = 0, l = e.length; i < l; i++)
|
||||
weightMax = Math.max(e[i][prefix + 'size'], weightMax);
|
||||
|
||||
for (i = 0, l = n.length; i < l; i++) {
|
||||
sizeMax = Math.max(n[i][prefix + 'size'], sizeMax);
|
||||
maxX = Math.max(n[i][prefix + 'x'], maxX);
|
||||
minX = Math.min(n[i][prefix + 'x'], minX);
|
||||
maxY = Math.max(n[i][prefix + 'y'], maxY);
|
||||
minY = Math.min(n[i][prefix + 'y'], minY);
|
||||
}
|
||||
|
||||
weightMax = weightMax || 1;
|
||||
sizeMax = sizeMax || 1;
|
||||
|
||||
return {
|
||||
weightMax: weightMax,
|
||||
sizeMax: sizeMax,
|
||||
minX: minX,
|
||||
minY: minY,
|
||||
maxX: maxX,
|
||||
maxY: maxY
|
||||
};
|
||||
};
|
||||
}).call(this);
|
@ -1,239 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.misc.animation.running');
|
||||
|
||||
/**
|
||||
* Generates a unique ID for the animation.
|
||||
*
|
||||
* @return {string} Returns the new ID.
|
||||
*/
|
||||
var _getID = (function() {
|
||||
var id = 0;
|
||||
return function() {
|
||||
return '' + (++id);
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* This function animates a camera. It has to be called with the camera to
|
||||
* animate, the values of the coordinates to reach and eventually some
|
||||
* options. It returns a number id, that you can use to kill the animation,
|
||||
* with the method sigma.misc.animation.kill(id).
|
||||
*
|
||||
* The available options are:
|
||||
*
|
||||
* {?number} duration The duration of the animation.
|
||||
* {?function} onNewFrame A callback to execute when the animation
|
||||
* enter a new frame.
|
||||
* {?function} onComplete A callback to execute when the animation
|
||||
* is completed or killed.
|
||||
* {?(string|function)} easing The name of a function from the package
|
||||
* sigma.utils.easings, or a custom easing
|
||||
* function.
|
||||
*
|
||||
* @param {camera} camera The camera to animate.
|
||||
* @param {object} target The coordinates to reach.
|
||||
* @param {?object} options Eventually an object to specify some options to
|
||||
* the function. The available options are
|
||||
* presented in the description of the function.
|
||||
* @return {number} The animation id, to make it easy to kill
|
||||
* through the method "sigma.misc.animation.kill".
|
||||
*/
|
||||
sigma.misc.animation.camera = function(camera, val, options) {
|
||||
if (
|
||||
!(camera instanceof sigma.classes.camera) ||
|
||||
typeof val !== 'object' ||
|
||||
!val
|
||||
)
|
||||
throw 'animation.camera: Wrong arguments.';
|
||||
|
||||
if (
|
||||
typeof val.x !== 'number' &&
|
||||
typeof val.y !== 'number' &&
|
||||
typeof val.ratio !== 'number' &&
|
||||
typeof val.angle !== 'number'
|
||||
)
|
||||
throw 'There must be at least one valid coordinate in the given val.';
|
||||
|
||||
var fn,
|
||||
id,
|
||||
anim,
|
||||
easing,
|
||||
duration,
|
||||
initialVal,
|
||||
o = options || {},
|
||||
start = sigma.utils.dateNow();
|
||||
|
||||
// Store initial values:
|
||||
initialVal = {
|
||||
x: camera.x,
|
||||
y: camera.y,
|
||||
ratio: camera.ratio,
|
||||
angle: camera.angle
|
||||
};
|
||||
|
||||
duration = o.duration;
|
||||
easing = typeof o.easing !== 'function' ?
|
||||
sigma.utils.easings[o.easing || 'quadraticInOut'] :
|
||||
o.easing;
|
||||
|
||||
fn = function() {
|
||||
var coef,
|
||||
t = o.duration ? (sigma.utils.dateNow() - start) / o.duration : 1;
|
||||
|
||||
// If the animation is over:
|
||||
if (t >= 1) {
|
||||
camera.isAnimated = false;
|
||||
camera.goTo({
|
||||
x: val.x !== undefined ? val.x : initialVal.x,
|
||||
y: val.y !== undefined ? val.y : initialVal.y,
|
||||
ratio: val.ratio !== undefined ? val.ratio : initialVal.ratio,
|
||||
angle: val.angle !== undefined ? val.angle : initialVal.angle
|
||||
});
|
||||
|
||||
cancelAnimationFrame(id);
|
||||
delete sigma.misc.animation.running[id];
|
||||
|
||||
// Check callbacks:
|
||||
if (typeof o.onComplete === 'function')
|
||||
o.onComplete();
|
||||
|
||||
// Else, let's keep going:
|
||||
} else {
|
||||
coef = easing(t);
|
||||
camera.isAnimated = true;
|
||||
camera.goTo({
|
||||
x: val.x !== undefined ?
|
||||
initialVal.x + (val.x - initialVal.x) * coef :
|
||||
initialVal.x,
|
||||
y: val.y !== undefined ?
|
||||
initialVal.y + (val.y - initialVal.y) * coef :
|
||||
initialVal.y,
|
||||
ratio: val.ratio !== undefined ?
|
||||
initialVal.ratio + (val.ratio - initialVal.ratio) * coef :
|
||||
initialVal.ratio,
|
||||
angle: val.angle !== undefined ?
|
||||
initialVal.angle + (val.angle - initialVal.angle) * coef :
|
||||
initialVal.angle
|
||||
});
|
||||
|
||||
// Check callbacks:
|
||||
if (typeof o.onNewFrame === 'function')
|
||||
o.onNewFrame();
|
||||
|
||||
anim.frameId = requestAnimationFrame(fn);
|
||||
}
|
||||
};
|
||||
|
||||
id = _getID();
|
||||
anim = {
|
||||
frameId: requestAnimationFrame(fn),
|
||||
target: camera,
|
||||
type: 'camera',
|
||||
options: o,
|
||||
fn: fn
|
||||
};
|
||||
sigma.misc.animation.running[id] = anim;
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kills a running animation. It triggers the eventual onComplete callback.
|
||||
*
|
||||
* @param {number} id The id of the animation to kill.
|
||||
* @return {object} Returns the sigma.misc.animation package.
|
||||
*/
|
||||
sigma.misc.animation.kill = function(id) {
|
||||
if (arguments.length !== 1 || typeof id !== 'number')
|
||||
throw 'animation.kill: Wrong arguments.';
|
||||
|
||||
var o = sigma.misc.animation.running[id];
|
||||
|
||||
if (o) {
|
||||
cancelAnimationFrame(id);
|
||||
delete sigma.misc.animation.running[o.frameId];
|
||||
|
||||
if (o.type === 'camera')
|
||||
o.target.isAnimated = false;
|
||||
|
||||
// Check callbacks:
|
||||
if (typeof (o.options || {}).onComplete === 'function')
|
||||
o.options.onComplete();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kills every running animations, or only the one with the specified type,
|
||||
* if a string parameter is given.
|
||||
*
|
||||
* @param {?(string|object)} filter A string to filter the animations to kill
|
||||
* on their type (example: "camera"), or an
|
||||
* object to filter on their target.
|
||||
* @return {number} Returns the number of animations killed
|
||||
* that way.
|
||||
*/
|
||||
sigma.misc.animation.killAll = function(filter) {
|
||||
var o,
|
||||
id,
|
||||
count = 0,
|
||||
type = typeof filter === 'string' ? filter : null,
|
||||
target = typeof filter === 'object' ? filter : null,
|
||||
running = sigma.misc.animation.running;
|
||||
|
||||
for (id in running)
|
||||
if (
|
||||
(!type || running[id].type === type) &&
|
||||
(!target || running[id].target === target)
|
||||
) {
|
||||
o = sigma.misc.animation.running[id];
|
||||
cancelAnimationFrame(o.frameId);
|
||||
delete sigma.misc.animation.running[id];
|
||||
|
||||
if (o.type === 'camera')
|
||||
o.target.isAnimated = false;
|
||||
|
||||
// Increment counter:
|
||||
count++;
|
||||
|
||||
// Check callbacks:
|
||||
if (typeof (o.options || {}).onComplete === 'function')
|
||||
o.options.onComplete();
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns "true" if any animation that is currently still running matches
|
||||
* the filter given to the function.
|
||||
*
|
||||
* @param {string|object} filter A string to filter the animations to kill
|
||||
* on their type (example: "camera"), or an
|
||||
* object to filter on their target.
|
||||
* @return {boolean} Returns true if any running animation
|
||||
* matches.
|
||||
*/
|
||||
sigma.misc.animation.has = function(filter) {
|
||||
var id,
|
||||
type = typeof filter === 'string' ? filter : null,
|
||||
target = typeof filter === 'object' ? filter : null,
|
||||
running = sigma.misc.animation.running;
|
||||
|
||||
for (id in running)
|
||||
if (
|
||||
(!type || running[id].type === type) &&
|
||||
(!target || running[id].target === target)
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
}).call(this);
|
@ -1,156 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.misc');
|
||||
|
||||
/**
|
||||
* This helper will bind any DOM renderer (for instance svg)
|
||||
* to its captors, to properly dispatch the good events to the sigma instance
|
||||
* to manage clicking, hovering etc...
|
||||
*
|
||||
* It has to be called in the scope of the related renderer.
|
||||
*/
|
||||
sigma.misc.bindDOMEvents = function(container) {
|
||||
var self = this,
|
||||
graph = this.graph;
|
||||
|
||||
// DOMElement abstraction
|
||||
function Element(domElement) {
|
||||
|
||||
// Helpers
|
||||
this.attr = function(attrName) {
|
||||
return domElement.getAttributeNS(null, attrName);
|
||||
};
|
||||
|
||||
// Properties
|
||||
this.tag = domElement.tagName;
|
||||
this.class = this.attr('class');
|
||||
this.id = this.attr('id');
|
||||
|
||||
// Methods
|
||||
this.isNode = function() {
|
||||
return !!~this.class.indexOf(self.settings('classPrefix') + '-node');
|
||||
};
|
||||
|
||||
this.isEdge = function() {
|
||||
return !!~this.class.indexOf(self.settings('classPrefix') + '-edge');
|
||||
};
|
||||
|
||||
this.isHover = function() {
|
||||
return !!~this.class.indexOf(self.settings('classPrefix') + '-hover');
|
||||
};
|
||||
}
|
||||
|
||||
// Click
|
||||
function click(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
// Generic event
|
||||
self.dispatchEvent('click', e);
|
||||
|
||||
// Are we on a node?
|
||||
var element = new Element(e.target);
|
||||
|
||||
if (element.isNode())
|
||||
self.dispatchEvent('clickNode', {
|
||||
node: graph.nodes(element.attr('data-node-id'))
|
||||
});
|
||||
else
|
||||
self.dispatchEvent('clickStage');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Double click
|
||||
function doubleClick(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
// Generic event
|
||||
self.dispatchEvent('doubleClick', e);
|
||||
|
||||
// Are we on a node?
|
||||
var element = new Element(e.target);
|
||||
|
||||
if (element.isNode())
|
||||
self.dispatchEvent('doubleClickNode', {
|
||||
node: graph.nodes(element.attr('data-node-id'))
|
||||
});
|
||||
else
|
||||
self.dispatchEvent('doubleClickStage');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// On over
|
||||
function onOver(e) {
|
||||
var target = e.toElement || e.target;
|
||||
|
||||
if (!self.settings('eventsEnabled') || !target)
|
||||
return;
|
||||
|
||||
var el = new Element(target);
|
||||
|
||||
if (el.isNode()) {
|
||||
self.dispatchEvent('overNode', {
|
||||
node: graph.nodes(el.attr('data-node-id'))
|
||||
});
|
||||
}
|
||||
else if (el.isEdge()) {
|
||||
var edge = graph.edges(el.attr('data-edge-id'));
|
||||
self.dispatchEvent('overEdge', {
|
||||
edge: edge,
|
||||
source: graph.nodes(edge.source),
|
||||
target: graph.nodes(edge.target)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// On out
|
||||
function onOut(e) {
|
||||
var target = e.fromElement || e.originalTarget;
|
||||
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
var el = new Element(target);
|
||||
|
||||
if (el.isNode()) {
|
||||
self.dispatchEvent('outNode', {
|
||||
node: graph.nodes(el.attr('data-node-id'))
|
||||
});
|
||||
}
|
||||
else if (el.isEdge()) {
|
||||
var edge = graph.edges(el.attr('data-edge-id'));
|
||||
self.dispatchEvent('outEdge', {
|
||||
edge: edge,
|
||||
source: graph.nodes(edge.source),
|
||||
target: graph.nodes(edge.target)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Registering Events:
|
||||
|
||||
// Click
|
||||
container.addEventListener('click', click, false);
|
||||
sigma.utils.doubleClick(container, 'click', doubleClick);
|
||||
|
||||
// Touch counterparts
|
||||
container.addEventListener('touchstart', click, false);
|
||||
sigma.utils.doubleClick(container, 'touchstart', doubleClick);
|
||||
|
||||
// Mouseover
|
||||
container.addEventListener('mouseover', onOver, true);
|
||||
|
||||
// Mouseout
|
||||
container.addEventListener('mouseout', onOut, true);
|
||||
};
|
||||
}).call(this);
|
@ -1,509 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.misc');
|
||||
|
||||
/**
|
||||
* This helper will bind any no-DOM renderer (for instance canvas or WebGL)
|
||||
* to its captors, to properly dispatch the good events to the sigma instance
|
||||
* to manage clicking, hovering etc...
|
||||
*
|
||||
* It has to be called in the scope of the related renderer.
|
||||
*/
|
||||
sigma.misc.bindEvents = function(prefix) {
|
||||
var i,
|
||||
l,
|
||||
mX,
|
||||
mY,
|
||||
captor,
|
||||
self = this;
|
||||
|
||||
function getNodes(e) {
|
||||
if (e) {
|
||||
mX = 'x' in e.data ? e.data.x : mX;
|
||||
mY = 'y' in e.data ? e.data.y : mY;
|
||||
}
|
||||
|
||||
var i,
|
||||
j,
|
||||
l,
|
||||
n,
|
||||
x,
|
||||
y,
|
||||
s,
|
||||
inserted,
|
||||
selected = [],
|
||||
modifiedX = mX + self.width / 2,
|
||||
modifiedY = mY + self.height / 2,
|
||||
point = self.camera.cameraPosition(
|
||||
mX,
|
||||
mY
|
||||
),
|
||||
nodes = self.camera.quadtree.point(
|
||||
point.x,
|
||||
point.y
|
||||
);
|
||||
|
||||
if (nodes.length)
|
||||
for (i = 0, l = nodes.length; i < l; i++) {
|
||||
n = nodes[i];
|
||||
x = n[prefix + 'x'];
|
||||
y = n[prefix + 'y'];
|
||||
s = n[prefix + 'size'];
|
||||
|
||||
if (
|
||||
!n.hidden &&
|
||||
modifiedX > x - s &&
|
||||
modifiedX < x + s &&
|
||||
modifiedY > y - s &&
|
||||
modifiedY < y + s &&
|
||||
Math.sqrt(
|
||||
Math.pow(modifiedX - x, 2) +
|
||||
Math.pow(modifiedY - y, 2)
|
||||
) < s
|
||||
) {
|
||||
// Insert the node:
|
||||
inserted = false;
|
||||
|
||||
for (j = 0; j < selected.length; j++)
|
||||
if (n.size > selected[j].size) {
|
||||
selected.splice(j, 0, n);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!inserted)
|
||||
selected.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
function getEdges(e) {
|
||||
if (!self.settings('enableEdgeHovering')) {
|
||||
// No event if the setting is off:
|
||||
return [];
|
||||
}
|
||||
|
||||
var isCanvas = (
|
||||
sigma.renderers.canvas && self instanceof sigma.renderers.canvas);
|
||||
|
||||
if (!isCanvas) {
|
||||
// A quick hardcoded rule to prevent people from using this feature
|
||||
// with the WebGL renderer (which is not good enough at the moment):
|
||||
throw new Error(
|
||||
'The edge events feature is not compatible with the WebGL renderer'
|
||||
);
|
||||
}
|
||||
|
||||
if (e) {
|
||||
mX = 'x' in e.data ? e.data.x : mX;
|
||||
mY = 'y' in e.data ? e.data.y : mY;
|
||||
}
|
||||
|
||||
var i,
|
||||
j,
|
||||
l,
|
||||
a,
|
||||
edge,
|
||||
s,
|
||||
maxEpsilon = self.settings('edgeHoverPrecision'),
|
||||
source,
|
||||
target,
|
||||
cp,
|
||||
nodeIndex = {},
|
||||
inserted,
|
||||
selected = [],
|
||||
modifiedX = mX + self.width / 2,
|
||||
modifiedY = mY + self.height / 2,
|
||||
point = self.camera.cameraPosition(
|
||||
mX,
|
||||
mY
|
||||
),
|
||||
edges = [];
|
||||
|
||||
if (isCanvas) {
|
||||
var nodesOnScreen = self.camera.quadtree.area(
|
||||
self.camera.getRectangle(self.width, self.height)
|
||||
);
|
||||
for (a = nodesOnScreen, i = 0, l = a.length; i < l; i++)
|
||||
nodeIndex[a[i].id] = a[i];
|
||||
}
|
||||
|
||||
if (self.camera.edgequadtree !== undefined) {
|
||||
edges = self.camera.edgequadtree.point(
|
||||
point.x,
|
||||
point.y
|
||||
);
|
||||
}
|
||||
|
||||
function insertEdge(selected, edge) {
|
||||
inserted = false;
|
||||
|
||||
for (j = 0; j < selected.length; j++)
|
||||
if (edge.size > selected[j].size) {
|
||||
selected.splice(j, 0, edge);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!inserted)
|
||||
selected.push(edge);
|
||||
}
|
||||
|
||||
if (edges.length)
|
||||
for (i = 0, l = edges.length; i < l; i++) {
|
||||
edge = edges[i];
|
||||
source = self.graph.nodes(edge.source);
|
||||
target = self.graph.nodes(edge.target);
|
||||
// (HACK) we can't get edge[prefix + 'size'] on WebGL renderer:
|
||||
s = edge[prefix + 'size'] ||
|
||||
edge['read_' + prefix + 'size'];
|
||||
|
||||
// First, let's identify which edges are drawn. To do this, we keep
|
||||
// every edges that have at least one extremity displayed according to
|
||||
// the quadtree and the "hidden" attribute. We also do not keep hidden
|
||||
// edges.
|
||||
// Then, let's check if the mouse is on the edge (we suppose that it
|
||||
// is a line segment).
|
||||
|
||||
if (
|
||||
!edge.hidden &&
|
||||
!source.hidden && !target.hidden &&
|
||||
(!isCanvas ||
|
||||
(nodeIndex[edge.source] || nodeIndex[edge.target])) &&
|
||||
sigma.utils.getDistance(
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
modifiedX,
|
||||
modifiedY) > source[prefix + 'size'] &&
|
||||
sigma.utils.getDistance(
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y'],
|
||||
modifiedX,
|
||||
modifiedY) > target[prefix + 'size']
|
||||
) {
|
||||
if (edge.type == 'curve' || edge.type == 'curvedArrow') {
|
||||
if (source.id === target.id) {
|
||||
cp = sigma.utils.getSelfLoopControlPoints(
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
source[prefix + 'size']
|
||||
);
|
||||
if (
|
||||
sigma.utils.isPointOnBezierCurve(
|
||||
modifiedX,
|
||||
modifiedY,
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y'],
|
||||
cp.x1,
|
||||
cp.y1,
|
||||
cp.x2,
|
||||
cp.y2,
|
||||
Math.max(s, maxEpsilon)
|
||||
)) {
|
||||
insertEdge(selected, edge);
|
||||
}
|
||||
}
|
||||
else {
|
||||
cp = sigma.utils.getQuadraticControlPoint(
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y']);
|
||||
if (
|
||||
sigma.utils.isPointOnQuadraticCurve(
|
||||
modifiedX,
|
||||
modifiedY,
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y'],
|
||||
cp.x,
|
||||
cp.y,
|
||||
Math.max(s, maxEpsilon)
|
||||
)) {
|
||||
insertEdge(selected, edge);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
sigma.utils.isPointOnSegment(
|
||||
modifiedX,
|
||||
modifiedY,
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y'],
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y'],
|
||||
Math.max(s, maxEpsilon)
|
||||
)) {
|
||||
insertEdge(selected, edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
function bindCaptor(captor) {
|
||||
var nodes,
|
||||
edges,
|
||||
overNodes = {},
|
||||
overEdges = {};
|
||||
|
||||
function onClick(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
self.dispatchEvent('click', e.data);
|
||||
|
||||
nodes = getNodes(e);
|
||||
edges = getEdges(e);
|
||||
|
||||
if (nodes.length) {
|
||||
self.dispatchEvent('clickNode', {
|
||||
node: nodes[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('clickNodes', {
|
||||
node: nodes,
|
||||
captor: e.data
|
||||
});
|
||||
} else if (edges.length) {
|
||||
self.dispatchEvent('clickEdge', {
|
||||
edge: edges[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('clickEdges', {
|
||||
edge: edges,
|
||||
captor: e.data
|
||||
});
|
||||
} else
|
||||
self.dispatchEvent('clickStage', {captor: e.data});
|
||||
}
|
||||
|
||||
function onDoubleClick(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
self.dispatchEvent('doubleClick', e.data);
|
||||
|
||||
nodes = getNodes(e);
|
||||
edges = getEdges(e);
|
||||
|
||||
if (nodes.length) {
|
||||
self.dispatchEvent('doubleClickNode', {
|
||||
node: nodes[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('doubleClickNodes', {
|
||||
node: nodes,
|
||||
captor: e.data
|
||||
});
|
||||
} else if (edges.length) {
|
||||
self.dispatchEvent('doubleClickEdge', {
|
||||
edge: edges[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('doubleClickEdges', {
|
||||
edge: edges,
|
||||
captor: e.data
|
||||
});
|
||||
} else
|
||||
self.dispatchEvent('doubleClickStage', {captor: e.data});
|
||||
}
|
||||
|
||||
function onRightClick(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
self.dispatchEvent('rightClick', e.data);
|
||||
|
||||
nodes = getNodes(e);
|
||||
edges = getEdges(e);
|
||||
|
||||
if (nodes.length) {
|
||||
self.dispatchEvent('rightClickNode', {
|
||||
node: nodes[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('rightClickNodes', {
|
||||
node: nodes,
|
||||
captor: e.data
|
||||
});
|
||||
} else if (edges.length) {
|
||||
self.dispatchEvent('rightClickEdge', {
|
||||
edge: edges[0],
|
||||
captor: e.data
|
||||
});
|
||||
self.dispatchEvent('rightClickEdges', {
|
||||
edge: edges,
|
||||
captor: e.data
|
||||
});
|
||||
} else
|
||||
self.dispatchEvent('rightClickStage', {captor: e.data});
|
||||
}
|
||||
|
||||
function onOut(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
var k,
|
||||
i,
|
||||
l,
|
||||
le,
|
||||
outNodes = [],
|
||||
outEdges = [];
|
||||
|
||||
for (k in overNodes)
|
||||
outNodes.push(overNodes[k]);
|
||||
|
||||
overNodes = {};
|
||||
// Dispatch both single and multi events:
|
||||
for (i = 0, l = outNodes.length; i < l; i++)
|
||||
self.dispatchEvent('outNode', {
|
||||
node: outNodes[i],
|
||||
captor: e.data
|
||||
});
|
||||
if (outNodes.length)
|
||||
self.dispatchEvent('outNodes', {
|
||||
nodes: outNodes,
|
||||
captor: e.data
|
||||
});
|
||||
|
||||
overEdges = {};
|
||||
// Dispatch both single and multi events:
|
||||
for (i = 0, le = outEdges.length; i < le; i++)
|
||||
self.dispatchEvent('outEdge', {
|
||||
edge: outEdges[i],
|
||||
captor: e.data
|
||||
});
|
||||
if (outEdges.length)
|
||||
self.dispatchEvent('outEdges', {
|
||||
edges: outEdges,
|
||||
captor: e.data
|
||||
});
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (!self.settings('eventsEnabled'))
|
||||
return;
|
||||
|
||||
nodes = getNodes(e);
|
||||
edges = getEdges(e);
|
||||
|
||||
var i,
|
||||
k,
|
||||
node,
|
||||
edge,
|
||||
newOutNodes = [],
|
||||
newOverNodes = [],
|
||||
currentOverNodes = {},
|
||||
l = nodes.length,
|
||||
newOutEdges = [],
|
||||
newOverEdges = [],
|
||||
currentOverEdges = {},
|
||||
le = edges.length;
|
||||
|
||||
// Check newly overred nodes:
|
||||
for (i = 0; i < l; i++) {
|
||||
node = nodes[i];
|
||||
currentOverNodes[node.id] = node;
|
||||
if (!overNodes[node.id]) {
|
||||
newOverNodes.push(node);
|
||||
overNodes[node.id] = node;
|
||||
}
|
||||
}
|
||||
|
||||
// Check no more overred nodes:
|
||||
for (k in overNodes)
|
||||
if (!currentOverNodes[k]) {
|
||||
newOutNodes.push(overNodes[k]);
|
||||
delete overNodes[k];
|
||||
}
|
||||
|
||||
// Dispatch both single and multi events:
|
||||
for (i = 0, l = newOverNodes.length; i < l; i++)
|
||||
self.dispatchEvent('overNode', {
|
||||
node: newOverNodes[i],
|
||||
captor: e.data
|
||||
});
|
||||
for (i = 0, l = newOutNodes.length; i < l; i++)
|
||||
self.dispatchEvent('outNode', {
|
||||
node: newOutNodes[i],
|
||||
captor: e.data
|
||||
});
|
||||
if (newOverNodes.length)
|
||||
self.dispatchEvent('overNodes', {
|
||||
nodes: newOverNodes,
|
||||
captor: e.data
|
||||
});
|
||||
if (newOutNodes.length)
|
||||
self.dispatchEvent('outNodes', {
|
||||
nodes: newOutNodes,
|
||||
captor: e.data
|
||||
});
|
||||
|
||||
// Check newly overred edges:
|
||||
for (i = 0; i < le; i++) {
|
||||
edge = edges[i];
|
||||
currentOverEdges[edge.id] = edge;
|
||||
if (!overEdges[edge.id]) {
|
||||
newOverEdges.push(edge);
|
||||
overEdges[edge.id] = edge;
|
||||
}
|
||||
}
|
||||
|
||||
// Check no more overred edges:
|
||||
for (k in overEdges)
|
||||
if (!currentOverEdges[k]) {
|
||||
newOutEdges.push(overEdges[k]);
|
||||
delete overEdges[k];
|
||||
}
|
||||
|
||||
// Dispatch both single and multi events:
|
||||
for (i = 0, le = newOverEdges.length; i < le; i++)
|
||||
self.dispatchEvent('overEdge', {
|
||||
edge: newOverEdges[i],
|
||||
captor: e.data
|
||||
});
|
||||
for (i = 0, le = newOutEdges.length; i < le; i++)
|
||||
self.dispatchEvent('outEdge', {
|
||||
edge: newOutEdges[i],
|
||||
captor: e.data
|
||||
});
|
||||
if (newOverEdges.length)
|
||||
self.dispatchEvent('overEdges', {
|
||||
edges: newOverEdges,
|
||||
captor: e.data
|
||||
});
|
||||
if (newOutEdges.length)
|
||||
self.dispatchEvent('outEdges', {
|
||||
edges: newOutEdges,
|
||||
captor: e.data
|
||||
});
|
||||
}
|
||||
|
||||
// Bind events:
|
||||
captor.bind('click', onClick);
|
||||
captor.bind('mousedown', onMove);
|
||||
captor.bind('mouseup', onMove);
|
||||
captor.bind('mousemove', onMove);
|
||||
captor.bind('mouseout', onOut);
|
||||
captor.bind('doubleclick', onDoubleClick);
|
||||
captor.bind('rightclick', onRightClick);
|
||||
self.bind('render', onMove);
|
||||
}
|
||||
|
||||
for (i = 0, l = this.captors.length; i < l; i++)
|
||||
bindCaptor(this.captors[i]);
|
||||
};
|
||||
}).call(this);
|
@ -1,220 +0,0 @@
|
||||
;(function(undefined) {
|
||||
'use strict';
|
||||
|
||||
if (typeof sigma === 'undefined')
|
||||
throw 'sigma is not declared';
|
||||
|
||||
// Initialize packages:
|
||||
sigma.utils.pkg('sigma.misc');
|
||||
|
||||
/**
|
||||
* This method listens to "overNode", "outNode", "overEdge" and "outEdge"
|
||||
* events from a renderer and renders the nodes differently on the top layer.
|
||||
* The goal is to make any node label readable with the mouse, and to
|
||||
* highlight hovered nodes and edges.
|
||||
*
|
||||
* It has to be called in the scope of the related renderer.
|
||||
*/
|
||||
sigma.misc.drawHovers = function(prefix) {
|
||||
var self = this,
|
||||
hoveredNodes = {},
|
||||
hoveredEdges = {};
|
||||
|
||||
this.bind('overNode', function(event) {
|
||||
var node = event.data.node;
|
||||
if (!node.hidden) {
|
||||
hoveredNodes[node.id] = node;
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
this.bind('outNode', function(event) {
|
||||
delete hoveredNodes[event.data.node.id];
|
||||
draw();
|
||||
});
|
||||
|
||||
this.bind('overEdge', function(event) {
|
||||
var edge = event.data.edge;
|
||||
if (!edge.hidden) {
|
||||
hoveredEdges[edge.id] = edge;
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
this.bind('outEdge', function(event) {
|
||||
delete hoveredEdges[event.data.edge.id];
|
||||
draw();
|
||||
});
|
||||
|
||||
this.bind('render', function(event) {
|
||||
draw();
|
||||
});
|
||||
|
||||
function draw() {
|
||||
// Clear self.contexts.hover:
|
||||
self.contexts.hover.canvas.width = self.contexts.hover.canvas.width;
|
||||
|
||||
var k,
|
||||
source,
|
||||
target,
|
||||
hoveredNode,
|
||||
hoveredEdge,
|
||||
defaultNodeType = self.settings('defaultNodeType'),
|
||||
defaultEdgeType = self.settings('defaultEdgeType'),
|
||||
nodeRenderers = sigma.canvas.hovers,
|
||||
edgeRenderers = sigma.canvas.edgehovers,
|
||||
extremitiesRenderers = sigma.canvas.extremities,
|
||||
embedSettings = self.settings.embedObjects({
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
// Node render: single hover
|
||||
if (
|
||||
embedSettings('enableHovering') &&
|
||||
embedSettings('singleHover') &&
|
||||
Object.keys(hoveredNodes).length
|
||||
) {
|
||||
hoveredNode = hoveredNodes[Object.keys(hoveredNodes)[0]];
|
||||
(
|
||||
nodeRenderers[hoveredNode.type] ||
|
||||
nodeRenderers[defaultNodeType] ||
|
||||
nodeRenderers.def
|
||||
)(
|
||||
hoveredNode,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
}
|
||||
|
||||
// Node render: multiple hover
|
||||
if (
|
||||
embedSettings('enableHovering') &&
|
||||
!embedSettings('singleHover')
|
||||
)
|
||||
for (k in hoveredNodes)
|
||||
(
|
||||
nodeRenderers[hoveredNodes[k].type] ||
|
||||
nodeRenderers[defaultNodeType] ||
|
||||
nodeRenderers.def
|
||||
)(
|
||||
hoveredNodes[k],
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
|
||||
// Edge render: single hover
|
||||
if (
|
||||
embedSettings('enableEdgeHovering') &&
|
||||
embedSettings('singleHover') &&
|
||||
Object.keys(hoveredEdges).length
|
||||
) {
|
||||
hoveredEdge = hoveredEdges[Object.keys(hoveredEdges)[0]];
|
||||
source = self.graph.nodes(hoveredEdge.source);
|
||||
target = self.graph.nodes(hoveredEdge.target);
|
||||
|
||||
if (! hoveredEdge.hidden) {
|
||||
(
|
||||
edgeRenderers[hoveredEdge.type] ||
|
||||
edgeRenderers[defaultEdgeType] ||
|
||||
edgeRenderers.def
|
||||
) (
|
||||
hoveredEdge,
|
||||
source,
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
|
||||
if (embedSettings('edgeHoverExtremities')) {
|
||||
(
|
||||
extremitiesRenderers[hoveredEdge.type] ||
|
||||
extremitiesRenderers.def
|
||||
)(
|
||||
hoveredEdge,
|
||||
source,
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
|
||||
} else {
|
||||
// Avoid edges rendered over nodes:
|
||||
(
|
||||
sigma.canvas.nodes[source.type] ||
|
||||
sigma.canvas.nodes.def
|
||||
) (
|
||||
source,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
(
|
||||
sigma.canvas.nodes[target.type] ||
|
||||
sigma.canvas.nodes.def
|
||||
) (
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edge render: multiple hover
|
||||
if (
|
||||
embedSettings('enableEdgeHovering') &&
|
||||
!embedSettings('singleHover')
|
||||
) {
|
||||
for (k in hoveredEdges) {
|
||||
hoveredEdge = hoveredEdges[k];
|
||||
source = self.graph.nodes(hoveredEdge.source);
|
||||
target = self.graph.nodes(hoveredEdge.target);
|
||||
|
||||
if (!hoveredEdge.hidden) {
|
||||
(
|
||||
edgeRenderers[hoveredEdge.type] ||
|
||||
edgeRenderers[defaultEdgeType] ||
|
||||
edgeRenderers.def
|
||||
) (
|
||||
hoveredEdge,
|
||||
source,
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
|
||||
if (embedSettings('edgeHoverExtremities')) {
|
||||
(
|
||||
extremitiesRenderers[hoveredEdge.type] ||
|
||||
extremitiesRenderers.def
|
||||
)(
|
||||
hoveredEdge,
|
||||
source,
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
} else {
|
||||
// Avoid edges rendered over nodes:
|
||||
(
|
||||
sigma.canvas.nodes[source.type] ||
|
||||
sigma.canvas.nodes.def
|
||||
) (
|
||||
source,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
(
|
||||
sigma.canvas.nodes[target.type] ||
|
||||
sigma.canvas.nodes.def
|
||||
) (
|
||||
target,
|
||||
self.contexts.hover,
|
||||
embedSettings
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}).call(this);
|
@ -1,76 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edgehovers');
|
||||
|
||||
/**
|
||||
* This hover renderer will display the edge with a different color or size.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edgehovers.arrow =
|
||||
function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
size = edge[prefix + 'size'] || 1,
|
||||
tSize = target[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'];
|
||||
|
||||
size = (edge.hover) ?
|
||||
settings('edgeHoverSizeRatio') * size : size;
|
||||
var aSize = size * 2.5,
|
||||
d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
|
||||
aX = sX + (tX - sX) * (d - aSize - tSize) / d,
|
||||
aY = sY + (tY - sY) * (d - aSize - tSize) / d,
|
||||
vX = (tX - sX) * aSize / d,
|
||||
vY = (tY - sY) * aSize / d;
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings('edgeHoverColor') === 'edge') {
|
||||
color = edge.hover_color || color;
|
||||
} else {
|
||||
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
context.lineTo(
|
||||
aX,
|
||||
aY
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(aX + vX, aY + vY);
|
||||
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
|
||||
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
|
||||
context.lineTo(aX + vX, aY + vY);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
};
|
||||
})();
|
@ -1,64 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edgehovers');
|
||||
|
||||
/**
|
||||
* This hover renderer will display the edge with a different color or size.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edgehovers.curve =
|
||||
function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
cp = {},
|
||||
sSize = source[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'];
|
||||
|
||||
cp = (source.id === target.id) ?
|
||||
sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
|
||||
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings('edgeHoverColor') === 'edge') {
|
||||
color = edge.hover_color || color;
|
||||
} else {
|
||||
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
if (source.id === target.id) {
|
||||
context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
|
||||
} else {
|
||||
context.quadraticCurveTo(cp.x, cp.y, tX, tY);
|
||||
}
|
||||
context.stroke();
|
||||
};
|
||||
})();
|
@ -1,96 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edgehovers');
|
||||
|
||||
/**
|
||||
* This hover renderer will display the edge with a different color or size.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edgehovers.curvedArrow =
|
||||
function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
cp = {},
|
||||
size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1),
|
||||
tSize = target[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'],
|
||||
d,
|
||||
aSize,
|
||||
aX,
|
||||
aY,
|
||||
vX,
|
||||
vY;
|
||||
|
||||
cp = (source.id === target.id) ?
|
||||
sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
|
||||
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
|
||||
|
||||
if (source.id === target.id) {
|
||||
d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
|
||||
aSize = size * 2.5;
|
||||
aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
|
||||
aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
|
||||
vX = (tX - cp.x1) * aSize / d;
|
||||
vY = (tY - cp.y1) * aSize / d;
|
||||
}
|
||||
else {
|
||||
d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
|
||||
aSize = size * 2.5;
|
||||
aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
|
||||
aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
|
||||
vX = (tX - cp.x) * aSize / d;
|
||||
vY = (tY - cp.y) * aSize / d;
|
||||
}
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings('edgeHoverColor') === 'edge') {
|
||||
color = edge.hover_color || color;
|
||||
} else {
|
||||
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
if (source.id === target.id) {
|
||||
context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
|
||||
} else {
|
||||
context.quadraticCurveTo(cp.x, cp.y, aX, aY);
|
||||
}
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(aX + vX, aY + vY);
|
||||
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
|
||||
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
|
||||
context.lineTo(aX + vX, aY + vY);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
};
|
||||
})();
|
@ -1,57 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edgehovers');
|
||||
|
||||
/**
|
||||
* This hover renderer will display the edge with a different color or size.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edgehovers.def =
|
||||
function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
size = edge[prefix + 'size'] || 1,
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor');
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings('edgeHoverColor') === 'edge') {
|
||||
color = edge.hover_color || color;
|
||||
} else {
|
||||
color = edge.hover_color || settings('defaultEdgeHoverColor') || color;
|
||||
}
|
||||
size *= settings('edgeHoverSizeRatio');
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(
|
||||
source[prefix + 'x'],
|
||||
source[prefix + 'y']
|
||||
);
|
||||
context.lineTo(
|
||||
target[prefix + 'x'],
|
||||
target[prefix + 'y']
|
||||
);
|
||||
context.stroke();
|
||||
};
|
||||
})();
|
@ -1,66 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edges');
|
||||
|
||||
/**
|
||||
* This edge renderer will display edges as arrows going from the source node
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edges.arrow = function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
size = edge[prefix + 'size'] || 1,
|
||||
tSize = target[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'],
|
||||
aSize = Math.max(size * 2.5, settings('minArrowSize')),
|
||||
d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)),
|
||||
aX = sX + (tX - sX) * (d - aSize - tSize) / d,
|
||||
aY = sY + (tY - sY) * (d - aSize - tSize) / d,
|
||||
vX = (tX - sX) * aSize / d,
|
||||
vY = (tY - sY) * aSize / d;
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
context.lineTo(
|
||||
aX,
|
||||
aY
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(aX + vX, aY + vY);
|
||||
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
|
||||
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
|
||||
context.lineTo(aX + vX, aY + vY);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
};
|
||||
})();
|
@ -1,57 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edges');
|
||||
|
||||
/**
|
||||
* This edge renderer will display edges as curves.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edges.curve = function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
size = edge[prefix + 'size'] || 1,
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
cp = {},
|
||||
sSize = source[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'];
|
||||
|
||||
cp = (source.id === target.id) ?
|
||||
sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) :
|
||||
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
if (source.id === target.id) {
|
||||
context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY);
|
||||
} else {
|
||||
context.quadraticCurveTo(cp.x, cp.y, tX, tY);
|
||||
}
|
||||
context.stroke();
|
||||
};
|
||||
})();
|
@ -1,88 +0,0 @@
|
||||
;(function() {
|
||||
'use strict';
|
||||
|
||||
sigma.utils.pkg('sigma.canvas.edges');
|
||||
|
||||
/**
|
||||
* This edge renderer will display edges as curves with arrow heading.
|
||||
*
|
||||
* @param {object} edge The edge object.
|
||||
* @param {object} source node The edge source node.
|
||||
* @param {object} target node The edge target node.
|
||||
* @param {CanvasRenderingContext2D} context The canvas context.
|
||||
* @param {configurable} settings The settings function.
|
||||
*/
|
||||
sigma.canvas.edges.curvedArrow =
|
||||
function(edge, source, target, context, settings) {
|
||||
var color = edge.color,
|
||||
prefix = settings('prefix') || '',
|
||||
edgeColor = settings('edgeColor'),
|
||||
defaultNodeColor = settings('defaultNodeColor'),
|
||||
defaultEdgeColor = settings('defaultEdgeColor'),
|
||||
cp = {},
|
||||
size = edge[prefix + 'size'] || 1,
|
||||
tSize = target[prefix + 'size'],
|
||||
sX = source[prefix + 'x'],
|
||||
sY = source[prefix + 'y'],
|
||||
tX = target[prefix + 'x'],
|
||||
tY = target[prefix + 'y'],
|
||||
aSize = Math.max(size * 2.5, settings('minArrowSize')),
|
||||
d,
|
||||
aX,
|
||||
aY,
|
||||
vX,
|
||||
vY;
|
||||
|
||||
cp = (source.id === target.id) ?
|
||||
sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) :
|
||||
sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY);
|
||||
|
||||
if (source.id === target.id) {
|
||||
d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2));
|
||||
aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d;
|
||||
aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d;
|
||||
vX = (tX - cp.x1) * aSize / d;
|
||||
vY = (tY - cp.y1) * aSize / d;
|
||||
}
|
||||
else {
|
||||
d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2));
|
||||
aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d;
|
||||
aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d;
|
||||
vX = (tX - cp.x) * aSize / d;
|
||||
vY = (tY - cp.y) * aSize / d;
|
||||
}
|
||||
|
||||
if (!color)
|
||||
switch (edgeColor) {
|
||||
case 'source':
|
||||
color = source.color || defaultNodeColor;
|
||||
break;
|
||||
case 'target':
|
||||
color = target.color || defaultNodeColor;
|
||||
break;
|
||||
default:
|
||||
color = defaultEdgeColor;
|
||||
break;
|
||||
}
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = size;
|
||||
context.beginPath();
|
||||
context.moveTo(sX, sY);
|
||||
if (source.id === target.id) {
|
||||
context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY);
|
||||
} else {
|
||||
context.quadraticCurveTo(cp.x, cp.y, aX, aY);
|
||||
}
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(aX + vX, aY + vY);
|
||||
context.lineTo(aX + vY * 0.6, aY - vX * 0.6);
|
||||
context.lineTo(aX - vY * 0.6, aY + vX * 0.6);
|
||||
context.lineTo(aX + vX, aY + vY);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
};
|
||||
})();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user