├── .gitignore ├── .gitmodules ├── Procfile ├── bin └── pre_compile ├── config.py ├── license ├── main.py ├── org ├── __init__.py └── collabdraw │ ├── __init__.py │ ├── dbclient │ ├── __init__.py │ ├── dbclientfactory.py │ ├── dbclienttypes.py │ ├── dbinterface.py │ └── redisdbclient.py │ ├── handler │ ├── __init__.py │ ├── loginhandler.py │ ├── logouthandler.py │ ├── registerhandler.py │ ├── uploadhandler.py │ └── websockethandler.py │ ├── pubsub │ ├── __init__.py │ ├── pubsubclientfactory.py │ ├── pubsubclienttypes.py │ ├── pubsubinterface.py │ └── redispubsubclient.py │ └── tools │ ├── __init__.py │ ├── tools.py │ ├── uploadprocessor.py │ └── videomaker.py ├── readme.md ├── requirements.txt ├── resource ├── css │ └── upload.css ├── html │ ├── index.html │ ├── login.html │ ├── register.html │ └── upload.html └── js │ ├── App.js │ ├── Connection.js │ ├── Login.js │ ├── MessageEvent.js │ ├── Register.js │ ├── Svg.js │ ├── Upload.js │ ├── jsxcompressor.js │ ├── package.js │ ├── raphael-min.js │ └── upload.js ├── run ├── run_tests.sh ├── runtime.txt ├── set_heroku_path.sh └── test ├── __init__.py ├── files └── sample.pdf ├── uploadprocessor_integ_test.py └── videomaker_integ_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | touch 3 | resource/touch 4 | venv 5 | *.pyc 6 | *~ 7 | .idea 8 | *.png 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "resource/enyo"] 2 | path = resource/enyo 3 | url = https://github.com/enyojs/enyo.git 4 | [submodule "resource/lib/layout"] 5 | path = resource/lib/layout 6 | url = https://github.com/enyojs/layout.git 7 | [submodule "resource/lib/onyx"] 8 | path = resource/lib/onyx 9 | url = https://github.com/enyojs/onyx.git 10 | [submodule "collabdraw-heroku-bin"] 11 | path = collabdraw-heroku-bin 12 | url = https://github.com/anandtrex/collabdraw-heroku-bin.git 13 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python main.py 2 | -------------------------------------------------------------------------------- /bin/pre_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | INSTALL_DIR=/app 4 | ROOT_DIR=/app 5 | 6 | function install_binary { 7 | BINARY=$1 8 | VENDOR=$2 9 | INSTALL_PATH=$INSTALL_DIR/$VENDOR 10 | BINARY_PATH=$ROOT_DIR/$BINARY 11 | echo "Installing $BINARY to $INSTALL_PATH" 12 | if [ ! -d "$INSTALL_PATH" ]; then 13 | mkdir -p $INSTALL_PATH 14 | echo "Created $INSTALL_PATH" 15 | echo "Downloading from $BINARY" 16 | tar -xz -C $INSTALL_PATH -f $BINARY_PATH 17 | fi 18 | } 19 | 20 | 21 | FFMPEG_BINARY="collabdraw-heroku-bin/ffmpeg.tgz" 22 | FFMPEG_VENDOR="vendor/ffmpeg" 23 | 24 | install_binary $FFMPEG_BINARY $FFMPEG_VENDOR 25 | 26 | IMAGEMAGICK_BINARY="collabdraw-heroku-bin/imagemagick.tgz" 27 | IMAGEMAGICK_VENDOR="vendor/imagemagick" 28 | 29 | install_binary $IMAGEMAGICK_BINARY $IMAGEMAGICK_VENDOR 30 | 31 | FONTCONFIG_BINARY="collabdraw-heroku-bin/fontconfig.tgz" 32 | FONTCONFIG_VENDOR="vendor/fontconfig-2.10" 33 | 34 | install_binary $FONTCONFIG_BINARY $FONTCONFIG_VENDOR 35 | 36 | POPPLER_BINARY="collabdraw-heroku-bin/poppler.tgz" 37 | POPPLER_VENDOR="vendor/poppler" 38 | 39 | install_binary $POPPLER_BINARY $POPPLER_VENDOR 40 | 41 | CAIRO_BINARY="collabdraw-heroku-bin/cairo.tgz" 42 | CAIRO_VENDOR="vendor/cairo" 43 | 44 | install_binary $CAIRO_BINARY $CAIRO_VENDOR 45 | 46 | PIXMAN_BINARY="collabdraw-heroku-bin/pixman.tgz" 47 | PIXMAN_VENDOR="vendor/pixman" 48 | 49 | install_binary $PIXMAN_BINARY $PIXMAN_VENDOR 50 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # App's host and port 4 | APP_IP_ADDRESS = "192.168.1.134" # Put your websocket endpoint here 5 | APP_PORT = os.environ.get('PORT', 5000) 6 | 7 | # Port in which websocket client should listen 8 | # Usually same as APP_PORT unless some other 9 | # port forwarding is set up (for ex. if you're using heroku) 10 | PUBLIC_LISTEN_PORT = APP_PORT 11 | 12 | PUBSUB_CLIENT_TYPE = 'redis' # only redis supported now 13 | DB_CLIENT_TYPE = 'redis' # only redis supported now 14 | 15 | REDIS_URL = os.environ.get('REDISCLOUD_URL', 'redis://localhost:6379') 16 | 17 | # Full path of "collabdraw" directory 18 | ROOT_DIR = "/".join(os.path.realpath(__file__).split('/')[:-1]) 19 | RESOURCE_DIR = os.path.join(ROOT_DIR, 'resource') 20 | HTML_ROOT = os.path.join(RESOURCE_DIR, 'html') 21 | 22 | # Hash salt for storing password in database 23 | HASH_SALT = "bollacboard" 24 | 25 | # Enable SSL/TLS 26 | ENABLE_SSL = False 27 | SERVER_CERT = os.path.join(os.getcwd(), "server.crt") 28 | SERVER_KEY = os.path.join(os.getcwd(), "server.key") 29 | 30 | # Demo mode disables login requirement 31 | DEMO_MODE = True 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | CollabDraw: Whiteboard application in HTML5 2 | Copyright (C) 2013 Anand S 3 | 4 | Licensed under the GNU GPLv2 available at http://www.gnu.org/licenses/gpl-2.0.html#SEC3 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | import config 4 | from os.path import join 5 | 6 | import tornado.httpserver 7 | import tornado.ioloop 8 | import tornado.web 9 | import tornado.template as template 10 | 11 | from org.collabdraw.handler.websockethandler import RealtimeHandler 12 | from org.collabdraw.handler.uploadhandler import UploadHandler 13 | from org.collabdraw.handler.loginhandler import LoginHandler 14 | from org.collabdraw.handler.logouthandler import LogoutHandler 15 | from org.collabdraw.handler.registerhandler import RegisterHandler 16 | 17 | logger = logging.getLogger('websocket') 18 | logger.setLevel(logging.INFO) 19 | 20 | ch = logging.StreamHandler() 21 | ch.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 22 | ch.setLevel(logging.INFO) 23 | logger.addHandler(ch) 24 | 25 | 26 | class IndexHandler(tornado.web.RequestHandler): 27 | def get_current_user(self): 28 | if not config.DEMO_MODE: 29 | return self.get_secure_cookie("loginId") 30 | else: 31 | return True 32 | 33 | @tornado.web.authenticated 34 | def get(self): 35 | loader = template.Loader(config.ROOT_DIR) 36 | return_str = loader.load(join(config.HTML_ROOT, "index.html")).generate(app_ip_address=config.APP_IP_ADDRESS, 37 | app_port=config.PUBLIC_LISTEN_PORT) 38 | self.finish(return_str) 39 | 40 | 41 | class Application(tornado.web.Application): 42 | def __init__(self): 43 | handlers = [ 44 | (r'/realtime/', RealtimeHandler), 45 | (r'/resource/(.*)', tornado.web.StaticFileHandler, 46 | dict(path=config.RESOURCE_DIR)), 47 | (r'/upload', UploadHandler), 48 | (r'/login.html', LoginHandler), 49 | (r'/logout.html', LogoutHandler), 50 | (r'/register.html', RegisterHandler), 51 | (r'/index.html', IndexHandler), 52 | (r'/', IndexHandler), 53 | (r'/(.*)', tornado.web.StaticFileHandler, 54 | dict(path=config.ROOT_DIR)), 55 | ] 56 | 57 | settings = dict( 58 | auto_reload=True, 59 | gzip=True, 60 | login_url="login.html", 61 | cookie_secret=str(uuid.uuid4()), 62 | ) 63 | 64 | tornado.web.Application.__init__(self, handlers, **settings) 65 | 66 | 67 | if __name__ == "__main__": 68 | if not config.ENABLE_SSL: 69 | http_server = tornado.httpserver.HTTPServer(Application()) 70 | else: 71 | http_server = tornado.httpserver.HTTPServer(Application(), ssl_options={ 72 | "certfile": config.SERVER_CERT, 73 | "keyfile": config.SERVER_KEY, 74 | }) 75 | logger.info("Listening on port %s" % config.APP_PORT) 76 | http_server.listen(config.APP_PORT) 77 | tornado.ioloop.IOLoop.instance().start() 78 | -------------------------------------------------------------------------------- /org/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anandtrex/collabdraw/4de4cc14e344b2bf18d7c89846bdcecb92ee5125/org/__init__.py -------------------------------------------------------------------------------- /org/collabdraw/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /org/collabdraw/dbclient/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /org/collabdraw/dbclient/dbclientfactory.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import logging 4 | 5 | from .redisdbclient import RedisDbClient 6 | from .dbclienttypes import DbClientTypes 7 | from .dbinterface import DbInterface 8 | 9 | 10 | class DbClientFactory: 11 | @staticmethod 12 | def getDbClient(db_client_type_str): 13 | """ 14 | @param db_client_type_str: 15 | @rtype : DbInterface 16 | """ 17 | logger = logging.getLogger('websocket') 18 | logger.info("Initializing with db client type %s" % db_client_type_str) 19 | if db_client_type_str == DbClientTypes.redis: 20 | return RedisDbClient() 21 | elif db_client_type_str == DbClientTypes.in_memory: 22 | pass 23 | else: 24 | raise RuntimeError("Unknown db client type %s" % db_client_type_str) -------------------------------------------------------------------------------- /org/collabdraw/dbclient/dbclienttypes.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | 4 | class DbClientTypes: 5 | redis = "redis" 6 | in_memory = "in-memory" -------------------------------------------------------------------------------- /org/collabdraw/dbclient/dbinterface.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | 4 | class DbInterface(): 5 | def set(self, key, value): 6 | raise RuntimeError("DB Interface method %s not implemented" % "set") 7 | 8 | def get(self, key): 9 | raise RuntimeError("DB Interface method %s not implemented" % "get") 10 | 11 | def delete(self, key): 12 | raise RuntimeError("DB Interface method %s not implemented" % "delete") -------------------------------------------------------------------------------- /org/collabdraw/dbclient/redisdbclient.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import logging 4 | 5 | import redis 6 | 7 | import config 8 | from .dbinterface import DbInterface 9 | 10 | 11 | class RedisDbClient(DbInterface): 12 | redis_client = redis.from_url(config.REDIS_URL) 13 | 14 | def __init__(self): 15 | self.logger = logging.getLogger('websocket') 16 | 17 | def set(self, key, value): 18 | self.redis_client.set(key, value) 19 | 20 | def get(self, key): 21 | value = self.redis_client.get(key) 22 | if value: 23 | return value.decode('utf-8').replace("'", '"') 24 | 25 | def delete(self, key): 26 | self.redis_client.delete(key) 27 | -------------------------------------------------------------------------------- /org/collabdraw/handler/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /org/collabdraw/handler/loginhandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import config 4 | 5 | import tornado.web 6 | 7 | from ..dbclient.dbclientfactory import DbClientFactory 8 | from ..tools.tools import hash_password 9 | 10 | 11 | class LoginHandler(tornado.web.RequestHandler): 12 | def initialize(self): 13 | self.db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 14 | self.logger = logging.getLogger('websocket') 15 | 16 | def get(self): 17 | self.render(os.path.join(config.HTML_ROOT, "login.html")) 18 | 19 | def post(self): 20 | login_id = self.get_argument("loginId") 21 | login_password = self.get_argument("loginPassword") 22 | redis_key = "users:%s" % login_id 23 | db_password = self.db_client.get(redis_key) 24 | if db_password: 25 | db_password = db_password.decode('utf-8') 26 | if db_password != hash_password(login_password): 27 | self.logger.debug("db_password was %s but login_password was %s" % (db_password, 28 | login_password)) 29 | self.finish('{"result": "failure"}') 30 | return 31 | self.logger.info("Logging in user %s", login_id) 32 | self.set_secure_cookie("loginId", login_id) 33 | self.finish('{"result": "success"}') 34 | -------------------------------------------------------------------------------- /org/collabdraw/handler/logouthandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import tornado.web 4 | 5 | 6 | class LogoutHandler(tornado.web.RequestHandler): 7 | def initialize(self): 8 | self.logger = logging.getLogger('websocket') 9 | 10 | def get(self): 11 | self.redirect("./login.html") 12 | 13 | def post(self): 14 | self.logout() 15 | 16 | def logout(self): 17 | loginId = self.get_secure_cookie("loginId") 18 | self.logger.info("Logging out %s" % loginId) 19 | self.set_secure_cookie("loginId", "") 20 | -------------------------------------------------------------------------------- /org/collabdraw/handler/registerhandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import config 4 | 5 | import tornado.web 6 | 7 | from ..dbclient.dbclientfactory import DbClientFactory 8 | from ..tools.tools import hash_password 9 | 10 | 11 | class RegisterHandler(tornado.web.RequestHandler): 12 | def initialize(self): 13 | self.logger = logging.getLogger('websocket') 14 | self.db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 15 | 16 | def get(self): 17 | self.render(os.path.join(config.HTML_ROOT, "register.html")) 18 | 19 | def post(self): 20 | login_id = self.get_argument("loginId") 21 | login_password = self.get_argument("loginPassword") 22 | redis_key = "users:%s" % login_id 23 | if self.db_client.get(redis_key): 24 | self.finish('{"result": "conflict"}') 25 | return 26 | self.db_client.set(redis_key, hash_password(login_password)) 27 | self.logger.info("Logging in user %s", login_id) 28 | self.set_secure_cookie("loginId", login_id) 29 | self.finish('{"result": "success"}') 30 | -------------------------------------------------------------------------------- /org/collabdraw/handler/uploadhandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import config 4 | 5 | import os 6 | import tornado.web 7 | import tornado.template as template 8 | 9 | from ..dbclient.dbclientfactory import DbClientFactory 10 | from ..tools.uploadprocessor import process_uploaded_file 11 | 12 | 13 | class UploadHandler(tornado.web.RequestHandler): 14 | def initialize(self): 15 | self.logger = logging.getLogger('websocket') 16 | self.db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 17 | 18 | def get(self): 19 | self.room_name = self.get_argument('room', '') 20 | loader = template.Loader(config.ROOT_DIR) 21 | return_str = loader.load(os.path.join(config.HTML_ROOT, "upload.html")).generate(room=self.room_name) 22 | self.logger.info("Room name is %s" % self.room_name) 23 | self.finish(return_str) 24 | 25 | def post(self): 26 | return_str = "%s. Will redirect back to the upload page in 5\ 29 | seconds" 30 | self.room_name = self.get_argument('room', '') 31 | self.logger.info("Room name is %s" % self.room_name) 32 | if not self.room_name: 33 | self.logger.error("Unknown room name. Ignoring upload") 34 | response_str = "Room name not provided" 35 | self.finish(return_str % (self.room_name, response_str)) 36 | return 37 | self.logger.debug("Room name is %s" % self.room_name) 38 | fileinfo = self.request.files['file'][0] 39 | fname = fileinfo['filename'] 40 | fext = os.path.splitext(fname)[1] 41 | if fext.lower() != '.pdf': 42 | self.logger.error("Extension is not pdf. It is %s" % fext) 43 | response_str = "Only pdf files are allowed" 44 | self.finish(return_str % (self.room_name, response_str)) 45 | return 46 | dir_path = os.path.join(config.ROOT_DIR, "files", self.room_name) 47 | os.makedirs(dir_path, exist_ok=True) 48 | file_path = os.path.join(dir_path, fname) 49 | fh = open(file_path, 'wb') 50 | fh.write(fileinfo['body']) 51 | fh.close() 52 | threading.Thread(target=process_uploaded_file, args=(dir_path, fname, self.room_name)).start() 53 | response_str = "Upload finished successfully" 54 | self.finish(return_str % (self.room_name, response_str)) 55 | 56 | -------------------------------------------------------------------------------- /org/collabdraw/handler/websockethandler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | from zlib import compress 4 | from urllib.parse import quote 5 | import config 6 | 7 | import os 8 | from base64 import b64encode 9 | import tornado.websocket 10 | import tornado.web 11 | from pystacia import read 12 | 13 | from ..dbclient.dbclientfactory import DbClientFactory 14 | from ..pubsub.pubsubclientfactory import PubSubClientFactory 15 | from ..tools.videomaker import make_video 16 | 17 | 18 | class RealtimeHandler(tornado.websocket.WebSocketHandler): 19 | room_name = '' 20 | paths = [] 21 | db_client = None 22 | page_no = 1 23 | num_pages = 1 24 | 25 | # @Override 26 | def open(self): 27 | self.logger = logging.getLogger('websocket') 28 | self.logger.info("Open connection") 29 | self.db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 30 | self.pubsub_client = PubSubClientFactory.getPubSubClient(config.PUBSUB_CLIENT_TYPE) 31 | self.send_message(self.construct_message("ready")) 32 | 33 | # @Override 34 | def on_message(self, message): 35 | m = json.loads(message) 36 | event = m.get('event', '').strip() 37 | data = m.get('data', {}) 38 | 39 | self.logger.debug("Processing event %s" % event) 40 | if not event: 41 | self.logger.error("No event specified") 42 | return 43 | 44 | if event == "init": 45 | self.logger.info("Initializing with room name %s" % self.room_name) 46 | room_name = data.get('room', '') 47 | if not room_name: 48 | self.logger.error("Room name not provided. Can't initialize") 49 | return 50 | page_no = data.get('page', '1') 51 | 52 | self.init(room_name, page_no) 53 | 54 | elif event == "draw-click": 55 | self.logger.debug("Received draw-click") 56 | single_path = data['singlePath'] 57 | if not self.paths: 58 | self.logger.debug("None") 59 | self.paths = [] 60 | 61 | self.paths.extend(single_path) 62 | self.broadcast_message(self.construct_message("draw", {'singlePath': single_path})) 63 | self.db_client.set(self.construct_key(self.room_name, self.page_no), self.paths) 64 | 65 | elif event == "clear": 66 | self.broadcast_message(self.construct_message("clear")) 67 | self.db_client.delete(self.construct_key(self.room_name, self.page_no)) 68 | 69 | elif event == "get-image": 70 | if self.room_name != data['room'] or self.page_no != data['page']: 71 | self.logger.warning("Room name %s and/or page no. %s doesn't match with current room name %s and/or", 72 | "page no. %s. Ignoring" % ( 73 | data['room'], data['page'], self.room_name, self.page_no)) 74 | image_url, width, height = self.get_image_data(self.room_name, self.page_no) 75 | self.send_message(self.construct_message("image", {'url': image_url, 76 | 'width': width, 'height': height})) 77 | 78 | elif event == "video": 79 | make_video(self.construct_key(self.room_name, self.page_no)) 80 | 81 | elif event == "new-page": 82 | self.logger.info("num_pages was %d" % self.num_pages) 83 | self.db_client.set(self.construct_key("info", self.room_name, "npages"), 84 | str(self.num_pages + 1)) 85 | self.num_pages += 1 86 | self.logger.info("num_pages is now %d" % self.num_pages) 87 | self.init(self.room_name, self.num_pages) 88 | 89 | # @Override 90 | def on_close(self): 91 | self.leave_room(self.room_name) 92 | 93 | ## Higher lever methods 94 | def init(self, room_name, page_no): 95 | self.logger.info("Initializing %s and %s" % (room_name, page_no)) 96 | 97 | self.room_name = room_name 98 | self.page_no = page_no 99 | self.join_room(self.room_name) 100 | 101 | n_pages = self.db_client.get(self.construct_key("info", self.room_name, "npages")) 102 | if n_pages: 103 | self.num_pages = int(n_pages) 104 | # First send the image if it exists 105 | image_url, width, height = self.get_image_data(self.room_name, self.page_no) 106 | self.send_message(self.construct_message("image", {'url': image_url, 107 | 'width': width, 'height': height})) 108 | # Then send the paths 109 | p = self.db_client.get(self.construct_key(self.room_name, self.page_no)) 110 | if p: 111 | self.paths = json.loads(p) 112 | else: 113 | self.paths = [] 114 | self.logger.info("No data in database") 115 | self.send_message(self.construct_message("draw-many", 116 | {'datas': self.paths, 'npages': self.num_pages})) 117 | 118 | 119 | 120 | def leave_room(self, room_name, clear_paths=True): 121 | self.logger.info("Leaving room %s" % room_name) 122 | self.pubsub_client.unsubscribe(self.construct_key(room_name, self.page_no), self) 123 | if clear_paths: 124 | self.paths = [] 125 | 126 | def join_room(self, room_name): 127 | self.logger.info("Joining room %s" % room_name) 128 | self.pubsub_client.subscribe(self.construct_key(room_name, self.page_no), self) 129 | 130 | ## Messaging related methods 131 | def construct_key(self, namespace, key, *keys): 132 | return ":".join([str(namespace), str(key)] + list(map(str, keys))) 133 | 134 | def construct_message(self, event, data={}): 135 | m = json.dumps({"event": event, "data": data}) 136 | return m 137 | 138 | def broadcast_message(self, message): 139 | self.pubsub_client.publish(self.construct_key(self.room_name, self.page_no), message, self) 140 | 141 | def send_message(self, message): 142 | message = b64encode(compress(bytes(quote(str(message)), 'utf-8'), 9)) 143 | self.write_message(message) 144 | 145 | def get_image_data(self, room_name, page_no): 146 | image_url = os.path.join("files", room_name, str(page_no) + "_image.png") 147 | image_path = os.path.join(config.ROOT_DIR, image_url) 148 | try: 149 | image = read(image_path) 150 | except IOError as e: 151 | self.logger.error("Error %s while reading image at location %s" % (e, 152 | image_path)) 153 | return '', -1, -1 154 | width, height = image.size 155 | return image_url, width, height 156 | -------------------------------------------------------------------------------- /org/collabdraw/pubsub/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /org/collabdraw/pubsub/pubsubclientfactory.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import logging 4 | 5 | from .redispubsubclient import RedisPubSubClient 6 | from .pubsubclienttypes import PubSubClientTypes 7 | from .pubsubinterface import PubSubInterface 8 | 9 | 10 | class PubSubClientFactory: 11 | @staticmethod 12 | def getPubSubClient(pubsub_client_type_str): 13 | """ 14 | @param pubsub_client_type_str: 15 | @rtype : PubSubInterface 16 | """ 17 | logger = logging.getLogger('websocket') 18 | logger.info("Initializing with pubsub client type %s" % pubsub_client_type_str) 19 | if pubsub_client_type_str == PubSubClientTypes.redis: 20 | return RedisPubSubClient() 21 | else: 22 | raise RuntimeError("Unknown pubsub type %s" % pubsub_client_type_str) -------------------------------------------------------------------------------- /org/collabdraw/pubsub/pubsubclienttypes.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | 4 | class PubSubClientTypes: 5 | redis = "redis" 6 | in_memory = "in-memory" -------------------------------------------------------------------------------- /org/collabdraw/pubsub/pubsubinterface.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | class PubSubInterface(): 4 | def subscribe(self, topic, listener): 5 | raise RuntimeError("PubSub Interface method %s not implemented" % "subscribe") 6 | 7 | def unsubscribe(self, topic, listener): 8 | raise RuntimeError("PubSub Interface method %s not implemented" % "unsubscribe") 9 | 10 | def publish(self, topic, message, publisher): 11 | raise RuntimeError("PubSub Interface method %s not implemented" % "publish") 12 | 13 | -------------------------------------------------------------------------------- /org/collabdraw/pubsub/redispubsubclient.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import logging 4 | import threading 5 | 6 | import redis 7 | 8 | import config 9 | from .pubsubinterface import PubSubInterface 10 | 11 | 12 | # TODOS 13 | ## Thread pooling 14 | 15 | class RedisPubSubClient(PubSubInterface): 16 | 17 | redis_client = redis.from_url(config.REDIS_URL) 18 | 19 | def __init__(self): 20 | self.logger = logging.getLogger('websocket') 21 | self.pubsub_client = self.redis_client.pubsub() 22 | self.logger.info("Initialized redis pubsub client") 23 | 24 | def subscribe(self, topic, listener): 25 | self.logger.debug("Subscribing to topic %s" % topic) 26 | self.pubsub_client.subscribe(topic) 27 | self.t = threading.Thread(target=self._redis_listener, args=(topic, listener, self.pubsub_client)) 28 | self.t.start() 29 | 30 | def unsubscribe(self, topic, listener): 31 | self.logger.debug("Unsubscribing from topic %s" % topic) 32 | 33 | if self.t: 34 | self.pubsub_client.unsubscribe(topic) 35 | self.t.join(60) 36 | 37 | def publish(self, topic, message, publisher): 38 | self.logger.debug("Publishing to topic %s" % topic) 39 | # TODO If publisher is subscribed to topic 40 | self.redis_client.publish(topic, message) 41 | 42 | def _redis_listener(self, topic, listener, pubsub_client): 43 | self.logger.info("Starting listener thread for topic %s" % topic) 44 | for message in pubsub_client.listen(): 45 | self.logger.debug("Sending message to topic %s" % topic) 46 | if message['type'] == 'message': 47 | listener.send_message(message['data'].decode('utf-8')) 48 | -------------------------------------------------------------------------------- /org/collabdraw/tools/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /org/collabdraw/tools/tools.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import hashlib 4 | import os 5 | import glob 6 | 7 | import cairo 8 | 9 | 10 | def createCairoContext(w, h): 11 | surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h) 12 | ctx = cairo.Context(surface) 13 | ctx.set_source_rgb(255, 255, 255) 14 | ctx.rectangle(0, 0, w, h) 15 | ctx.fill() 16 | return ctx 17 | 18 | 19 | def hexColorToRGB(colorstring): 20 | logger = logging.getLogger('websocket') 21 | """ convert #RRGGBB to an (R, G, B) tuple """ 22 | colorstring = colorstring.strip() 23 | if colorstring == "black": 24 | return (0, 0, 0) 25 | elif colorstring == "blue": 26 | return (0, 0, 255) 27 | elif colorstring == "green": 28 | return (0, 255, 0) 29 | elif colorstring == "red": 30 | return (255, 0, 0) 31 | logger.debug("Converting string %s to rgb" % colorstring) 32 | if colorstring[0] == '#': colorstring = colorstring[1:] 33 | if len(colorstring) != 6: 34 | logger.error("input #%s is not in #RRGGBB format" % colorstring) 35 | return (0, 0, 0) 36 | r, g, b = colorstring[:2], colorstring[2:4], colorstring[4:] 37 | r, g, b = [int(n, 16) for n in (r, g, b)] 38 | logger.debug("Returning %d, %d, %d" % (r, g, b)) 39 | return (r, g, b) 40 | 41 | 42 | def hash_password(password): 43 | s = config.HASH_SALT + password 44 | return hashlib.md5(s.encode("utf-8")).hexdigest() 45 | 46 | def delete_files(pattern): 47 | """ 48 | Works only for files, not directories 49 | """ 50 | filelist = glob.glob(pattern) 51 | for f in filelist: 52 | os.remove(f) 53 | -------------------------------------------------------------------------------- /org/collabdraw/tools/uploadprocessor.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import subprocess 4 | import glob 5 | import os 6 | import logging 7 | 8 | import config 9 | from ..dbclient.dbclientfactory import DbClientFactory 10 | from org.collabdraw.tools.tools import delete_files 11 | 12 | 13 | def process_uploaded_file(dir_path, fname, key): 14 | logger = logging.getLogger('websocket') 15 | db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 16 | 17 | file_path = os.path.join(dir_path, fname) 18 | logger.info("Processing file %s" % file_path) 19 | # Split the pdf files by pages 20 | subprocess.call(['pdfseparate', file_path, dir_path + '/%d_image.pdf']) 21 | # Convert the pdf files to png 22 | subprocess.call(['mogrify', '-format', 'png', '--', dir_path + '/*image.pdf']) 23 | # Delete all the files 24 | delete_files(dir_path + '/*image.pdf') 25 | logger.info("Finished processing file") 26 | # Insert the number of pages processed for that room 27 | db_key = "info:%s:npages" % key 28 | db_client.set(db_key, len(glob.glob(dir_path + '/*.png'))) -------------------------------------------------------------------------------- /org/collabdraw/tools/videomaker.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import os 4 | import subprocess 5 | import json 6 | import uuid 7 | import logging 8 | 9 | import config 10 | from ..tools.tools import hexColorToRGB, createCairoContext 11 | from ..dbclient.dbclientfactory import DbClientFactory 12 | from ..tools.tools import delete_files 13 | 14 | 15 | def make_video(key): 16 | logger = logging.getLogger('websocket') 17 | db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 18 | 19 | p = db_client.get(key) 20 | tmp_path = os.path.abspath("./tmp") 21 | os.makedirs(tmp_path, exist_ok=True) 22 | path_prefix = os.path.join(tmp_path, str(uuid.uuid4())) 23 | if p: 24 | points = json.loads(p) 25 | i = 0 26 | c = createCairoContext(920, 550) 27 | for point in points: 28 | c.set_line_width(float(point['lineWidth'].replace('px', ''))) 29 | c.set_source_rgb(*hexColorToRGB(point['lineColor'])) 30 | if point['type'] == 'dragstart' or point['type'] == 'touchstart': 31 | c.move_to(point['oldx'], point['oldy']) 32 | elif point['type'] == 'drag' or point['type'] == 'touchmove': 33 | c.move_to(point['oldx'], point['oldy']) 34 | c.line_to(point['x'], point['y']) 35 | c.stroke() 36 | f = open(path_prefix + "_img_" + str(i) + ".png", "wb") 37 | c.get_target().write_to_png(f) 38 | f.close() 39 | i += 1 40 | video_file_name = path_prefix + '_video.mp4' 41 | retval = subprocess.call(['ffmpeg', '-f', 'image2', '-i', path_prefix + '_img_%d.png', video_file_name]) 42 | logger.info("Image for key %s successfully created. File name is %s" % (key, video_file_name)) 43 | if retval == 0: 44 | # Clean up if successful 45 | cleanup_files = path_prefix + '_img_*' 46 | logger.info("Cleaning up %s" % cleanup_files) 47 | delete_files(cleanup_files) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Maintenance](https://img.shields.io/badge/maintained%3F-no!-red.svg?style=flat)]() 2 | 3 | 4 | ABOUT: 5 | ------------- 6 | Collabdraw is an open-source online whiteboard application built to work on desktops and tablets 7 | alike. The user interface is HTML5 (using enyojs), and the backend runs on python tornado and redis. 8 | 9 | 10 | FEATURES: 11 | ------------- 12 | 1. Works on most tablets out of the box, interface designed for touch interfaces 13 | 2. Multiple rooms, pages for collaboration 14 | 3. Take quick snapshots of the board 15 | 4. Upload pdf and annotate on whiteboard 16 | 5. Support for SSL, and authentication 17 | 6. Fast, handles lots of users simultaneously 18 | 7. **Runs on heroku out of the box!** 19 | 20 | SERVER REQUIREMENTS: 21 | ------------- 22 | 1. Python 3.2+ 23 | 2. Redis server 24 | 3. All python packages specified in requirements.txt (which might involve installing other 25 | non-python dependencies like cairo, redis) 26 | 4. libpoppler (for pdfseparate), imagemagick (for mogrify) for upload functionality 27 | 5. ffmpeg to enable video functionality 28 | 29 | INSTALLATION: 30 | ------------- 31 | 1. Install all system requirements. If on Ubuntu/Debian do: 32 | ``` 33 | apt-get install python3 redis-server poppler-utils imagemagick ffmpeg python3-pip git pkg-config libcairo2-dev 34 | ``` 35 | 36 | 2. Clone the git repository 37 | ``` 38 | git clone git://github.com/anandtrex/collabdraw.git 39 | ``` 40 | 41 | 3. Initialize submodules to get enyojs libraries. 42 | ``` 43 | cd collabdraw 44 | git submodule init 45 | git submodule update 46 | ``` 47 | 48 | 4. Install python library requirements 49 | ``` 50 | pip-3.2 install virtualenv 51 | virtualenv venv 52 | source venv/bin/activate 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | 5. Set the hostnames, ports and other options in config.py. Most of the options are explained in the 57 | config file. You need the url that points to your redis server, and the url that points to your 58 | websocket endpoint 59 | 60 | 6. Test if your setup works 61 | ``` 62 | collabdraw> ./run_tests.sh 63 | ``` 64 | 65 | RUNNING: 66 | ------------- 67 | 1. Start the redis server (On Ubuntu/Debian, on most setups, this is started automatically on installation) 68 | 2. Run `python main.py` 69 | 70 | 71 | HEROKU DEPLOYMENT: 72 | -------------------- 73 | 1. Create a [heroku](http://heroku.com) account, create an app, and add the "Redis cloud" plugin. 74 | Install heroku toolbelt on your box. Login to heroku with `heroku login`. You can follow the 75 | instructions on the [heroku quickstart page](https://devcenter.heroku.com/articles/quickstart) 76 | 2. Clone the git repository `git clone git://github.com/anandtrex/collabdraw.git && cd collabdraw` 77 | 3. Edit config.py to point to your app. If you use the "Redis cloud" heroku addon, you can leave the 78 | redis url as it is. 79 | 4. Add your app as a remote in git with: 80 | ``` 81 | heroku git:remote -a 82 | ``` 83 | 5. Run `./set_heroku_path.sh` to set the LD_LIBRARY_PATH in your heroku app config to point properly 84 | to ffmpeg 85 | 6. Run: 86 | ``` 87 | git push heroku master 88 | ``` 89 | 7. Profit!!! 90 | 8. You can check your heroku installation by logging into the heroku dynamo with `heroku run bash`, and running `./run_tests.sh`. If this passes, all's good. 91 | 92 | 93 | 94 | NOTES: 95 | ------------- 96 | * All rooms are currently "public". Anyone registered user can join any room, if they know the room name. Private rooms on the way. 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | decorator==3.4.0 2 | pystacia==0.1 3 | redis==2.7.2 4 | six==1.2.0 5 | tornado==2.4.1 6 | git+http://anongit.freedesktop.org/git/pycairo 7 | nose==1.3.0 8 | -------------------------------------------------------------------------------- /resource/css/upload.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Arial, sans-serif; 3 | font-size: 90%; 4 | } 5 | h1, h2, h3, h4 { 6 | margin-top: 0px; 7 | } 8 | div.row { 9 | margin-bottom: 10px; 10 | } 11 | *:focus { 12 | outline: none; 13 | } 14 | .floatLeft { 15 | float: left; 16 | } 17 | .floatRight { 18 | float: right; 19 | } 20 | .clear { 21 | clear: both; 22 | } 23 | 24 | form { 25 | padding: 20px; 26 | border: 1px solid #cccccc; 27 | border-radius: 10px; 28 | -moz-border-radius: 10px; 29 | -webkit-box-shadow: 0 0 10px #ccc; 30 | -moz-box-shadow: 0 0 10px #ccc; 31 | box-shadow: 0 0 10px #ccc; 32 | width: 400px; 33 | margin: 20px auto; 34 | background-image: -moz-linear-gradient(top, #ffffff,#f2f2f2); 35 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f2f2f2)); 36 | } 37 | 38 | input { 39 | border: 1px solid #ccc; 40 | font-size: 13pt; 41 | padding: 5px 10px 5px 10px; 42 | border-radius: 10px; 43 | -moz-border-radius: 10px; 44 | -webkit-transition: all 0.5s ease-in-out; 45 | -moz-transition: all 0.5s ease-in-out; 46 | transition: all 0.5s ease-in-out; 47 | } 48 | 49 | input[type=button] { 50 | background-image: -moz-linear-gradient(top, #ffffff, #dfdfdf); 51 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#dfdfdf)); 52 | } 53 | 54 | input:focus { 55 | -webkit-box-shadow: 0 0 10px #ccc; 56 | -moz-box-shadow: 0 0 10px #ccc; 57 | box-shadow: 0 0 5px #ccc; 58 | -webkit-transform: scale(1.05); 59 | -moz-transform: scale(1.05); 60 | transform: scale(1.05); 61 | } 62 | 63 | #fileToUpload { 64 | width: 378px; 65 | } 66 | 67 | #progressIndicator { 68 | font-size: 10pt; 69 | } 70 | 71 | #fileInfo { 72 | font-size: 10pt; 73 | font-style: italic; 74 | color: #aaa; 75 | margin-top: 10px; 76 | } 77 | 78 | #progressBar { 79 | height: 14px; 80 | border: 1px solid #cccccc; 81 | display: none; 82 | border-radius: 10px; 83 | -moz-border-radius: 10px; 84 | background-image: -moz-linear-gradient(top, #66cc00, #4b9500); 85 | background-image: -webkit-gradient(linear, left top, left bottom, from(#66cc00), to(#4b9500)); 86 | } 87 | 88 | #uploadResponse { 89 | margin-top: 10px; 90 | padding: 20px; 91 | overflow: hidden; 92 | display: none; 93 | border-radius: 10px; 94 | -moz-border-radius: 10px; 95 | border: 1px solid #ccc; 96 | box-shadow: 0 0 5px #ccc; 97 | background-image: -moz-linear-gradient(top, #ff9900, #c77801); 98 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ff9900), to(#c77801)); 99 | } -------------------------------------------------------------------------------- /resource/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CollabDraw 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resource/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CollabDraw 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resource/html/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CollabDraw 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resource/html/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CollabDraw 7 | 8 | 9 | 10 | 11 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resource/js/App.js: -------------------------------------------------------------------------------- 1 | enyo.kind({ 2 | name: "App", 3 | kind: "FittableRows", 4 | fit: true, 5 | 6 | published: { 7 | whiteboard: '', 8 | curves: { 9 | color: 'black', 10 | width: '3px', 11 | }, 12 | uid: 'test', 13 | room: 'one', 14 | canvasWidth: 1000, 15 | canvasHeight: 550, 16 | appIpAddress: "", 17 | appPort: "", 18 | }, 19 | 20 | components: [{ 21 | kind: "FittableRows", 22 | fit: true, 23 | style: "text-align: center; padding: 20px; background-color: #F0F0F0; z-index: 0", 24 | components: [{ 25 | style: "margin: auto; background-color: #FFFFFF;", 26 | ondragstart: "touchstart", 27 | ondragover: "touchmove", 28 | ondragfinish: "touchend", 29 | name: "canvasContainer", 30 | rendered: function() { 31 | this.applyStyle("width", this.owner.canvasWidth + "px"); 32 | this.applyStyle("height", this.owner.canvasHeight + "px"); 33 | if (window.location.protocol == 'https:') { 34 | var websocketAddress = 'wss://' + this.owner.appIpAddress + ':' + this.owner.appPort + '/realtime/'; 35 | } else { 36 | var websocketAddress = 'ws://' + this.owner.appIpAddress + ':' + this.owner.appPort + '/realtime/'; 37 | } 38 | if (this.hasNode()) { 39 | var _this = this; 40 | this.owner.$.loadingPopup.show(); 41 | this.owner.whiteboard = new WhiteboardSvg(this.node.getAttribute("id"), this.owner.canvasWidth, this.owner.canvasHeight, this.owner.uid, this.owner.room, 1, websocketAddress, function(numPages, currentPage) { 42 | _this.owner.$.currentPage.setMax(numPages); 43 | _this.owner.$.currentPage.setValue(currentPage); 44 | _this.owner.$.loadingPopup.hide(); 45 | }); 46 | } 47 | }, 48 | }], 49 | }, { 50 | kind: "onyx.MoreToolbar", 51 | components: [{ 52 | kind: "onyx.Button", 53 | content: "Eraser", 54 | ontap: "selectEraser" 55 | }, { 56 | kind: "onyx.Button", 57 | content: "Pen", 58 | ontap: "selectPen" 59 | }, { 60 | kind: "onyx.PickerDecorator", 61 | components: [{ 62 | name: "colorPicker", 63 | style: "background-color: black; color:black", 64 | content: "C", 65 | }, { 66 | kind: "onyx.Picker", 67 | onChange: "colorItemSelected", 68 | components: [{ 69 | name: "red", 70 | style: "background-color: red; color: red", 71 | content: "C", 72 | }, { 73 | name: "blue", 74 | style: "background-color: blue; color: blue", 75 | content: "C", 76 | }, { 77 | name: "green", 78 | style: "background-color: green; color: green", 79 | content: "C", 80 | }, { 81 | name: "black", 82 | style: "background-color: black; color:black", 83 | content: "C", 84 | }, ] 85 | }, ], 86 | }, { 87 | kind: "onyx.MenuDecorator", 88 | onSelect: "optionSelected", 89 | components: [{ 90 | content: "More Options..." 91 | }, { 92 | kind: "onyx.Menu", 93 | components: [{ 94 | name: "clear", 95 | content: "Clear", 96 | }, { 97 | name: "createJoinRoom", 98 | content: "Create/Join Room", 99 | popup: "createJoinRoomPopup", 100 | }, { 101 | name: "getVideo", 102 | content: "Get Video...", 103 | }, { 104 | name: "exportToSvg", 105 | content: "Export to SVG", 106 | }, { 107 | name: "upload", 108 | content: "Upload", 109 | }, ] 110 | }, ] 111 | }, { 112 | kind: "onyx.Button", 113 | content: "Previous", 114 | ontap: "selectPrevious" 115 | }, { 116 | kind: "onyx.Button", 117 | content: "New Page", 118 | ontap: "selectNewPage" 119 | }, { 120 | kind: "onyx.Button", 121 | content: "Next", 122 | ontap: "selectNext" 123 | }, { 124 | style: "width: 35%", 125 | content: " " 126 | }, { 127 | kind: "onyx.PickerDecorator", 128 | components: [{}, { 129 | kind: "onyx.IntegerPicker", 130 | name: "currentPage", 131 | onSelect: "gotoPage", 132 | min: 1, 133 | }, ], 134 | }, { 135 | kind: "onyx.Button", 136 | content: "Logout", 137 | ontap: "logout" 138 | }, { 139 | name: "createJoinRoomPopup", 140 | kind: "onyx.Popup", 141 | centered: true, 142 | modal: true, 143 | floating: true, 144 | style: "width: 300px; height: 200px; padding: 0 20px 5px 20px", 145 | components: [{ 146 | content: "

Enter Room name

", 147 | allowHtml: true, 148 | }, { 149 | content: "If your room doesn't exist already in your account, it will be created", 150 | }, { 151 | kind: "onyx.InputDecorator", 152 | style: "margin: 10px 10px 10px 0; width: 250px", 153 | alwaysLooksFocused: true, 154 | components: [{ 155 | kind: "onyx.Input", 156 | name: "roomName", 157 | }] 158 | }, { 159 | tag: "br" 160 | }, { 161 | kind: "onyx.Button", 162 | content: "Cancel", 163 | ontap: "selectCreateJoinRoomPopupCancel", 164 | }, { 165 | kind: "onyx.Button", 166 | content: "OK", 167 | ontap: "selectCreateJoinRoomPopupOk", 168 | popup: "lightPopup", 169 | style: "margin-left: 10px", 170 | }], 171 | }, { 172 | name: "loadingPopup", 173 | kind: "onyx.Popup", 174 | centered: true, 175 | autoDismiss: false, 176 | modal: true, 177 | floating: true, 178 | components: [{ 179 | kind: "onyx.Spinner" 180 | }, ], 181 | }] 182 | }, ], 183 | 184 | drawRectangle: function(inSender, inEvent) { 185 | this.whiteboard.drawRectangle(); 186 | }, 187 | 188 | touchstart: function(inSender, inEvent) { 189 | var canvasBounds = this.$.canvasContainer.getBounds(); 190 | this.curves.oldx = inEvent.pageX - canvasBounds.left; 191 | this.curves.oldy = inEvent.pageY - canvasBounds.top; 192 | this.whiteboard.startPath(this.curves.oldx, this.curves.oldy, this.curves.color, this.curves.width, true); 193 | }, 194 | 195 | touchmove: function(inSender, inEvent) { 196 | if (this.curves.oldx != -1 && this.curves.oldy != -1) { 197 | var canvasBounds = this.$.canvasContainer.getBounds(); 198 | x = inEvent.pageX - canvasBounds.left; 199 | y = inEvent.pageY - canvasBounds.top; 200 | this.whiteboard.continuePath(this.curves.oldx, this.curves.oldy, x, y, this.curves.color, this.curves.width, true); 201 | this.curves.oldx = x; 202 | this.curves.oldy = y; 203 | } 204 | }, 205 | 206 | touchend: function(inSender, inEvent) { 207 | if (this.curves.oldx != -1 && this.curves.oldy != -1) { 208 | var canvasBounds = this.$.canvasContainer.getBounds(); 209 | x = inEvent.pageX - canvasBounds.left; 210 | y = inEvent.pageY - canvasBounds.top; 211 | this.whiteboard.endPath(this.curves.oldx, this.curves.oldy, x, y, this.curves.color, this.curves.width, true); 212 | this.curves.oldx = -1; 213 | this.curves.oldy = -1; 214 | } 215 | }, 216 | 217 | selectEraser: function(inSender, inEvent) { 218 | this.curves.color = '#ffffff'; 219 | this.curves.width = '10px'; 220 | }, 221 | 222 | selectPen: function(inSender, inEvent) { 223 | this.curves.color = '#000000'; 224 | this.curves.width = '3px'; 225 | }, 226 | 227 | optionSelected: function(inSender, inEvent) { 228 | var name = inEvent.originator.name; 229 | switch (name) { 230 | case "clear": 231 | this.selectClear(inSender, inEvent); 232 | break; 233 | case "createJoinRoom": 234 | this.selectCreateJoinRoom(inSender, inEvent); 235 | break; 236 | case "getVideo": 237 | this.selectGetVideo(inSender, inEvent); 238 | break; 239 | case "exportToSvg": 240 | this.selectExportToSvg(inSender, inEvent); 241 | break; 242 | case "upload": 243 | this.selectUpload(inSender, inEvent); 244 | break; 245 | } 246 | }, 247 | 248 | colorItemSelected: function(inSender, inEvent) { 249 | var color = inEvent.selected.name; 250 | this.$.colorPicker.applyStyle("background-color", color); 251 | this.$.colorPicker.applyStyle("color", color); 252 | this.curves.color = color; 253 | this.curves.width = "3px"; 254 | }, 255 | 256 | selectClear: function(inSender, inEvent) { 257 | this.whiteboard.clear(true); 258 | }, 259 | 260 | selectCreateJoinRoom: function(inSender, inEvent) { 261 | var p = this.$[inEvent.originator.popup]; 262 | if (p) { 263 | p.show(); 264 | } 265 | }, 266 | 267 | selectGetVideo: function(inSender, inEvent) { 268 | this.whiteboard.makeVideo(); 269 | }, 270 | 271 | selectExportToSvg: function(inSender, inEvent) { 272 | var svg = document.getElementsByTagName('svg')[0]; 273 | var svg_xml = (new XMLSerializer).serializeToString(svg); 274 | window.open("data:image/svg+xml;base64," + btoa(svg_xml), "Export"); 275 | }, 276 | 277 | selectUpload: function(inSender, inEvent) { 278 | window.location = "./upload?room=" + this.room; 279 | }, 280 | 281 | selectNext: function(inSender, inEvent) { 282 | this.$.loadingPopup.show(); 283 | var result = this.whiteboard.nextPage(); 284 | this.updatePageInfo(); 285 | if (!result) this.$.loadingPopup.hide(); 286 | }, 287 | 288 | selectPrevious: function(inSender, inEvent) { 289 | this.$.loadingPopup.show(); 290 | var result = this.whiteboard.prevPage(); 291 | this.updatePageInfo(); 292 | if (!result) this.$.loadingPopup.hide(); 293 | }, 294 | 295 | selectCreateJoinRoomPopupCancel: function(inSender, inEvent) { 296 | this.$.createJoinRoomPopup.hide(); 297 | }, 298 | 299 | selectCreateJoinRoomPopupOk: function(inSender, inEvent) { 300 | var value = this.$.roomName.getValue(); 301 | if (value) { 302 | this.whiteboard.joinRoom(value); 303 | } 304 | this.$.createJoinRoomPopup.hide(); 305 | }, 306 | logout: function() { 307 | window.location = "./logout.html"; 308 | }, 309 | 310 | selectNewPage: function(inSender, inEvent) { 311 | this.whiteboard.newPage(); 312 | this.updatePageInfo(); 313 | }, 314 | 315 | updatePageInfo: function() { 316 | this.$.currentPage.setMax(this.whiteboard.getNumPages()); 317 | this.$.currentPage.setValue(this.whiteboard.getCurrentPage()); 318 | }, 319 | 320 | gotoPage: function(inSender, inEvent) { 321 | this.whiteboard.gotoPage(inEvent.selected.content); 322 | }, 323 | 324 | }); 325 | -------------------------------------------------------------------------------- /resource/js/Connection.js: -------------------------------------------------------------------------------- 1 | enyo.kind({ 2 | name: 'Connection', 3 | kind: null, 4 | 5 | socket: 'undefined', 6 | whiteboard: 'undefined', 7 | singlePath: [], 8 | currentPathLength: 0, 9 | uid: 'uid', 10 | room: 'undefined', 11 | page: 1, 12 | 13 | constructor: function(address, whiteboard, room) { 14 | this.whiteboard = whiteboard; 15 | //console.log("Connecting to address " + address); 16 | this.socket = new WebSocket(address); 17 | this.room = room; 18 | this.page = 1; 19 | //console.log("Room is " + room); 20 | 21 | _this = this; 22 | this.socket.onmessage = function(evt) { 23 | message = JXG.decompress(evt.data); 24 | message = JSON.parse(message); 25 | evnt = message['event']; 26 | data = message['data']; 27 | switch (evnt) { 28 | case 'ready': 29 | _this.init(_this.uid, _this.room, _this.page); 30 | break; 31 | case 'draw': 32 | _this.remoteDraw(_this, data); 33 | break; 34 | case 'draw-many': 35 | _this.remoteDrawMany(_this, data); 36 | break; 37 | case 'clear': 38 | _this.remoteClear(_this, data); 39 | break; 40 | case 'image': 41 | _this.remoteImage(_this, data); 42 | break; 43 | } 44 | } 45 | }, 46 | 47 | sendMessage: function(evt, data) { 48 | message = JSON.stringify({ 49 | "event": evt, 50 | "data": data 51 | }); 52 | this.socket.send(message); 53 | }, 54 | 55 | init: function(uid, room, currentPage) { 56 | console.log("Sending init for room " + room + " and page " + currentPage); 57 | this.whiteboard.clear(false, false); 58 | this.sendMessage("init", { 59 | "room": room, 60 | "page": currentPage 61 | }); 62 | }, 63 | 64 | /** 65 | * Get data from server to initialize this whiteboard 66 | * @param {Object} uid 67 | * @param {Object} room 68 | * @param {Object} page 69 | */ 70 | joinRoom: function(room, page) { 71 | this.room = room; 72 | this.singlePath = []; 73 | this.currentPathLength = 0; 74 | this.whiteboard.clear(false, false); 75 | //console.log("Sending init for room " + room); 76 | this.sendMessage("init", { 77 | "room": this.room 78 | }); 79 | }, 80 | 81 | /** 82 | * Send a single path (segment) to the server 83 | * @param {x, y, type, lineColor, lineWidth} a point on the path 84 | */ 85 | sendPath: function(data) { 86 | this.singlePath.push(data); 87 | this.currentPathLength++; 88 | 89 | // Send path every two points or when user removes finger 90 | if (this.currentPathLength > 2 || data.type === "touchend") { 91 | this.sendMessage("draw-click", { 92 | "singlePath": this.singlePath 93 | }); 94 | this.singlePath = []; 95 | this.currentPathLength = 0; 96 | } 97 | }, 98 | 99 | /** 100 | * Clear all other canvases (in the same room on the same page) 101 | */ 102 | sendClear: function() { 103 | this.singlePath = []; 104 | this.currentPathLength = 0; 105 | this.sendMessage("clear", {}); 106 | }, 107 | 108 | getImage: function() { 109 | //console.log("Getting image for page " + this.page); 110 | this.sendMessage("get-image", { 111 | "room": this.room, 112 | "page": this.page 113 | }); 114 | }, 115 | 116 | /** 117 | * Make video remotely 118 | */ 119 | makeVideo: function() { 120 | this.sendMessage("video", {}); 121 | }, 122 | 123 | /* 124 | * Create a new page 125 | */ 126 | newPage: function() { 127 | this.whiteboard.clear(false, false); 128 | this.sendMessage("new-page", {}); 129 | }, 130 | 131 | /*** 132 | * All remote functions below 133 | */ 134 | 135 | /** 136 | * Draw from realtime data incoming from server 137 | * Called when server sends @event 'draw' 138 | * @param {Object} self 139 | * @param {singlePath: [points...]} input 140 | */ 141 | remoteDraw: function(self, input) { 142 | var sPath = input.singlePath; 143 | var data = {}; 144 | // point on path 145 | for (d in sPath) { 146 | data = sPath[d]; 147 | if (data == null) continue; 148 | if (data.type == 'touchstart') self.whiteboard.startPath(data.oldx, data.oldy, data.lineColor, data.lineWidth, false); 149 | else if (data.type == 'touchmove') self.whiteboard.continuePath(data.oldx, data.oldy, data.x, data.y, data.lineColor, data.lineWidth, false); 150 | else if (data.type == 'touchend') self.whiteboard.endPath(data.oldx, data.oldy, data.x, data.y, data.lineColor, data.lineWidth, false); 151 | } 152 | }, 153 | 154 | /** 155 | * Draw from stored data incoming from server 156 | * Called when server sends @event 'draw-many' 157 | * @param {Object} self 158 | * @param {datas:[points...]} data 159 | */ 160 | remoteDrawMany: function(self, data) { 161 | ds = data.datas; 162 | for (d in ds) { 163 | if (ds[d] === null) continue; 164 | if (ds[d].type == 'touchstart') self.whiteboard.startPath(ds[d].oldx, ds[d].oldy, ds[d].lineColor, ds[d].lineWidth, false); 165 | else if (ds[d].type == 'touchmove') self.whiteboard.continuePath(ds[d].oldx, ds[d].oldy, ds[d].x, ds[d].y, ds[d].lineColor, ds[d].lineWidth, false); 166 | else if (ds[d].type == 'touchend') self.whiteboard.endPath(ds[d].oldx, ds[d].oldy, ds[d].x, ds[d].y, ds[d].lineColor, ds[d].lineWidth, false); 167 | } 168 | //console.log("Total pages is " + data.npages); 169 | self.whiteboard.setTotalPages(data.npages); 170 | }, 171 | 172 | /** 173 | * Clear from server 174 | * Called when server sends @event 'clear' 175 | * @param {Object} self 176 | * @param {Object} data 177 | */ 178 | remoteClear: function(self, data) { 179 | self.whiteboard.clear(false); 180 | }, 181 | 182 | remoteImage: function(self, data) { 183 | if (data.url != "") { 184 | var img = document.createElement('img'); 185 | img.src = data.url; 186 | //console.log("Image url is " + data.url); 187 | self.whiteboard.loadImage(data.url, data.width, data.height); 188 | } 189 | }, 190 | 191 | }); 192 | -------------------------------------------------------------------------------- /resource/js/Login.js: -------------------------------------------------------------------------------- 1 | enyo.kind({ 2 | name: "Login", 3 | kind: "FittableRows", 4 | fit: true, 5 | 6 | published: { 7 | uid: 'test', 8 | room: 'one', 9 | }, 10 | 11 | components: [{ 12 | kind: "FittableRows", 13 | fit: true, 14 | components: [{ 15 | kind: "FittableColumns", 16 | style: "padding: 20px; z-index: 0; margin-top: 50px;", 17 | classes: "enyo-center", 18 | width: '500px', 19 | height: '500px', 20 | fit: false, 21 | components: [{ 22 | kind: "onyx.Groupbox", 23 | components: [{ 24 | kind: "onyx.GroupboxHeader", 25 | content: "Please Login" 26 | }, { 27 | kind: "onyx.InputDecorator", 28 | style: "text-align: left", 29 | components: [{ 30 | content: "Login ID ", 31 | style: "margin-right: 15px", 32 | }, { 33 | kind: "onyx.Input", 34 | name: "loginId", 35 | style: "float: right", 36 | }, ], 37 | }, { 38 | kind: "onyx.InputDecorator", 39 | style: "text-align: left", 40 | components: [{ 41 | content: "Password ", 42 | style: "margin-right: 15px", 43 | }, { 44 | kind: "onyx.Input", 45 | name: "loginPassword", 46 | type: "password", 47 | style: "float: right", 48 | }], 49 | }, { 50 | kind: "FittableColumns", 51 | classes: "enyo-center", 52 | style: "padding: 15px", 53 | components: [{ 54 | kind: "onyx.Button", 55 | style: "margin: 5px 5px 5px auto", 56 | content: "Login", 57 | ontap: "login", 58 | name: "loginButton", 59 | }, { 60 | kind: "onyx.Button", 61 | style: "margin: 5px auto 5px 5px", 62 | content: "Back", 63 | ontap: "goToApp" 64 | }, { 65 | kind: "onyx.Button", 66 | style: "margin: 5px auto 5px 5px", 67 | content: "Register", 68 | ontap: "goToRegister" 69 | }, ], 70 | }, { 71 | kind: "FittableRows", 72 | showing: false, 73 | name: "loginResult", 74 | style: "padding: 10px", 75 | components: [{ 76 | name: "loginStatus", 77 | allowHtml: true, 78 | content: "", 79 | }, ], 80 | }, ], 81 | }, ], 82 | }, ], 83 | }, { 84 | kind: "onyx.Toolbar", 85 | components: [{ 86 | kind: "onyx.Button", 87 | content: "Back", 88 | ontap: "goToApp" 89 | }, ], 90 | }], 91 | 92 | goToApp: function() { 93 | window.location = "./index.html"; 94 | }, 95 | 96 | goToRegister: function() { 97 | window.location = "./register.html"; 98 | }, 99 | 100 | login: function() { 101 | var loginid = this.$.loginId.getValue(); 102 | var password = this.$.loginPassword.getValue(); 103 | console.log("User id and password are: " + loginid + ", " + password); 104 | var ajax = new enyo.Ajax({ 105 | method: "POST", 106 | postBody: { 107 | url: "./login.html", 108 | }, 109 | contentType: "application/json", 110 | }); 111 | ajax.go({ 112 | loginId: loginid, 113 | loginPassword: password, 114 | }); 115 | ajax.response(this, "handleLoginResponse"); 116 | }, 117 | 118 | handleLoginResponse: function(inSender, inResponse) { 119 | console.log("Response"); 120 | console.log(JSON.stringify(inResponse)); 121 | if (inResponse.result == "success") { 122 | window.location = "./index.html"; 123 | } else if (inResponse.result == "failure") { 124 | this.$.loginStatus.setContent("Wrong username/password"); 125 | } else { 126 | this.$.loginStatus.setContent("Error occurred. Try again."); 127 | } 128 | this.$.loginResult.show(); 129 | }, 130 | }); 131 | -------------------------------------------------------------------------------- /resource/js/MessageEvent.js: -------------------------------------------------------------------------------- 1 | Ext.define('Whiteboard.MessageEvent', { 2 | extend: 'Ext.util.Observable', 3 | constructor: function(config){ 4 | this.addEvents({ 5 | "showMessage" : true, 6 | }); 7 | 8 | // Copy configured listeners into *this* object so 9 | // that the base class's constructor will add them. 10 | this.listeners = config.listeners; 11 | 12 | // Call our superclass constructor to complete 13 | // construction process. 14 | this.callParent(arguments) 15 | } 16 | }); -------------------------------------------------------------------------------- /resource/js/Register.js: -------------------------------------------------------------------------------- 1 | enyo.kind({ 2 | name: "Register", 3 | kind: "FittableRows", 4 | fit: true, 5 | 6 | published: { 7 | uid: 'test', 8 | room: 'one', 9 | }, 10 | 11 | components: [{ 12 | kind: "FittableRows", 13 | fit: true, 14 | components: [{ 15 | kind: "FittableColumns", 16 | style: "padding: 20px; z-index: 0; margin-top: 50px;", 17 | classes: "enyo-center", 18 | width: '500px', 19 | height: '500px', 20 | fit: false, 21 | components: [{ 22 | kind: "onyx.Groupbox", 23 | components: [{ 24 | kind: "onyx.GroupboxHeader", 25 | content: "Please Register" 26 | }, { 27 | kind: "onyx.InputDecorator", 28 | style: "text-align: left", 29 | components: [{ 30 | content: "Login ID ", 31 | style: "margin-right: 15px", 32 | }, { 33 | kind: "onyx.Input", 34 | name: "loginId", 35 | style: "float: right", 36 | }, ], 37 | }, { 38 | kind: "onyx.InputDecorator", 39 | style: "text-align: left", 40 | components: [{ 41 | content: "Password ", 42 | style: "margin-right: 15px", 43 | }, { 44 | kind: "onyx.Input", 45 | name: "loginPassword", 46 | type: "password", 47 | style: "float: right", 48 | }], 49 | }, { 50 | kind: "onyx.InputDecorator", 51 | style: "text-align: left", 52 | components: [{ 53 | content: "Re-enter Password ", 54 | style: "margin-right: 15px", 55 | }, { 56 | kind: "onyx.Input", 57 | name: "loginPasswordRepeat", 58 | type: "password", 59 | style: "float: right", 60 | }], 61 | }, { 62 | kind: "FittableColumns", 63 | classes: "enyo-center", 64 | style: "padding: 15px", 65 | components: [{ 66 | kind: "onyx.Button", 67 | style: "margin: 5px 5px 5px auto", 68 | content: "Register", 69 | ontap: "register", 70 | name: "registerButton", 71 | }, { 72 | kind: "onyx.Button", 73 | style: "margin: 5px auto 5px 5px", 74 | content: "Back", 75 | ontap: "goToApp" 76 | }, ], 77 | }, { 78 | kind: "FittableRows", 79 | showing: false, 80 | name: "registerResult", 81 | style: "padding: 10px", 82 | components: [{ 83 | name: "registerStatus", 84 | allowHtml: true, 85 | content: "", 86 | }, ], 87 | }, ], 88 | }, ], 89 | }, ], 90 | }, { 91 | kind: "onyx.Toolbar", 92 | components: [{ 93 | kind: "onyx.Button", 94 | content: "Back", 95 | ontap: "goToApp" 96 | }, ], 97 | }], 98 | 99 | goToApp: function() { 100 | window.location = "./index.html"; 101 | }, 102 | 103 | register: function() { 104 | var loginid = this.$.loginId.getValue(); 105 | var password = this.$.loginPassword.getValue(); 106 | var repeatPassword = this.$.loginPasswordRepeat.getValue(); 107 | if (password != repeatPassword) { 108 | this.$.registerStatus.setContent("Passwords don't match"); 109 | this.$.registerResult.show(); 110 | return; 111 | } 112 | var ajax = new enyo.Ajax({ 113 | method: "POST", 114 | postBody: { 115 | url: "./register.html", 116 | }, 117 | contentType: "application/json", 118 | }); 119 | ajax.go({ 120 | loginId: loginid, 121 | loginPassword: password, 122 | }); 123 | ajax.response(this, "handleLoginResponse"); 124 | console.log("User id and password are: " + loginid + ", " + password); 125 | }, 126 | 127 | handleLoginResponse: function(inSender, inResponse) { 128 | console.log(JSON.stringify(inResponse)); 129 | if (inResponse.result == "success") { 130 | window.location = "./index.html"; 131 | } else if (inResponse.result == "conflict") { 132 | this.$.registerStatus.setContent("Login ID already exists"); 133 | } else { 134 | this.$.registerStatus.setContent("Error occurred. Try again."); 135 | } 136 | this.$.registerResult.show(); 137 | }, 138 | }); 139 | -------------------------------------------------------------------------------- /resource/js/Svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This contains all the local functions to interact with the whiteboard. It also contains 3 | * interfaces to the Connection class. 4 | */ 5 | enyo.kind({ 6 | name: 'WhiteboardSvg', 7 | kind: null, 8 | 9 | cvs: 'undefined', 10 | currentPage: 1, 11 | totalPages: -1, 12 | uid: "", 13 | room: "", 14 | connection: 'undefined', 15 | callback: 'undefined', 16 | 17 | getNumPages: function() { 18 | return this.totalPages; 19 | }, 20 | 21 | getCurrentPage: function() { 22 | return this.currentPage; 23 | }, 24 | 25 | constructor: function(name, width, height, uid, room, page, websocketAddress, callback) { 26 | this.uid = uid; 27 | this.room = room; 28 | this.cvs = new Raphael(name, width, height); 29 | this.connection = new Connection(websocketAddress, this, room); 30 | this.callback = callback; 31 | }, 32 | 33 | /** 34 | * Join specified room 35 | * @param {Object} room 36 | */ 37 | joinRoom: function(room) { 38 | this.room = room; 39 | this.connection.joinRoom(room); 40 | }, 41 | 42 | /** 43 | * Getter for cvs 44 | */ 45 | getCanvas: function() { 46 | return this.cvs; 47 | }, 48 | 49 | /** 50 | * Called when user starts a path 51 | * @param {Object} x 52 | * @param {Object} y 53 | * @param {Object} send 54 | */ 55 | startPath: function(x, y, lc, lw, send) { 56 | if (send) { 57 | this.connection.sendPath({ 58 | oldx: x, 59 | oldy: y, 60 | type: 'touchstart', 61 | lineColor: lc, 62 | lineWidth: lw, 63 | }); 64 | } 65 | }, 66 | 67 | /** 68 | * Called when user continues path (without lifting finger) 69 | */ 70 | continuePath: function(oldx, oldy, x, y, lc, lw, send) { 71 | this.drawAndSendPath('touchmove', oldx, oldy, x, y, lc, lw, send) 72 | }, 73 | 74 | /** 75 | * Called when user lifts finger 76 | */ 77 | endPath: function(oldx, oldy, x, y, lc, lw, send) { 78 | this.drawAndSendPath('touchend', oldx, oldy, x, y, lc, lw, send) 79 | }, 80 | 81 | drawAndSendPath: function(type, oldx, oldy, x, y, lc, lw, send) { 82 | path = "M " + oldx + " " + oldy + " L " + x + " " + y + " Z"; 83 | var p = this.cvs.path(path); 84 | p.attr("stroke", lc); 85 | p.attr("stroke-width", lw) 86 | if (send) { 87 | this.connection.sendPath({ 88 | oldx: oldx, 89 | oldy: oldy, 90 | x: x, 91 | y: y, 92 | type: type, 93 | lineColor: lc, 94 | lineWidth: lw, 95 | }); 96 | } 97 | }, 98 | 99 | /** 100 | * Clear canvas 101 | * @param {Object} send 102 | */ 103 | clear: function(send, reloadImage) { 104 | reloadImage = typeof reloadImage == 'undefined' ? true : reloadImage; 105 | this.cvs.clear(); 106 | if (reloadImage) this.connection.getImage(); 107 | if (send) this.connection.sendClear(); 108 | }, 109 | 110 | /** 111 | * Load an image onto the canvas 112 | * @param {Object} url 113 | */ 114 | loadImage: function(url, width, height) { 115 | this.cvs.image(url, 5, 5, width, height); 116 | }, 117 | 118 | getImage: function() { 119 | images = document.getElementsByTagName("image"); 120 | // TODO More specific targetting of image 121 | if (images.length != 0) { 122 | for (var i = 0; i < images.length; i++) { 123 | images[i].parentNode.removeChild(images[i]); 124 | } 125 | } 126 | this.connection.getImage(this.currentPage); 127 | }, 128 | 129 | /** 130 | * Go to the next page 131 | */ 132 | nextPage: function() { 133 | if (this.currentPage + 1 > this.totalPages) { 134 | // Blank canvas 135 | return false; 136 | } else { 137 | this.currentPage += 1; 138 | this.connection.init(this.uid, this.room, this.currentPage); 139 | return true; 140 | } 141 | }, 142 | 143 | /** 144 | * Go to the previous page 145 | */ 146 | prevPage: function() { 147 | 148 | if (this.currentPage - 1 <= 0) { 149 | // do nothing 150 | return false; 151 | } else { 152 | this.currentPage -= 1; 153 | this.connection.init(this.uid, this.room, this.currentPage); 154 | return true; 155 | } 156 | }, 157 | 158 | gotoPage: function(pageNum) { 159 | this.connection.init(this.uid, this.room, pageNum); 160 | this.currentPage = pageNum; 161 | }, 162 | 163 | newPage: function() { 164 | this.currentPage = this.totalPages + 1; 165 | this.totalPages += 1; 166 | this.connection.newPage(this.uid, this.room, this.currentPage); 167 | }, 168 | 169 | getColor: function() { 170 | return this.color; 171 | }, 172 | 173 | setTotalPages: function(pages) { 174 | this.totalPages = pages; 175 | this.callback(this.totalPages, this.currentPage); 176 | }, 177 | 178 | /** 179 | * Ask server to make video of current whiteboard 180 | */ 181 | makeVideo: function() { 182 | this.connection.makeVideo(); 183 | }, 184 | 185 | drawRectangle: function() { 186 | this.cvs.rect(10, 10, 50, 50); 187 | }, 188 | }); 189 | -------------------------------------------------------------------------------- /resource/js/Upload.js: -------------------------------------------------------------------------------- 1 | enyo.kind({ 2 | name: "Upload", 3 | kind: "FittableRows", 4 | fit: true, 5 | 6 | published: { 7 | uid: 'test', 8 | room: 'one', 9 | }, 10 | 11 | components: [{ 12 | kind: "FittableRows", 13 | fit: true, 14 | components: [{ 15 | kind: "FittableColumns", 16 | style: "padding: 20px; z-index: 0; margin-top: 50px;", 17 | classes: "enyo-center", 18 | width: '500px', 19 | height: '500px', 20 | fit: false, 21 | components: [{ 22 | kind: "onyx.Groupbox", 23 | components: [{ 24 | kind: "onyx.GroupboxHeader", 25 | content: "Select a pdf file to upload" 26 | }, { 27 | content: "Warning: This will replace the file for this room" 28 | }, { 29 | kind: "onyx.InputDecorator", 30 | components: [{ 31 | kind: "enyo.Input", 32 | type: "file", 33 | name: "fileInput", 34 | onchange: "fileSelected", 35 | }], 36 | }, { 37 | kind: "FittableColumns", 38 | classes: "enyo-center", 39 | style: "padding: 15px", 40 | components: [{ 41 | kind: "onyx.Button", 42 | style: "margin: 5px 5px 5px auto", 43 | content: "Upload", 44 | ontap: "uploadFile", 45 | name: "uploadButton", 46 | }, { 47 | kind: "onyx.Button", 48 | style: "margin: 5px auto 5px 5px", 49 | content: "Back", 50 | ontap: "goToApp" 51 | }, ], 52 | }, { 53 | kind: "onyx.ProgressBar", 54 | progress: 0, 55 | name: "fileUploadProgress", 56 | showing: false, 57 | }, { 58 | kind: "FittableRows", 59 | showing: false, 60 | name: "fileInfo", 61 | style: "padding: 10px", 62 | components: [{ 63 | name: "fileName", 64 | style: "color:#808080", 65 | }, { 66 | name: "fileSize", 67 | style: "color:#808080", 68 | }, { 69 | name: "fileType", 70 | style: "color:#808080", 71 | }, { 72 | name: "uploadStatus", 73 | allowHtml: true, 74 | content: "Press the upload button to upload", 75 | }, ], 76 | }, ], 77 | 78 | }, ], 79 | }, ], 80 | }, { 81 | kind: "onyx.Toolbar", 82 | components: [{ 83 | kind: "onyx.Button", 84 | content: "Back", 85 | ontap: "goToApp" 86 | }, ], 87 | }], 88 | 89 | goToApp: function() { 90 | window.location = "./index.html"; 91 | }, 92 | 93 | uploadFile: function(inSender, inEvent) { 94 | 95 | var progressBar = this.$.fileUploadProgress; 96 | var uploadStatus = this.$.uploadStatus; 97 | 98 | uploadStatus.setContent("Uploading file"); 99 | progressBar.show() 100 | 101 | var xhr = new XMLHttpRequest(); 102 | xhr.upload.addEventListener('progress', function(ev) { 103 | progressBar.setProgress((ev.loaded * 100 / ev.total)); 104 | }, false); 105 | 106 | xhr.onreadystatechange = function(ev) { 107 | if (this.readyState == this.DONE) { 108 | progressBar.setProgress(100); 109 | uploadStatus.setContent("File uploaded.
Press back to go back to main page"); 110 | } 111 | }; 112 | xhr.open('POST', "./upload", true); 113 | var file = this.$.fileInput.node.files[0]; 114 | var data = new FormData(); 115 | data.append('room', 'one'); 116 | data.append('file', file); 117 | xhr.send(data); 118 | }, 119 | 120 | fileSelected: function(inSender, inEvent) { 121 | this.$.uploadButton.setDisabled(false); 122 | this.$.uploadStatus.setContent("Press the upload button to upload"); 123 | var file = this.$.fileInput.node.files[0]; 124 | 125 | var fileSize = 0; 126 | if (file.size > 1024 * 1024) fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; 127 | else fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; 128 | 129 | console.log("File name is: " + file.name); 130 | this.$.fileName.setContent('Name: ' + file.name); 131 | this.$.fileSize.setContent('Size: ' + fileSize); 132 | this.$.fileType.setContent('Type: ' + file.type); 133 | 134 | if (file.type != "application/pdf") { 135 | this.$.uploadStatus.setContent("Only pdf files allowed
Please select a pdf file
"); 136 | this.$.uploadButton.setDisabled(true); 137 | } 138 | 139 | this.$.fileInfo.show(); 140 | }, 141 | }); 142 | -------------------------------------------------------------------------------- /resource/js/jsxcompressor.js: -------------------------------------------------------------------------------- 1 | JXG = {exists: (function(undefined){return function(v){return !(v===undefined || v===null);}})()}; 2 | JXG.decompress = function(str) {return unescape((new JXG.Util.Unzip(JXG.Util.Base64.decodeAsArray(str))).unzip()[0][0]);}; 3 | /* 4 | Copyright 2008-2012 5 | Matthias Ehmann, 6 | Michael Gerhaeuser, 7 | Carsten Miller, 8 | Bianca Valentin, 9 | Alfred Wassermann, 10 | Peter Wilfahrt 11 | 12 | This file is part of JSXGraph. 13 | 14 | Dual licensed under the Apache License Version 2.0, or LGPL Version 3 licenses. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with JSXCompressor. If not, see . 18 | 19 | You should have received a copy of the Apache License along with JSXCompressor. 20 | If not, see . 21 | 22 | */ 23 | 24 | /** 25 | * @fileoverview Utilities for uncompressing and base64 decoding 26 | */ 27 | 28 | /** 29 | * @class Util class 30 | * Class for gunzipping, unzipping and base64 decoding of files. 31 | * It is used for reading GEONExT, Geogebra and Intergeo files. 32 | * 33 | * Only Huffman codes are decoded in gunzip. 34 | * The code is based on the source code for gunzip.c by Pasi Ojala 35 | * @see http://www.cs.tut.fi/~albert/Dev/gunzip/gunzip.c 36 | * @see http://www.cs.tut.fi/~albert 37 | */ 38 | JXG.Util = {}; 39 | 40 | /** 41 | * Unzip zip files 42 | */ 43 | JXG.Util.Unzip = function (barray){ 44 | var outputArr = [], 45 | output = "", 46 | debug = false, 47 | gpflags, 48 | files = 0, 49 | unzipped = [], 50 | crc, 51 | buf32k = new Array(32768), 52 | bIdx = 0, 53 | modeZIP=false, 54 | 55 | CRC, SIZE, 56 | 57 | bitReverse = [ 58 | 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 59 | 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, 60 | 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 61 | 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, 62 | 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 63 | 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, 64 | 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 65 | 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, 66 | 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 67 | 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, 68 | 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 69 | 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, 70 | 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 71 | 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, 72 | 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 73 | 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, 74 | 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 75 | 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, 76 | 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 77 | 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, 78 | 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 79 | 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, 80 | 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 81 | 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, 82 | 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 83 | 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, 84 | 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 85 | 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, 86 | 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 87 | 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, 88 | 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 89 | 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff 90 | ], 91 | 92 | cplens = [ 93 | 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 94 | 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 95 | ], 96 | 97 | cplext = [ 98 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 99 | 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99 100 | ], /* 99==invalid */ 101 | 102 | cpdist = [ 103 | 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0007, 0x0009, 0x000d, 104 | 0x0011, 0x0019, 0x0021, 0x0031, 0x0041, 0x0061, 0x0081, 0x00c1, 105 | 0x0101, 0x0181, 0x0201, 0x0301, 0x0401, 0x0601, 0x0801, 0x0c01, 106 | 0x1001, 0x1801, 0x2001, 0x3001, 0x4001, 0x6001 107 | ], 108 | 109 | cpdext = [ 110 | 0, 0, 0, 0, 1, 1, 2, 2, 111 | 3, 3, 4, 4, 5, 5, 6, 6, 112 | 7, 7, 8, 8, 9, 9, 10, 10, 113 | 11, 11, 12, 12, 13, 13 114 | ], 115 | 116 | border = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15], 117 | 118 | bA = barray, 119 | 120 | bytepos=0, 121 | bitpos=0, 122 | bb = 1, 123 | bits=0, 124 | 125 | NAMEMAX = 256, 126 | 127 | nameBuf = [], 128 | 129 | fileout; 130 | 131 | function readByte(){ 132 | bits+=8; 133 | if (bytepos"); 136 | return bA[bytepos++]; 137 | } else 138 | return -1; 139 | }; 140 | 141 | function byteAlign(){ 142 | bb = 1; 143 | }; 144 | 145 | function readBit(){ 146 | var carry; 147 | bits++; 148 | carry = (bb & 1); 149 | bb >>= 1; 150 | if (bb==0){ 151 | bb = readByte(); 152 | carry = (bb & 1); 153 | bb = (bb>>1) | 0x80; 154 | } 155 | return carry; 156 | }; 157 | 158 | function readBits(a) { 159 | var res = 0, 160 | i = a; 161 | 162 | while(i--) { 163 | res = (res<<1) | readBit(); 164 | } 165 | if(a) { 166 | res = bitReverse[res]>>(8-a); 167 | } 168 | return res; 169 | }; 170 | 171 | function flushBuffer(){ 172 | //document.write('FLUSHBUFFER:'+buf32k); 173 | bIdx = 0; 174 | }; 175 | function addBuffer(a){ 176 | SIZE++; 177 | //CRC=updcrc(a,crc); 178 | buf32k[bIdx++] = a; 179 | outputArr.push(String.fromCharCode(a)); 180 | //output+=String.fromCharCode(a); 181 | if(bIdx==0x8000){ 182 | //document.write('ADDBUFFER:'+buf32k); 183 | bIdx=0; 184 | } 185 | }; 186 | 187 | function HufNode() { 188 | this.b0=0; 189 | this.b1=0; 190 | this.jump = null; 191 | this.jumppos = -1; 192 | }; 193 | 194 | var LITERALS = 288; 195 | 196 | var literalTree = new Array(LITERALS); 197 | var distanceTree = new Array(32); 198 | var treepos=0; 199 | var Places = null; 200 | var Places2 = null; 201 | 202 | var impDistanceTree = new Array(64); 203 | var impLengthTree = new Array(64); 204 | 205 | var len = 0; 206 | var fpos = new Array(17); 207 | fpos[0]=0; 208 | var flens; 209 | var fmax; 210 | 211 | function IsPat() { 212 | while (1) { 213 | if (fpos[len] >= fmax) 214 | return -1; 215 | if (flens[fpos[len]] == len) 216 | return fpos[len]++; 217 | fpos[len]++; 218 | } 219 | }; 220 | 221 | function Rec() { 222 | var curplace = Places[treepos]; 223 | var tmp; 224 | if (debug) 225 | document.write("
len:"+len+" treepos:"+treepos); 226 | if(len==17) { //war 17 227 | return -1; 228 | } 229 | treepos++; 230 | len++; 231 | 232 | tmp = IsPat(); 233 | if (debug) 234 | document.write("
IsPat "+tmp); 235 | if(tmp >= 0) { 236 | curplace.b0 = tmp; /* leaf cell for 0-bit */ 237 | if (debug) 238 | document.write("
b0 "+curplace.b0); 239 | } else { 240 | /* Not a Leaf cell */ 241 | curplace.b0 = 0x8000; 242 | if (debug) 243 | document.write("
b0 "+curplace.b0); 244 | if(Rec()) 245 | return -1; 246 | } 247 | tmp = IsPat(); 248 | if(tmp >= 0) { 249 | curplace.b1 = tmp; /* leaf cell for 1-bit */ 250 | if (debug) 251 | document.write("
b1 "+curplace.b1); 252 | curplace.jump = null; /* Just for the display routine */ 253 | } else { 254 | /* Not a Leaf cell */ 255 | curplace.b1 = 0x8000; 256 | if (debug) 257 | document.write("
b1 "+curplace.b1); 258 | curplace.jump = Places[treepos]; 259 | curplace.jumppos = treepos; 260 | if(Rec()) 261 | return -1; 262 | } 263 | len--; 264 | return 0; 265 | }; 266 | 267 | function CreateTree(currentTree, numval, lengths, show) { 268 | var i; 269 | /* Create the Huffman decode tree/table */ 270 | //document.write("
createtree
"); 271 | if (debug) 272 | document.write("currentTree "+currentTree+" numval "+numval+" lengths "+lengths+" show "+show); 273 | Places = currentTree; 274 | treepos=0; 275 | flens = lengths; 276 | fmax = numval; 277 | for (i=0;i<17;i++) 278 | fpos[i] = 0; 279 | len = 0; 280 | if(Rec()) { 281 | //fprintf(stderr, "invalid huffman tree\n"); 282 | if (debug) 283 | alert("invalid huffman tree\n"); 284 | return -1; 285 | } 286 | if (debug){ 287 | document.write('
Tree: '+Places.length); 288 | for (var a=0;a<32;a++){ 289 | document.write("Places["+a+"].b0="+Places[a].b0+"
"); 290 | document.write("Places["+a+"].b1="+Places[a].b1+"
"); 291 | } 292 | } 293 | 294 | /*if(show) { 295 | var tmp; 296 | for(tmp=currentTree;tmpjump?tmp->jump-currentTree:0,(tmp->jump?tmp->jump-currentTree:0)*6+0xcf0); 298 | if(!(tmp.b0 & 0x8000)) { 299 | //fprintf(stdout, " 0x%03x (%c)", tmp->b0,(tmp->b0<256 && isprint(tmp->b0))?tmp->b0:'�'); 300 | } 301 | if(!(tmp.b1 & 0x8000)) { 302 | if((tmp.b0 & 0x8000)) 303 | fprintf(stdout, " "); 304 | fprintf(stdout, " 0x%03x (%c)", tmp->b1,(tmp->b1<256 && isprint(tmp->b1))?tmp->b1:'�'); 305 | } 306 | fprintf(stdout, "\n"); 307 | } 308 | }*/ 309 | return 0; 310 | }; 311 | 312 | function DecodeValue(currentTree) { 313 | var len, i, 314 | xtreepos=0, 315 | X = currentTree[xtreepos], 316 | b; 317 | 318 | /* decode one symbol of the data */ 319 | while(1) { 320 | b=readBit(); 321 | if (debug) 322 | document.write("b="+b); 323 | if(b) { 324 | if(!(X.b1 & 0x8000)){ 325 | if (debug) 326 | document.write("ret1"); 327 | return X.b1; /* If leaf node, return data */ 328 | } 329 | X = X.jump; 330 | len = currentTree.length; 331 | for (i=0;i>1); 429 | if(j > 23) { 430 | j = (j<<1) | readBit(); /* 48..255 */ 431 | 432 | if(j > 199) { /* 200..255 */ 433 | j -= 128; /* 72..127 */ 434 | j = (j<<1) | readBit(); /* 144..255 << */ 435 | } else { /* 48..199 */ 436 | j -= 48; /* 0..151 */ 437 | if(j > 143) { 438 | j = j+136; /* 280..287 << */ 439 | /* 0..143 << */ 440 | } 441 | } 442 | } else { /* 0..23 */ 443 | j += 256; /* 256..279 << */ 444 | } 445 | if(j < 256) { 446 | addBuffer(j); 447 | //document.write("out:"+String.fromCharCode(j)); 448 | /*fprintf(errfp, "@%d %02x\n", SIZE, j);*/ 449 | } else if(j == 256) { 450 | /* EOF */ 451 | break; 452 | } else { 453 | var len, dist; 454 | 455 | j -= 256 + 1; /* bytes + EOF */ 456 | len = readBits(cplext[j]) + cplens[j]; 457 | 458 | j = bitReverse[readBits(5)]>>3; 459 | if(cpdext[j] > 8) { 460 | dist = readBits(8); 461 | dist |= (readBits(cpdext[j]-8)<<8); 462 | } else { 463 | dist = readBits(cpdext[j]); 464 | } 465 | dist += cpdist[j]; 466 | 467 | /*fprintf(errfp, "@%d (l%02x,d%04x)\n", SIZE, len, dist);*/ 468 | for(j=0;jparam: "+literalCodes+" "+distCodes+" "+lenCodes+"
"); 484 | for(j=0; j<19; j++) { 485 | ll[j] = 0; 486 | } 487 | 488 | // Get the decode tree code lengths 489 | 490 | //document.write("
"); 491 | for(j=0; jll:'+ll); 497 | len = distanceTree.length; 498 | for (i=0; idistanceTree"); 506 | for(var a=0;a"+distanceTree[a].b0+" "+distanceTree[a].b1+" "+distanceTree[a].jump+" "+distanceTree[a].jumppos); 508 | /*if (distanceTree[a].jumppos!=-1) 509 | document.write(" "+distanceTree[a].jump.b0+" "+distanceTree[a].jump.b1); 510 | */ 511 | } 512 | } 513 | //document.write('
tree created'); 514 | 515 | //read in literal and distance code lengths 516 | n = literalCodes + distCodes; 517 | i = 0; 518 | var z=-1; 519 | if (debug) 520 | document.write("
n="+n+" bits: "+bits+"
"); 521 | while(i < n) { 522 | z++; 523 | j = DecodeValue(distanceTree); 524 | if (debug) 525 | document.write("
"+z+" i:"+i+" decode: "+j+" bits "+bits+"
"); 526 | if(j<16) { // length of code in bits (0..15) 527 | ll[i++] = j; 528 | } else if(j==16) { // repeat last length 3 to 6 times 529 | var l; 530 | j = 3 + readBits(2); 531 | if(i+j > n) { 532 | flushBuffer(); 533 | return 1; 534 | } 535 | l = i ? ll[i-1] : 0; 536 | while(j--) { 537 | ll[i++] = l; 538 | } 539 | } else { 540 | if(j==17) { // 3 to 10 zero length codes 541 | j = 3 + readBits(3); 542 | } else { // j == 18: 11 to 138 zero length codes 543 | j = 11 + readBits(7); 544 | } 545 | if(i+j > n) { 546 | flushBuffer(); 547 | return 1; 548 | } 549 | while(j--) { 550 | ll[i++] = 0; 551 | } 552 | } 553 | } 554 | /*for(j=0; jliteralTree"); 581 | while(1) { 582 | j = DecodeValue(literalTree); 583 | if(j >= 256) { // In C64: if carry set 584 | var len, dist; 585 | j -= 256; 586 | if(j == 0) { 587 | // EOF 588 | break; 589 | } 590 | j--; 591 | len = readBits(cplext[j]) + cplens[j]; 592 | 593 | j = DecodeValue(distanceTree); 594 | if(cpdext[j] > 8) { 595 | dist = readBits(8); 596 | dist |= (readBits(cpdext[j]-8)<<8); 597 | } else { 598 | dist = readBits(cpdext[j]); 599 | } 600 | dist += cpdist[j]; 601 | while(len--) { 602 | var c = buf32k[(bIdx - dist) & 0x7fff]; 603 | addBuffer(c); 604 | } 605 | } else { 606 | addBuffer(j); 607 | } 608 | } 609 | } 610 | } while(!last); 611 | flushBuffer(); 612 | 613 | byteAlign(); 614 | return 0; 615 | }; 616 | 617 | JXG.Util.Unzip.prototype.unzipFile = function(name) { 618 | var i; 619 | this.unzip(); 620 | //alert(unzipped[0][1]); 621 | for (i=0;i"); 648 | } 649 | */ 650 | //alert(bA); 651 | nextFile(); 652 | return unzipped; 653 | }; 654 | 655 | function nextFile(){ 656 | if (debug) 657 | alert("NEXTFILE"); 658 | outputArr = []; 659 | var tmp = []; 660 | modeZIP = false; 661 | tmp[0] = readByte(); 662 | tmp[1] = readByte(); 663 | if (debug) 664 | alert("type: "+tmp[0]+" "+tmp[1]); 665 | if (tmp[0] == parseInt("78",16) && tmp[1] == parseInt("da",16)){ //GZIP 666 | if (debug) 667 | alert("GEONExT-GZIP"); 668 | DeflateLoop(); 669 | if (debug) 670 | alert(outputArr.join('')); 671 | unzipped[files] = new Array(2); 672 | unzipped[files][0] = outputArr.join(''); 673 | unzipped[files][1] = "geonext.gxt"; 674 | files++; 675 | } 676 | if (tmp[0] == parseInt("1f",16) && tmp[1] == parseInt("8b",16)){ //GZIP 677 | if (debug) 678 | alert("GZIP"); 679 | //DeflateLoop(); 680 | skipdir(); 681 | if (debug) 682 | alert(outputArr.join('')); 683 | unzipped[files] = new Array(2); 684 | unzipped[files][0] = outputArr.join(''); 685 | unzipped[files][1] = "file"; 686 | files++; 687 | } 688 | if (tmp[0] == parseInt("50",16) && tmp[1] == parseInt("4b",16)){ //ZIP 689 | modeZIP = true; 690 | tmp[2] = readByte(); 691 | tmp[3] = readByte(); 692 | if (tmp[2] == parseInt("3",16) && tmp[3] == parseInt("4",16)){ 693 | //MODE_ZIP 694 | tmp[0] = readByte(); 695 | tmp[1] = readByte(); 696 | if (debug) 697 | alert("ZIP-Version: "+tmp[1]+" "+tmp[0]/10+"."+tmp[0]%10); 698 | 699 | gpflags = readByte(); 700 | gpflags |= (readByte()<<8); 701 | if (debug) 702 | alert("gpflags: "+gpflags); 703 | 704 | var method = readByte(); 705 | method |= (readByte()<<8); 706 | if (debug) 707 | alert("method: "+method); 708 | 709 | readByte(); 710 | readByte(); 711 | readByte(); 712 | readByte(); 713 | 714 | var crc = readByte(); 715 | crc |= (readByte()<<8); 716 | crc |= (readByte()<<16); 717 | crc |= (readByte()<<24); 718 | 719 | var compSize = readByte(); 720 | compSize |= (readByte()<<8); 721 | compSize |= (readByte()<<16); 722 | compSize |= (readByte()<<24); 723 | 724 | var size = readByte(); 725 | size |= (readByte()<<8); 726 | size |= (readByte()<<16); 727 | size |= (readByte()<<24); 728 | 729 | if (debug) 730 | alert("local CRC: "+crc+"\nlocal Size: "+size+"\nlocal CompSize: "+compSize); 731 | 732 | var filelen = readByte(); 733 | filelen |= (readByte()<<8); 734 | 735 | var extralen = readByte(); 736 | extralen |= (readByte()<<8); 737 | 738 | if (debug) 739 | alert("filelen "+filelen); 740 | i = 0; 741 | nameBuf = []; 742 | while (filelen--){ 743 | var c = readByte(); 744 | if (c == "/" | c ==":"){ 745 | i = 0; 746 | } else if (i < NAMEMAX-1) 747 | nameBuf[i++] = String.fromCharCode(c); 748 | } 749 | if (debug) 750 | alert("nameBuf: "+nameBuf); 751 | 752 | //nameBuf[i] = "\0"; 753 | if (!fileout) 754 | fileout = nameBuf; 755 | 756 | var i = 0; 757 | while (i < extralen){ 758 | c = readByte(); 759 | i++; 760 | } 761 | 762 | CRC = 0xffffffff; 763 | SIZE = 0; 764 | 765 | if (size = 0 && fileOut.charAt(fileout.length-1)=="/"){ 766 | //skipdir 767 | if (debug) 768 | alert("skipdir"); 769 | } 770 | if (method == 8){ 771 | DeflateLoop(); 772 | if (debug) 773 | alert(outputArr.join('')); 774 | unzipped[files] = new Array(2); 775 | unzipped[files][0] = outputArr.join(''); 776 | unzipped[files][1] = nameBuf.join(''); 777 | files++; 778 | //return outputArr.join(''); 779 | } 780 | skipdir(); 781 | } 782 | } 783 | }; 784 | 785 | function skipdir(){ 786 | var crc, 787 | tmp = [], 788 | compSize, size, os, i, c; 789 | 790 | if ((gpflags & 8)) { 791 | tmp[0] = readByte(); 792 | tmp[1] = readByte(); 793 | tmp[2] = readByte(); 794 | tmp[3] = readByte(); 795 | 796 | if (tmp[0] == parseInt("50",16) && 797 | tmp[1] == parseInt("4b",16) && 798 | tmp[2] == parseInt("07",16) && 799 | tmp[3] == parseInt("08",16)) 800 | { 801 | crc = readByte(); 802 | crc |= (readByte()<<8); 803 | crc |= (readByte()<<16); 804 | crc |= (readByte()<<24); 805 | } else { 806 | crc = tmp[0] | (tmp[1]<<8) | (tmp[2]<<16) | (tmp[3]<<24); 807 | } 808 | 809 | compSize = readByte(); 810 | compSize |= (readByte()<<8); 811 | compSize |= (readByte()<<16); 812 | compSize |= (readByte()<<24); 813 | 814 | size = readByte(); 815 | size |= (readByte()<<8); 816 | size |= (readByte()<<16); 817 | size |= (readByte()<<24); 818 | 819 | if (debug) 820 | alert("CRC:"); 821 | } 822 | 823 | if (modeZIP) 824 | nextFile(); 825 | 826 | tmp[0] = readByte(); 827 | if (tmp[0] != 8) { 828 | if (debug) 829 | alert("Unknown compression method!"); 830 | return 0; 831 | } 832 | 833 | gpflags = readByte(); 834 | if (debug){ 835 | if ((gpflags & ~(parseInt("1f",16)))) 836 | alert("Unknown flags set!"); 837 | } 838 | 839 | readByte(); 840 | readByte(); 841 | readByte(); 842 | readByte(); 843 | 844 | readByte(); 845 | os = readByte(); 846 | 847 | if ((gpflags & 4)){ 848 | tmp[0] = readByte(); 849 | tmp[2] = readByte(); 850 | len = tmp[0] + 256*tmp[1]; 851 | if (debug) 852 | alert("Extra field size: "+len); 853 | for (i=0;ihttp://www.webtoolkit.info/ 904 | */ 905 | JXG.Util.Base64 = { 906 | 907 | // private property 908 | _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 909 | 910 | // public method for encoding 911 | encode : function (input) { 912 | var output = [], 913 | chr1, chr2, chr3, enc1, enc2, enc3, enc4, 914 | i = 0; 915 | 916 | input = JXG.Util.Base64._utf8_encode(input); 917 | 918 | while (i < input.length) { 919 | 920 | chr1 = input.charCodeAt(i++); 921 | chr2 = input.charCodeAt(i++); 922 | chr3 = input.charCodeAt(i++); 923 | 924 | enc1 = chr1 >> 2; 925 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 926 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 927 | enc4 = chr3 & 63; 928 | 929 | if (isNaN(chr2)) { 930 | enc3 = enc4 = 64; 931 | } else if (isNaN(chr3)) { 932 | enc4 = 64; 933 | } 934 | 935 | output.push([this._keyStr.charAt(enc1), 936 | this._keyStr.charAt(enc2), 937 | this._keyStr.charAt(enc3), 938 | this._keyStr.charAt(enc4)].join('')); 939 | } 940 | 941 | return output.join(''); 942 | }, 943 | 944 | // public method for decoding 945 | decode : function (input, utf8) { 946 | var output = [], 947 | chr1, chr2, chr3, 948 | enc1, enc2, enc3, enc4, 949 | i = 0; 950 | 951 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 952 | 953 | while (i < input.length) { 954 | 955 | enc1 = this._keyStr.indexOf(input.charAt(i++)); 956 | enc2 = this._keyStr.indexOf(input.charAt(i++)); 957 | enc3 = this._keyStr.indexOf(input.charAt(i++)); 958 | enc4 = this._keyStr.indexOf(input.charAt(i++)); 959 | 960 | chr1 = (enc1 << 2) | (enc2 >> 4); 961 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 962 | chr3 = ((enc3 & 3) << 6) | enc4; 963 | 964 | output.push(String.fromCharCode(chr1)); 965 | 966 | if (enc3 != 64) { 967 | output.push(String.fromCharCode(chr2)); 968 | } 969 | if (enc4 != 64) { 970 | output.push(String.fromCharCode(chr3)); 971 | } 972 | } 973 | 974 | output = output.join(''); 975 | 976 | if (utf8) { 977 | output = JXG.Util.Base64._utf8_decode(output); 978 | } 979 | return output; 980 | 981 | }, 982 | 983 | // private method for UTF-8 encoding 984 | _utf8_encode : function (string) { 985 | string = string.replace(/\r\n/g,"\n"); 986 | var utftext = ""; 987 | 988 | for (var n = 0; n < string.length; n++) { 989 | 990 | var c = string.charCodeAt(n); 991 | 992 | if (c < 128) { 993 | utftext += String.fromCharCode(c); 994 | } 995 | else if((c > 127) && (c < 2048)) { 996 | utftext += String.fromCharCode((c >> 6) | 192); 997 | utftext += String.fromCharCode((c & 63) | 128); 998 | } 999 | else { 1000 | utftext += String.fromCharCode((c >> 12) | 224); 1001 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 1002 | utftext += String.fromCharCode((c & 63) | 128); 1003 | } 1004 | 1005 | } 1006 | 1007 | return utftext; 1008 | }, 1009 | 1010 | // private method for UTF-8 decoding 1011 | _utf8_decode : function (utftext) { 1012 | var string = [], 1013 | i = 0, 1014 | c = 0, c2 = 0, c3 = 0; 1015 | 1016 | while ( i < utftext.length ) { 1017 | c = utftext.charCodeAt(i); 1018 | if (c < 128) { 1019 | string.push(String.fromCharCode(c)); 1020 | i++; 1021 | } 1022 | else if((c > 191) && (c < 224)) { 1023 | c2 = utftext.charCodeAt(i+1); 1024 | string.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63))); 1025 | i += 2; 1026 | } 1027 | else { 1028 | c2 = utftext.charCodeAt(i+1); 1029 | c3 = utftext.charCodeAt(i+2); 1030 | string.push(String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))); 1031 | i += 3; 1032 | } 1033 | } 1034 | return string.join(''); 1035 | }, 1036 | 1037 | _destrip: function (stripped, wrap){ 1038 | var lines = [], lineno, i, 1039 | destripped = []; 1040 | 1041 | if (wrap==null) 1042 | wrap = 76; 1043 | 1044 | stripped.replace(/ /g, ""); 1045 | lineno = stripped.length / wrap; 1046 | for (i = 0; i < lineno; i++) 1047 | lines[i]=stripped.substr(i * wrap, wrap); 1048 | if (lineno != stripped.length / wrap) 1049 | lines[lines.length]=stripped.substr(lineno * wrap, stripped.length-(lineno * wrap)); 1050 | 1051 | for (i = 0; i < lines.length; i++) 1052 | destripped.push(lines[i]); 1053 | return destripped.join('\n'); 1054 | }, 1055 | 1056 | decodeAsArray: function (input){ 1057 | var dec = this.decode(input), 1058 | ar = [], i; 1059 | for (i=0;i255){ 1076 | switch (c) { 1077 | case 8364: c=128; 1078 | break; 1079 | case 8218: c=130; 1080 | break; 1081 | case 402: c=131; 1082 | break; 1083 | case 8222: c=132; 1084 | break; 1085 | case 8230: c=133; 1086 | break; 1087 | case 8224: c=134; 1088 | break; 1089 | case 8225: c=135; 1090 | break; 1091 | case 710: c=136; 1092 | break; 1093 | case 8240: c=137; 1094 | break; 1095 | case 352: c=138; 1096 | break; 1097 | case 8249: c=139; 1098 | break; 1099 | case 338: c=140; 1100 | break; 1101 | case 381: c=142; 1102 | break; 1103 | case 8216: c=145; 1104 | break; 1105 | case 8217: c=146; 1106 | break; 1107 | case 8220: c=147; 1108 | break; 1109 | case 8221: c=148; 1110 | break; 1111 | case 8226: c=149; 1112 | break; 1113 | case 8211: c=150; 1114 | break; 1115 | case 8212: c=151; 1116 | break; 1117 | case 732: c=152; 1118 | break; 1119 | case 8482: c=153; 1120 | break; 1121 | case 353: c=154; 1122 | break; 1123 | case 8250: c=155; 1124 | break; 1125 | case 339: c=156; 1126 | break; 1127 | case 382: c=158; 1128 | break; 1129 | case 376: c=159; 1130 | break; 1131 | default: 1132 | break; 1133 | } 1134 | } 1135 | return c; 1136 | }; 1137 | 1138 | /** 1139 | * Decoding string into utf-8 1140 | * @param {String} string to decode 1141 | * @return {String} utf8 decoded string 1142 | */ 1143 | JXG.Util.utf8Decode = function(utftext) { 1144 | var string = []; 1145 | var i = 0; 1146 | var c = 0, c1 = 0, c2 = 0, c3; 1147 | if (!JXG.exists(utftext)) return ''; 1148 | 1149 | while ( i < utftext.length ) { 1150 | c = utftext.charCodeAt(i); 1151 | 1152 | if (c < 128) { 1153 | string.push(String.fromCharCode(c)); 1154 | i++; 1155 | } else if((c > 191) && (c < 224)) { 1156 | c2 = utftext.charCodeAt(i+1); 1157 | string.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63))); 1158 | i += 2; 1159 | } else { 1160 | c2 = utftext.charCodeAt(i+1); 1161 | c3 = utftext.charCodeAt(i+2); 1162 | string.push(String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))); 1163 | i += 3; 1164 | } 1165 | }; 1166 | return string.join(''); 1167 | }; 1168 | 1169 | /** 1170 | * Generate a random uuid. 1171 | * http://www.broofa.com 1172 | * mailto:robert@broofa.com 1173 | * 1174 | * Copyright (c) 2010 Robert Kieffer 1175 | * Dual licensed under the MIT and GPL licenses. 1176 | * 1177 | * EXAMPLES: 1178 | * >>> Math.uuid() 1179 | * "92329D39-6F5C-4520-ABFC-AAB64544E172" 1180 | */ 1181 | JXG.Util.genUUID = function() { 1182 | // Private array of chars to use 1183 | var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''), 1184 | uuid = new Array(36), rnd=0, r; 1185 | 1186 | for (var i = 0; i < 36; i++) { 1187 | if (i==8 || i==13 || i==18 || i==23) { 1188 | uuid[i] = '-'; 1189 | } else if (i==14) { 1190 | uuid[i] = '4'; 1191 | } else { 1192 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; 1193 | r = rnd & 0xf; 1194 | rnd = rnd >> 4; 1195 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 1196 | } 1197 | } 1198 | 1199 | return uuid.join(''); 1200 | }; 1201 | 1202 | -------------------------------------------------------------------------------- /resource/js/package.js: -------------------------------------------------------------------------------- 1 | enyo.depends( 2 | "$lib/layout", 3 | "$lib/onyx", 4 | "App.js", 5 | "Upload.js", 6 | "Svg.js", 7 | "Connection.js", 8 | "Login.js", 9 | "Register.js" 10 | // Cannot have a trailing comma! 11 | ); 12 | -------------------------------------------------------------------------------- /resource/js/raphael-min.js: -------------------------------------------------------------------------------- 1 | // ┌────────────────────────────────────────────────────────────────────┐ \\ 2 | // │ Raphaël 2.1.0 - JavaScript Vector Library │ \\ 3 | // ├────────────────────────────────────────────────────────────────────┤ \\ 4 | // │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ 5 | // │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ 6 | // ├────────────────────────────────────────────────────────────────────┤ \\ 7 | // │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ 8 | // └────────────────────────────────────────────────────────────────────┘ \\ 9 | 10 | (function(a){var b="0.3.4",c="hasOwnProperty",d=/[\.\/]/,e="*",f=function(){},g=function(a,b){return a-b},h,i,j={n:{}},k=function(a,b){var c=j,d=i,e=Array.prototype.slice.call(arguments,2),f=k.listeners(a),l=0,m=!1,n,o=[],p={},q=[],r=h,s=[];h=a,i=0;for(var t=0,u=f.length;tf*b.top){e=b.percents[y],p=b.percents[y-1]||0,t=t/b.top*(e-p),o=b.percents[y+1],j=b.anim[e];break}f&&d.attr(b.anim[b.percents[y]])}if(!!j){if(!k){for(var A in j)if(j[g](A))if(U[g](A)||d.paper.customAttributes[g](A)){u[A]=d.attr(A),u[A]==null&&(u[A]=T[A]),v[A]=j[A];switch(U[A]){case C:w[A]=(v[A]-u[A])/t;break;case"colour":u[A]=a.getRGB(u[A]);var B=a.getRGB(v[A]);w[A]={r:(B.r-u[A].r)/t,g:(B.g-u[A].g)/t,b:(B.b-u[A].b)/t};break;case"path":var D=bR(u[A],v[A]),E=D[1];u[A]=D[0],w[A]=[];for(y=0,z=u[A].length;yd)return d;while(cf?c=e:d=e,e=(d-c)/2+c}return e}function n(a,b){var c=o(a,b);return((l*c+k)*c+j)*c}function m(a){return((i*a+h)*a+g)*a}var g=3*b,h=3*(d-b)-g,i=1-g-h,j=3*c,k=3*(e-c)-j,l=1-j-k;return n(a,1/(200*f))}function cq(){return this.x+q+this.y+q+this.width+" × "+this.height}function cp(){return this.x+q+this.y}function cb(a,b,c,d,e,f){a!=null?(this.a=+a,this.b=+b,this.c=+c,this.d=+d,this.e=+e,this.f=+f):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0)}function bH(b,c,d){b=a._path2curve(b),c=a._path2curve(c);var e,f,g,h,i,j,k,l,m,n,o=d?0:[];for(var p=0,q=b.length;p=0&&y<=1&&A>=0&&A<=1&&(d?n++:n.push({x:x.x,y:x.y,t1:y,t2:A}))}}return n}function bF(a,b){return bG(a,b,1)}function bE(a,b){return bG(a,b)}function bD(a,b,c,d,e,f,g,h){if(!(x(a,c)x(e,g)||x(b,d)x(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(!k)return;var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(n<+y(a,c).toFixed(2)||n>+x(a,c).toFixed(2)||n<+y(e,g).toFixed(2)||n>+x(e,g).toFixed(2)||o<+y(b,d).toFixed(2)||o>+x(b,d).toFixed(2)||o<+y(f,h).toFixed(2)||o>+x(f,h).toFixed(2))return;return{x:l,y:m}}}function bC(a,b,c,d,e,f,g,h,i){if(!(i<0||bB(a,b,c,d,e,f,g,h)n)k/=2,l+=(m1?1:i<0?0:i;var j=i/2,k=12,l=[-0.1252,.1252,-0.3678,.3678,-0.5873,.5873,-0.7699,.7699,-0.9041,.9041,-0.9816,.9816],m=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],n=0;for(var o=0;od;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}function bx(){return this.hex}function bv(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),h=d.cache=d.cache||{},i=d.count=d.count||[];if(h[g](f)){bu(i,f);return c?c(h[f]):h[f]}i.length>=1e3&&delete h[i.shift()],i.push(f),h[f]=a[m](b,e);return c?c(h[f]):h[f]}return d}function bu(a,b){for(var c=0,d=a.length;c',bl=bk.firstChild,bl.style.behavior="url(#default#VML)";if(!bl||typeof bl.adj!="object")return a.type=p;bk=null}a.svg=!(a.vml=a.type=="VML"),a._Paper=j,a.fn=k=j.prototype=a.prototype,a._id=0,a._oid=0,a.is=function(a,b){b=v.call(b);if(b=="finite")return!M[g](+a);if(b=="array")return a instanceof Array;return b=="null"&&a===null||b==typeof a&&a!==null||b=="object"&&a===Object(a)||b=="array"&&Array.isArray&&Array.isArray(a)||H.call(a).slice(8,-1).toLowerCase()==b},a.angle=function(b,c,d,e,f,g){if(f==null){var h=b-d,i=c-e;if(!h&&!i)return 0;return(180+w.atan2(-i,-h)*180/B+360)%360}return a.angle(b,c,f,g)-a.angle(d,e,f,g)},a.rad=function(a){return a%360*B/180},a.deg=function(a){return a*180/B%360},a.snapTo=function(b,c,d){d=a.is(d,"finite")?d:10;if(a.is(b,E)){var e=b.length;while(e--)if(z(b[e]-c)<=d)return b[e]}else{b=+b;var f=c%b;if(fb-d)return c-f+b}return c};var bn=a.createUUID=function(a,b){return function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(a,b).toUpperCase()}}(/[xy]/g,function(a){var b=w.random()*16|0,c=a=="x"?b:b&3|8;return c.toString(16)});a.setWindow=function(b){eve("raphael.setWindow",a,h.win,b),h.win=b,h.doc=h.win.document,a._engine.initWin&&a._engine.initWin(h.win)};var bo=function(b){if(a.vml){var c=/^\s+|\s+$/g,d;try{var e=new ActiveXObject("htmlfile");e.write(""),e.close(),d=e.body}catch(f){d=createPopup().document.body}var g=d.createTextRange();bo=bv(function(a){try{d.style.color=r(a).replace(c,p);var b=g.queryCommandValue("ForeColor");b=(b&255)<<16|b&65280|(b&16711680)>>>16;return"#"+("000000"+b.toString(16)).slice(-6)}catch(e){return"none"}})}else{var i=h.doc.createElement("i");i.title="Raphaël Colour Picker",i.style.display="none",h.doc.body.appendChild(i),bo=bv(function(a){i.style.color=a;return h.doc.defaultView.getComputedStyle(i,p).getPropertyValue("color")})}return bo(b)},bp=function(){return"hsb("+[this.h,this.s,this.b]+")"},bq=function(){return"hsl("+[this.h,this.s,this.l]+")"},br=function(){return this.hex},bs=function(b,c,d){c==null&&a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b&&(d=b.b,c=b.g,b=b.r);if(c==null&&a.is(b,D)){var e=a.getRGB(b);b=e.r,c=e.g,d=e.b}if(b>1||c>1||d>1)b/=255,c/=255,d/=255;return[b,c,d]},bt=function(b,c,d,e){b*=255,c*=255,d*=255;var f={r:b,g:c,b:d,hex:a.rgb(b,c,d),toString:br};a.is(e,"finite")&&(f.opacity=e);return f};a.color=function(b){var c;a.is(b,"object")&&"h"in b&&"s"in b&&"b"in b?(c=a.hsb2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):a.is(b,"object")&&"h"in b&&"s"in b&&"l"in b?(c=a.hsl2rgb(b),b.r=c.r,b.g=c.g,b.b=c.b,b.hex=c.hex):(a.is(b,"string")&&(b=a.getRGB(b)),a.is(b,"object")&&"r"in b&&"g"in b&&"b"in b?(c=a.rgb2hsl(b),b.h=c.h,b.s=c.s,b.l=c.l,c=a.rgb2hsb(b),b.v=c.b):(b={hex:"none"},b.r=b.g=b.b=b.h=b.s=b.v=b.l=-1)),b.toString=br;return b},a.hsb2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,a=a.h,d=a.o),a*=360;var e,f,g,h,i;a=a%360/60,i=c*b,h=i*(1-z(a%2-1)),e=f=g=c-i,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.hsl2rgb=function(a,b,c,d){this.is(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h);if(a>1||b>1||c>1)a/=360,b/=100,c/=100;a*=360;var e,f,g,h,i;a=a%360/60,i=2*b*(c<.5?c:1-c),h=i*(1-z(a%2-1)),e=f=g=c-i/2,a=~~a,e+=[i,h,0,0,h,i][a],f+=[h,i,i,h,0,0][a],g+=[0,0,h,i,i,h][a];return bt(e,f,g,d)},a.rgb2hsb=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;f=x(a,b,c),g=f-y(a,b,c),d=g==0?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=g==0?0:g/f;return{h:d,s:e,b:f,toString:bp}},a.rgb2hsl=function(a,b,c){c=bs(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;g=x(a,b,c),h=y(a,b,c),i=g-h,d=i==0?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=i==0?0:f<.5?i/(2*f):i/(2-2*f);return{h:d,s:e,l:f,toString:bq}},a._path2string=function(){return this.join(",").replace(Y,"$1")};var bw=a._preload=function(a,b){var c=h.doc.createElement("img");c.style.cssText="position:absolute;left:-9999em;top:-9999em",c.onload=function(){b.call(this),this.onload=null,h.doc.body.removeChild(this)},c.onerror=function(){h.doc.body.removeChild(this)},h.doc.body.appendChild(c),c.src=a};a.getRGB=bv(function(b){if(!b||!!((b=r(b)).indexOf("-")+1))return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx};if(b=="none")return{r:-1,g:-1,b:-1,hex:"none",toString:bx};!X[g](b.toLowerCase().substring(0,2))&&b.charAt()!="#"&&(b=bo(b));var c,d,e,f,h,i,j,k=b.match(L);if(k){k[2]&&(f=R(k[2].substring(5),16),e=R(k[2].substring(3,5),16),d=R(k[2].substring(1,3),16)),k[3]&&(f=R((i=k[3].charAt(3))+i,16),e=R((i=k[3].charAt(2))+i,16),d=R((i=k[3].charAt(1))+i,16)),k[4]&&(j=k[4][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),k[1].toLowerCase().slice(0,4)=="rgba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100));if(k[5]){j=k[5][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsba"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsb2rgb(d,e,f,h)}if(k[6]){j=k[6][s](W),d=Q(j[0]),j[0].slice(-1)=="%"&&(d*=2.55),e=Q(j[1]),j[1].slice(-1)=="%"&&(e*=2.55),f=Q(j[2]),j[2].slice(-1)=="%"&&(f*=2.55),(j[0].slice(-3)=="deg"||j[0].slice(-1)=="°")&&(d/=360),k[1].toLowerCase().slice(0,4)=="hsla"&&(h=Q(j[3])),j[3]&&j[3].slice(-1)=="%"&&(h/=100);return a.hsl2rgb(d,e,f,h)}k={r:d,g:e,b:f,toString:bx},k.hex="#"+(16777216|f|e<<8|d<<16).toString(16).slice(1),a.is(h,"finite")&&(k.opacity=h);return k}return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:bx}},a),a.hsb=bv(function(b,c,d){return a.hsb2rgb(b,c,d).hex}),a.hsl=bv(function(b,c,d){return a.hsl2rgb(b,c,d).hex}),a.rgb=bv(function(a,b,c){return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)}),a.getColor=function(a){var b=this.getColor.start=this.getColor.start||{h:0,s:1,b:a||.75},c=this.hsb2rgb(b.h,b.s,b.b);b.h+=.075,b.h>1&&(b.h=0,b.s-=.2,b.s<=0&&(this.getColor.start={h:0,s:1,b:b.b}));return c.hex},a.getColor.reset=function(){delete this.start},a.parsePathString=function(b){if(!b)return null;var c=bz(b);if(c.arr)return bJ(c.arr);var d={a:7,c:6,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,z:0},e=[];a.is(b,E)&&a.is(b[0],E)&&(e=bJ(b)),e.length||r(b).replace(Z,function(a,b,c){var f=[],g=b.toLowerCase();c.replace(_,function(a,b){b&&f.push(+b)}),g=="m"&&f.length>2&&(e.push([b][n](f.splice(0,2))),g="l",b=b=="m"?"l":"L");if(g=="r")e.push([b][n](f));else while(f.length>=d[g]){e.push([b][n](f.splice(0,d[g])));if(!d[g])break}}),e.toString=a._path2string,c.arr=bJ(e);return e},a.parseTransformString=bv(function(b){if(!b)return null;var c={r:3,s:4,t:2,m:6},d=[];a.is(b,E)&&a.is(b[0],E)&&(d=bJ(b)),d.length||r(b).replace($,function(a,b,c){var e=[],f=v.call(b);c.replace(_,function(a,b){b&&e.push(+b)}),d.push([b][n](e))}),d.toString=a._path2string;return d});var bz=function(a){var b=bz.ps=bz.ps||{};b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[g](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])});return b[a]};a.findDotsAtSegment=function(a,b,c,d,e,f,g,h,i){var j=1-i,k=A(j,3),l=A(j,2),m=i*i,n=m*i,o=k*a+l*3*i*c+j*3*i*i*e+n*g,p=k*b+l*3*i*d+j*3*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,x=j*e+i*g,y=j*f+i*h,z=90-w.atan2(q-s,r-t)*180/B;(q>s||r=a.x&&b<=a.x2&&c>=a.y&&c<=a.y2},a.isBBoxIntersect=function(b,c){var d=a.isPointInsideBBox;return d(c,b.x,b.y)||d(c,b.x2,b.y)||d(c,b.x,b.y2)||d(c,b.x2,b.y2)||d(b,c.x,c.y)||d(b,c.x2,c.y)||d(b,c.x,c.y2)||d(b,c.x2,c.y2)||(b.xc.x||c.xb.x)&&(b.yc.y||c.yb.y)},a.pathIntersection=function(a,b){return bH(a,b)},a.pathIntersectionNumber=function(a,b){return bH(a,b,1)},a.isPointInsidePath=function(b,c,d){var e=a.pathBBox(b);return a.isPointInsideBBox(e,c,d)&&bH(b,[["M",c,d],["H",e.x2+10]],1)%2==1},a._removedFactory=function(a){return function(){eve("raphael.log",null,"Raphaël: you are calling to method “"+a+"” of removed object",a)}};var bI=a.pathBBox=function(a){var b=bz(a);if(b.bbox)return b.bbox;if(!a)return{x:0,y:0,width:0,height:0,x2:0,y2:0};a=bR(a);var c=0,d=0,e=[],f=[],g;for(var h=0,i=a.length;h1&&(v=w.sqrt(v),c=v*c,d=v*d);var x=c*c,y=d*d,A=(f==g?-1:1)*w.sqrt(z((x*y-x*u*u-y*t*t)/(x*u*u+y*t*t))),C=A*c*u/d+(a+h)/2,D=A*-d*t/c+(b+i)/2,E=w.asin(((b-D)/d).toFixed(9)),F=w.asin(((i-D)/d).toFixed(9));E=aF&&(E=E-B*2),!g&&F>E&&(F=F-B*2)}else E=j[0],F=j[1],C=j[2],D=j[3];var G=F-E;if(z(G)>k){var H=F,I=h,J=i;F=E+k*(g&&F>E?1:-1),h=C+c*w.cos(F),i=D+d*w.sin(F),m=bO(h,i,c,d,e,0,g,I,J,[F,H,C,D])}G=F-E;var K=w.cos(E),L=w.sin(E),M=w.cos(F),N=w.sin(F),O=w.tan(G/4),P=4/3*c*O,Q=4/3*d*O,R=[a,b],S=[a+P*L,b-Q*K],T=[h+P*N,i-Q*M],U=[h,i];S[0]=2*R[0]-S[0],S[1]=2*R[1]-S[1];if(j)return[S,T,U][n](m);m=[S,T,U][n](m).join()[s](",");var V=[];for(var W=0,X=m.length;W"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y)),i=f-2*d+b-(h-2*f+d),j=2*(d-b)-2*(f-d),k=b-d,l=(-j+w.sqrt(j*j-4*i*k))/2/i,n=(-j-w.sqrt(j*j-4*i*k))/2/i,z(l)>"1e12"&&(l=.5),z(n)>"1e12"&&(n=.5),l>0&&l<1&&(q=bP(a,b,c,d,e,f,g,h,l),p.push(q.x),o.push(q.y)),n>0&&n<1&&(q=bP(a,b,c,d,e,f,g,h,n),p.push(q.x),o.push(q.y));return{min:{x:y[m](0,p),y:y[m](0,o)},max:{x:x[m](0,p),y:x[m](0,o)}}}),bR=a._path2curve=bv(function(a,b){var c=!b&&bz(a);if(!b&&c.curve)return bJ(c.curve);var d=bL(a),e=b&&bL(b),f={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},g={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},h=function(a,b){var c,d;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null);switch(a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"][n](bO[m](0,[b.x,b.y][n](a.slice(1))));break;case"S":c=b.x+(b.x-(b.bx||b.x)),d=b.y+(b.y-(b.by||b.y)),a=["C",c,d][n](a.slice(1));break;case"T":b.qx=b.x+(b.x-(b.qx||b.x)),b.qy=b.y+(b.y-(b.qy||b.y)),a=["C"][n](bN(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"][n](bN(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"][n](bM(b.x,b.y,a[1],a[2]));break;case"H":a=["C"][n](bM(b.x,b.y,a[1],b.y));break;case"V":a=["C"][n](bM(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"][n](bM(b.x,b.y,b.X,b.Y))}return a},i=function(a,b){if(a[b].length>7){a[b].shift();var c=a[b];while(c.length)a.splice(b++,0,["C"][n](c.splice(0,6)));a.splice(b,1),l=x(d.length,e&&e.length||0)}},j=function(a,b,c,f,g){a&&b&&a[g][0]=="M"&&b[g][0]!="M"&&(b.splice(g,0,["M",f.x,f.y]),c.bx=0,c.by=0,c.x=a[g][1],c.y=a[g][2],l=x(d.length,e&&e.length||0))};for(var k=0,l=x(d.length,e&&e.length||0);ke){if(c&&!l.start){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n),k+=["C"+m.start.x,m.start.y,m.m.x,m.m.y,m.x,m.y];if(f)return k;l.start=k,k=["M"+m.x,m.y+"C"+m.n.x,m.n.y,m.end.x,m.end.y,i[5],i[6]].join(),n+=j,g=+i[5],h=+i[6];continue}if(!b&&!c){m=cs(g,h,i[1],i[2],i[3],i[4],i[5],i[6],e-n);return{x:m.x,y:m.y,alpha:m.alpha}}}n+=j,g=+i[5],h=+i[6]}k+=i.shift()+i}l.end=k,m=b?n:c?l:a.findDotsAtSegment(g,h,i[0],i[1],i[2],i[3],i[4],i[5],1),m.alpha&&(m={x:m.x,y:m.y,alpha:m.alpha});return m}},cu=ct(1),cv=ct(),cw=ct(0,1);a.getTotalLength=cu,a.getPointAtLength=cv,a.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return cw(a,b).end;var d=cw(a,c,1);return b?cw(d,b).end:d},cl.getTotalLength=function(){if(this.type=="path"){if(this.node.getTotalLength)return this.node.getTotalLength();return cu(this.attrs.path)}},cl.getPointAtLength=function(a){if(this.type=="path")return cv(this.attrs.path,a)},cl.getSubpath=function(b,c){if(this.type=="path")return a.getSubpath(this.attrs.path,b,c)};var cx=a.easing_formulas={linear:function(a){return a},"<":function(a){return A(a,1.7)},">":function(a){return A(a,.48)},"<>":function(a){var b=.48-a/1.04,c=w.sqrt(.1734+b*b),d=c-b,e=A(z(d),1/3)*(d<0?-1:1),f=-c-b,g=A(z(f),1/3)*(f<0?-1:1),h=e+g+.5;return(1-h)*3*h*h+h*h*h},backIn:function(a){var b=1.70158;return a*a*((b+1)*a-b)},backOut:function(a){a=a-1;var b=1.70158;return a*a*((b+1)*a+b)+1},elastic:function(a){if(a==!!a)return a;return A(2,-10*a)*w.sin((a-.075)*2*B/.3)+1},bounce:function(a){var b=7.5625,c=2.75,d;a<1/c?d=b*a*a:a<2/c?(a-=1.5/c,d=b*a*a+.75):a<2.5/c?(a-=2.25/c,d=b*a*a+.9375):(a-=2.625/c,d=b*a*a+.984375);return d}};cx.easeIn=cx["ease-in"]=cx["<"],cx.easeOut=cx["ease-out"]=cx[">"],cx.easeInOut=cx["ease-in-out"]=cx["<>"],cx["back-in"]=cx.backIn,cx["back-out"]=cx.backOut;var cy=[],cz=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){setTimeout(a,16)},cA=function(){var b=+(new Date),c=0;for(;c1&&!d.next){for(s in k)k[g](s)&&(r[s]=d.totalOrigin[s]);d.el.attr(r),cE(d.anim,d.el,d.anim.percents[0],null,d.totalOrigin,d.repeat-1)}d.next&&!d.stop&&cE(d.anim,d.el,d.next,null,d.totalOrigin,d.repeat)}}a.svg&&m&&m.paper&&m.paper.safari(),cy.length&&cz(cA)},cB=function(a){return a>255?255:a<0?0:a};cl.animateWith=function(b,c,d,e,f,g){var h=this;if(h.removed){g&&g.call(h);return h}var i=d instanceof cD?d:a.animation(d,e,f,g),j,k;cE(i,h,i.percents[0],null,h.attr());for(var l=0,m=cy.length;l.5)*2-1;i(m-.5,2)+i(n-.5,2)>.25&&(n=f.sqrt(.25-i(m-.5,2))*e+.5)&&n!=.5&&(n=n.toFixed(5)-1e-5*e)}return l}),e=e.split(/\s*\-\s*/);if(j=="linear"){var t=e.shift();t=-d(t);if(isNaN(t))return null;var u=[0,0,f.cos(a.rad(t)),f.sin(a.rad(t))],v=1/(g(h(u[2]),h(u[3]))||1);u[2]*=v,u[3]*=v,u[2]<0&&(u[0]=-u[2],u[2]=0),u[3]<0&&(u[1]=-u[3],u[3]=0)}var w=a._parseDots(e);if(!w)return null;k=k.replace(/[\(\)\s,\xb0#]/g,"_"),b.gradient&&k!=b.gradient.id&&(p.defs.removeChild(b.gradient),delete b.gradient);if(!b.gradient){s=q(j+"Gradient",{id:k}),b.gradient=s,q(s,j=="radial"?{fx:m,fy:n}:{x1:u[0],y1:u[1],x2:u[2],y2:u[3],gradientTransform:b.matrix.invert()}),p.defs.appendChild(s);for(var x=0,y=w.length;x1?G.opacity/100:G.opacity});case"stroke":G=a.getRGB(p),i.setAttribute(o,G.hex),o=="stroke"&&G[b]("opacity")&&q(i,{"stroke-opacity":G.opacity>1?G.opacity/100:G.opacity}),o=="stroke"&&d._.arrows&&("startString"in d._.arrows&&t(d,d._.arrows.startString),"endString"in d._.arrows&&t(d,d._.arrows.endString,1));break;case"gradient":(d.type=="circle"||d.type=="ellipse"||c(p).charAt()!="r")&&r(d,p);break;case"opacity":k.gradient&&!k[b]("stroke-opacity")&&q(i,{"stroke-opacity":p>1?p/100:p});case"fill-opacity":if(k.gradient){H=a._g.doc.getElementById(i.getAttribute("fill").replace(/^url\(#|\)$/g,l)),H&&(I=H.getElementsByTagName("stop"),q(I[I.length-1],{"stop-opacity":p}));break};default:o=="font-size"&&(p=e(p,10)+"px");var J=o.replace(/(\-.)/g,function(a){return a.substring(1).toUpperCase()});i.style[J]=p,d._.dirty=1,i.setAttribute(o,p)}}y(d,f),i.style.visibility=m},x=1.2,y=function(d,f){if(d.type=="text"&&!!(f[b]("text")||f[b]("font")||f[b]("font-size")||f[b]("x")||f[b]("y"))){var g=d.attrs,h=d.node,i=h.firstChild?e(a._g.doc.defaultView.getComputedStyle(h.firstChild,l).getPropertyValue("font-size"),10):10;if(f[b]("text")){g.text=f.text;while(h.firstChild)h.removeChild(h.firstChild);var j=c(f.text).split("\n"),k=[],m;for(var n=0,o=j.length;n"));var $=X.getBoundingClientRect();t.W=m.w=($.right-$.left)/Y,t.H=m.h=($.bottom-$.top)/Y,t.X=m.x,t.Y=m.y+t.H/2,("x"in i||"y"in i)&&(t.path.v=a.format("m{0},{1}l{2},{1}",f(m.x*u),f(m.y*u),f(m.x*u)+1));var _=["x","y","text","font","font-family","font-weight","font-style","font-size"];for(var ba=0,bb=_.length;ba.25&&(c=e.sqrt(.25-i(b-.5,2))*((c>.5)*2-1)+.5),m=b+n+c);return o}),f=f.split(/\s*\-\s*/);if(l=="linear"){var p=f.shift();p=-d(p);if(isNaN(p))return null}var q=a._parseDots(f);if(!q)return null;b=b.shape||b.node;if(q.length){b.removeChild(g),g.on=!0,g.method="none",g.color=q[0].color,g.color2=q[q.length-1].color;var r=[];for(var s=0,t=q.length;s')}}catch(c){F=function(a){return b.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}},a._engine.initWin(a._g.win),a._engine.create=function(){var b=a._getContainer.apply(0,arguments),c=b.container,d=b.height,e,f=b.width,g=b.x,h=b.y;if(!c)throw new Error("VML container not found.");var i=new a._Paper,j=i.canvas=a._g.doc.createElement("div"),k=j.style;g=g||0,h=h||0,f=f||512,d=d||342,i.width=f,i.height=d,f==+f&&(f+="px"),d==+d&&(d+="px"),i.coordsize=u*1e3+n+u*1e3,i.coordorigin="0 0",i.span=a._g.doc.createElement("span"),i.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;",j.appendChild(i.span),k.cssText=a.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden",f,d),c==1?(a._g.doc.body.appendChild(j),k.left=g+"px",k.top=h+"px",k.position="absolute"):c.firstChild?c.insertBefore(j,c.firstChild):c.appendChild(j),i.renderfix=function(){};return i},a.prototype.clear=function(){a.eve("raphael.clear",this),this.canvas.innerHTML=o,this.span=a._g.doc.createElement("span"),this.span.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;",this.canvas.appendChild(this.span),this.bottom=this.top=null},a.prototype.remove=function(){a.eve("raphael.remove",this),this.canvas.parentNode.removeChild(this.canvas);for(var b in this)this[b]=typeof this[b]=="function"?a._removedFactory(b):null;return!0};var G=a.st;for(var H in E)E[b](H)&&!G[b](H)&&(G[H]=function(a){return function(){var b=arguments;return this.forEach(function(c){c[a].apply(c,b)})}}(H))}(window.Raphael) -------------------------------------------------------------------------------- /resource/js/upload.js: -------------------------------------------------------------------------------- 1 | function fileSelected() 2 | { 3 | var file = document.getElementById('fileToUpload').files[0]; 4 | var fileSize = 0; 5 | if (file.size > 1024 * 1024) 6 | fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; 7 | else 8 | fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; 9 | document.getElementById('fileInfo').style.display = 'block'; 10 | document.getElementById('fileName').innerHTML = 'Name: ' + file.name; 11 | document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize; 12 | document.getElementById('fileType').innerHTML = 'Type: ' + file.type; 13 | } 14 | 15 | function getParam(paramName) 16 | { 17 | keyValStrings = document.URL.split('#')[1].split(';'); 18 | for(var i = 0; i < keyValStrings.length; i++){ 19 | if(keyValStrings[i].split('=')[0] == paramName){ 20 | return keyValStrings[i].split('=')[1]; 21 | } 22 | } 23 | } 24 | 25 | function documentReady(){ 26 | document.getElementById('roomName').setAttribute('value', getParam('room')); 27 | } 28 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source venv/bin/activate 4 | export PORT=6000 5 | python main.py >> output.`date +"%y-%m-%d"`.log 2>&1 6 | deactivate 7 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | pushd test 2 | nosetests 3 | popd 4 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.3.0 2 | -------------------------------------------------------------------------------- /set_heroku_path.sh: -------------------------------------------------------------------------------- 1 | heroku config:set LD_LIBRARY_PATH=/app/vendor/ffmpeg/ffmpeg/lib:\$LD_LIBRARY_PATH 2 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | -------------------------------------------------------------------------------- /test/files/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anandtrex/collabdraw/4de4cc14e344b2bf18d7c89846bdcecb92ee5125/test/files/sample.pdf -------------------------------------------------------------------------------- /test/uploadprocessor_integ_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import glob 4 | 5 | import config 6 | from org.collabdraw.tools.uploadprocessor import process_uploaded_file 7 | from org.collabdraw.dbclient.dbclientfactory import DbClientFactory 8 | from org.collabdraw.tools.tools import delete_files 9 | 10 | 11 | class UploadProcessorIntegTest(unittest.TestCase): 12 | def uploadprocessor_test(self): 13 | dir_path = os.path.abspath("./files") 14 | key = "nosetest:upload" 15 | process_uploaded_file(dir_path, "sample.pdf", key) 16 | output_files = os.listdir(dir_path) 17 | self.assertEquals(len(output_files), 11) 18 | self.assertIn("sample.pdf", output_files) 19 | for i in range(0, 10): 20 | self.assertIn(str(i + 1) + "_image.png", output_files) 21 | 22 | key = "info:%s:npages" % key 23 | db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 24 | self.assertEquals(int(db_client.get(key)), 10) 25 | 26 | # Cleanup 27 | db_client.delete(key) 28 | delete_files(dir_path + "/*.png") -------------------------------------------------------------------------------- /test/videomaker_integ_test.py: -------------------------------------------------------------------------------- 1 | __author__ = 'anand' 2 | 3 | import unittest 4 | import glob 5 | import os 6 | 7 | import config 8 | from org.collabdraw.dbclient.dbclientfactory import DbClientFactory 9 | from org.collabdraw.tools.videomaker import make_video 10 | from org.collabdraw.tools.tools import delete_files 11 | 12 | class VideoMakerIntegTest(unittest.TestCase): 13 | def videomaker_test(self): 14 | # Make sure tmp directory is clean to start with 15 | delete_files("tmp/*.mp4") 16 | 17 | key = "nosetest:video" 18 | path = [{'lineColor': 'black', 'y': 397, 'oldx': 813, 'oldy': 397, 'lineWidth': '3px', 'type': 'touchmove', 'x': 811}, 19 | {'lineColor': 'black', 'y': 398, 'oldx': 811, 'oldy': 397, 'lineWidth': '3px', 'type': 'touchmove', 'x': 809}, 20 | {'lineColor': 'black', 'y': 398, 'oldx': 809, 'oldy': 398, 'lineWidth': '3px', 'type': 'touchend', 'x': 809}, 21 | {'lineColor': 'black', 'y': 399, 'oldx': 809, 'oldy': 398, 'lineWidth': '3px', 'type': 'touchend', 'x': 900}, 22 | {'lineColor': 'black', 'y': 400, 'oldx': 900, 'oldy': 399, 'lineWidth': '3px', 'type': 'touchend', 'x': 910}, 23 | {'lineColor': 'black', 'y': 401, 'oldx': 910, 'oldy': 400, 'lineWidth': '3px', 'type': 'touchend', 'x': 920}, 24 | {'lineColor': 'black', 'y': 402, 'oldx': 920, 'oldy': 401, 'lineWidth': '3px', 'type': 'touchend', 'x': 930}, 25 | {'lineColor': 'black', 'y': 403, 'oldx': 930, 'oldy': 402, 'lineWidth': '3px', 'type': 'touchend', 'x': 940}, 26 | {'lineColor': 'black', 'y': 404, 'oldx': 940, 'oldy': 403, 'lineWidth': '3px', 'type': 'touchend', 'x': 950}, 27 | {'lineColor': 'black', 'y': 500, 'oldx': 950, 'oldy': 404, 'lineWidth': '3px', 'type': 'touchend', 'x': 960}, 28 | {'lineColor': 'black', 'y': 508, 'oldx': 960, 'oldy': 500, 'lineWidth': '3px', 'type': 'touchend', 'x': 1000}, 29 | {'lineColor': 'black', 'y': 511, 'oldx': 1000, 'oldy': 508, 'lineWidth': '3px', 'type': 'touchend', 'x': 900}, 30 | ] 31 | db_client = DbClientFactory.getDbClient(config.DB_CLIENT_TYPE) 32 | db_client.set(key, path) 33 | make_video(key) 34 | 35 | # Validation 36 | files = glob.glob("tmp/*") 37 | self.assertEquals(len(files), 1, "Expecting exactly one file in tmp directory") 38 | self.assertIn("mp4", files[0], "Expecting an video file") 39 | 40 | # Cleanup 41 | db_client.delete(key) 42 | delete_files("tmp/*.mp4") 43 | os.rmdir("tmp") --------------------------------------------------------------------------------