Escape GET variables centrally
Escape GET variables centrally

file:a/about.php -> file:b/about.php
--- a/about.php
+++ b/about.php
@@ -25,7 +25,7 @@
 All offers are not binding and without obligation. The Author expressly reserves the right, in his discretion, to suspend, 
 change, modify, add or remove portions of the Site and to restrict or terminate the use and accessibility of the Site 
 without prior notice. </small>
-<?
+<?php
 include_footer();
 ?>
 

file:b/aws/awsStartup.sh (new)
--- /dev/null
+++ b/aws/awsStartup.sh
@@ -1,1 +1,39 @@
+#!/bin/bash
+#this script should be run from a fresh git checkout from github
+#ami base must have yum install lighttpd-fastcgi, git, tomcat6 
+#php-cli php-gd tomcat6-webapps tomcat6-admin-webapps svn maven2
+#postgres postgres-server php-pg
+#http://www.how2forge.org/installing-lighttpd-with-php5-and-mysql-support-on-fedora-12
 
+cp /root/aws.php /tmp/
+mkdir /var/www/lib/staticmaplite/cache 
+chcon -h system_u:object_r:httpd_sys_content_t /var/www
+chcon -R -h root:object_r:httpd_sys_content_t /var/www/*
+chcon -R -t httpd_sys_content_rw_t /var/www/lib/staticmaplite/cache
+chmod -R 777 /var/www/lib/staticmaplite/cache 
+chcon -R -t httpd_sys_content_rw_t /var/www/labs/tiles
+chmod -R 777 /var/www/labs/tiles
+wget http://s3-ap-southeast-1.amazonaws.com/busresources/cbrfeed.zip \
+-O /var/www/cbrfeed.zip
+
+createdb transitdata
+createlang -d transitdata plpgsql
+psql -d transitdata -f /var/www/lib/postgis.sql
+# curl https://github.com/maxious/ACTBus-ui/raw/master/transitdata.cbrfeed.sql.gz -o transitdata.cbrfeed.sql.gz 
+#made with pg_dump transitdata | gzip -c >  transitdata.cbrfeed.sql.gz
+gunzip /var/www/transitdata.cbrfeed.sql.gz
+psql -d transitdata -f /var/www/transitdata.cbrfeed.sql
+#createuser transitdata -SDRP
+#password transitdata
+#psql -d transitdata -c \"GRANT SELECT ON TABLE agency,calendar,calendar_dates,routes,stop_times,stops,trips TO transitdata;\"
+php /var/www/updatedb.php
+
+wget http://s3-ap-southeast-1.amazonaws.com/busresources/Graph.obj \
+-O /tmp/Graph.obj
+rm -rfv /usr/share/tomcat6/webapps/opentripplanner*
+wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-webapp.war \
+-O /usr/share/tomcat6/webapps/opentripplanner-webapp.war
+wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-api-webapp.war \
+-O /usr/share/tomcat6/webapps/opentripplanner-api-webapp.war
+/etc/init.d/tomcat6 restart
+

file:b/aws/pg_hba.conf (new)
--- /dev/null
+++ b/aws/pg_hba.conf
@@ -1,1 +1,77 @@
+# PostgreSQL Client Authentication Configuration File
+# ===================================================
+#
+# Refer to the "Client Authentication" section in the
+# PostgreSQL documentation for a complete description
+# of this file.  A short synopsis follows.
+#
+# This file controls: which hosts are allowed to connect, how clients
+# are authenticated, which PostgreSQL user names they can use, which
+# databases they can access.  Records take one of these forms:
+#
+# local      DATABASE  USER  METHOD  [OPTIONS]
+# host       DATABASE  USER  CIDR-ADDRESS  METHOD  [OPTIONS]
+# hostssl    DATABASE  USER  CIDR-ADDRESS  METHOD  [OPTIONS]
+# hostnossl  DATABASE  USER  CIDR-ADDRESS  METHOD  [OPTIONS]
+#
+# (The uppercase items must be replaced by actual values.)
+#
+# The first field is the connection type: "local" is a Unix-domain socket,
+# "host" is either a plain or SSL-encrypted TCP/IP socket, "hostssl" is an
+# SSL-encrypted TCP/IP socket, and "hostnossl" is a plain TCP/IP socket.
+#
+# DATABASE can be "all", "sameuser", "samerole", a database name, or
+# a comma-separated list thereof.
+#
+# USER can be "all", a user name, a group name prefixed with "+", or
+# a comma-separated list thereof.  In both the DATABASE and USER fields
+# you can also write a file name prefixed with "@" to include names from
+# a separate file.
+#
+# CIDR-ADDRESS specifies the set of hosts the record matches.
+# It is made up of an IP address and a CIDR mask that is an integer
+# (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that specifies
+# the number of significant bits in the mask.  Alternatively, you can write
+# an IP address and netmask in separate columns to specify the set of hosts.
+#
+# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", "krb5",
+# "ident", "pam", "ldap" or "cert".  Note that "password" sends passwords
+# in clear text; "md5" is preferred since it sends encrypted passwords.
+#
+# OPTIONS are a set of options for the authentication in the format
+# NAME=VALUE. The available options depend on the different authentication
+# methods - refer to the "Client Authentication" section in the documentation
+# for a list of which options are available for which authentication methods.
+#
+# Database and user names containing spaces, commas, quotes and other special
+# characters must be quoted. Quoting one of the keywords "all", "sameuser" or
+# "samerole" makes the name lose its special character, and just match a
+# database or username with that name.
+#
+# This file is read on server startup and when the postmaster receives
+# a SIGHUP signal.  If you edit the file on a running system, you have
+# to SIGHUP the postmaster for the changes to take effect.  You can use
+# "pg_ctl reload" to do that.
 
+# Put your actual configuration here
+# ----------------------------------
+#
+# If you want to allow non-local connections, you need to add more
+# "host" records. In that case you will also need to make PostgreSQL listen
+# on a non-local interface via the listen_addresses configuration parameter,
+# or via the -i or -h command line switches.
+#
+
+
+
+# TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD
+
+# "local" is for Unix domain socket connections only
+local   all         all                               trust
+# IPv4 local connections:
+host    all         all         127.0.0.1/32          trust
+# IPv6 local connections:
+host    all         all         ::1/128               trust
+#Allow any IP to connect, with a password:
+host    all         all         0.0.0.0          0.0.0.0      md5
+

--- /dev/null
+++ b/aws/postgresql.conf
@@ -1,1 +1,502 @@
-
+# -----------------------------
+# PostgreSQL configuration file
+# -----------------------------
+#
+# This file consists of lines of the form:
+#
+#   name = value
+#
+# (The "=" is optional.)  Whitespace may be used.  Comments are introduced with
+# "#" anywhere on a line.  The complete list of parameter names and allowed
+# values can be found in the PostgreSQL documentation.
+#
+# The commented-out settings shown in this file represent the default values.
+# Re-commenting a setting is NOT sufficient to revert it to the default value;
+# you need to reload the server.
+#
+# This file is read on server startup and when the server receives a SIGHUP
+# signal.  If you edit the file on a running system, you have to SIGHUP the
+# server for the changes to take effect, or use "pg_ctl reload".  Some
+# parameters, which are marked below, require a server shutdown and restart to
+# take effect.
+#
+# Any parameter can also be given as a command-line option to the server, e.g.,
+# "postgres -c log_connections=on".  Some parameters can be changed at run time
+# with the "SET" SQL command.
+#
+# Memory units:  kB = kilobytes        Time units:  ms  = milliseconds
+#                MB = megabytes                     s   = seconds
+#                GB = gigabytes                     min = minutes
+#                                                   h   = hours
+#                                                   d   = days
+
+
+#------------------------------------------------------------------------------
+# FILE LOCATIONS
+#------------------------------------------------------------------------------
+
+# The default values of these variables are driven from the -D command-line
+# option or PGDATA environment variable, represented here as ConfigDir.
+
+#data_directory = 'ConfigDir'		# use data in another directory
+					# (change requires restart)
+#hba_file = 'ConfigDir/pg_hba.conf'	# host-based authentication file
+					# (change requires restart)
+#ident_file = 'ConfigDir/pg_ident.conf'	# ident configuration file
+					# (change requires restart)
+
+# If external_pid_file is not explicitly set, no extra PID file is written.
+#external_pid_file = '(none)'		# write an extra PID file
+					# (change requires restart)
+
+
+#------------------------------------------------------------------------------
+# CONNECTIONS AND AUTHENTICATION
+#------------------------------------------------------------------------------
+
+# - Connection Settings -
+
+listen_addresses = '*'		# what IP address(es) to listen on;
+					# comma-separated list of addresses;
+					# defaults to 'localhost', '*' = all
+					# (change requires restart)
+#port = 5432				# (change requires restart)
+max_connections = 100			# (change requires restart)
+# Note:  Increasing max_connections costs ~400 bytes of shared memory per 
+# connection slot, plus lock space (see max_locks_per_transaction).
+#superuser_reserved_connections = 3	# (change requires restart)
+#unix_socket_directory = ''		# (change requires restart)
+#unix_socket_group = ''			# (change requires restart)
+#unix_socket_permissions = 0777		# begin with 0 to use octal notation
+					# (change requires restart)
+#bonjour_name = ''			# defaults to the computer name
+					# (change requires restart)
+
+# - Security and Authentication -
+
+#authentication_timeout = 1min		# 1s-600s
+#ssl = off				# (change requires restart)
+#ssl_ciphers = 'ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH'	# allowed SSL ciphers
+					# (change requires restart)
+#ssl_renegotiation_limit = 512MB	# amount of data between renegotiations
+#password_encryption = on
+#db_user_namespace = off
+
+# Kerberos and GSSAPI
+#krb_server_keyfile = ''
+#krb_srvname = 'postgres'		# (Kerberos only)
+#krb_caseins_users = off
+
+# - TCP Keepalives -
+# see "man 7 tcp" for details
+
+#tcp_keepalives_idle = 0		# TCP_KEEPIDLE, in seconds;
+					# 0 selects the system default
+#tcp_keepalives_interval = 0		# TCP_KEEPINTVL, in seconds;
+					# 0 selects the system default
+#tcp_keepalives_count = 0		# TCP_KEEPCNT;
+					# 0 selects the system default
+
+
+#------------------------------------------------------------------------------
+# RESOURCE USAGE (except WAL)
+#------------------------------------------------------------------------------
+
+# - Memory -
+
+shared_buffers = 32MB			# min 128kB
+					# (change requires restart)
+#temp_buffers = 8MB			# min 800kB
+#max_prepared_transactions = 0		# zero disables the feature
+					# (change requires restart)
+# Note:  Increasing max_prepared_transactions costs ~600 bytes of shared memory
+# per transaction slot, plus lock space (see max_locks_per_transaction).
+# It is not advisable to set max_prepared_transactions nonzero unless you
+# actively intend to use prepared transactions.
+#work_mem = 1MB				# min 64kB
+#maintenance_work_mem = 16MB		# min 1MB
+#max_stack_depth = 2MB			# min 100kB
+
+# - Kernel Resource Usage -
+
+#max_files_per_process = 1000		# min 25
+					# (change requires restart)
+#shared_preload_libraries = ''		# (change requires restart)
+
+# - Cost-Based Vacuum Delay -
+
+#vacuum_cost_delay = 0ms		# 0-100 milliseconds
+#vacuum_cost_page_hit = 1		# 0-10000 credits
+#vacuum_cost_page_miss = 10		# 0-10000 credits
+#vacuum_cost_page_dirty = 20		# 0-10000 credits
+#vacuum_cost_limit = 200		# 1-10000 credits
+
+# - Background Writer -
+
+#bgwriter_delay = 200ms			# 10-10000ms between rounds
+#bgwriter_lru_maxpages = 100		# 0-1000 max buffers written/round
+#bgwriter_lru_multiplier = 2.0		# 0-10.0 multipler on buffers scanned/round
+
+# - Asynchronous Behavior -
+
+#effective_io_concurrency = 1		# 1-1000. 0 disables prefetching
+
+
+#------------------------------------------------------------------------------
+# WRITE AHEAD LOG
+#------------------------------------------------------------------------------
+
+# - Settings -
+
+#fsync = on				# turns forced synchronization on or off
+#synchronous_commit = on		# immediate fsync at commit
+#wal_sync_method = fsync		# the default is the first option 
+					# supported by the operating system:
+					#   open_datasync
+					#   fdatasync
+					#   fsync
+					#   fsync_writethrough
+					#   open_sync
+#full_page_writes = on			# recover from partial page writes
+#wal_buffers = 64kB			# min 32kB
+					# (change requires restart)
+#wal_writer_delay = 200ms		# 1-10000 milliseconds
+
+#commit_delay = 0			# range 0-100000, in microseconds
+#commit_siblings = 5			# range 1-1000
+
+# - Checkpoints -
+
+#checkpoint_segments = 3		# in logfile segments, min 1, 16MB each
+#checkpoint_timeout = 5min		# range 30s-1h
+#checkpoint_completion_target = 0.5	# checkpoint target duration, 0.0 - 1.0
+#checkpoint_warning = 30s		# 0 disables
+
+# - Archiving -
+
+#archive_mode = off		# allows archiving to be done
+				# (change requires restart)
+#archive_command = ''		# command to use to archive a logfile segment
+#archive_timeout = 0		# force a logfile segment switch after this
+				# number of seconds; 0 disables
+
+
+#------------------------------------------------------------------------------
+# QUERY TUNING
+#------------------------------------------------------------------------------
+
+# - Planner Method Configuration -
+
+#enable_bitmapscan = on
+#enable_hashagg = on
+#enable_hashjoin = on
+#enable_indexscan = on
+#enable_mergejoin = on
+#enable_nestloop = on
+#enable_seqscan = on
+#enable_sort = on
+#enable_tidscan = on
+
+# - Planner Cost Constants -
+
+#seq_page_cost = 1.0			# measured on an arbitrary scale
+#random_page_cost = 4.0			# same scale as above
+#cpu_tuple_cost = 0.01			# same scale as above
+#cpu_index_tuple_cost = 0.005		# same scale as above
+#cpu_operator_cost = 0.0025		# same scale as above
+#effective_cache_size = 128MB
+
+# - Genetic Query Optimizer -
+
+#geqo = on
+#geqo_threshold = 12
+#geqo_effort = 5			# range 1-10
+#geqo_pool_size = 0			# selects default based on effort
+#geqo_generations = 0			# selects default based on effort
+#geqo_selection_bias = 2.0		# range 1.5-2.0
+
+# - Other Planner Options -
+
+#default_statistics_target = 100	# range 1-10000
+#constraint_exclusion = partition	# on, off, or partition
+#cursor_tuple_fraction = 0.1		# range 0.0-1.0
+#from_collapse_limit = 8
+#join_collapse_limit = 8		# 1 disables collapsing of explicit 
+					# JOIN clauses
+
+
+#------------------------------------------------------------------------------
+# ERROR REPORTING AND LOGGING
+#------------------------------------------------------------------------------
+
+# - Where to Log -
+
+#log_destination = 'stderr'		# Valid values are combinations of
+					# stderr, csvlog, syslog and eventlog,
+					# depending on platform.  csvlog
+					# requires logging_collector to be on.
+
+# This is used when logging to stderr:
+logging_collector = on			# Enable capturing of stderr and csvlog
+					# into log files. Required to be on for
+					# csvlogs.
+					# (change requires restart)
+
+# These are only used if logging_collector is on:
+log_directory = 'pg_log'		# directory where log files are written,
+					# can be absolute or relative to PGDATA
+log_filename = 'postgresql-%a.log'	# log file name pattern,
+					# can include strftime() escapes
+log_truncate_on_rotation = on		# If on, an existing log file of the
+					# same name as the new log file will be
+					# truncated rather than appended to.
+					# But such truncation only occurs on
+					# time-driven rotation, not on restarts
+					# or size-driven rotation.  Default is
+					# off, meaning append to existing files
+					# in all cases.
+log_rotation_age = 1d			# Automatic rotation of logfiles will
+					# happen after that time.  0 disables.
+log_rotation_size = 0			# Automatic rotation of logfiles will 
+					# happen after that much log output.
+					# 0 disables.
+
+# These are relevant when logging to syslog:
+#syslog_facility = 'LOCAL0'
+#syslog_ident = 'postgres'
+
+#silent_mode = off			# Run server silently.
+					# DO NOT USE without syslog or
+					# logging_collector
+					# (change requires restart)
+
+
+# - When to Log -
+
+#client_min_messages = notice		# values in order of decreasing detail:
+					#   debug5
+					#   debug4
+					#   debug3
+					#   debug2
+					#   debug1
+					#   log
+					#   notice
+					#   warning
+					#   error
+
+#log_min_messages = warning		# values in order of decreasing detail:
+					#   debug5
+					#   debug4
+					#   debug3
+					#   debug2
+					#   debug1
+					#   info
+					#   notice
+					#   warning
+					#   error
+					#   log
+					#   fatal
+					#   panic
+
+#log_error_verbosity = default		# terse, default, or verbose messages
+
+#log_min_error_statement = error	# values in order of decreasing detail:
+				 	#   debug5
+					#   debug4
+					#   debug3
+					#   debug2
+					#   debug1
+				 	#   info
+					#   notice
+					#   warning
+					#   error
+					#   log
+					#   fatal
+					#   panic (effectively off)
+
+#log_min_duration_statement = -1	# -1 is disabled, 0 logs all statements
+					# and their durations, > 0 logs only
+					# statements running at least this number
+					# of milliseconds
+
+
+# - What to Log -
+
+#debug_print_parse = off
+#debug_print_rewritten = off
+#debug_print_plan = off
+#debug_pretty_print = on
+#log_checkpoints = off
+#log_connections = off
+#log_disconnections = off
+#log_duration = off
+#log_hostname = off
+#log_line_prefix = ''			# special values:
+					#   %u = user name
+					#   %d = database name
+					#   %r = remote host and port
+					#   %h = remote host
+					#   %p = process ID
+					#   %t = timestamp without milliseconds
+					#   %m = timestamp with milliseconds
+					#   %i = command tag
+					#   %c = session ID
+					#   %l = session line number
+					#   %s = session start timestamp
+					#   %v = virtual transaction ID
+					#   %x = transaction ID (0 if none)
+					#   %q = stop here in non-session
+					#        processes
+					#   %% = '%'
+					# e.g. '<%u%%%d> '
+#log_lock_waits = off			# log lock waits >= deadlock_timeout
+#log_statement = 'none'			# none, ddl, mod, all
+#log_temp_files = -1			# log temporary files equal or larger
+					# than the specified size in kilobytes;
+					# -1 disables, 0 logs all temp files
+#log_timezone = unknown			# actually, defaults to TZ environment
+					# setting
+
+
+#------------------------------------------------------------------------------
+# RUNTIME STATISTICS
+#------------------------------------------------------------------------------
+
+# - Query/Index Statistics Collector -
+
+#track_activities = on
+#track_counts = on
+#track_functions = none			# none, pl, all
+#track_activity_query_size = 1024
+#update_process_title = on
+#stats_temp_directory = 'pg_stat_tmp'
+
+
+# - Statistics Monitoring -
+
+#log_parser_stats = off
+#log_planner_stats = off
+#log_executor_stats = off
+#log_statement_stats = off
+
+
+#------------------------------------------------------------------------------
+# AUTOVACUUM PARAMETERS
+#------------------------------------------------------------------------------
+
+#autovacuum = on			# Enable autovacuum subprocess?  'on' 
+					# requires track_counts to also be on.
+#log_autovacuum_min_duration = -1	# -1 disables, 0 logs all actions and
+					# their durations, > 0 logs only
+					# actions running at least this number
+					# of milliseconds.
+#autovacuum_max_workers = 3		# max number of autovacuum subprocesses
+#autovacuum_naptime = 1min		# time between autovacuum runs
+#autovacuum_vacuum_threshold = 50	# min number of row updates before
+					# vacuum
+#autovacuum_analyze_threshold = 50	# min number of row updates before 
+					# analyze
+#autovacuum_vacuum_scale_factor = 0.2	# fraction of table size before vacuum
+#autovacuum_analyze_scale_factor = 0.1	# fraction of table size before analyze
+#autovacuum_freeze_max_age = 200000000	# maximum XID age before forced vacuum
+					# (change requires restart)
+#autovacuum_vacuum_cost_delay = 20ms	# default vacuum cost delay for
+					# autovacuum, in milliseconds;
+					# -1 means use vacuum_cost_delay
+#autovacuum_vacuum_cost_limit = -1	# default vacuum cost limit for
+					# autovacuum, -1 means use
+					# vacuum_cost_limit
+
+
+#------------------------------------------------------------------------------
+# CLIENT CONNECTION DEFAULTS
+#------------------------------------------------------------------------------
+
+# - Statement Behavior -
+
+#search_path = '"$user",public'		# schema names
+#default_tablespace = ''		# a tablespace name, '' uses the default
+#temp_tablespaces = ''			# a list of tablespace names, '' uses
+					# only default tablespace
+#check_function_bodies = on
+#default_transaction_isolation = 'read committed'
+#default_transaction_read_only = off
+#session_replication_role = 'origin'
+#statement_timeout = 0			# in milliseconds, 0 is disabled
+#vacuum_freeze_min_age = 50000000
+#vacuum_freeze_table_age = 150000000
+#xmlbinary = 'base64'
+#xmloption = 'content'
+
+# - Locale and Formatting -
+
+datestyle = 'iso, mdy'
+#intervalstyle = 'postgres'
+#timezone = unknown			# actually, defaults to TZ environment
+					# setting
+#timezone_abbreviations = 'Default'     # Select the set of available time zone
+					# abbreviations.  Currently, there are
+					#   Default
+					#   Australia
+					#   India
+					# You can create your own file in
+					# share/timezonesets/.
+#extra_float_digits = 0			# min -15, max 2
+#client_encoding = sql_ascii		# actually, defaults to database
+					# encoding
+
+# These settings are initialized by initdb, but they can be changed.
+lc_messages = 'en_US.UTF-8'			# locale for system error message
+					# strings
+lc_monetary = 'en_US.UTF-8'			# locale for monetary formatting
+lc_numeric = 'en_US.UTF-8'			# locale for number formatting
+lc_time = 'en_US.UTF-8'				# locale for time formatting
+
+# default configuration for text search
+default_text_search_config = 'pg_catalog.english'
+
+# - Other Defaults -
+
+#dynamic_library_path = '$libdir'
+#local_preload_libraries = ''
+
+
+#------------------------------------------------------------------------------
+# LOCK MANAGEMENT
+#------------------------------------------------------------------------------
+
+#deadlock_timeout = 1s
+#max_locks_per_transaction = 64		# min 10
+					# (change requires restart)
+# Note:  Each lock table slot uses ~270 bytes of shared memory, and there are
+# max_locks_per_transaction * (max_connections + max_prepared_transactions)
+# lock table slots.
+
+
+#------------------------------------------------------------------------------
+# VERSION/PLATFORM COMPATIBILITY
+#------------------------------------------------------------------------------
+
+# - Previous PostgreSQL Versions -
+
+#add_missing_from = off
+#array_nulls = on
+#backslash_quote = safe_encoding	# on, off, or safe_encoding
+#default_with_oids = off
+#escape_string_warning = on
+#regex_flavor = advanced		# advanced, extended, or basic
+#sql_inheritance = on
+#standard_conforming_strings = off
+#synchronize_seqscans = on
+
+# - Other Platforms and Clients -
+
+#transform_null_equals = off
+
+
+#------------------------------------------------------------------------------
+# CUSTOMIZED OPTIONS
+#------------------------------------------------------------------------------
+
+#custom_variable_classes = ''		# list of custom variable class names
+

 Binary files a/css/images/01-refresh.png and /dev/null differ
file:a/css/images/02-redo.png (deleted)
 Binary files a/css/images/02-redo.png and /dev/null differ
 Binary files a/css/images/06-magnify.png and /dev/null differ
 Binary files a/css/images/07-map-marker.png and /dev/null differ
 Binary files a/css/images/101-gameplan.png and /dev/null differ
file:a/css/images/102-walk.png (deleted)
 Binary files a/css/images/102-walk.png and /dev/null differ
file:a/css/images/103-map.png (deleted)
 Binary files a/css/images/103-map.png and /dev/null differ
 Binary files a/css/images/121-landscape.png and /dev/null differ
 Binary files a/css/images/13-target.png and /dev/null differ
 Binary files a/css/images/139-flags.png and /dev/null differ
 Binary files a/css/images/145-persondot.png and /dev/null differ
 Binary files a/css/images/184-warning.png and /dev/null differ
 Binary files a/css/images/193-location-arrow.png and /dev/null differ
file:a/css/images/28-star.png (deleted)
 Binary files a/css/images/28-star.png and /dev/null differ
file:a/css/images/53-house.png (deleted)
 Binary files a/css/images/53-house.png and /dev/null differ
 Binary files a/css/images/55-network.png and /dev/null differ
 Binary files a/css/images/57-download.png and /dev/null differ
 Binary files a/css/images/58-bookmark.png and /dev/null differ
file:a/css/images/59-flag.png (deleted)
 Binary files a/css/images/59-flag.png and /dev/null differ
 Binary files a/css/images/60-signpost.png and /dev/null differ
file:a/css/images/73-radar.png (deleted)
 Binary files a/css/images/73-radar.png and /dev/null differ
 Binary files a/css/images/74-location.png and /dev/null differ
 Binary files a/css/images/83-calendar.png and /dev/null differ
 Binary files /dev/null and b/css/images/91-beaker-2.png differ
--- /dev/null
+++ b/dotcloud/postinstall
@@ -1,1 +1,19 @@
+#!/bin/bash
+#dotcloud postinstall
 
+curl http://s3-ap-southeast-1.amazonaws.com/busresources/cbrfeed.zip \
+-o /home/dotcloud/current/cbrfeed.zip
+wget http://s3-ap-southeast-1.amazonaws.com/busresources/Graph.obj \
+-O /tmp/Graph.obj
+
+#db setup
+#curl https://github.com/maxious/ACTBus-ui/raw/master/transitdata.cbrfeed.sql.gz -o transitdata.cbrfeed.sql.gz
+#curl https://github.com/maxious/ACTBus-ui/raw/master/lib/postgis.sql -o postgis.sql
+#createlang -d transitdata plpgsql
+#psql -d transitdata -f postgis.sql
+#gunzip /var/www/transitdata.cbrfeed.sql.gz
+#psql -d transitdata -f transitdata.cbrfeed.sql
+#createuser transitdata -SDRP
+#password transitdata
+#psql -c \"GRANT SELECT ON TABLE agency,calendar,calendar_dates,routes,stop_times,stops,trips TO transitdata;\"
+

file:b/dotcloud/push.sh (new)
--- /dev/null
+++ b/dotcloud/push.sh
@@ -1,1 +1,7 @@
+#wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-webapp.war 
+cp ~/workspace/opentripplanner/maven.1277125291275/opentripplanner-webapp/target/opentripplanner-webapp.war ./
+#wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-api-webapp.war 
+cp ~/workspace/opentripplanner/maven.1277125291275/opentripplanner-api-webapp/target/opentripplanner-api-webapp.war ./
 
+dotcloud push actbus.otp ./
+

--- a/feedback.php
+++ b/feedback.php
@@ -26,7 +26,7 @@
 }
 if (isset($_REQUEST['feedback']) || isset($_REQUEST['newlocation'])){
 	sendEmail("bus.lambda feedback",print_r($_REQUEST,true));
-	echo "<center><h2>Thank you for your feedback!</h2></center>";
+	echo "<h2 style='text-align: center;'>Thank you for your feedback!</h2>";
 } else {
 $stopid = "";
 $stopcode = "";
@@ -48,7 +48,7 @@
 <small> if you click on feedback from a stop page, these will get filled in automatically. else describe the location/street of the stop in one of these boxes </small><br>
 
 Suggested Stop Location (lat/long or words):  <input type="text" name="newlocation"/><br>
-<small> if your device supports javascript, you can pick a location from the map above</small><br>
+<!--<small> if your device supports javascript, you can pick a location from the map above</small><br>-->
 
 <input type="submit" value="Submit!"/>
 </form>

--- /dev/null
+++ b/include/common-db.inc.php
@@ -1,1 +1,21 @@
+<?php
+  if (php_uname('n') == "actbus-www") {
+    $conn = pg_connect("dbname=transitdata user=transitdata password=transitdata host=bus-main.lambdacomplex.org");
+  } else if (isDebugServer()) {
+    $conn = pg_connect("dbname=transitdata user=postgres password=snmc");
+  } else {
+    $conn = pg_connect("dbname=transitdata user=transitdata password=transitdata ");
+  }
+  if (!$conn) {
+      die("A database error occurred.\n");
+  }
+  
+  function databaseError($errMsg) {
+    die($errMsg);
+  }
+ 
+  include('db/route-dao.inc.php');
+  include('db/trip-dao.inc.php');
+  include('db/stop-dao.inc.php');  
+  ?>
 

file:a/include/common-db.php (deleted)
--- a/include/common-db.php
+++ /dev/null

--- a/include/common-geo.inc.php
+++ b/include/common-geo.inc.php
@@ -46,9 +46,9 @@
 		$center = $totalLat / sizeof($mapPoints) . "," . $totalLon / sizeof($mapPoints);
 	}
 	$output = "";
-	if ($collapsible) $output.= '<div data-role="collapsible" data-collapsed="true"><h3>Open Map...</h3>';
-	$output.= '<center><img src="' . curPageURL() . '/lib/staticmaplite/staticmap.php?center=' . $center . '&zoom=' . $zoom . '&size=' . $width . 'x' . $height . '&markers=' . 
-$markers . '" width=' . $width . ' height=' . $height . '></center>';
+	if ($collapsible) $output.= '<div class="map" data-role="collapsible" data-collapsed="true"><h3>Open Map...</h3>';
+	$output.= '<img class="map" src="' . curPageURL() . '/lib/staticmaplite/staticmap.php?center=' . $center . '&amp;zoom=' . $zoom . '&amp;size=' . $width . 'x' . $height . '&amp;markers=' . 
+$markers . '" width=' . $width . ' height=' . $height . '>';
 	if ($collapsible) $output.= '</div>';
 	return $output;
 }
@@ -70,6 +70,7 @@
 	  else return round($km,2)."k";
 	} else return floor($km * 1000);
 }
+
 function decodePolylineToArray($encoded)
 {
 	// source: http://latlongeeks.com/forum/viewtopic.php?f=4&t=5

--- a/include/common-net.inc.php
+++ b/include/common-net.inc.php
@@ -5,7 +5,7 @@
 	$ch = curl_init($url);
 	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 	curl_setopt($ch, CURLOPT_HEADER, 0);
-	curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+	curl_setopt($ch, CURLOPT_TIMEOUT, 45);
 	$page = curl_exec($ch);
 	if (curl_errno($ch)) {
 		echo "<font color=red> Database temporarily unavailable: ";

--- a/include/common-session.inc.php
+++ b/include/common-session.inc.php
@@ -9,7 +9,7 @@
 	$_SESSION['time'] = filter_var($_REQUEST['time'], FILTER_SANITIZE_STRING);
 	sessionUpdated();
 }
-if (isset($_REQUEST['geolocate'])) {
+if (isset($_REQUEST['geolocate']) && $_REQUEST['geolocate'] != "Enter co-ordinates or address here") {
 	$geocoded = false;
 	if (isset($_REQUEST['lat']) && isset($_REQUEST['lon'])) {
 		$_SESSION['lat'] = trim(filter_var($_REQUEST['lat'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION));
@@ -17,7 +17,6 @@
 	}
 	else {
 		$geolocate = filter_var($_REQUEST['geolocate'], FILTER_SANITIZE_URL);
-		echo $_REQUEST['geolocate'];
 		if (startsWith($geolocate, "-")) {
 			$locateparts = explode(",", $geolocate);
 			$_SESSION['lat'] = $locateparts[0];
@@ -52,6 +51,9 @@
 	session_destroy();
 	session_start();
 }
-debug(print_r($_SESSION, true) , "session");
+//debug(print_r($_SESSION, true) , "session");
 
+function current_time() {
+	return ($_SESSION['time']? $_SESSION['time'] : date("H:i:s"));
+}
 ?>

--- a/include/common-template.inc.php
+++ b/include/common-template.inc.php
@@ -1,28 +1,27 @@
 <?php
-  // Copyright 2009 Google Inc. All Rights Reserved.
-  $GA_ACCOUNT = "MO-22173039-1";
-  $GA_PIXEL = "/lib/ga.php";
-
-  function googleAnalyticsGetImageUrl() {
-    global $GA_ACCOUNT, $GA_PIXEL;
-    $url = "";
-    $url .= $GA_PIXEL . "?";
-    $url .= "utmac=" . $GA_ACCOUNT;
-    $url .= "&utmn=" . rand(0, 0x7fffffff);
-    $referer = $_SERVER["HTTP_REFERER"];
-    $query = $_SERVER["QUERY_STRING"];
-    $path = $_SERVER["REQUEST_URI"];
-    if (empty($referer)) {
-      $referer = "-";
-    }
-    $url .= "&utmr=" . urlencode($referer);
-    if (!empty($path)) {
-      $url .= "&utmp=" . urlencode($path);
-    }
-    $url .= "&guid=ON";
-    return str_replace("&", "&amp;", $url);
-  }
-
+// Copyright 2009 Google Inc. All Rights Reserved.
+$GA_ACCOUNT = "MO-22173039-1";
+$GA_PIXEL = "/lib/ga.php";
+function googleAnalyticsGetImageUrl()
+{
+	global $GA_ACCOUNT, $GA_PIXEL;
+	$url = "";
+	$url.= $GA_PIXEL . "?";
+	$url.= "utmac=" . $GA_ACCOUNT;
+	$url.= "&utmn=" . rand(0, 0x7fffffff);
+	$referer = $_SERVER["HTTP_REFERER"];
+	$query = $_SERVER["QUERY_STRING"];
+	$path = $_SERVER["REQUEST_URI"];
+	if (empty($referer)) {
+		$referer = "-";
+	}
+	$url.= "&utmr=" . urlencode($referer);
+	if (!empty($path)) {
+		$url.= "&utmp=" . urlencode($path);
+	}
+	$url.= "&guid=ON";
+	return str_replace("&", "&amp;", $url);
+}
 function include_header($pageTitle, $pageType, $opendiv = true, $geolocate = false, $datepicker = false)
 {
 	echo '
@@ -34,27 +33,34 @@
         <meta name="google-site-verification" 
 content="-53T5Qn4TB_de1NyfR_ZZkEVdUNcNFSaYKSFkWKx-sY" />';
 	if ($datepicker) echo '<link rel="stylesheet"  href="css/jquery.ui.datepicker.mobile.css" />';
-	if (isDebugServer()) echo '<link rel="stylesheet"  href="css/jquery.mobile-1.0a4.css" />
+	if (isDebugServer()) {
+		echo '<link rel="stylesheet"  href="css/jquery.mobile-1.0a4.css" />
+	
          <script type="text/javascript" src="js/jquery-1.5.js"></script>
 	 <script>$(document).bind("mobileinit", function(){
   $.mobile.ajaxEnabled = false;
 });
 </script>
         <script type="text/javascript" src="js/jquery.mobile-1.0a4.js"></script>';
-	else echo '<link rel="stylesheet"  href="http://code.jquery.com/mobile/1.0a4/jquery.mobile-1.0a4.min.css" />
+	}
+	else {
+		echo '<link rel="stylesheet"  href="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.css" />
         <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
 	 <script>$(document).bind("mobileinit", function(){
   $.mobile.ajaxEnabled = false;
 });
 </script>
-        <script type="text/javascript" src="http://code.jquery.com/mobile/1.0a4/jquery.mobile-1.0a4.min.js"></script>';
-	if ($datepicker) echo '<script> 
+        <script type="text/javascript" src="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.js"></script>';
+	}
+	if ($datepicker) {
+		echo '<script> 
 		//reset type=date inputs to text
 		$( document ).bind( "mobileinit", function(){
 			$.mobile.page.prototype.options.degradeInputs.date = true;
 		});	
 	</script> 
 	<script src="js/jQuery.ui.datepicker.js"></script>';
+	}
 	echo '<style type="text/css">
      .ui-navbar {
      width: 100%;
@@ -68,10 +74,14 @@
     .ui-listview-filter {
         margin: 0 !important;
      }
-     .ui-icon-navigation {
+    .ui-icon-navigation {
         background-image: url(css/images/113-navigation.png);
         background-position: 1px 0;
      }
+    .ui-icon-beaker {
+        background-image: url(css/images/91-beaker-2.png);
+        background-position: 1px 0;
+    }
     #footer {
         text-size: 0.75em;
         text-align: center;
@@ -92,6 +102,14 @@
     #extrainfo {
     visibility: hidden;
     display: none;
+    }
+    #servicewarning {
+    padding: 1em;
+    margin-bottom: 0.5em;
+    text-size: 0.2em;
+    background-color: #FF9;
+    -moz-border-radius: 15px;
+border-radius: 15px;
     }
     // source http://webaim.org/techniques/skipnav/
     #skip a, #skip a:hover, #skip a:visited 
@@ -145,17 +163,17 @@
 $('#here').show();
 });
 ";
-if (!isset($_SESSION['lat']) || $_SESSION['lat'] == "") echo "geolocate();";
-echo "</script> ";
+		if (!isset($_SESSION['lat']) || $_SESSION['lat'] == "") echo "geolocate();";
+		echo "</script> ";
 	}
 	if (isAnalyticsOn()) echo '
-<script type="text/javascript">'."
+<script type="text/javascript">' . "
 
   var _gaq = _gaq || [];
   _gaq.push(['_setAccount', 'UA-22173039-1']);
   _gaq.push(['_trackPageview']);
 </script>";
-echo '</head>
+	echo '</head>
 <body>
     <div id="skip">
     <a href="#maincontent">Skip to content</a>
@@ -163,27 +181,32 @@
  ';
 	if ($opendiv) {
 		echo '<div data-role="page"> 
- <script>
-$(document).ready(function ()
-{
-    document.title = "' . $pageTitle . '";
-});
-</script>
 	<div data-role="header" data-position="inline">
-	<a href="'.$_SERVER["HTTP_REFERER"].'" data-icon="arrow-l" data-rel="back" class="ui-btn-left">Back</a> 
+	<a href="' . (isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : "javascript:history.go(-1)") . '" data-icon="arrow-l" data-rel="back" class="ui-btn-left">Back</a> 
 		<h1>' . $pageTitle . '</h1>
 		<a href="/index.php" data-icon="home" class="ui-btn-right">Home</a>
 	</div><!-- /header -->
         <a name="maincontent" id="maincontent"></a>
         <div data-role="content"> ';
-	}
+		$overrides = getServiceOverride();
+		if ($overrides['service_id']) {
+				if ($overrides['service_id'] == "noservice") {
+					echo '<div id="servicewarning">Buses are <strong>not running today</strong> due to industrial action/public holiday. See <a 
+href="http://www.action.act.gov.au">http://www.action.act.gov.au</a> for details.</div>';
+				}
+				else {
+					echo '<div id="servicewarning">Buses are running on an altered timetable today due to industrial action/public holiday. See <a href="http://www.action.act.gov.au">http://www.action.act.gov.au</a> for details.</div>';
+				}
+			}
+		}
+
 }
 function include_footer()
 {
-	echo '<div id="footer"><a href="about.php">About/Contact Us</a>&nbsp;<a href="feedback.php">Feedback/Bug Report</a></a>';
+	echo '<div id="footer"><a href="about.php">About/Contact Us</a>&nbsp;<a href="feedback.php">Feedback/Bug Report</a>';
 	echo '</div>';
-        if (isAnalyticsOn()) {
-	echo "<script>  (function() {
+	if (isAnalyticsOn()) {
+		echo "<script>  (function() {
     var ga = document.createElement('script'); ga.type = 
 'text/javascript'; ga.async = true;
     ga.src = ('https:' == document.location.protocol ? 
@@ -191,9 +214,11 @@
     var s = document.getElementsByTagName('script')[0]; 
 s.parentNode.insertBefore(ga, s);
   })();</script>";
-         $googleAnalyticsImageUrl = googleAnalyticsGetImageUrl();
-  echo '<noscript><img src="' . $googleAnalyticsImageUrl . '" /></noscript>';
-    }
+		$googleAnalyticsImageUrl = googleAnalyticsGetImageUrl();
+		echo '<noscript><img src="' . $googleAnalyticsImageUrl . '" /></noscript>';
+
+	}
+			echo "\n</div></div></body></html>";
 }
 function timePlaceSettings($geolocate = false)
 {
@@ -209,7 +234,7 @@
 	}
 	echo '<div data-role="collapsible" data-collapsed="' . !$geoerror . '">
         <h3>Change Time/Place (' . (isset($_SESSION['time']) ? $_SESSION['time'] : "Current Time,") . ' ' . ucwords(service_period()) . ')...</h3>
-        <form action="'.basename($_SERVER['PHP_SELF'])."?".$_SERVER['QUERY_STRING'].'" method="post">
+        <form action="' . basename($_SERVER['PHP_SELF']) . "?" . $_SERVER['QUERY_STRING'] . '" method="post">
         <div class="ui-body"> 
 		<div data-role="fieldcontain">
 	            <label for="geolocate"> Current Location: </label>
@@ -218,7 +243,7 @@
     		<div data-role="fieldcontain">
 		        <label for="time"> Time: </label>
 		    	<input type="time" name="time" id="time" value="' . (isset($_SESSION['time']) ? $_SESSION['time'] : date("H:i")) . '"/>
-			<a href="#" name="currentTime" id="currentTime" onClick="var d = new Date();'. "$('#time').val(d.getHours() +':'+ (d.getMinutes().toString().length = 1 ? '0'+ d.getMinutes():  d.getMinutes()));".'">Current Time?</a>
+			<a href="#" name="currentTime" id="currentTime" onClick="var d = new Date();' . "$('#time').val(d.getHours() +':'+ (d.getMinutes().toString().length == 1 ? '0'+ d.getMinutes():  d.getMinutes()));" . '">Current Time?</a>
 	        </div>
 		<div data-role="fieldcontain">
 		    <label for="service_period"> Service Period:  </label>
@@ -227,17 +252,18 @@
 		echo "<option value=\"$service_period\"" . (service_period() === $service_period ? " SELECTED" : "") . '>' . ucwords($service_period) . '</option>';
 	}
 	echo '</select>
-			<a href="#" style="display:none" name="currentPeriod" id="currentPeriod"/>Current Period?</a>
+			<a href="#" style="display:none" name="currentPeriod" id="currentPeriod">Current Period?</a>
 		</div>
 		
 		<input type="submit" value="Update"/>
-                </form>
-            </div></div>';
-}
-function trackEvent($category, $action, $label = "", $value = -1) {
-  if (isAnalyticsOn()) {
-    echo "\n<script> _gaq.push(['_trackEvent', '$category', '$action'".($label != "" ? ", '$label'" : "").($value != -1 ? ", $value" : "")."]);</script>";
-  }
+                </div></form>
+            </div>';
+}
+function trackEvent($category, $action, $label = "", $value = - 1)
+{
+	if (isAnalyticsOn()) {
+		echo "\n<script> _gaq.push(['_trackEvent', '$category', '$action'" . ($label != "" ? ", '$label'" : "") . ($value != - 1 ? ", $value" : "") . "]);</script>";
+	}
 }
 ?>
 

--- a/include/common-transit.inc.php
+++ b/include/common-transit.inc.php
@@ -4,9 +4,26 @@
 	'saturday',
 	'weekday'
 );
+function getServiceOverride() {
+	global $conn;
+	$query = "Select * from calendar_dates where date = '".date("Ymd")."' and exception_type = '1'";
+	 debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_assoc($result);
+}
 function service_period()
 {
+	
 	if (isset($_SESSION['service_period'])) return $_SESSION['service_period'];
+	$override = getServiceOverride();
+	if ($override['service_id']){
+		return $override['service_id'];
+	}
+
 	switch (date('w')) {
 	case 0:
 		return 'sunday';
@@ -35,5 +52,5 @@
 		return "";
 	}
 }
+?>
 
-?>

--- a/include/common.inc.php
+++ b/include/common.inc.php
@@ -1,24 +1,23 @@
 <?php
 date_default_timezone_set('Australia/ACT');
-$APIurl = "http://localhost:8765";
 $debugOkay = Array(
 	"session",
 	"json",
 	"phperror",
-	//"awsgtfs",
 	"awsotp",
 	//"squallotp",
-	//"vanilleotp",
+	"vanilleotp",
+	"database",
 	"other"
 );
-if (isDebug("awsgtfs")) {
-	$APIurl = "http://bus-main.lambdacomplex.org:8765";
-}
 $cloudmadeAPIkey = "daa03470bb8740298d4b10e3f03d63e6";
 $googleMapsAPIkey = "ABQIAAAA95XYXN0cki3Yj_Sb71CFvBTPaLd08ONybQDjcH_VdYtHHLgZvRTw2INzI_m17_IoOUqH3RNNmlTk1Q";
 $otpAPIurl = 'http://localhost:8080/opentripplanner-api-webapp/';
 if (isDebug("awsotp") || php_uname('n') == "maxious.xen.prgmr.com") {
 	$otpAPIurl = 'http://bus-main.lambdacomplex.org:8080/opentripplanner-api-webapp/';
+}
+if (isDebug("dotcloudotp") || php_uname('n') == "actbus-www") {
+	$otpAPIurl = 'http://otp.actbus.dotcloud.com/opentripplanner-api-webapp/';
 }
 if (isDebug("squallotp")) {
 		$otpAPIurl = 'http://10.0.1.108:5080/opentripplanner-api-webapp/';
@@ -31,9 +30,10 @@
 include_once ("common-geo.inc.php");
 include_once ("common-net.inc.php");
 include_once ("common-transit.inc.php");
-
 include_once ("common-session.inc.php");
+include_once ("common-db.inc.php");
 include_once ("common-template.inc.php");
+include_once ("common-request.inc.php");
 
 function isDebugServer()
 {
@@ -143,5 +143,42 @@
 	if ($sort_ascending) $array = array_reverse($temp_array);
 	else $array = $temp_array;
 }
+function sktimesort(&$array, $subkey = "id", $sort_ascending = false)
+{
+	if (count($array)) $temp_array[key($array) ] = array_shift($array);
+	foreach ($array as $key => $val) {
+		$offset = 0;
+		$found = false;
+		foreach ($temp_array as $tmp_key => $tmp_val) {
+			if (!$found and strtotime($val[$subkey]) > strtotime($tmp_val[$subkey])) {
+				$temp_array = array_merge((array)array_slice($temp_array, 0, $offset) , array(
+					$key => $val
+				) , array_slice($temp_array, $offset));
+				$found = true;
+			}
+			$offset++;
+		}
+		if (!$found) $temp_array = array_merge($temp_array, array(
+			$key => $val
+		));
+	}
+	if ($sort_ascending) $array = array_reverse($temp_array);
+	else $array = $temp_array;
+}
+function r_implode( $glue, $pieces ) 
+{ 
+  foreach( $pieces as $r_pieces ) 
+  { 
+    if( is_array( $r_pieces ) ) 
+    { 
+      $retVal[] = r_implode( $glue, $r_pieces ); 
+    } 
+    else 
+    { 
+      $retVal[] = $r_pieces; 
+    } 
+  } 
+  return implode( $glue, $retVal ); 
+} 
 ?>
 

--- a/include/db/route-dao.inc.php
+++ b/include/db/route-dao.inc.php
@@ -1,78 +1,160 @@
 <?php
 
 function getRoute($routeID) {
-/*
-        def handle_json_GET_routerow(self, params):
-    schedule = self.server.schedule
-    route = schedule.GetRoute(params.get('route', None))
-    return [transitfeed.Route._FIELD_NAMES, route.GetFieldValuesTuple()]
-*/
+		global $conn;
+        $query = "Select * from routes where route_id = '$routeID' LIMIT 1";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_assoc($result);   
 }
 function getRoutes() {
-/* def handle_json_GET_routes(self, params):
-    """Return a list of all routes."""
-    schedule = self.server.schedule
-    result = []
-    for r in schedule.GetRouteList():
-      servicep = None
-      for t in schedule.GetTripList():
-        if t.route_id == r.route_id:
-          servicep = t.service_period
-          break
-      result.append( (r.route_id, r.route_short_name, r.route_long_name, servicep.service_id) )
-    result.sort(key = lambda x: x[1:3])
-    return result
-*/    
+    	global $conn;
+	$query = "Select * from routes order by route_short_name;";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);    
 }
 
-function findRouteByNumber($routeNumber) {
-    /*
-  def handle_json_GET_routesearch(self, params):
-    """Return a list of routes with matching short name."""
-    schedule = self.server.schedule
-    routeshortname = params.get('routeshortname', None)
-    result = []
-    for r in schedule.GetRouteList():
-      if r.route_short_name == routeshortname:
-        servicep = None
-        for t in schedule.GetTripList():
-          if t.route_id == r.route_id:
-            servicep = t.service_period
-            break
-        result.append( (r.route_id, r.route_short_name, r.route_long_name, servicep.service_id) )
-    result.sort(key = lambda x: x[1:3])
-    return result
-    */
+function getRoutesByNumber($routeNumber = "") {
+  	global $conn;
+        if ($routeNumber != "") {
+       	$query = "Select distinct routes.route_id,routes.route_short_name,routes.route_long_name,service_id from routes  join trips on trips.route_id =
+routes.route_id join stop_times on stop_times.trip_id = trips.trip_id where route_short_name = '$routeNumber' order by route_short_name;";
+        } else {
+            $query = "SELECT DISTINCT route_short_name from routes order by route_short_name";
+        }
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);    
 }
 
 function getRouteNextTrip($routeID) {
-    /*
-  def handle_json_GET_routetrips(self, params):
-    """ Get a trip for a route_id (preferablly the next one) """
-    schedule = self.server.schedule
-    query = params.get('route_id', None).lower()
-    result = []
-    for t in schedule.GetTripList():
-      if t.route_id == query:
-        try:
-          starttime = t.GetStartTime()  
-        except:
-          print "Error for GetStartTime of trip #" + t.trip_id + sys.exc_info()[0]
-        else:
-          cursor = t._schedule._connection.cursor()
-          cursor.execute(
-              'SELECT arrival_secs,departure_secs FROM stop_times WHERE '
-              'trip_id=? ORDER BY stop_sequence DESC LIMIT 1', (t.trip_id,))
-          (arrival_secs, departure_secs) = cursor.fetchone()
-          if arrival_secs != None:
-            endtime = arrival_secs
-          elif departure_secs != None:
-            endtime = departure_secs
-          else:
-            endtime =0
-          result.append ( (starttime, t.trip_id, endtime) )
-    return sorted(result, key=lambda trip: trip[2])
-    */
+     global $conn;
+    $query = "select * from routes join trips on trips.route_id = routes.route_id
+join stop_times on stop_times.trip_id = trips.trip_id where
+arrival_time > '".current_time()."' and routes.route_id = '$routeID' order by
+arrival_time limit 1";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {   
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+        $r = pg_fetch_assoc($result);   
+        // past last trip of the day special case
+       if (sizeof($r) == 0) {
+            $query = "select * from routes join trips on trips.route_id = routes.route_id
+join stop_times on stop_times.trip_id = trips.trip_id where routes.route_id = '$routeID' order by
+arrival_time DESC limit 1";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {   
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+        $r = pg_fetch_assoc($result); 
+       }
+	return $r;       
+  }
+  
+  function getTimeInterpolatedRouteAtStop($routeID, $stop_id)
+{
+    $nextTrip = getRouteNextTrip($routeID);
+    if ($nextTrip['trip_id']){
+    	foreach (getTimeInterpolatedTrip($nextTrip['trip_id']) as $tripStop) {
+		if ($tripStop['stop_id'] == $stop_id) return $tripStop;
+	}
+    }
+	return Array();
+}
+  
+function getRouteTrips($routeID) {
+        global $conn;
+    $query = "select routes.route_id,trips.trip_id,service_id,arrival_time, stop_id, stop_sequence from routes join trips on trips.route_id = routes.route_id
+join stop_times on stop_times.trip_id = trips.trip_id where routes.route_id = '$routeID' and stop_sequence = '1' order by
+arrival_time ";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);       
+  }
+function getRoutesByDestination($destination = "", $service_period = "") {
+    global $conn;
+         if ($service_period == "") $service_period = service_period();
+         if ($destination != "")  {
+             $query = "SELECT DISTINCT trips.route_id,route_short_name,route_long_name, service_id
+FROM stop_times join trips on trips.trip_id =
+stop_times.trip_id join routes on trips.route_id = routes.route_id
+WHERE route_long_name = '$destination' AND  service_id='$service_period' order by route_short_name";
+         } else {
+        $query = "SELECT DISTINCT route_long_name
+FROM stop_times join trips on trips.trip_id =
+stop_times.trip_id join routes on trips.route_id = routes.route_id
+WHERE service_id='$service_period' order by route_long_name";
+    }
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
 }
 
+function getRoutesBySuburb($suburb, $service_period = "") {
+         if ($service_period == "") $service_period = service_period();
+    global $conn;
+        $query = "SELECT DISTINCT service_id,trips.route_id,route_short_name,route_long_name
+FROM stop_times join trips on trips.trip_id = stop_times.trip_id
+join routes on trips.route_id = routes.route_id
+join stops on stops.stop_id = stop_times.stop_id
+WHERE zone_id LIKE '%$suburb;%' AND service_id='$service_period' ORDER BY route_short_name";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
+}
+
+function getRoutesNearby($lat, $lng, $limit = "", $distance = 500) {
+
+        
+                 if ($service_period == "") $service_period = service_period();
+                  if ($limit != "") $limit = " LIMIT $limit "; 
+    global $conn;
+        $query = "SELECT service_id,trips.route_id,route_short_name,route_long_name,min(stops.stop_id) as stop_id,
+        min(ST_Distance(position, ST_GeographyFromText('SRID=4326;POINT($lng $lat)'), FALSE)) as distance
+FROM stop_times
+join trips on trips.trip_id = stop_times.trip_id
+join routes on trips.route_id = routes.route_id
+join stops on stops.stop_id = stop_times.stop_id
+WHERE service_id='$service_period'
+AND ST_DWithin(position, ST_GeographyFromText('SRID=4326;POINT($lng $lat)'), $distance, FALSE)
+        group by service_id,trips.route_id,route_short_name,route_long_name
+        order by distance $limit";
+        debug($query,"database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
+}
 ?>

--- a/include/db/stop-dao.inc.php
+++ b/include/db/stop-dao.inc.php
@@ -1,97 +1,144 @@
 <?php
-/* def StopZoneToTuple(stop):
-  """Return tuple as expected by javascript function addStopMarkerFromList"""
-  return (stop.stop_id, stop.stop_name, float(stop.stop_lat),
-          float(stop.stop_lon), stop.location_type, stop.stop_code, stop.zone_id)
-*/
-
-function getStop($stopID) {
-    
+function getStop($stopID)
+{
+	global $conn;
+	$query = "Select * from stops where stop_id = '$stopID' LIMIT 1";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_assoc($result);
 }
-
-function getStops($timingPointsOnly = false) {
-    
+function getStops($timingPointsOnly = false, $firstLetter = "")
+{
+	global $conn;
+	$conditions = Array();
+	if ($timingPointsOnly) $conditions[] = "substr(stop_code,1,2) != 'Wj'";
+	if ($firstLetter != "") $conditions[] = "substr(stop_name,1,1) = '$firstLetter'";
+	$query = "Select * from stops";
+	if (sizeof($conditions) > 0) {
+		if (sizeof($conditions) > 1) {
+			$query.= " Where " . implode(" AND ", $conditions) . " ";
+		}
+		else {
+			$query.= " Where " . $conditions[0] . " ";
+		}
+	}
+	$query.= " order by stop_name;";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
 }
-
-function stopsNear($lat,$lng,$limit) {
-    
-    /*
-        -- Show a distance query and note, London is outside the 1000km tolerance
-  SELECT name FROM global_points WHERE ST_DWithin(location, ST_GeographyFromText('SRID=4326;POINT(-110 29)'), 1000000, FALSE);
-  // All the geography functions have the option of using a sphere calculation, by setting a final boolean parameter to 'FALSE'. This will somewhat speed up calculations, particularly for cases where the geometries are very simple.
-    */
+function getNearbyStops($lat, $lng, $limit = "", $distance = 1000)
+{
+	if ($lat == null || $lng == null) return Array();
+	if ($limit != "") $limit = " LIMIT $limit ";
+	global $conn;
+	$query = "Select *, ST_Distance(position, ST_GeographyFromText('SRID=4326;POINT($lng $lat)'), FALSE) as distance
+        from stops WHERE ST_DWithin(position, ST_GeographyFromText('SRID=4326;POINT($lng $lat)'), $distance, FALSE)
+        order by distance $limit;";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
 }
-
-function stopsBySuburb($suburb) {
-    
+function getStopsBySuburb($suburb)
+{
+	global $conn;
+	$query = "Select * from stops where zone_id LIKE '%$suburb;%' order by stop_name;";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
 }
-
-function stopRoutes($stopID,$service_period)
-/*
-  def handle_json_GET_stoproutes(self, params):
-    """Given a stop_id return all routes to visit the stop."""
-    schedule = self.server.schedule
-    stop = schedule.GetStop(params.get('stop', None))
-    service_period = params.get('service_period', None)
-    trips = stop.GetTrips(schedule)
-    result = {}
-    for trip in trips:
-      route = schedule.GetRoute(trip.route_id)
-      if service_period == None or trip.service_id == service_period:
-        if not route.route_short_name+route.route_long_name+trip.service_id in result:
-          result[route.route_short_name+route.route_long_name+trip.service_id] = (route.route_id, route.route_short_name, route.route_long_name, trip.trip_id, trip.service_id)
-    return result
-*/
-
-function stopTrips($stopID) {
-    /*
-  def handle_json_GET_stopalltrips(self, params):
-    """Given a stop_id return all trips to visit the stop (without times)."""
-    schedule = self.server.schedule
-    stop = schedule.GetStop(params.get('stop', None))
-    service_period = params.get('service_period', None)
-    trips = stop.GetTrips(schedule)
-    result = []
-    for trip in trips:
-      if service_period == None or trip.service_id == service_period:
-        result.append((trip.trip_id, trip.service_id))
-    return result
-    */
+function getStopRoutes($stopID, $service_period)
+{
+	if ($service_period == "") $service_period = service_period();
+	global $conn;
+	$query = "SELECT service_id,trips.route_id,route_short_name,route_long_name
+FROM stop_times join trips on trips.trip_id =
+stop_times.trip_id join routes on trips.route_id = routes.route_id WHERE stop_id = '$stopID' AND service_id='$service_period'";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
 }
-function stopTripsWithTimes($stopID, $time, $service_period) {
-    /*
-  def handle_json_GET_stoptrips(self, params):
-    """Given a stop_id and time in seconds since midnight return the next
-    trips to visit the stop."""
-    schedule = self.server.schedule
-    stop = schedule.GetStop(params.get('stop', None))
-    requested_time = int(params.get('time', 0))
-    limit = int(params.get('limit', 15))
-    service_period = params.get('service_period', None)
-    time_range = int(params.get('time_range', 24*60*60))
-    
-    filtered_time_trips = []
-    for trip, index in stop._GetTripIndex(schedule):
-      tripstarttime = trip.GetStartTime()
-      if tripstarttime > requested_time and tripstarttime < (requested_time + time_range):
-        time, stoptime, tp = trip.GetTimeInterpolatedStops()[index]
-        if time > requested_time and time < (requested_time + time_range):
-          bisect.insort(filtered_time_trips, (time, (trip, index), tp))
-    result = []
-    for time, (trip, index), tp in filtered_time_trips:
-      if len(result) > limit:
-        break
-      route = schedule.GetRoute(trip.route_id)
-      trip_name = ''
-      if route.route_short_name:
-        trip_name += route.route_short_name
-      if route.route_long_name:
-        if len(trip_name):
-          trip_name += " - "
-        trip_name += route.route_long_name
-      if service_period == None or trip.service_id == service_period:
-        result.append((time, (trip.trip_id, trip_name, trip.service_id), tp))
-    return result
-    */
+function getStopTrips($stopID, $service_period = "", $afterTime = "")
+{
+	if ($service_period == "") $service_period = service_period();
+	$afterCondition = "AND arrival_time > '$afterTime'";
+	global $conn;
+	if ($afterTime != "") {
+		$query = " SELECT stop_times.trip_id,stop_times.arrival_time,stop_times.stop_id,stop_sequence,service_id,trips.route_id,route_short_name,route_long_name, end_times.arrival_time as end_time
+FROM stop_times
+join trips on trips.trip_id =
+stop_times.trip_id
+join routes on trips.route_id = routes.route_id , (SELECT trip_id,max(arrival_time) as arrival_time from stop_times
+	WHERE stop_times.arrival_time IS NOT NULL group by trip_id) as end_times 
+WHERE stop_times.stop_id = '$stopID'
+AND stop_times.trip_id = end_times.trip_id
+AND service_id='$service_period'
+AND end_times.arrival_time > '$afterTime'
+ORDER BY end_time";
+	}
+	else {
+		$query = "SELECT stop_times.trip_id,arrival_time,stop_times.stop_id,stop_sequence,service_id,trips.route_id,route_short_name,route_long_name
+FROM stop_times
+join trips on trips.trip_id =
+stop_times.trip_id
+join routes on trips.route_id = routes.route_id
+WHERE stop_times.stop_id = '$stopID'
+AND service_id='$service_period'
+ORDER BY arrival_time";
+	}
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
+}
+function getStopTripsWithTimes($stopID, $time = "", $service_period = "", $time_range = "", $limit = "")
+{
+	if ($service_period == "") $service_period = service_period();
+	if ($time_range == "") $time_range = (24 * 60 * 60);
+	if ($time == "") $time = current_time();
+	if ($limit == "") $limit = 10;
+	$trips = getStopTrips($stopID, $service_period, $time);
+	$timedTrips = Array();
+	if ($trips && sizeof($trips) > 0) {
+            foreach ($trips as $trip) {
+		if ($trip['arrival_time'] != "") {
+			if (strtotime($trip['arrival_time']) > strtotime($time) and strtotime($trip['arrival_time']) < (strtotime($time) + $time_range)) {
+				$timedTrips[] = $trip;
+			}
+		}
+		else {
+			$timedTrip = getTimeInterpolatedTripAtStop($trip['trip_id'], $trip['stop_sequence']);
+			if ($timedTrip['arrival_time'] > $time and strtotime($timedTrip['arrival_time']) < (strtotime($time) + $time_range)) {
+				$timedTrips[] = $timedTrip;
+			}
+		}
+		if (sizeof($timedTrips) > $limit) break;
+	}
+	sktimesort($timedTrips, "arrival_time", true);
+        }
+	return $timedTrips;
 }
 ?>

--- a/include/db/trip-dao.inc.php
+++ b/include/db/trip-dao.inc.php
@@ -1,132 +1,186 @@
 <?php
-function getTrip($tripID) {
-    /* def handle_json_GET_triprows(self, params):
-    """Return a list of rows from the feed file that are related to this
-    trip."""
-    schedule = self.server.schedule
-    try:
-      trip = schedule.GetTrip(params.get('trip', None))
-    except KeyError:
-      # if a non-existent trip is searched for, the return nothing
-      return
-    route = schedule.GetRoute(trip.route_id)
-    trip_row = dict(trip.iteritems())
-    route_row = dict(route.iteritems())
-    return [['trips.txt', trip_row], ['routes.txt', route_row]]
-    */
+function getTrip($tripID)
+{
+	global $conn;
+	$query = "Select * from trips
+	join routes on trips.route_id = routes.route_id
+	where trip_id = '$tripID'
+	LIMIT 1";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_assoc($result);
 }
-function getTripShape() {
-    /* def handle_json_GET_tripstoptimes(self, params):
-    schedule = self.server.schedule
-    try:
-      trip = schedule.GetTrip(params.get('trip'))
-    except KeyError:
-       # if a non-existent trip is searched for, the return nothing
-      return
-    time_stops = trip.GetTimeInterpolatedStops()
-    stops = []
-    times = []
-    for arr,ts,is_timingpoint in time_stops:
-      stops.append(StopToTuple(ts.stop))
-      times.append(arr)
-    return [stops, times]
-
-  def handle_json_GET_tripshape(self, params):
-    schedule = self.server.schedule
-    try:
-      trip = schedule.GetTrip(params.get('trip'))
-    except KeyError:
-       # if a non-existent trip is searched for, the return nothing
-      return
-    points = []
-    if trip.shape_id:
-      shape = schedule.GetShape(trip.shape_id)
-      for (lat, lon, dist) in shape.points:
-        points.append((lat, lon))
-    else:
-      time_stops = trip.GetTimeStops()
-      for arr,dep,stop in time_stops:
-        points.append((stop.stop_lat, stop.stop_lon))
-    return points*/
+function getTripShape()
+{
+	/* def handle_json_GET_tripstopTimes(self, params):
+	   schedule = self.server.schedule
+	   try:
+	     trip = schedule.GetTrip(params.get('trip'))
+	   except KeyError:
+	      # if a non-existent trip is searched for, the return nothing
+	     return
+	   time_stops = trip.GetTimeInterpolatedStops()
+	   stops = []
+	   times = []
+	   for arr,ts,is_timingpoint in time_stops:
+	     stops.append(StopToTuple(ts.stop))
+	     times.append(arr)
+	   return [stops, times]
+	
+	 def handle_json_GET_tripshape(self, params):
+	   schedule = self.server.schedule
+	   try:
+	     trip = schedule.GetTrip(params.get('trip'))
+	   except KeyError:
+	      # if a non-existent trip is searched for, the return nothing
+	     return
+	   points = []
+	   if trip.shape_id:
+	     shape = schedule.GetShape(trip.shape_id)
+	     for (lat, lon, dist) in shape.points:
+	       points.append((lat, lon))
+	   else:
+	     time_stops = trip.GetTimeStops()
+	     for arr,dep,stop in time_stops:
+	       points.append((stop.stop_lat, stop.stop_lon))
+	   return points*/
 }
-function tripStopTimes($tripID, $after_time, $limit) {
-    /*     rv = []
-
-    stoptimes = self.GetStopTimes()
-    # If there are no stoptimes [] is the correct return value but if the start
-    # or end are missing times there is no correct return value.
-    if not stoptimes:
-      return []
-    if (stoptimes[0].GetTimeSecs() is None or
-        stoptimes[-1].GetTimeSecs() is None):
-      raise ValueError("%s must have time at first and last stop" % (self))
-
-    cur_timepoint = None
-    next_timepoint = None
-    distance_between_timepoints = 0
-    distance_traveled_between_timepoints = 0
-
-    for i, st in enumerate(stoptimes):
-      if st.GetTimeSecs() != None:
-        cur_timepoint = st
-        distance_between_timepoints = 0
-        distance_traveled_between_timepoints = 0
-        if i + 1 < len(stoptimes):
-          k = i + 1
-          distance_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[k-1].stop, stoptimes[k].stop)
-          while stoptimes[k].GetTimeSecs() == None:
-            k += 1
-            distance_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[k-1].stop, stoptimes[k].stop)
-          next_timepoint = stoptimes[k]
-        rv.append( (st.GetTimeSecs(), st, True) )
-      else:
-        distance_traveled_between_timepoints += util.ApproximateDistanceBetweenStops(stoptimes[i-1].stop, st.stop)
-        distance_percent = distance_traveled_between_timepoints / distance_between_timepoints
-        total_time = next_timepoint.GetTimeSecs() - cur_timepoint.GetTimeSecs()
-        time_estimate = distance_percent * total_time + cur_timepoint.GetTimeSecs()
-        rv.append( (int(round(time_estimate)), st, False) )
-
-    return rv*/
-}
-
-function tripStartTime($tripID) {
-    $query = 'SELECT arrival_secs,departure_secs FROM stop_times WHERE trip_id=? ORDER BY stop_sequence LIMIT 1';
-    
-}
-
-function viaPoints($tripid, $stopid, $timingPointsOnly = false)
+function getTimeInterpolatedTrip($tripID, $range = "")
 {
-	global $APIurl;
-	$url = $APIurl . "/json/tripstoptimes?trip=" . $tripid;
-	$json = json_decode(getPage($url));
-	debug(print_r($json, true));
-	$stops = $json[0];
-	$times = $json[1];
-	$foundStop = false;
-	$viaPoints = Array();
-	foreach ($stops as $key => $row) {
-		if ($foundStop) {
-			if (!$timingPointsOnly || !startsWith($row[5], "Wj")) {
-				$viaPoints[] = Array(
-					"id" => $row[0],
-					"name" => $row[1],
-					"time" => $times[$key]
-				);
+	global $conn;
+	$query = "SELECT stop_times.trip_id,arrival_time,stop_times.stop_id,stop_lat,stop_lon,stop_name,stop_code,
+	stop_sequence,service_id,trips.route_id,route_short_name,route_long_name
+FROM stop_times
+join trips on trips.trip_id = stop_times.trip_id
+join routes on trips.route_id = routes.route_id
+join stops on stops.stop_id = stop_times.stop_id
+WHERE trips.trip_id = '$tripID' $range ORDER BY stop_sequence";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	$stopTimes = pg_fetch_all($result);
+	$cur_timepoint = Array();
+	$next_timepoint = Array();
+	$distance_between_timepoints = 0.0;
+	$distance_traveled_between_timepoints = 0.0;
+	$rv = Array();
+	foreach ($stopTimes as $i => $stopTime) {
+		if ($stopTime['arrival_time'] != "") {
+		    // is timepoint
+			$cur_timepoint = $stopTime;
+			$distance_between_timepoints = 0.0;
+			$distance_traveled_between_timepoints = 0.0;
+			if ($i + 1 < sizeof($stopTimes)) {
+				$k = $i + 1;
+				$distance_between_timepoints += distance($stopTimes[$k - 1]["stop_lat"], $stopTimes[$k - 1]["stop_lon"], $stopTimes[$k]["stop_lat"], $stopTimes[$k]["stop_lon"]);
+				while ($stopTimes[$k]["arrival_time"] == "" && $k + 1 < sizeof($stopTimes)) {
+					$k += 1;
+					//echo "k".$k;
+					$distance_between_timepoints += distance($stopTimes[$k - 1]["stop_lat"], $stopTimes[$k - 1]["stop_lon"], $stopTimes[$k]["stop_lat"], $stopTimes[$k]["stop_lon"]);
+				}
+				$next_timepoint = $stopTimes[$k];
+				$rv[] = $stopTime;
 			}
 		}
 		else {
-			if ($row[0] == $stopid) $foundStop = true;
+		    // is untimed point
+		    //echo "i".$i;
+			$distance_traveled_between_timepoints += distance($stopTimes[$i - 1]["stop_lat"], $stopTimes[$i - 1]["stop_lon"], $stopTimes[$i]["stop_lat"], $stopTimes[$i]["stop_lon"]);
+			//echo "$distance_traveled_between_timepoints / $distance_between_timepoints<br>";
+			$distance_percent = $distance_traveled_between_timepoints / $distance_between_timepoints;
+			if ($next_timepoint["arrival_time"] != "") {
+			$total_time = strtotime($next_timepoint["arrival_time"]) - strtotime($cur_timepoint["arrival_time"]);
+			//echo strtotime($next_timepoint["arrival_time"])." - ".strtotime($cur_timepoint["arrival_time"])."<br>";
+			$time_estimate = ($distance_percent * $total_time) + strtotime($cur_timepoint["arrival_time"]);
+			$stopTime["arrival_time"] = date("H:i:s", $time_estimate);
+			} else {
+			    $stopTime["arrival_time"] = $cur_timepoint["arrival_time"];
+			}
+			$rv[] = $stopTime;
+			//var_dump($rv);
 		}
 	}
-	return $viaPoints;
+	return $rv;
 }
-function viaPointNames($tripid, $stopid)
+function getTimeInterpolatedTripAtStop($tripID, $stop_sequence)
 {
-	$points = viaPoints($tripid, $stopid, true);
-	$pointNames = Array();
-	foreach ($points as $point) {
-		$pointNames[] = $point['name'];
+    global $conn;
+    // limit interpolation to between nearest actual points.
+    $prevTimePoint = pg_fetch_assoc(pg_query($conn," SELECT trip_id,stop_id,
+	stop_sequence
+FROM stop_times
+WHERE trip_id = '$tripID' and stop_sequence < $stop_sequence and stop_times.arrival_time IS NOT NULL ORDER BY stop_sequence DESC LIMIT 1"));
+    $nextTimePoint = pg_fetch_assoc(pg_query($conn," SELECT trip_id,stop_id,
+	stop_sequence
+FROM stop_times
+WHERE trip_id = '$tripID' and stop_sequence > $stop_sequence and stop_times.arrival_time IS NOT NULL ORDER BY stop_sequence LIMIT 1"));
+    $range = "AND stop_sequence >= '{$prevTimePoint['stop_sequence']}' AND stop_sequence <= '{$nextTimePoint['stop_sequence']}'";
+    	foreach (getTimeInterpolatedTrip($tripID,$range) as $tripStop) {
+		if ($tripStop['stop_sequence'] == $stop_sequence) return $tripStop;
 	}
-	return implode(", ", $pointNames);
+	return Array();
+}
+function getTripStartTime($tripID)
+{
+    	global $conn;
+	$query = "Select * from stop_times
+	where trip_id = '$tripID'
+	AND arrival_time IS NOT NULL
+	AND stop_sequence = '1'";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	$r = pg_fetch_assoc($result);
+	return $r['arrival_time'];
+}
+function getActiveTrips($time)
+{
+    	global $conn;
+	if ($time == "") $time = current_time();
+	$query = "Select distinct stop_times.trip_id, start_times.arrival_time as start_time, end_times.arrival_time as end_time from stop_times, (SELECT trip_id,arrival_time from stop_times WHERE stop_times.arrival_time IS NOT NULL
+AND stop_sequence = '1') as start_times, (SELECT trip_id,max(arrival_time) as arrival_time from stop_times WHERE stop_times.arrival_time IS NOT NULL group by trip_id) as end_times
+WHERE start_times.trip_id = end_times.trip_id AND stop_times.trip_id = end_times.trip_id AND $time > start_times.arrival_time  AND $time < end_times.arrival_time";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
+}
+
+function viaPoints($tripid, $stop_sequence = "")
+{
+	global $conn;
+	$query = "SELECT stops.stop_id, stop_name, arrival_time
+FROM stop_times join stops on stops.stop_id = stop_times.stop_id
+WHERE stop_times.trip_id = '$tripid'
+".($stop_sequence != "" ? "AND stop_sequence > '$stop_sequence'" : "").
+"AND substr(stop_code,1,2) != 'Wj' ORDER BY stop_sequence";
+	debug($query, "database");
+	$result = pg_query($conn, $query);
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	return pg_fetch_all($result);
+}
+function viaPointNames($tripid, $stop_sequence = "")
+{
+	$viaPointNames = Array();
+	foreach(viaPoints($tripid, $stop_sequence) as $point) {
+		$viaPointNames[] = $point['stop_name'];
+	}
+	return r_implode(", ", $viaPointNames);
 }
 ?>

file:a/index.php -> file:b/index.php
--- a/index.php
+++ b/index.php
@@ -13,18 +13,19 @@
                 <li data-role="list-divider">Timetables - Stops</li>
                 <li><a href="stopList.php">Major (Timing Point) Stops</a></li>
 		<li><a href="stopList.php?allstops=yes">All Stops</a></li>
-		<li><a href="stopList.php?suburbs=yes">Stops By Suburb</a></li>
+		<li><a href="stopList.php?bysuburbs=yes">Stops By Suburb</a></li>
 		<li><a class="nearby" href="stopList.php?nearby=yes">Nearby Stops</a></li>
             </ul>
 	    <ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="b">
                 <li data-role="list-divider">Timetables - Routes</li>
                 <li><a href="routeList.php">Routes By Final Destination</a></li>
 		<li><a href="routeList.php?bynumber=yes">Routes By Number</a></li>
-		<li><a href="routeList.php?bysuburb=yes">Routes By Suburb</a></li>
+		<li><a href="routeList.php?bysuburbs=yes">Routes By Suburb</a></li>
 		<li><a class="nearby" href="routeList.php?nearby=yes">Nearby Routes</a></li>
             </ul>
 <?php
 echo timePlaceSettings();
+echo ' <a href="labs/index.php" data-role="button" data-icon="beaker">Busness R&amp;D</a>';
 include_footer(true)
 ?>
         

--- /dev/null
+++ b/js/flotr/flotr-0.2.0-alpha.js
@@ -1,1 +1,2 @@
-
+//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, <http://solutoire.com>, MIT License.
+var Flotr={version:"0.2.0-alpha",author:"Bas Wenneker",website:"http://www.solutoire.com",_registeredTypes:{lines:"drawSeriesLines",points:"drawSeriesPoints",bars:"drawSeriesBars",candles:"drawSeriesCandles",pie:"drawSeriesPie"},register:function(A,B){Flotr._registeredTypes[A]=B+""},draw:function(B,D,A,C){C=C||Flotr.Graph;return new C(B,D,A)},getSeries:function(A){return A.collect(function(C){var B,C=(C.data)?Object.clone(C):{data:C};for(B=C.data.length-1;B>-1;--B){C.data[B][1]=(C.data[B][1]===null?null:parseFloat(C.data[B][1]))}return C})},merge:function(D,B){var A=B||{};for(var C in D){A[C]=(D[C]!=null&&typeof (D[C])=="object"&&!(D[C].constructor==Array||D[C].constructor==RegExp)&&!Object.isElement(D[C]))?Flotr.merge(D[C],B[C]):A[C]=D[C]}return A},getTickSize:function(E,D,A,B){var H=(A-D)/E;var G=Flotr.getMagnitude(H);var C=H/G;var F=10;if(C<1.5){F=1}else{if(C<2.25){F=2}else{if(C<3){F=((B==0)?2:2.5)}else{if(C<7.5){F=5}}}}return F*G},defaultTickFormatter:function(A){return A+""},defaultTrackFormatter:function(A){return"("+A.x+", "+A.y+")"},defaultPieLabelFormatter:function(A){return(A.fraction*100).toFixed(2)+"%"},getMagnitude:function(A){return Math.pow(10,Math.floor(Math.log(A)/Math.LN10))},toPixel:function(A){return Math.floor(A)+0.5},toRad:function(A){return -A*(Math.PI/180)},parseColor:function(D){if(D instanceof Flotr.Color){return D}var A,C=Flotr.Color;if((A=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(D))){return new C(parseInt(A[1]),parseInt(A[2]),parseInt(A[3]))}if((A=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(D))){return new C(parseInt(A[1]),parseInt(A[2]),parseInt(A[3]),parseFloat(A[4]))}if((A=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(D))){return new C(parseFloat(A[1])*2.55,parseFloat(A[2])*2.55,parseFloat(A[3])*2.55)}if((A=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(D))){return new C(parseFloat(A[1])*2.55,parseFloat(A[2])*2.55,parseFloat(A[3])*2.55,parseFloat(A[4]))}if((A=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(D))){return new C(parseInt(A[1],16),parseInt(A[2],16),parseInt(A[3],16))}if((A=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(D))){return new C(parseInt(A[1]+A[1],16),parseInt(A[2]+A[2],16),parseInt(A[3]+A[3],16))}var B=D.strip().toLowerCase();if(B=="transparent"){return new C(255,255,255,0)}return((A=C.lookupColors[B]))?new C(A[0],A[1],A[2]):false},extractColor:function(B){var A;do{A=B.getStyle("background-color").toLowerCase();if(!(A==""||A=="transparent")){break}B=B.up(0)}while(!B.nodeName.match(/^body$/i));return(A=="rgba(0, 0, 0, 0)")?"transparent":A}};Flotr.Graph=Class.create({initialize:function(B,C,A){this.el=$(B);if(!this.el){throw"The target container doesn't exist"}this.data=C;this.series=Flotr.getSeries(C);this.setOptions(A);this.lastMousePos={pageX:null,pageY:null};this.selection={first:{x:-1,y:-1},second:{x:-1,y:-1}};this.prevSelection=null;this.selectionInterval=null;this.ignoreClick=false;this.prevHit=null;this.constructCanvas();this.initEvents();this.findDataRanges();this.calculateTicks(this.axes.x);this.calculateTicks(this.axes.x2);this.calculateTicks(this.axes.y);this.calculateTicks(this.axes.y2);this.calculateSpacing();this.draw();this.insertLegend();if(this.options.spreadsheet.show){this.constructTabs()}},setOptions:function(B){var P={colors:["#00A8F0","#C0D800","#CB4B4B","#4DA74D","#9440ED"],title:null,subtitle:null,legend:{show:true,noColumns:1,labelFormatter:Prototype.K,labelBoxBorderColor:"#CCCCCC",labelBoxWidth:14,labelBoxHeight:10,labelBoxMargin:5,container:null,position:"nw",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{ticks:null,showLabels:true,labelsAngle:0,title:null,titleAngle:0,noTicks:5,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscaleMargin:0,color:null},x2axis:{},yaxis:{ticks:null,showLabels:true,labelsAngle:0,title:null,titleAngle:90,noTicks:5,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscaleMargin:0,color:null},y2axis:{titleAngle:270},points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#FFFFFF",fillOpacity:0.4},lines:{show:false,lineWidth:2,fill:false,fillColor:null,fillOpacity:0.4},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,fillOpacity:0.4,horizontal:false,stacked:false},candles:{show:false,lineWidth:1,wickLineWidth:1,candleWidth:0.6,fill:true,upFillColor:"#00A8F0",downFillColor:"#CB4B4B",fillOpacity:0.5,barcharts:false},pie:{show:false,lineWidth:1,fill:true,fillColor:null,fillOpacity:0.6,explode:6,sizeRatio:0.6,startAngle:Math.PI/4,labelFormatter:Flotr.defaultPieLabelFormatter,pie3D:false,pie3DviewAngle:(Math.PI/2*0.8),pie3DspliceThickness:20},grid:{color:"#545454",backgroundColor:null,tickColor:"#DDDDDD",labelMargin:3,verticalLines:true,horizontalLines:true,outlineWidth:2},selection:{mode:null,color:"#B6D9FF",fps:20},mouse:{track:false,position:"se",relative:false,trackFormatter:Flotr.defaultTrackFormatter,margin:5,lineColor:"#FF3F19",trackDecimals:1,sensibility:2,radius:3},shadowSize:4,defaultType:"lines",HtmlText:true,fontSize:7.5,spreadsheet:{show:false,tabGraphLabel:"Graph",tabDataLabel:"Data",toolbarDownload:"Download CSV",toolbarSelectAll:"Select all"}};P.x2axis=Object.extend(Object.clone(P.xaxis),P.x2axis);P.y2axis=Object.extend(Object.clone(P.yaxis),P.y2axis);this.options=Flotr.merge((B||{}),P);this.axes={x:{options:this.options.xaxis,n:1},x2:{options:this.options.x2axis,n:2},y:{options:this.options.yaxis,n:1},y2:{options:this.options.y2axis,n:2}};var H=[],C=[],K=this.series.length,N=this.series.length,D=this.options.colors,A=[],G=0,M,J,I,O,E;for(J=N-1;J>-1;--J){M=this.series[J].color;if(M!=null){--N;if(Object.isNumber(M)){H.push(M)}else{A.push(Flotr.parseColor(M))}}}for(J=H.length-1;J>-1;--J){N=Math.max(N,H[J]+1)}for(J=0;C.length<N;){M=(D.length==J)?new Flotr.Color(100,100,100):Flotr.parseColor(D[J]);var F=G%2==1?-1:1;var L=1+F*Math.ceil(G/2)*0.2;M.scale(L,L,L);C.push(M);if(++J>=D.length){J=0;++G}}for(J=0,I=0;J<K;++J){O=this.series[J];if(O.color==null){O.color=C[I++].toString()}else{if(Object.isNumber(O.color)){O.color=C[O.color].toString()}}if(!O.xaxis){O.xaxis=this.axes.x}if(O.xaxis==1){O.xaxis=this.axes.x}else{if(O.xaxis==2){O.xaxis=this.axes.x2}}if(!O.yaxis){O.yaxis=this.axes.y}if(O.yaxis==1){O.yaxis=this.axes.y}else{if(O.yaxis==2){O.yaxis=this.axes.y2}}O.lines=Object.extend(Object.clone(this.options.lines),O.lines);O.points=Object.extend(Object.clone(this.options.points),O.points);O.bars=Object.extend(Object.clone(this.options.bars),O.bars);O.candles=Object.extend(Object.clone(this.options.candles),O.candles);O.pie=Object.extend(Object.clone(this.options.pie),O.pie);O.mouse=Object.extend(Object.clone(this.options.mouse),O.mouse);if(O.shadowSize==null){O.shadowSize=this.options.shadowSize}}},constructCanvas:function(){var C=this.el,B,D,A;this.canvas=C.select(".flotr-canvas")[0];this.overlay=C.select(".flotr-overlay")[0];C.childElements().invoke("remove");C.setStyle({position:"relative",cursor:"default"});this.canvasWidth=C.getWidth();this.canvasHeight=C.getHeight();B={width:this.canvasWidth,height:this.canvasHeight};if(this.canvasWidth<=0||this.canvasHeight<=0){throw"Invalid dimensions for plot, width = "+this.canvasWidth+", height = "+this.canvasHeight}if(!this.canvas){D=this.canvas=new Element("canvas",B);D.className="flotr-canvas";D=D.writeAttribute("style","position:absolute;left:0px;top:0px;")}else{D=this.canvas.writeAttribute(B)}C.insert(D);if(Prototype.Browser.IE){D=window.G_vmlCanvasManager.initElement(D)}this.ctx=D.getContext("2d");if(!this.overlay){A=this.overlay=new Element("canvas",B);A.className="flotr-overlay";A=A.writeAttribute("style","position:absolute;left:0px;top:0px;")}else{A=this.overlay.writeAttribute(B)}C.insert(A);if(Prototype.Browser.IE){A=window.G_vmlCanvasManager.initElement(A)}this.octx=A.getContext("2d");if(window.CanvasText){CanvasText.enable(this.ctx);CanvasText.enable(this.octx);this.textEnabled=true}},getTextDimensions:function(F,C,B,D){if(!F){return{width:0,height:0}}if(!this.options.HtmlText&&this.textEnabled){var E=this.ctx.getTextBounds(F,C);return{width:E.width+2,height:E.height+6}}else{var A=this.el.insert('<div style="position:absolute;top:-10000px;'+B+'" class="'+D+' flotr-dummy-div">'+F+"</div>").select(".flotr-dummy-div")[0];dim=A.getDimensions();A.remove();return dim}},loadDataGrid:function(){if(this.seriesData){return this.seriesData}var A=this.series;var B=[];for(i=0;i<A.length;++i){A[i].data.each(function(D){var C=D[0],F=D[1];if(r=B.find(function(G){return G[0]==C})){r[i+1]=F}else{var E=[];E[0]=C;E[i+1]=F;B.push(E)}})}B=B.sortBy(function(C){return C[0]});return this.seriesData=B},showTab:function(B,C){var A="canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle";switch(B){case"graph":this.datagrid.up().hide();this.el.select(A).invoke("show");this.tabs.data.removeClassName("selected");this.tabs.graph.addClassName("selected");break;case"data":this.constructDataGrid();this.datagrid.up().show();this.el.select(A).invoke("hide");this.tabs.data.addClassName("selected");this.tabs.graph.removeClassName("selected");break}},constructTabs:function(){var A=new Element("div",{className:"flotr-tabs-group",style:"position:absolute;left:0px;top:"+this.canvasHeight+"px;width:"+this.canvasWidth+"px;"});this.el.insert({bottom:A});this.tabs={graph:new Element("div",{className:"flotr-tab selected",style:"float:left;"}).update(this.options.spreadsheet.tabGraphLabel),data:new Element("div",{className:"flotr-tab",style:"float:left;"}).update(this.options.spreadsheet.tabDataLabel)};A.insert(this.tabs.graph).insert(this.tabs.data);this.el.setStyle({height:this.canvasHeight+this.tabs.data.getHeight()+2+"px"});this.tabs.graph.observe("click",(function(){this.showTab("graph")}).bind(this));this.tabs.data.observe("click",(function(){this.showTab("data")}).bind(this))},constructDataGrid:function(){if(this.datagrid){return this.datagrid}var D,B,L=this.series,J=this.loadDataGrid();var K=this.datagrid=new Element("table",{className:"flotr-datagrid",style:"height:100px;"});var C=["<colgroup><col />"];var F=['<tr class="first-row">'];F.push("<th>&nbsp;</th>");for(D=0;D<L.length;++D){F.push('<th scope="col">'+(L[D].label||String.fromCharCode(65+D))+"</th>");C.push("<col />")}F.push("</tr>");for(B=0;B<J.length;++B){F.push("<tr>");for(D=0;D<L.length+1;++D){var M="td";var G=(J[B][D]!=null?Math.round(J[B][D]*100000)/100000:"");if(D==0){M="th";var I;if(this.options.xaxis.ticks){var E=this.options.xaxis.ticks.find(function(N){return N[0]==J[B][D]});if(E){I=E[1]}}else{I=this.options.xaxis.tickFormatter(G)}if(I){G=I}}F.push("<"+M+(M=="th"?' scope="row"':"")+">"+G+"</"+M+">")}F.push("</tr>")}C.push("</colgroup>");K.update(C.join("")+F.join(""));if(!Prototype.Browser.IE){K.select("td").each(function(N){N.observe("mouseover",function(O){N=O.element();var P=N.previousSiblings();K.select("th[scope=col]")[P.length-1].addClassName("hover");K.select("colgroup col")[P.length].addClassName("hover")});N.observe("mouseout",function(){K.select("colgroup col.hover, th.hover").each(function(O){O.removeClassName("hover")})})})}var H=new Element("div",{className:"flotr-datagrid-toolbar"}).insert(new Element("button",{type:"button",className:"flotr-datagrid-toolbar-button"}).update(this.options.spreadsheet.toolbarDownload).observe("click",this.downloadCSV.bind(this))).insert(new Element("button",{type:"button",className:"flotr-datagrid-toolbar-button"}).update(this.options.spreadsheet.toolbarSelectAll).observe("click",this.selectAllData.bind(this)));var A=new Element("div",{className:"flotr-datagrid-container",style:"left:0px;top:0px;width:"+this.canvasWidth+"px;height:"+this.canvasHeight+"px;overflow:auto;"});A.insert(H);K.wrap(A.hide());this.el.insert(A);return K},selectAllData:function(){if(this.tabs){var B,A,E,D,C=this.constructDataGrid();this.showTab("data");(function(){if((E=C.ownerDocument)&&(D=E.defaultView)&&D.getSelection&&E.createRange&&(B=window.getSelection())&&B.removeAllRanges){A=E.createRange();A.selectNode(C);B.removeAllRanges();B.addRange(A)}else{if(document.body&&document.body.createTextRange&&(A=document.body.createTextRange())){A.moveToElementText(C);A.select()}}}).defer();return true}else{return false}},downloadCSV:function(){var D,A='"x"',C=this.series,E=this.loadDataGrid();for(D=0;D<C.length;++D){A+='%09"'+(C[D].label||String.fromCharCode(65+D))+'"'}A+="%0D%0A";for(D=0;D<E.length;++D){if(this.options.xaxis.ticks){var B=this.options.xaxis.ticks.find(function(F){return F[0]==E[D][0]});if(B){E[D][0]=B[1]}}else{E[D][0]=this.options.xaxis.tickFormatter(E[D][0])}A+=E[D].join("%09")+"%0D%0A"}if(Prototype.Browser.IE){A=A.gsub("%09","\t").gsub("%0A","\n").gsub("%0D","\r");window.open().document.write(A)}else{window.open("data:text/csv,"+A)}},initEvents:function(){this.overlay.stopObserving();this.overlay.observe("mousedown",this.mouseDownHandler.bind(this));this.overlay.observe("mousemove",this.mouseMoveHandler.bind(this));this.overlay.observe("click",this.clickHandler.bind(this))},findDataRanges:function(){var J=this.series,G=this.axes;G.x.datamin=0;G.x.datamax=0;G.x2.datamin=0;G.x2.datamax=0;G.y.datamin=0;G.y.datamax=0;G.y2.datamin=0;G.y2.datamax=0;if(J.length>0){var C,A,D,H,F,B,I,E;for(C=0;C<J.length;++C){B=J[C].data,I=J[C].xaxis,E=J[C].yaxis;if(B.length>0&&!J[C].hide){if(!I.used){I.datamin=I.datamax=B[0][0]}if(!E.used){E.datamin=E.datamax=B[0][1]}I.used=true;E.used=true;for(D=B.length-1;D>-1;--D){H=B[D][0];if(H<I.datamin){I.datamin=H}else{if(H>I.datamax){I.datamax=H}}for(A=1;A<B[D].length;A++){F=B[D][A];if(F<E.datamin){E.datamin=F}else{if(F>E.datamax){E.datamax=F}}}}}}}this.findXAxesValues();this.calculateRange(G.x);this.extendXRangeIfNeededByBar(G.x);if(G.x2.used){this.calculateRange(G.x2);this.extendXRangeIfNeededByBar(G.x2)}this.calculateRange(G.y);this.extendYRangeIfNeededByBar(G.y);if(G.y2.used){this.calculateRange(G.y2);this.extendYRangeIfNeededByBar(G.y2)}},calculateRange:function(D){var F=D.options,C=F.min!=null?F.min:D.datamin,A=F.max!=null?F.max:D.datamax,E;if(A-C==0){var B=(A==0)?1:0.01;C-=B;A+=B}D.tickSize=Flotr.getTickSize(F.noTicks,C,A,F.tickDecimals);if(F.min==null){E=F.autoscaleMargin;if(E!=0){C-=D.tickSize*E;if(C<0&&D.datamin>=0){C=0}C=D.tickSize*Math.floor(C/D.tickSize)}}if(F.max==null){E=F.autoscaleMargin;if(E!=0){A+=D.tickSize*E;if(A>0&&D.datamax<=0){A=0}A=D.tickSize*Math.ceil(A/D.tickSize)}}D.min=C;D.max=A},extendXRangeIfNeededByBar:function(A){if(A.options.max==null){var D=A.max,B,I,F,E,H=[],C=null;for(B=0;B<this.series.length;++B){I=this.series[B];F=I.bars;E=I.candles;if(I.axis==A&&(F.show||E.show)){if(!F.horizontal&&(F.barWidth+A.datamax>D)||(E.candleWidth+A.datamax>D)){D=A.max+I.bars.barWidth}if(F.stacked&&F.horizontal){for(j=0;j<I.data.length;j++){if(I.bars.show&&I.bars.stacked){var G=I.data[j][0];H[G]=(H[G]||0)+I.data[j][1];C=I}}for(j=0;j<H.length;j++){D=Math.max(H[j],D)}}}}A.lastSerie=C;A.max=D}},extendYRangeIfNeededByBar:function(A){if(A.options.max==null){var D=A.max,B,I,F,E,H=[],C=null;for(B=0;B<this.series.length;++B){I=this.series[B];F=I.bars;E=I.candles;if(I.yaxis==A&&F.show&&!I.hide){if(F.horizontal&&(F.barWidth+A.datamax>D)||(E.candleWidth+A.datamax>D)){D=A.max+F.barWidth}if(F.stacked&&!F.horizontal){for(j=0;j<I.data.length;j++){if(I.bars.show&&I.bars.stacked){var G=I.data[j][0];H[G]=(H[G]||0)+I.data[j][1];C=I}}for(j=0;j<H.length;j++){D=Math.max(H[j],D)}}}}A.lastSerie=C;A.max=D}},findXAxesValues:function(){for(i=this.series.length-1;i>-1;--i){s=this.series[i];s.xaxis.values=s.xaxis.values||[];for(j=s.data.length-1;j>-1;--j){s.xaxis.values[s.data[j][0]]={}}}},calculateTicks:function(D){var B=D.options,E,H;D.ticks=[];if(B.ticks){var G=B.ticks,I,F;if(Object.isFunction(G)){G=G({min:D.min,max:D.max})}for(E=0;E<G.length;++E){I=G[E];if(typeof (I)=="object"){H=I[0];F=(I.length>1)?I[1]:B.tickFormatter(H)}else{H=I;F=B.tickFormatter(H)}D.ticks[E]={v:H,label:F}}}else{var A=D.tickSize*Math.ceil(D.min/D.tickSize),C;for(E=0;A+E*D.tickSize<=D.max;++E){H=A+E*D.tickSize;C=B.tickDecimals;if(C==null){C=1-Math.floor(Math.log(D.tickSize)/Math.LN10)}if(C<0){C=0}H=H.toFixed(C);D.ticks.push({v:H,label:B.tickFormatter(H)})}}},calculateSpacing:function(){var L=this.axes,N=this.options,H=this.series,D=N.grid.labelMargin,M=L.x,A=L.x2,J=L.y,K=L.y2,F=2,G,E,C,I;[M,A,J,K].each(function(P){var O="";if(P.options.showLabels){for(G=0;G<P.ticks.length;++G){C=P.ticks[G].label.length;if(C>O.length){O=P.ticks[G].label}}}P.maxLabel=this.getTextDimensions(O,{size:N.fontSize,angle:Flotr.toRad(P.options.labelsAngle)},"font-size:smaller;","flotr-grid-label");P.titleSize=this.getTextDimensions(P.options.title,{size:N.fontSize*1.2,angle:Flotr.toRad(P.options.titleAngle)},"font-weight:bold;","flotr-axis-title")},this);I=this.getTextDimensions(N.title,{size:N.fontSize*1.5},"font-size:1em;font-weight:bold;","flotr-title");this.titleHeight=I.height;I=this.getTextDimensions(N.subtitle,{size:N.fontSize},"font-size:smaller;","flotr-subtitle");this.subtitleHeight=I.height;if(N.show){F=Math.max(F,N.points.radius+N.points.lineWidth/2)}for(E=0;E<N.length;++E){if(H[E].points.show){F=Math.max(F,H[E].points.radius+H[E].points.lineWidth/2)}}var B=this.plotOffset={left:0,right:0,top:0,bottom:0};B.left=B.right=B.top=B.bottom=F;B.bottom+=(M.options.showLabels?(M.maxLabel.height+D):0)+(M.options.title?(M.titleSize.height+D):0);B.top+=(A.options.showLabels?(A.maxLabel.height+D):0)+(A.options.title?(A.titleSize.height+D):0)+this.subtitleHeight+this.titleHeight;B.left+=(J.options.showLabels?(J.maxLabel.width+D):0)+(J.options.title?(J.titleSize.width+D):0);B.right+=(K.options.showLabels?(K.maxLabel.width+D):0)+(K.options.title?(K.titleSize.width+D):0);B.top=Math.floor(B.top);this.plotWidth=this.canvasWidth-B.left-B.right;this.plotHeight=this.canvasHeight-B.bottom-B.top;M.scale=this.plotWidth/(M.max-M.min);A.scale=this.plotWidth/(A.max-A.min);J.scale=this.plotHeight/(J.max-J.min);K.scale=this.plotHeight/(K.max-K.min)},draw:function(){this.drawGrid();this.drawLabels();this.drawTitles();if(this.series.length){this.el.fire("flotr:beforedraw",[this.series,this]);for(var A=0;A<this.series.length;A++){if(!this.series[A].hide){this.drawSeries(this.series[A])}}}this.el.fire("flotr:afterdraw",[this.series,this])},tHoz:function(A,B){B=B||this.axes.x;return(A-B.min)*B.scale},tVert:function(B,A){A=A||this.axes.y;return this.plotHeight-(B-A.min)*A.scale},drawGrid:function(){var B,E=this.options,A=this.ctx;if(E.grid.verticalLines||E.grid.horizontalLines){this.el.fire("flotr:beforegrid",[this.axes.x,this.axes.y,E,this])}A.save();A.translate(this.plotOffset.left,this.plotOffset.top);if(E.grid.backgroundColor!=null){A.fillStyle=E.grid.backgroundColor;A.fillRect(0,0,this.plotWidth,this.plotHeight)}A.lineWidth=1;A.strokeStyle=E.grid.tickColor;A.beginPath();if(E.grid.verticalLines){for(var D=0;D<this.axes.x.ticks.length;++D){B=this.axes.x.ticks[D].v;if((B==this.axes.x.min||B==this.axes.x.max)&&E.grid.outlineWidth!=0){continue}A.moveTo(Math.floor(this.tHoz(B))+A.lineWidth/2,0);A.lineTo(Math.floor(this.tHoz(B))+A.lineWidth/2,this.plotHeight)}}if(E.grid.horizontalLines){for(var C=0;C<this.axes.y.ticks.length;++C){B=this.axes.y.ticks[C].v;if((B==this.axes.y.min||B==this.axes.y.max)&&E.grid.outlineWidth!=0){continue}A.moveTo(0,Math.floor(this.tVert(B))+A.lineWidth/2);A.lineTo(this.plotWidth,Math.floor(this.tVert(B))+A.lineWidth/2)}}A.stroke();if(E.grid.outlineWidth!=0){A.lineWidth=E.grid.outlineWidth;A.strokeStyle=E.grid.color;A.lineJoin="round";A.strokeRect(0,0,this.plotWidth,this.plotHeight)}A.restore();if(E.grid.verticalLines||E.grid.horizontalLines){this.el.fire("flotr:aftergrid",[this.axes.x,this.axes.y,E,this])}},drawLabels:function(){var C=0,D,B,E,F,G,J=this.options,I=this.ctx,H=this.axes;for(E=0;E<H.x.ticks.length;++E){if(H.x.ticks[E].label){++C}}B=this.plotWidth/C;if(!J.HtmlText&&this.textEnabled){var A={size:J.fontSize,adjustAlign:true};D=H.x;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="c";A.valign="t";I.drawText(G.label,this.plotOffset.left+this.tHoz(G.v,D),this.plotOffset.top+this.plotHeight+J.grid.labelMargin,A)}D=H.x2;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="c";A.valign="b";I.drawText(G.label,this.plotOffset.left+this.tHoz(G.v,D),this.plotOffset.top+J.grid.labelMargin,A)}D=H.y;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="r";A.valign="m";I.drawText(G.label,this.plotOffset.left-J.grid.labelMargin,this.plotOffset.top+this.tVert(G.v,D),A)}D=H.y2;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="l";A.valign="m";I.drawText(G.label,this.plotOffset.left+this.plotWidth+J.grid.labelMargin,this.plotOffset.top+this.tVert(G.v,D),A);I.save();I.strokeStyle=A.color;I.beginPath();I.moveTo(this.plotOffset.left+this.plotWidth-8,this.plotOffset.top+this.tVert(G.v,D));I.lineTo(this.plotOffset.left+this.plotWidth,this.plotOffset.top+this.tVert(G.v,D));I.stroke();I.restore()}}else{if(H.x.options.showLabels||H.x2.options.showLabels||H.y.options.showLabels||H.y2.options.showLabels){F=['<div style="font-size:smaller;color:'+J.grid.color+';" class="flotr-labels">'];D=H.x;if(D.options.showLabels){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight+J.grid.labelMargin)+"px;left:"+(this.plotOffset.left+this.tHoz(G.v,D)-B/2)+"px;width:"+B+"px;text-align:center;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.x2;if(D.options.showLabels&&D.used){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top-J.grid.labelMargin-D.maxLabel.height)+"px;left:"+(this.plotOffset.left+this.tHoz(G.v,D)-B/2)+"px;width:"+B+"px;text-align:center;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.y;if(D.options.showLabels){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.tVert(G.v,D)-D.maxLabel.height/2)+"px;left:0;width:"+(this.plotOffset.left-J.grid.labelMargin)+"px;text-align:right;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.y2;if(D.options.showLabels&&D.used){I.save();I.strokeStyle=D.options.color||J.grid.color;I.beginPath();for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.tVert(G.v,D)-D.maxLabel.height/2)+"px;right:0;width:"+(this.plotOffset.right-J.grid.labelMargin)+"px;text-align:left;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>");I.moveTo(this.plotOffset.left+this.plotWidth-8,this.plotOffset.top+this.tVert(G.v,D));I.lineTo(this.plotOffset.left+this.plotWidth,this.plotOffset.top+this.tVert(G.v,D))}I.stroke();I.restore()}F.push("</div>");this.el.insert(F.join(""))}}},drawTitles:function(){var D,C=this.options,F=C.grid.labelMargin,B=this.ctx,A=this.axes;if(!C.HtmlText&&this.textEnabled){var E={size:C.fontSize,color:C.grid.color,halign:"c"};if(C.subtitle){B.drawText(C.subtitle,this.plotOffset.left+this.plotWidth/2,this.titleHeight+this.subtitleHeight-2,E)}E.weight=1.5;E.size*=1.5;if(C.title){B.drawText(C.title,this.plotOffset.left+this.plotWidth/2,this.titleHeight-2,E)}E.weight=1.8;E.size*=0.8;E.adjustAlign=true;if(A.x.options.title&&A.x.used){E.halign="c";E.valign="t";E.angle=Flotr.toRad(A.x.options.titleAngle);B.drawText(A.x.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top+A.x.maxLabel.height+this.plotHeight+2*F,E)}if(A.x2.options.title&&A.x2.used){E.halign="c";E.valign="b";E.angle=Flotr.toRad(A.x2.options.titleAngle);B.drawText(A.x2.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top-A.x2.maxLabel.height-2*F,E)}if(A.y.options.title&&A.y.used){E.halign="r";E.valign="m";E.angle=Flotr.toRad(A.y.options.titleAngle);B.drawText(A.y.options.title,this.plotOffset.left-A.y.maxLabel.width-2*F,this.plotOffset.top+this.plotHeight/2,E)}if(A.y2.options.title&&A.y2.used){E.halign="l";E.valign="m";E.angle=Flotr.toRad(A.y2.options.titleAngle);B.drawText(A.y2.options.title,this.plotOffset.left+this.plotWidth+A.y2.maxLabel.width+2*F,this.plotOffset.top+this.plotHeight/2,E)}}else{D=['<div style="color:'+C.grid.color+';" class="flotr-titles">'];if(C.title){D.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+"px;font-size:1em;font-weight:bold;text-align:center;width:"+this.plotWidth+'px;" class="flotr-title">'+C.title+"</div>")}if(C.subtitle){D.push('<div style="position:absolute;top:'+this.titleHeight+"px;left:"+this.plotOffset.left+"px;font-size:smaller;text-align:center;width:"+this.plotWidth+'px;" class="flotr-subtitle">'+C.subtitle+"</div>")}D.push("</div>");D.push('<div class="flotr-axis-title" style="font-weight:bold;">');if(A.x.options.title&&A.x.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight+C.grid.labelMargin+A.x.titleSize.height)+"px;left:"+this.plotOffset.left+"px;width:"+this.plotWidth+'px;text-align:center;" class="flotr-axis-title">'+A.x.options.title+"</div>")}if(A.x2.options.title&&A.x2.used){D.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+"px;width:"+this.plotWidth+'px;text-align:center;" class="flotr-axis-title">'+A.x2.options.title+"</div>")}if(A.y.options.title&&A.y.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight/2-A.y.titleSize.height/2)+'px;left:0;text-align:right;" class="flotr-axis-title">'+A.y.options.title+"</div>")}if(A.y2.options.title&&A.y2.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight/2-A.y.titleSize.height/2)+'px;right:0;text-align:right;" class="flotr-axis-title">'+A.y2.options.title+"</div>")}D.push("</div>");this.el.insert(D.join(""))}},drawSeries:function(A){A=A||this.series;var C=false;for(var B in Flotr._registeredTypes){if(A[B]&&A[B].show){this[Flotr._registeredTypes[B]](A);C=true}}if(!C){this[Flotr._registeredTypes[this.options.defaultType]](A)}},plotLine:function(I,F){var O=this.ctx,A=I.xaxis,K=I.yaxis,J=this.tHoz.bind(this),M=this.tVert.bind(this),H=I.data;if(H.length<2){return }var E=J(H[0][0],A),D=M(H[0][1],K)+F;O.beginPath();O.moveTo(E,D);for(var G=0;G<H.length-1;++G){var C=H[G][0],N=H[G][1],B=H[G+1][0],L=H[G+1][1];if(N===null||L===null){continue}if(N<=L&&N<K.min){if(L<K.min){continue}C=(K.min-N)/(L-N)*(B-C)+C;N=K.min}else{if(L<=N&&L<K.min){if(N<K.min){continue}B=(K.min-N)/(L-N)*(B-C)+C;L=K.min}}if(N>=L&&N>K.max){if(L>K.max){continue}C=(K.max-N)/(L-N)*(B-C)+C;N=K.max}else{if(L>=N&&L>K.max){if(N>K.max){continue}B=(K.max-N)/(L-N)*(B-C)+C;L=K.max}}if(C<=B&&C<A.min){if(B<A.min){continue}N=(A.min-C)/(B-C)*(L-N)+N;C=A.min}else{if(B<=C&&B<A.min){if(C<A.min){continue}L=(A.min-C)/(B-C)*(L-N)+N;B=A.min}}if(C>=B&&C>A.max){if(B>A.max){continue}N=(A.max-C)/(B-C)*(L-N)+N;C=A.max}else{if(B>=C&&B>A.max){if(C>A.max){continue}L=(A.max-C)/(B-C)*(L-N)+N;B=A.max}}if(E!=J(C,A)||D!=M(N,K)+F){O.moveTo(J(C,A),M(N,K)+F)}E=J(B,A);D=M(L,K)+F;O.lineTo(E,D)}O.stroke()},plotLineArea:function(J,D){var S=J.data;if(S.length<2){return }var L,G=0,N=this.ctx,Q=J.xaxis,B=J.yaxis,E=this.tHoz.bind(this),M=this.tVert.bind(this),H=Math.min(Math.max(0,B.min),B.max),F=true;N.beginPath();for(var O=0;O<S.length-1;++O){var R=S[O][0],C=S[O][1],P=S[O+1][0],A=S[O+1][1];if(R<=P&&R<Q.min){if(P<Q.min){continue}C=(Q.min-R)/(P-R)*(A-C)+C;R=Q.min}else{if(P<=R&&P<Q.min){if(R<Q.min){continue}A=(Q.min-R)/(P-R)*(A-C)+C;P=Q.min}}if(R>=P&&R>Q.max){if(P>Q.max){continue}C=(Q.max-R)/(P-R)*(A-C)+C;R=Q.max}else{if(P>=R&&P>Q.max){if(R>Q.max){continue}A=(Q.max-R)/(P-R)*(A-C)+C;P=Q.max}}if(F){N.moveTo(E(R,Q),M(H,B)+D);F=false}if(C>=B.max&&A>=B.max){N.lineTo(E(R,Q),M(B.max,B)+D);N.lineTo(E(P,Q),M(B.max,B)+D);continue}else{if(C<=B.min&&A<=B.min){N.lineTo(E(R,Q),M(B.min,B)+D);N.lineTo(E(P,Q),M(B.min,B)+D);continue}}var I=R,K=P;if(C<=A&&C<B.min&&A>=B.min){R=(B.min-C)/(A-C)*(P-R)+R;C=B.min}else{if(A<=C&&A<B.min&&C>=B.min){P=(B.min-C)/(A-C)*(P-R)+R;A=B.min}}if(C>=A&&C>B.max&&A<=B.max){R=(B.max-C)/(A-C)*(P-R)+R;C=B.max}else{if(A>=C&&A>B.max&&C<=B.max){P=(B.max-C)/(A-C)*(P-R)+R;A=B.max}}if(R!=I){L=(C<=B.min)?L=B.min:B.max;N.lineTo(E(I,Q),M(L,B)+D);N.lineTo(E(R,Q),M(L,B)+D)}N.lineTo(E(R,Q),M(C,B)+D);N.lineTo(E(P,Q),M(A,B)+D);if(P!=K){L=(A<=B.min)?B.min:B.max;N.lineTo(E(K,Q),M(L,B)+D);N.lineTo(E(P,Q),M(L,B)+D)}G=Math.max(P,K)}N.lineTo(E(G,Q),M(H,B)+D);N.closePath();N.fill()},drawSeriesLines:function(C){C=C||this.series;var B=this.ctx;B.save();B.translate(this.plotOffset.left,this.plotOffset.top);B.lineJoin="round";var D=C.lines.lineWidth;var A=C.shadowSize;if(A>0){B.lineWidth=A/2;var E=D/2+B.lineWidth/2;B.strokeStyle="rgba(0,0,0,0.1)";this.plotLine(C,E+A/2);B.strokeStyle="rgba(0,0,0,0.2)";this.plotLine(C,E);if(C.lines.fill){B.fillStyle="rgba(0,0,0,0.05)";this.plotLineArea(C,E+A/2)}}B.lineWidth=D;B.strokeStyle=C.color;if(C.lines.fill){B.fillStyle=C.lines.fillColor!=null?C.lines.fillColor:Flotr.parseColor(C.color).scale(null,null,null,C.lines.fillOpacity).toString();this.plotLineArea(C,0)}this.plotLine(C,0);B.restore()},drawSeriesPoints:function(C){var B=this.ctx;B.save();B.translate(this.plotOffset.left,this.plotOffset.top);var D=C.lines.lineWidth;var A=C.shadowSize;if(A>0){B.lineWidth=A/2;B.strokeStyle="rgba(0,0,0,0.1)";this.plotPointShadows(C,A/2+B.lineWidth/2,C.points.radius);B.strokeStyle="rgba(0,0,0,0.2)";this.plotPointShadows(C,B.lineWidth/2,C.points.radius)}B.lineWidth=C.points.lineWidth;B.strokeStyle=C.color;B.fillStyle=C.points.fillColor!=null?C.points.fillColor:C.color;this.plotPoints(C,C.points.radius,C.points.fill);B.restore()},plotPoints:function(C,E,I){var A=C.xaxis,F=C.yaxis,J=this.ctx,D,B=C.data;for(D=B.length-1;D>-1;--D){var H=B[D][0],G=B[D][1];if(H<A.min||H>A.max||G<F.min||G>F.max){continue}J.beginPath();J.arc(this.tHoz(H,A),this.tVert(G,F),E,0,2*Math.PI,true);if(I){J.fill()}J.stroke()}},plotPointShadows:function(D,B,F){var A=D.xaxis,G=D.yaxis,J=this.ctx,E,C=D.data;for(E=C.length-1;E>-1;--E){var I=C[E][0],H=C[E][1];if(I<A.min||I>A.max||H<G.min||H>G.max){continue}J.beginPath();J.arc(this.tHoz(I,A),this.tVert(H,G)+B,F,0,Math.PI,false);J.stroke()}},drawSeriesBars:function(B){var A=this.ctx,D=B.bars.barWidth,C=Math.min(B.bars.lineWidth,D);A.save();A.translate(this.plotOffset.left,this.plotOffset.top);A.lineJoin="miter";A.lineWidth=C;A.strokeStyle=B.color;this.plotBarsShadows(B,D,0,B.bars.fill);if(B.bars.fill){A.fillStyle=B.bars.fillColor!=null?B.bars.fillColor:Flotr.parseColor(B.color).scale(null,null,null,B.bars.fillOpacity).toString()}this.plotBars(B,D,0,B.bars.fill);A.restore()},plotBars:function(K,N,D,Q){var U=K.data;if(U.length<1){return }var S=K.xaxis,B=K.yaxis,P=this.ctx,F=this.tHoz.bind(this),O=this.tVert.bind(this);for(var R=0;R<U.length;R++){var J=U[R][0],I=U[R][1];var E=true,L=true,A=true;var H=0;if(K.bars.stacked){S.values.each(function(W,V){if(V==J){H=W.stack||0;W.stack=H+I}})}if(K.bars.horizontal){var C=H,T=J+H,G=I,M=I+N}else{var C=J,T=J+N,G=H,M=I+H}if(T<S.min||C>S.max||M<B.min||G>B.max){continue}if(C<S.min){C=S.min;E=false}if(T>S.max){T=S.max;if(S.lastSerie!=K&&K.bars.horizontal){L=false}}if(G<B.min){G=B.min}if(M>B.max){M=B.max;if(B.lastSerie!=K&&!K.bars.horizontal){L=false}}if(Q){P.beginPath();P.moveTo(F(C,S),O(G,B)+D);P.lineTo(F(C,S),O(M,B)+D);P.lineTo(F(T,S),O(M,B)+D);P.lineTo(F(T,S),O(G,B)+D);P.fill()}if(K.bars.lineWidth!=0&&(E||A||L)){P.beginPath();P.moveTo(F(C,S),O(G,B)+D);P[E?"lineTo":"moveTo"](F(C,S),O(M,B)+D);P[L?"lineTo":"moveTo"](F(T,S),O(M,B)+D);P[A?"lineTo":"moveTo"](F(T,S),O(G,B)+D);P.stroke()}}},plotBarsShadows:function(I,K,C){var T=I.data;if(T.length<1){return }var R=I.xaxis,A=I.yaxis,P=this.ctx,D=this.tHoz.bind(this),M=this.tVert.bind(this),N=this.options.shadowSize;for(var Q=0;Q<T.length;Q++){var H=T[Q][0],G=T[Q][1];var E=0;if(I.bars.stacked){R.values.each(function(V,U){if(U==H){E=V.stackShadow||0;V.stackShadow=E+G}})}if(I.bars.horizontal){var B=E,S=H+E,F=G,J=G+K}else{var B=H,S=H+K,F=E,J=G+E}if(S<R.min||B>R.max||J<A.min||F>A.max){continue}if(B<R.min){B=R.min}if(S>R.max){S=R.max}if(F<A.min){F=A.min}if(J>A.max){J=A.max}var O=D(S,R)-D(B,R)-((D(S,R)+N<=this.plotWidth)?0:N);var L=Math.max(0,M(F,A)-M(J,A)-((M(F,A)+N<=this.plotHeight)?0:N));P.fillStyle="rgba(0,0,0,0.05)";P.fillRect(Math.min(D(B,R)+N,this.plotWidth),Math.min(M(J,A)+N,this.plotWidth),O,L)}},drawSeriesCandles:function(B){var A=this.ctx,C=B.candles.candleWidth;A.save();A.translate(this.plotOffset.left,this.plotOffset.top);A.lineJoin="miter";A.lineWidth=B.candles.lineWidth;this.plotCandlesShadows(B,C/2);this.plotCandles(B,C/2);A.restore()},plotCandles:function(K,D){var W=K.data;if(W.length<1){return }var T=K.xaxis,B=K.yaxis,P=this.ctx,E=this.tHoz.bind(this),O=this.tVert.bind(this);for(var S=0;S<W.length;S++){var U=W[S],J=U[0],L=U[1],I=U[2],X=U[3],N=U[4];var C=J,V=J+K.candles.candleWidth,G=Math.max(B.min,X),M=Math.min(B.max,I),A=Math.max(B.min,Math.min(L,N)),R=Math.min(B.max,Math.max(L,N));if(V<T.min||C>T.max||M<B.min||G>B.max){continue}var Q=K.candles[L>N?"downFillColor":"upFillColor"];if(K.candles.fill&&!K.candles.barcharts){P.fillStyle=Flotr.parseColor(Q).scale(null,null,null,K.candles.fillOpacity).toString();P.fillRect(E(C,T),O(R,B)+D,E(V,T)-E(C,T),O(A,B)-O(R,B))}if(K.candles.lineWidth||K.candles.wickLineWidth){var J,H,F=(K.candles.wickLineWidth%2)/2;J=Math.floor(E((C+V)/2),T)+F;P.save();P.strokeStyle=Q;P.lineWidth=K.candles.wickLineWidth;P.lineCap="butt";if(K.candles.barcharts){P.beginPath();P.moveTo(J,Math.floor(O(M,B)+D));P.lineTo(J,Math.floor(O(G,B)+D));H=Math.floor(O(L,B)+D)+0.5;P.moveTo(Math.floor(E(C,T))+F,H);P.lineTo(J,H);H=Math.floor(O(N,B)+D)+0.5;P.moveTo(Math.floor(E(V,T))+F,H);P.lineTo(J,H)}else{P.strokeRect(E(C,T),O(R,B)+D,E(V,T)-E(C,T),O(A,B)-O(R,B));P.beginPath();P.moveTo(J,Math.floor(O(R,B)+D));P.lineTo(J,Math.floor(O(M,B)+D));P.moveTo(J,Math.floor(O(A,B)+D));P.lineTo(J,Math.floor(O(G,B)+D))}P.stroke();P.restore()}}},plotCandlesShadows:function(H,C){var T=H.data;if(T.length<1||H.candles.barcharts){return }var Q=H.xaxis,A=H.yaxis,D=this.tHoz.bind(this),M=this.tVert.bind(this),N=this.options.shadowSize;for(var P=0;P<T.length;P++){var R=T[P],G=R[0],I=R[1],F=R[2],U=R[3],K=R[4];var B=G,S=G+H.candles.candleWidth,E=Math.max(A.min,Math.min(I,K)),J=Math.min(A.max,Math.max(I,K));if(S<Q.min||B>Q.max||J<A.min||E>A.max){continue}var O=D(S,Q)-D(B,Q)-((D(S,Q)+N<=this.plotWidth)?0:N);var L=Math.max(0,M(E,A)-M(J,A)-((M(E,A)+N<=this.plotHeight)?0:N));this.ctx.fillStyle="rgba(0,0,0,0.05)";this.ctx.fillRect(Math.min(D(B,Q)+N,this.plotWidth),Math.min(M(J,A)+N,this.plotWidth),O,L)}},drawSeriesPie:function(G){if(!this.options.pie.drawn){var K=this.ctx,C=this.options,E=G.pie.lineWidth,I=G.shadowSize,R=G.data,D=(Math.min(this.canvasWidth,this.canvasHeight)*G.pie.sizeRatio)/2,H=[];var L=1;var P=Math.sin(G.pie.viewAngle)*G.pie.spliceThickness/L;var M={size:C.fontSize*1.2,color:C.grid.color,weight:1.5};var Q={x:(this.canvasWidth+this.plotOffset.left)/2,y:(this.canvasHeight-this.plotOffset.bottom)/2};var O=this.series.collect(function(T,S){if(T.pie.show){return{name:(T.label||T.data[0][1]),value:[S,T.data[0][1]],explode:T.pie.explode}}});var B=O.pluck("value").pluck(1).inject(0,function(S,T){return S+T});var F=0,N=G.pie.startAngle,J=0;var A=O.collect(function(S){N+=F;J=parseFloat(S.value[1]);F=J/B;return{name:S.name,fraction:F,x:S.value[0],y:J,explode:S.explode,startAngle:2*N*Math.PI,endAngle:2*(N+F)*Math.PI}});K.save();if(I>0){A.each(function(V){var S=(V.startAngle+V.endAngle)/2;var T=Q.x+Math.cos(S)*V.explode+I;var U=Q.y+Math.sin(S)*V.explode+I;this.plotSlice(T,U,D,V.startAngle,V.endAngle,false,L);K.fillStyle="rgba(0,0,0,0.1)";K.fill()},this)}if(C.HtmlText){H=['<div style="color:'+this.options.grid.color+'" class="flotr-labels">']}A.each(function(c,X){var W=(c.startAngle+c.endAngle)/2;var V=C.colors[X];var Y=Q.x+Math.cos(W)*c.explode;var U=Q.y+Math.sin(W)*c.explode;this.plotSlice(Y,U,D,c.startAngle,c.endAngle,false,L);if(G.pie.fill){K.fillStyle=Flotr.parseColor(V).scale(null,null,null,G.pie.fillOpacity).toString();K.fill()}K.lineWidth=E;K.strokeStyle=V;K.stroke();var b=C.pie.labelFormatter(c);var S=(Math.cos(W)<0);var a=Y+Math.cos(W)*(G.pie.explode+D);var Z=U+Math.sin(W)*(G.pie.explode+D);if(c.fraction&&b){if(C.HtmlText){var T="position:absolute;top:"+(Z-5)+"px;";if(S){T+="right:"+(this.canvasWidth-a)+"px;text-align:right;"}else{T+="left:"+a+"px;text-align:left;"}H.push('<div style="'+T+'" class="flotr-grid-label">'+b+"</div>")}else{M.halign=S?"r":"l";K.drawText(b,a,Z+M.size/2,M)}}},this);if(C.HtmlText){H.push("</div>");this.el.insert(H.join(""))}K.restore();C.pie.drawn=true}},plotSlice:function(B,H,A,E,D,F,G){var C=this.ctx;G=G||1;C.save();C.scale(1,G);C.beginPath();C.moveTo(B,H);C.arc(B,H,A,E,D,F);C.lineTo(B,H);C.closePath();C.restore()},plotPie:function(){},insertLegend:function(){if(!this.options.legend.show){return }var H=this.series,I=this.plotOffset,B=this.options,b=[],A=false,O=this.ctx,R;var Q=H.findAll(function(c){return(c.label&&!c.hide)}).size();if(Q){if(!B.HtmlText&&this.textEnabled){var T={size:B.fontSize*1.1,color:B.grid.color};var M=B.legend.position,N=B.legend.margin,L=B.legend.labelBoxWidth,Z=B.legend.labelBoxHeight,S=B.legend.labelBoxMargin,W=I.left+N,U=I.top+N;var a=0;for(R=H.length-1;R>-1;--R){if(!H[R].label||H[R].hide){continue}var E=B.legend.labelFormatter(H[R].label);a=Math.max(a,O.measureText(E,T))}var K=Math.round(L+S*3+a),C=Math.round(Q*(S+Z)+S);if(M.charAt(0)=="s"){U=I.top+this.plotHeight-(N+C)}if(M.charAt(1)=="e"){W=I.left+this.plotWidth-(N+K)}var P=Flotr.parseColor(B.legend.backgroundColor||"rgb(240,240,240)").scale(null,null,null,B.legend.backgroundOpacity||0.1).toString();O.fillStyle=P;O.fillRect(W,U,K,C);O.strokeStyle=B.legend.labelBoxBorderColor;O.strokeRect(Flotr.toPixel(W),Flotr.toPixel(U),K,C);var G=W+S;var F=U+S;for(R=0;R<H.length;R++){if(!H[R].label||H[R].hide){continue}var E=B.legend.labelFormatter(H[R].label);O.fillStyle=H[R].color;O.fillRect(G,F,L-1,Z-1);O.strokeStyle=B.legend.labelBoxBorderColor;O.lineWidth=1;O.strokeRect(Math.ceil(G)-1.5,Math.ceil(F)-1.5,L+2,Z+2);O.drawText(E,G+L+S,F+(Z+T.size-O.fontDescent(T))/2,T);F+=Z+S}}else{for(R=0;R<H.length;++R){if(!H[R].label||H[R].hide){continue}if(R%B.legend.noColumns==0){b.push(A?"</tr><tr>":"<tr>");A=true}var E=B.legend.labelFormatter(H[R].label);b.push('<td class="flotr-legend-color-box"><div style="border:1px solid '+B.legend.labelBoxBorderColor+';padding:1px"><div style="width:'+B.legend.labelBoxWidth+"px;height:"+B.legend.labelBoxHeight+"px;background-color:"+H[R].color+'"></div></div></td><td class="flotr-legend-label">'+E+"</td>")}if(A){b.push("</tr>")}if(b.length>0){var V='<table style="font-size:smaller;color:'+B.grid.color+'">'+b.join("")+"</table>";if(B.legend.container!=null){$(B.legend.container).update(V)}else{var D="";var M=B.legend.position,N=B.legend.margin;if(M.charAt(0)=="n"){D+="top:"+(N+I.top)+"px;"}else{if(M.charAt(0)=="s"){D+="bottom:"+(N+I.bottom)+"px;"}}if(M.charAt(1)=="e"){D+="right:"+(N+I.right)+"px;"}else{if(M.charAt(1)=="w"){D+="left:"+(N+I.left)+"px;"}}var J=this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;'+D+'">'+V+"</div>").select("div.flotr-legend").first();if(B.legend.backgroundOpacity!=0){var Y=B.legend.backgroundColor;if(Y==null){var X=(B.grid.backgroundColor!=null)?B.grid.backgroundColor:Flotr.extractColor(J);Y=Flotr.parseColor(X).adjust(null,null,null,1).toString()}this.el.insert('<div class="flotr-legend-bg" style="position:absolute;width:'+J.getWidth()+"px;height:"+J.getHeight()+"px;"+D+"background-color:"+Y+';"> </div>').select("div.flotr-legend-bg").first().setStyle({opacity:B.legend.backgroundOpacity})}}}}}},getEventPosition:function(C){var G=this.overlay.cumulativeOffset(),F=(C.pageX-G.left-this.plotOffset.left),E=(C.pageY-G.top-this.plotOffset.top),D=0,B=0;if(C.pageX==null&&C.clientX!=null){var H=document.documentElement,A=document.body;D=C.clientX+(H&&H.scrollLeft||A.scrollLeft||0);B=C.clientY+(H&&H.scrollTop||A.scrollTop||0)}else{D=C.pageX;B=C.pageY}return{x:this.axes.x.min+F/this.axes.x.scale,x2:this.axes.x2.min+F/this.axes.x2.scale,y:this.axes.y.max-E/this.axes.y.scale,y2:this.axes.y2.max-E/this.axes.y2.scale,relX:F,relY:E,absX:D,absY:B}},clickHandler:function(A){if(this.ignoreClick){this.ignoreClick=false;return }this.el.fire("flotr:click",[this.getEventPosition(A),this])},mouseMoveHandler:function(A){var B=this.getEventPosition(A);this.lastMousePos.pageX=B.absX;this.lastMousePos.pageY=B.absY;if(this.selectionInterval==null&&(this.options.mouse.track||this.series.any(function(C){return C.mouse&&C.mouse.track}))){this.hit(B)}this.el.fire("flotr:mousemove",[A,B,this])},mouseDownHandler:function(C){if(C.isRightClick()){C.stop();var B=this.overlay;B.hide();function A(){B.show();$(document).stopObserving("mousemove",A)}$(document).observe("mousemove",A);return }if(!this.options.selection.mode||!C.isLeftClick()){return }this.setSelectionPos(this.selection.first,C);if(this.selectionInterval!=null){clearInterval(this.selectionInterval)}this.lastMousePos.pageX=null;this.selectionInterval=setInterval(this.updateSelection.bind(this),1000/this.options.selection.fps);this.mouseUpHandler=this.mouseUpHandler.bind(this);$(document).observe("mouseup",this.mouseUpHandler)},fireSelectEvent:function(){var A=this.axes,F=this.selection,C=(F.first.x<=F.second.x)?F.first.x:F.second.x,B=(F.first.x<=F.second.x)?F.second.x:F.first.x,E=(F.first.y>=F.second.y)?F.first.y:F.second.y,D=(F.first.y>=F.second.y)?F.second.y:F.first.y;C=A.x.min+C/A.x.scale;B=A.x.min+B/A.x.scale;E=A.y.max-E/A.y.scale;D=A.y.max-D/A.y.scale;this.el.fire("flotr:select",[{x1:C,y1:E,x2:B,y2:D},this])},mouseUpHandler:function(A){$(document).stopObserving("mouseup",this.mouseUpHandler);A.stop();if(this.selectionInterval!=null){clearInterval(this.selectionInterval);this.selectionInterval=null}this.setSelectionPos(this.selection.second,A);this.clearSelection();if(this.selectionIsSane()){this.drawSelection();this.fireSelectEvent();this.ignoreClick=true}},setSelectionPos:function(D,B){var A=this.options,C=$(this.overlay).cumulativeOffset();if(A.selection.mode.indexOf("x")==-1){D.x=(D==this.selection.first)?0:this.plotWidth}else{D.x=B.pageX-C.left-this.plotOffset.left;D.x=Math.min(Math.max(0,D.x),this.plotWidth)}if(A.selection.mode.indexOf("y")==-1){D.y=(D==this.selection.first)?0:this.plotHeight}else{D.y=B.pageY-C.top-this.plotOffset.top;D.y=Math.min(Math.max(0,D.y),this.plotHeight)}},updateSelection:function(){if(this.lastMousePos.pageX==null){return }this.setSelectionPos(this.selection.second,this.lastMousePos);this.clearSelection();if(this.selectionIsSane()){this.drawSelection()}},clearSelection:function(){if(this.prevSelection==null){return }var G=this.prevSelection,E=this.octx,C=this.plotOffset,A=Math.min(G.first.x,G.second.x),F=Math.min(G.first.y,G.second.y),B=Math.abs(G.second.x-G.first.x),D=Math.abs(G.second.y-G.first.y);E.clearRect(A+C.left-E.lineWidth,F+C.top-E.lineWidth,B+E.lineWidth*2,D+E.lineWidth*2);this.prevSelection=null},setSelection:function(G){var B=this.options,H=this.axes.x,A=this.axes.y,F=yaxis.scale,D=xaxis.scale,E=B.selection.mode.indexOf("x")!=-1,C=B.selection.mode.indexOf("y")!=-1;this.clearSelection();this.selection.first.y=E?0:(A.max-G.y1)*F;this.selection.second.y=E?this.plotHeight:(A.max-G.y2)*F;this.selection.first.x=C?0:(G.x1-H.min)*D;this.selection.second.x=C?this.plotWidth:(G.x2-H.min)*D;this.drawSelection();this.fireSelectEvent()},drawSelection:function(){var C=this.prevSelection,F=this.selection,H=this.octx,I=this.options,A=this.plotOffset;if(C!=null&&F.first.x==C.first.x&&F.first.y==C.first.y&&F.second.x==C.second.x&&F.second.y==C.second.y){return }H.strokeStyle=Flotr.parseColor(I.selection.color).scale(null,null,null,0.8).toString();H.lineWidth=1;H.lineJoin="round";H.fillStyle=Flotr.parseColor(I.selection.color).scale(null,null,null,0.4).toString();this.prevSelection={first:{x:F.first.x,y:F.first.y},second:{x:F.second.x,y:F.second.y}};var E=Math.min(F.first.x,F.second.x),D=Math.min(F.first.y,F.second.y),G=Math.abs(F.second.x-F.first.x),B=Math.abs(F.second.y-F.first.y);H.fillRect(E+A.left,D+A.top,G,B);H.strokeRect(E+A.left,D+A.top,G,B)},selectionIsSane:function(){var A=this.selection;return Math.abs(A.second.x-A.first.x)>=5&&Math.abs(A.second.y-A.first.y)>=5},clearHit:function(){if(this.prevHit){var B=this.options,A=this.plotOffset,C=this.prevHit;this.octx.clearRect(this.tHoz(C.x)+A.left-B.points.radius*2,this.tVert(C.y)+A.top-B.points.radius*2,B.points.radius*3+B.points.lineWidth*3,B.points.radius*3+B.points.lineWidth*3);this.prevHit=null}},hit:function(I){var G=this.series,C=this.options,R=this.prevHit,H=this.plotOffset,D=this.octx,S,A,M,Q,L={dist:Number.MAX_VALUE,x:null,y:null,relX:I.relX,relY:I.relY,absX:I.absX,absY:I.absY,mouse:null};for(Q=0;Q<G.length;Q++){s=G[Q];if(!s.mouse.track){continue}S=s.data;A=(s.xaxis.scale*s.mouse.sensibility);M=(s.yaxis.scale*s.mouse.sensibility);for(var P=0,B,E;P<S.length;P++){if(S[P][1]===null){continue}B=Math.pow(s.xaxis.scale*(S[P][0]-I.x),2);E=Math.pow(s.yaxis.scale*(S[P][1]-I.y),2);if(B<A&&E<M&&Math.sqrt(B+E)<L.dist){L.dist=Math.sqrt(B+E);L.x=S[P][0];L.y=S[P][1];L.mouse=s.mouse}}}if(L.mouse&&L.mouse.track&&!R||(R&&(L.x!=R.x||L.y!=R.y))){var K=this.mouseTrack||this.el.select(".flotr-mouse-value")[0],F="",J=C.mouse.position,N=C.mouse.margin,O="opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;";if(!C.mouse.relative){if(J.charAt(0)=="n"){F+="top:"+(N+H.top)+"px;"}else{if(J.charAt(0)=="s"){F+="bottom:"+(N+H.bottom)+"px;"}}if(J.charAt(1)=="e"){F+="right:"+(N+H.right)+"px;"}else{if(J.charAt(1)=="w"){F+="left:"+(N+H.left)+"px;"}}}else{if(J.charAt(0)=="n"){F+="bottom:"+(N-H.top-this.tVert(L.y)+this.canvasHeight)+"px;"}else{if(J.charAt(0)=="s"){F+="top:"+(N+H.top+this.tVert(L.y))+"px;"}}if(J.charAt(1)=="e"){F+="left:"+(N+H.left+this.tHoz(L.x))+"px;"}else{if(J.charAt(1)=="w"){F+="right:"+(N-H.left-this.tHoz(L.x)+this.canvasWidth)+"px;"}}}O+=F;if(!K){this.el.insert('<div class="flotr-mouse-value" style="'+O+'"></div>');K=this.mouseTrack=this.el.select(".flotr-mouse-value").first()}else{this.mouseTrack=K.setStyle(O)}if(L.x!==null&&L.y!==null){K.show();this.clearHit();if(L.mouse.lineColor!=null){D.save();D.translate(H.left,H.top);D.lineWidth=C.points.lineWidth;D.strokeStyle=L.mouse.lineColor;D.fillStyle="#ffffff";D.beginPath();D.arc(this.tHoz(L.x),this.tVert(L.y),C.mouse.radius,0,2*Math.PI,true);D.fill();D.stroke();D.restore()}this.prevHit=L;var T=L.mouse.trackDecimals;if(T==null||T<0){T=0}K.innerHTML=L.mouse.trackFormatter({x:L.x.toFixed(T),y:L.y.toFixed(T)});K.fire("flotr:hit",[L,this])}else{if(R){K.hide();this.clearHit()}}}},saveImage:function(D,C,A,B){var E=null;switch(D){case"jpeg":case"jpg":E=Canvas2Image.saveAsJPEG(this.canvas,B,C,A);break;default:case"png":E=Canvas2Image.saveAsPNG(this.canvas,B,C,A);break;case"bmp":E=Canvas2Image.saveAsBMP(this.canvas,B,C,A);break}if(Object.isElement(E)&&B){this.restoreCanvas();this.canvas.hide();this.overlay.hide();this.el.insert(E.setStyle({position:"absolute"}))}},restoreCanvas:function(){this.canvas.show();this.overlay.show();this.el.select("img").invoke("remove")}});Flotr.Color=Class.create({initialize:function(E,D,B,C){this.rgba=["r","g","b","a"];var A=4;while(-1<--A){this[this.rgba[A]]=arguments[A]||((A==3)?1:0)}this.normalize()},adjust:function(D,C,E,B){var A=4;while(-1<--A){if(arguments[A]!=null){this[this.rgba[A]]+=arguments[A]}}return this.normalize()},clone:function(){return new Flotr.Color(this.r,this.b,this.g,this.a)},limit:function(B,A,C){return Math.max(Math.min(B,C),A)},normalize:function(){var A=this.limit;this.r=A(parseInt(this.r),0,255);this.g=A(parseInt(this.g),0,255);this.b=A(parseInt(this.b),0,255);this.a=A(this.a,0,1);return this},scale:function(D,C,E,B){var A=4;while(-1<--A){if(arguments[A]!=null){this[this.rgba[A]]*=arguments[A]}}return this.normalize()},distance:function(B){if(!B){return }B=new Flotr.parseColor(B);var C=0;var A=3;while(-1<--A){C+=Math.abs(this[this.rgba[A]]-B[this.rgba[A]])}return C},toString:function(){return(this.a>=1)?"rgb("+[this.r,this.g,this.b].join(",")+")":"rgba("+[this.r,this.g,this.b,this.a].join(",")+")"}});Flotr.Color.lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]};Flotr.Date={format:function(F,E){if(!F){return }var A=function(H){H=H.toString();return H.length==1?"0"+H:H};var D=[];var C=false;for(var B=0;B<E.length;++B){var G=E.charAt(B);if(C){switch(G){case"h":G=F.getUTCHours().toString();break;case"H":G=A(F.getUTCHours());break;case"M":G=A(F.getUTCMinutes());break;case"S":G=A(F.getUTCSeconds());break;case"d":G=F.getUTCDate().toString();break;case"m":G=(F.getUTCMonth()+1).toString();break;case"y":G=F.getUTCFullYear().toString();break;case"b":G=Flotr.Date.monthNames[F.getUTCMonth()];break}D.push(G);C=false}else{if(G=="%"){C=true}else{D.push(G)}}}return D.join("")},timeUnits:{second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000},spec:[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]],monthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]};

--- /dev/null
+++ b/js/flotr/flotr.debug-0.2.0-alpha_radar1.js
@@ -1,1 +1,3349 @@
+//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, <http://solutoire.com>, MIT License.

+//

+//Radar chart added by Ryan Simmons

+//
+/* $Id: flotr.js 82 2009-01-12 19:19:31Z fabien.menager $ */

+

+var Flotr = {

+	version: '0.2.0-alpha',

+	author: 'Bas Wenneker',

+	website: 'http://www.solutoire.com',

+	/**

+	 * An object of the default registered graph types. Use Flotr.register(type, functionName)

+	 * to add your own type.

+	 */

+	_registeredTypes:{

+		'lines': 'drawSeriesLines',

+		'points': 'drawSeriesPoints',

+		'bars': 'drawSeriesBars',

+		'candles': 'drawSeriesCandles',

+		'pie': 'drawSeriesPie',

+		'radar':'drawSeriesRadar'

+	},

+	/**

+	 * Can be used to register your own chart type. Default types are 'lines', 'points' and 'bars'.

+	 * This is still experimental.

+	 * @todo Test and confirm.

+	 * @param {String} type - type of chart, like 'pies', 'bars' etc.

+	 * @param {String} functionName - Name of the draw function, like 'drawSeriesPies', 'drawSeriesBars' etc.

+	 */

+	register: function(type, functionName){

+		Flotr._registeredTypes[type] = functionName+'';	

+	},

+	/**

+	 * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.

+	 * You could also draw graphs by directly calling Flotr.Graph(element, data, options).

+	 * @param {Element} el - element to insert the graph into

+	 * @param {Object} data - an array or object of dataseries

+	 * @param {Object} options - an object containing options

+	 * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph

+	 * @return {Class} returns a new graph object and of course draws the graph.

+	 */

+	draw: function(el, data, options, _GraphKlass_){	

+		_GraphKlass_ = _GraphKlass_ || Flotr.Graph;

+		return new _GraphKlass_(el, data, options);

+	},

+	/**

+	 * Collects dataseries from input and parses the series into the right format. It returns an Array 

+	 * of Objects each having at least the 'data' key set.

+	 * @param {Array/Object} data - Object or array of dataseries

+	 * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)})

+	 */

+	getSeries: function(data){

+		return data.collect(function(serie){

+			var i, serie = (serie.data) ? Object.clone(serie) : {'data': serie};

+			for (i = serie.data.length-1; i > -1; --i) {

+				serie.data[i][1] = (serie.data[i][1] === null ? null : parseFloat(serie.data[i][1])); 

+			}

+			return serie;

+		});

+	},

+	/**

+	 * Recursively merges two objects.

+	 * @param {Object} src - source object (likely the object with the least properties)

+	 * @param {Object} dest - destination object (optional, object with the most properties)

+	 * @return {Object} recursively merged Object

+	 */

+	merge: function(src, dest){

+		var result = dest || {};

+		for(var i in src){

+			result[i] = (src[i] != null && typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp) && !Object.isElement(src[i])) ? Flotr.merge(src[i], dest[i]) : result[i] = src[i];		

+		}

+		return result;

+	},	

+	/**

+	 * Function calculates the ticksize and returns it.

+	 * @param {Integer} noTicks - number of ticks

+	 * @param {Integer} min - lower bound integer value for the current axis

+	 * @param {Integer} max - upper bound integer value for the current axis

+	 * @param {Integer} decimals - number of decimals for the ticks

+	 * @return {Integer} returns the ticksize in pixels

+	 */

+	getTickSize: function(noTicks, min, max, decimals){

+		var delta = (max - min) / noTicks;	

+		var magn = Flotr.getMagnitude(delta);

+		

+		// Norm is between 1.0 and 10.0.

+		var norm = delta / magn;

+		

+		var tickSize = 10;

+		if(norm < 1.5) tickSize = 1;

+		else if(norm < 2.25) tickSize = 2;

+		else if(norm < 3) tickSize = ((decimals == 0) ? 2 : 2.5);

+		else if(norm < 7.5) tickSize = 5;

+		

+		return tickSize * magn;

+	},

+	/**

+	 * Default tick formatter.

+	 * @param {String/Integer} val - tick value integer

+	 * @return {String} formatted tick string

+	 */

+	defaultTickFormatter: function(val){

+		return val+'';

+	},

+	/**

+	 * Formats the mouse tracker values.

+	 * @param {Object} obj - Track value Object {x:..,y:..}

+	 * @return {String} Formatted track string

+	 */

+	defaultTrackFormatter: function(obj){

+		return '('+obj.x+', '+obj.y+')';

+	}, 

+	defaultPieLabelFormatter: function(slice) {

+	  return (slice.fraction*100).toFixed(2)+'%';

+	},

+	/**

+	 * Returns the magnitude of the input value.

+	 * @param {Integer/Float} x - integer or float value

+	 * @return {Integer/Float} returns the magnitude of the input value

+	 */

+	getMagnitude: function(x){

+		return Math.pow(10, Math.floor(Math.log(x) / Math.LN10));

+	},

+	toPixel: function(val){

+		return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val);

+	},

+	toRad: function(angle){

+		return -angle * (Math.PI/180);

+	},

+	/**

+	 * Parses a color string and returns a corresponding Color.

+	 * @param {String} str - string thats representing a color

+	 * @return {Color} returns a Color object or false

+	 */

+	parseColor: function(str){

+		if (str instanceof Flotr.Color) return str;

+		

+		var result, Color = Flotr.Color;

+

+		// rgb(num,num,num)

+		if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))

+			return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));

+	

+		// rgba(num,num,num,num)

+		if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))

+			return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));

+			

+		// rgb(num%,num%,num%)

+		if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))

+			return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);

+	

+		// rgba(num%,num%,num%,num)

+		if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))

+			return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));

+			

+		// #a0b1c2

+		if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))

+			return new Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));

+	

+		// #fff

+		if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))

+			return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));

+

+		// Otherwise, we're most likely dealing with a named color.

+		var name = str.strip().toLowerCase();

+		if(name == 'transparent'){

+			return new Color(255, 255, 255, 0);

+		}

+		return ((result = Color.lookupColors[name])) ? new Color(result[0], result[1], result[2]) : false;

+	},

+	/**

+	 * Extracts the background-color of the passed element.

+	 * @param {Element} element

+	 * @return {String} color string

+	 */

+	extractColor: function(element){

+		var color;

+		// Loop until we find an element with a background color and stop when we hit the body element. 

+		do {

+			color = element.getStyle('background-color').toLowerCase();

+			if(!(color == '' || color == 'transparent')) break;

+			element = element.up(0);

+		} while(!element.nodeName.match(/^body$/i));

+

+		// Catch Safari's way of signaling transparent.

+		return (color == 'rgba(0, 0, 0, 0)') ? 'transparent' : color;

+	}

+};

+/**

+ * Flotr Graph class that plots a graph on creation.

+

+ */

+Flotr.Graph = Class.create({

+	/**

+	 * Flotr Graph constructor.

+	 * @param {Element} el - element to insert the graph into

+	 * @param {Object} data - an array or object of dataseries

+ 	 * @param {Object} options - an object containing options

+	 */

+	initialize: function(el, data, options){

+		this.el = $(el);

+		

+		if (!this.el) throw 'The target container doesn\'t exist';

+		

+		this.data = data;

+		this.series = Flotr.getSeries(data);

+		this.setOptions(options);

+		

+		// Initialize some variables

+		this.lastMousePos = { pageX: null, pageY: null };

+		this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };

+		this.prevSelection = null;

+		this.selectionInterval = null;

+		this.ignoreClick = false;   

+		this.prevHit = null;

+    

+		// Create and prepare canvas.

+		this.constructCanvas();

+		

+		// Add event handlers for mouse tracking, clicking and selection

+		this.initEvents();

+		

+		this.findDataRanges();

+		this.calculateTicks(this.axes.x);

+		this.calculateTicks(this.axes.x2);

+		this.calculateTicks(this.axes.y);

+		this.calculateTicks(this.axes.y2);

+		

+		this.calculateSpacing();

+		this.draw();

+		this.insertLegend();

+    

+		// Graph and Data tabs

+		if (this.options.spreadsheet.show) 

+		this.constructTabs();

+	},

+	/**

+	 * Sets options and initializes some variables and color specific values, used by the constructor. 

+	 * @param {Object} opts - options object

+	 */

+  setOptions: function(opts){

+    var options = {

+      colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.

+      title: null,

+      subtitle: null,

+      legend: {

+        show: true,            // => setting to true will show the legend, hide otherwise

+        noColumns: 1,          // => number of colums in legend table // @todo: doesn't work for HtmlText = false

+        labelFormatter: Prototype.K, // => fn: string -> string

+        labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes

+        labelBoxWidth: 14,

+        labelBoxHeight: 10,

+        labelBoxMargin: 5,

+        container: null,       // => container (as jQuery object) to put legend in, null means default on top of graph

+        position: 'nw',        // => position of default legend container within plot

+        margin: 5,             // => distance from grid edge to default legend container within plot

+        backgroundColor: null, // => null means auto-detect

+        backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background

+      },

+      xaxis: {

+        ticks: null,           // => format: either [1, 3] or [[1, 'a'], 3]

+        showLabels: true,      // => setting to true will show the axis ticks labels, hide otherwise

+        labelsAngle: 0,        // => Labels' angle, in degrees

+        title: null,           // => axis title

+        titleAngle: 0,         // => axis title's angle, in degrees

+        noTicks: 5,            // => number of ticks for automagically generated ticks

+        tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string

+        tickDecimals: null,    // => no. of decimals, null means auto

+        min: null,             // => min. value to show, null means set automatically

+        max: null,             // => max. value to show, null means set automatically

+        autoscaleMargin: 0,    // => margin in % to add if auto-setting min/max

+        color: null

+      },

+      x2axis: {},

+      yaxis: {

+        ticks: null,           // => format: either [1, 3] or [[1, 'a'], 3]

+        showLabels: true,      // => setting to true will show the axis ticks labels, hide otherwise

+        labelsAngle: 0,        // => Labels' angle, in degrees

+        title: null,           // => axis title

+        titleAngle: 90,        // => axis title's angle, in degrees

+        noTicks: 5,            // => number of ticks for automagically generated ticks

+        tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string

+        tickDecimals: null,    // => no. of decimals, null means auto

+        min: null,             // => min. value to show, null means set automatically

+        max: null,             // => max. value to show, null means set automatically

+        autoscaleMargin: 0,    // => margin in % to add if auto-setting min/max

+        color: null

+      },

+      y2axis: {

+      	titleAngle: 270

+      },

+      points: {

+        show: false,           // => setting to true will show points, false will hide

+        radius: 3,             // => point radius (pixels)

+        lineWidth: 2,          // => line width in pixels

+        fill: true,            // => true to fill the points with a color, false for (transparent) no fill

+        fillColor: '#FFFFFF',  // => fill color

+        fillOpacity: 0.4

+      },

+      lines: {

+        show: false,           // => setting to true will show lines, false will hide

+        lineWidth: 2,          // => line width in pixels

+        fill: false,           // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4       // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+      },

+      radar: {

+        show: false,           // => setting to true will show radar chart, false will hide

+        lineWidth: 2,          // => line width in pixels

+        fill: false,           // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4       // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+      },

+      bars: {

+        show: false,           // => setting to true will show bars, false will hide

+        lineWidth: 2,          // => in pixels

+        barWidth: 1,           // => in units of the x axis

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        horizontal: false,

+        stacked: false

+      },

+      candles: {

+        show: false,           // => setting to true will show candle sticks, false will hide

+        lineWidth: 1,          // => in pixels

+        wickLineWidth: 1,      // => in pixels

+        candleWidth: 0.6,      // => in units of the x axis

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        upFillColor: '#00A8F0',// => up sticks fill color

+        downFillColor: '#CB4B4B',// => down sticks fill color

+        fillOpacity: 0.5,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        barcharts: false       // => draw as barcharts (not standard bars but financial barcharts)

+      },

+      pie: {

+        show: false,           // => setting to true will show bars, false will hide

+        lineWidth: 1,          // => in pixels

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.6,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        explode: 6,

+        sizeRatio: 0.6,

+        startAngle: Math.PI/4,

+        labelFormatter: Flotr.defaultPieLabelFormatter,

+        pie3D: false,

+        pie3DviewAngle: (Math.PI/2 * 0.8),

+        pie3DspliceThickness: 20

+      },

+      grid: {

+        color: '#545454',      // => primary color used for outline and labels

+        backgroundColor: null, // => null for transparent, else color

+        tickColor: '#DDDDDD',  // => color used for the ticks

+        labelMargin: 3,        // => margin in pixels

+        verticalLines: true,   // => whether to show gridlines in vertical direction

+        horizontalLines: true, // => whether to show gridlines in horizontal direction

+        outlineWidth: 2        // => width of the grid outline/border in pixels

+      },

+      selection: {

+        mode: null,            // => one of null, 'x', 'y' or 'xy'

+        color: '#B6D9FF',      // => selection box color

+        fps: 20                // => frames-per-second

+      },

+      mouse: {

+        track: false,          // => true to track the mouse, no tracking otherwise

+        position: 'se',        // => position of the value box (default south-east)

+        relative: false,       // => next to the mouse cursor

+        trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box

+        margin: 5,             // => margin in pixels of the valuebox

+        lineColor: '#FF3F19',  // => line color of points that are drawn when mouse comes near a value of a series

+        trackDecimals: 1,      // => decimals for the track values

+        sensibility: 2,        // => the lower this number, the more precise you have to aim to show a value

+        radius: 3              // => radius of the track point

+      },

+      radarChartMode: false, // => true to render radar grid / and setup scaling for radar chart

+      shadowSize: 4,           // => size of the 'fake' shadow

+      defaultType: 'lines',    // => default series type

+      HtmlText: true,          // => wether to draw the text using HTML or on the canvas

+      fontSize: 7.5,             // => canvas' text font size

+      spreadsheet: {

+      	show: false,           // => show the data grid using two tabs

+      	tabGraphLabel: 'Graph',

+      	tabDataLabel: 'Data',

+      	toolbarDownload: 'Download CSV', // @todo: add language support

+      	toolbarSelectAll: 'Select all'

+      }

+    }

+    

+    options.x2axis = Object.extend(Object.clone(options.xaxis), options.x2axis);

+    options.y2axis = Object.extend(Object.clone(options.yaxis), options.y2axis);

+    this.options = Flotr.merge((opts || {}), options);

+    

+    this.axes = {

+      x:  {options: this.options.xaxis,  n: 1}, 

+      x2: {options: this.options.x2axis, n: 2}, 

+      y:  {options: this.options.yaxis,  n: 1}, 

+      y2: {options: this.options.y2axis, n: 2}

+    };

+		

+		// Initialize some variables used throughout this function.

+		var assignedColors = [],

+		    colors = [],

+		    ln = this.series.length,

+		    neededColors = this.series.length,

+		    oc = this.options.colors, 

+		    usedColors = [],

+		    variation = 0,

+		    c, i, j, s, tooClose;

+

+		// Collect user-defined colors from series.

+		for(i = neededColors - 1; i > -1; --i){

+			c = this.series[i].color;

+			if(c != null){

+				--neededColors;

+				if(Object.isNumber(c)) assignedColors.push(c);

+				else usedColors.push(Flotr.parseColor(c));

+			}

+		}

+		

+		// Calculate the number of colors that need to be generated.

+		for(i = assignedColors.length - 1; i > -1; --i)

+			neededColors = Math.max(neededColors, assignedColors[i] + 1);

+

+		// Generate needed number of colors.

+		for(i = 0; colors.length < neededColors;){

+			c = (oc.length == i) ? new Flotr.Color(100, 100, 100) : Flotr.parseColor(oc[i]);

+			

+			// Make sure each serie gets a different color.

+			var sign = variation % 2 == 1 ? -1 : 1;

+			var factor = 1 + sign * Math.ceil(variation / 2) * 0.2;

+			c.scale(factor, factor, factor);

+

+			/**

+			 * @todo if we're getting too close to something else, we should probably skip this one

+			 */

+			colors.push(c);

+			

+			if(++i >= oc.length){

+				i = 0;

+				++variation;

+			}

+		}

+	

+		// Fill the options with the generated colors.

+		for(i = 0, j = 0; i < ln; ++i){

+			s = this.series[i];

+

+			// Assign the color.

+			if(s.color == null){

+				s.color = colors[j++].toString();

+			}else if(Object.isNumber(s.color)){

+				s.color = colors[s.color].toString();

+			}

+			

+      if (!s.xaxis) s.xaxis = this.axes.x;

+           if (s.xaxis == 1) s.xaxis = this.axes.x;

+      else if (s.xaxis == 2) s.xaxis = this.axes.x2;

+  

+      if (!s.yaxis) s.yaxis = this.axes.y;

+           if (s.yaxis == 1) s.yaxis = this.axes.y;

+      else if (s.yaxis == 2) s.yaxis = this.axes.y2;

+			

+			// Apply missing options to the series.

+			s.lines   = Object.extend(Object.clone(this.options.lines), s.lines);

+			s.points  = Object.extend(Object.clone(this.options.points), s.points);

+			s.bars    = Object.extend(Object.clone(this.options.bars), s.bars);

+			s.candles = Object.extend(Object.clone(this.options.candles), s.candles);

+			s.pie     = Object.extend(Object.clone(this.options.pie), s.pie);

+			s.radar   = Object.extend(Object.clone(this.options.radar), s.radar);

+			s.mouse   = Object.extend(Object.clone(this.options.mouse), s.mouse);

+			

+			if(s.shadowSize == null) s.shadowSize = this.options.shadowSize;

+		}

+	},

+	/**

+	 * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use 

+	 * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements

+	 * are created, the elements are inserted into the container element.

+	 */

+	constructCanvas: function(){

+		var el = this.el,

+			size, c, oc;

+		

+  	this.canvas = el.select('.flotr-canvas')[0];

+		this.overlay = el.select('.flotr-overlay')[0];

+		

+		el.childElements().invoke('remove');

+

+		// For positioning labels and overlay.

+		el.setStyle({position:'relative', cursor:'default'});

+

+		this.canvasWidth = el.getWidth();

+		this.canvasHeight = el.getHeight();

+		size = {'width': this.canvasWidth, 'height': this.canvasHeight};

+

+		if(this.canvasWidth <= 0 || this.canvasHeight <= 0){

+			throw 'Invalid dimensions for plot, width = ' + this.canvasWidth + ', height = ' + this.canvasHeight;

+		}

+

+		// Insert main canvas.

+		if (!this.canvas) {

+			c = this.canvas = new Element('canvas', size);

+			c.className = 'flotr-canvas';

+			c = c.writeAttribute('style', 'position:absolute;left:0px;top:0px;');

+		} else {

+			c = this.canvas.writeAttribute(size);

+		}

+		el.insert(c);

+		

+		if(Prototype.Browser.IE){

+			c = window.G_vmlCanvasManager.initElement(c);

+		}

+		this.ctx = c.getContext('2d');

+    

+		// Insert overlay canvas for interactive features.

+		if (!this.overlay) {

+			oc = this.overlay = new Element('canvas', size);

+			oc.className = 'flotr-overlay';

+			oc = oc.writeAttribute('style', 'position:absolute;left:0px;top:0px;');

+		} else {

+			oc = this.overlay.writeAttribute(size);

+		}

+		el.insert(oc);

+		

+		if(Prototype.Browser.IE){

+			oc = window.G_vmlCanvasManager.initElement(oc);

+		}

+		this.octx = oc.getContext('2d');

+

+		// Enable text functions

+		if (window.CanvasText) {

+		  CanvasText.enable(this.ctx);

+		  CanvasText.enable(this.octx);

+		  this.textEnabled = true;

+		}

+	},

+  getTextDimensions: function(text, canvasStyle, HtmlStyle, className) {

+    if (!text) return {width:0, height:0};

+    

+    if (!this.options.HtmlText && this.textEnabled) {

+      var bounds = this.ctx.getTextBounds(text, canvasStyle);

+      return {

+        width: bounds.width+2, 

+        height: bounds.height+6

+      };

+    }

+    else {

+      var dummyDiv = this.el.insert('<div style="position:absolute;top:-10000px;'+HtmlStyle+'" class="'+className+' flotr-dummy-div">' + text + '</div>').select(".flotr-dummy-div")[0];

+      dim = dummyDiv.getDimensions();

+      dummyDiv.remove();

+      return dim;

+    }

+  },

+	loadDataGrid: function(){

+    if (this.seriesData) return this.seriesData;

+

+		var s = this.series;

+		var dg = [];

+

+    /* The data grid is a 2 dimensions array. There is a row for each X value.

+     * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)

+    **/

+		for(i = 0; i < s.length; ++i){

+			s[i].data.each(function(v) {

+				var x = v[0],

+				    y = v[1];

+				if (r = dg.find(function(row) {return row[0] == x})) {

+					r[i+1] = y;

+				}

+				else {

+					var newRow = [];

+					newRow[0] = x;

+					newRow[i+1] = y

+					dg.push(newRow);

+				}

+			});

+		}

+		

+    // The data grid is sorted by x value

+		dg = dg.sortBy(function(v) {

+			return v[0];

+		});

+		return this.seriesData = dg;

+	},

+	

+	// @todo: make a tab manager (Flotr.Tabs)

+  showTab: function(tabName, onComplete){

+    var elementsClassNames = 'canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle';

+    switch(tabName) {

+      case 'graph':

+        this.datagrid.up().hide();

+        this.el.select(elementsClassNames).invoke('show');

+        this.tabs.data.removeClassName('selected');

+        this.tabs.graph.addClassName('selected');

+      break;

+      case 'data':

+        this.constructDataGrid();

+        this.datagrid.up().show();

+        this.el.select(elementsClassNames).invoke('hide');

+        this.tabs.data.addClassName('selected');

+        this.tabs.graph.removeClassName('selected');

+      break;

+    }

+  },

+  constructTabs: function(){

+    var tabsContainer = new Element('div', {className:'flotr-tabs-group', style:'position:absolute;left:0px;top:'+this.canvasHeight+'px;width:'+this.canvasWidth+'px;'});

+    this.el.insert({bottom: tabsContainer});

+    this.tabs = {

+    	graph: new Element('div', {className:'flotr-tab selected', style:'float:left;'}).update(this.options.spreadsheet.tabGraphLabel),

+    	data: new Element('div', {className:'flotr-tab', style:'float:left;'}).update(this.options.spreadsheet.tabDataLabel)

+    }

+    

+    tabsContainer.insert(this.tabs.graph).insert(this.tabs.data);

+    

+    this.el.setStyle({height: this.canvasHeight+this.tabs.data.getHeight()+2+'px'});

+

+    this.tabs.graph.observe('click', (function() {this.showTab('graph')}).bind(this));

+    this.tabs.data.observe('click', (function() {this.showTab('data')}).bind(this));

+  },

+  

+  // @todo: make a spreadsheet manager (Flotr.Spreadsheet)

+	constructDataGrid: function(){

+    // If the data grid has already been built, nothing to do here

+    if (this.datagrid) return this.datagrid;

+    

+		var i, j, 

+        s = this.series,

+        datagrid = this.loadDataGrid();

+

+		var t = this.datagrid = new Element('table', {className:'flotr-datagrid', style:'height:100px;'});

+		var colgroup = ['<colgroup><col />'];

+		

+		// First row : series' labels

+		var html = ['<tr class="first-row">'];

+		html.push('<th>&nbsp;</th>');

+		for (i = 0; i < s.length; ++i) {

+			html.push('<th scope="col">'+(s[i].label || String.fromCharCode(65+i))+'</th>');

+			colgroup.push('<col />');

+		}

+		html.push('</tr>');

+		

+		// Data rows

+		for (j = 0; j < datagrid.length; ++j) {

+			html.push('<tr>');

+			for (i = 0; i < s.length+1; ++i) {

+        var tag = 'td';

+        var content = (datagrid[j][i] != null ? Math.round(datagrid[j][i]*100000)/100000 : '');

+        

+        if (i == 0) {

+          tag = 'th';

+          var label;

+          if(this.options.xaxis.ticks) {

+            var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == datagrid[j][i] });

+            if (tick) label = tick[1];

+          } 

+          else {

+            label = this.options.xaxis.tickFormatter(content);

+          }

+          

+          if (label) content = label;

+        }

+

+				html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');

+			}

+			html.push('</tr>');

+		}

+		colgroup.push('</colgroup>');

+    t.update(colgroup.join('')+html.join(''));

+    

+    if (!Prototype.Browser.IE) {

+      t.select('td').each(function(td) {

+      	td.observe('mouseover', function(e){

+      		td = e.element();

+      		var siblings = td.previousSiblings();

+      		

+      		t.select('th[scope=col]')[siblings.length-1].addClassName('hover');

+      		t.select('colgroup col')[siblings.length].addClassName('hover');

+      	});

+      	

+      	td.observe('mouseout', function(){

+      		t.select('colgroup col.hover, th.hover').each(function(e){e.removeClassName('hover')});

+      	});

+      });

+    }

+    

+		var toolbar = new Element('div', {className: 'flotr-datagrid-toolbar'}).

+	    insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarDownload).observe('click', this.downloadCSV.bind(this))).

+	    insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarSelectAll).observe('click', this.selectAllData.bind(this)));

+		

+		var container = new Element('div', {className:'flotr-datagrid-container', style:'left:0px;top:0px;width:'+this.canvasWidth+'px;height:'+this.canvasHeight+'px;overflow:auto;'});

+		container.insert(toolbar);

+		t.wrap(container.hide());

+		

+		this.el.insert(container);

+    return t;

+  },

+  selectAllData: function(){

+    if (this.tabs) {

+      var selection, range, doc, win, node = this.constructDataGrid();

+  

+      this.showTab('data');

+      

+      // deferred to be able to select the table

+      (function () {

+        if ((doc = node.ownerDocument) && (win = doc.defaultView) && 

+          win.getSelection && doc.createRange && 

+          (selection = window.getSelection()) && 

+          selection.removeAllRanges) {

+           range = doc.createRange();

+           range.selectNode(node);

+           selection.removeAllRanges();

+           selection.addRange(range);

+        }

+        else if (document.body && document.body.createTextRange && 

+          (range = document.body.createTextRange())) {

+           range.moveToElementText(node);

+           range.select();

+        }

+      }).defer();

+      return true;

+    }

+    else return false;

+  },

+  downloadCSV: function(){

+    var i, csv = '"x"',

+        series = this.series,

+        dg = this.loadDataGrid();

+    

+    for (i = 0; i < series.length; ++i) {

+      csv += '%09"'+(series[i].label || String.fromCharCode(65+i))+'"'; // \t

+    }

+    csv += "%0D%0A"; // \r\n

+    

+    for (i = 0; i < dg.length; ++i) {

+      if (this.options.xaxis.ticks) {

+        var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == dg[i][0] });

+        if (tick) dg[i][0] = tick[1];

+      } else {

+        dg[i][0] = this.options.xaxis.tickFormatter(dg[i][0]);

+      }

+      csv += dg[i].join('%09')+"%0D%0A"; // \t and \r\n

+    }

+    if (Prototype.Browser.IE) {

+      csv = csv.gsub('%09', '\t').gsub('%0A', '\n').gsub('%0D', '\r');

+      window.open().document.write(csv);

+    }

+    else {

+      window.open('data:text/csv,'+csv);

+    }

+  },

+	/**

+	 * Initializes event some handlers.

+	 */

+	initEvents: function () {

+  	//@todo: maybe stopObserving with only flotr functions

+  	this.overlay.stopObserving();

+  	this.overlay.observe('mousedown', this.mouseDownHandler.bind(this));

+		this.overlay.observe('mousemove', this.mouseMoveHandler.bind(this));

+		this.overlay.observe('click', this.clickHandler.bind(this));

+	},

+	/**

+	 * Function determines the min and max values for the xaxis and yaxis.

+	 */

+	findDataRanges: function(){

+		var s = this.series, 

+		    a = this.axes;

+		

+		a.x.datamin = 0;  a.x.datamax  = 0;

+		a.x2.datamin = 0; a.x2.datamax = 0;

+		a.y.datamin = 0;  a.y.datamax  = 0;

+		a.y2.datamin = 0; a.y2.datamax = 0;

+		

+		if(s.length > 0){

+			var i, j, h, x, y, data, xaxis, yaxis;

+		

+			// Get datamin, datamax start values 

+			for(i = 0; i < s.length; ++i) {

+				data = s[i].data, 

+				xaxis = s[i].xaxis, 

+				yaxis = s[i].yaxis;

+				

+				if (data.length > 0 && !s[i].hide) {

+					if (!xaxis.used) xaxis.datamin = xaxis.datamax = data[0][0];

+					if (!yaxis.used) yaxis.datamin = yaxis.datamax = data[0][1];

+					xaxis.used = true;

+					yaxis.used = true;

+

+					for(h = data.length - 1; h > -1; --h){

+  					x = data[h][0];

+  			         if(x < xaxis.datamin) xaxis.datamin = x;

+   					else if(x > xaxis.datamax) xaxis.datamax = x;

+  			    

+  					for(j = 1; j < data[h].length; j++){

+  						y = data[h][j];

+  				         if(y < yaxis.datamin) yaxis.datamin = y;

+  	  				else if(y > yaxis.datamax) yaxis.datamax = y;

+  					}

+					}

+				}

+				if (this.options.radarChartMode) {

+					xaxis.datamin = yaxis.datamin = - yaxis.datamax;

+					xaxis.datamax = yaxis.datamax;

+					if (!this.options.radarChartSides) this.options.radarChartSides = data.length;

+				}

+			}

+		}

+		

+		this.findXAxesValues();

+		

+		this.calculateRange(a.x);

+		this.extendXRangeIfNeededByBar(a.x);

+		

+		if (a.x2.used) {

+			this.calculateRange(a.x2);

+		  this.extendXRangeIfNeededByBar(a.x2);

+		}

+		

+		this.calculateRange(a.y);

+		this.extendYRangeIfNeededByBar(a.y);

+		

+		if (a.y2.used) {

+  		this.calculateRange(a.y2);

+  		this.extendYRangeIfNeededByBar(a.y2);

+		}

+	},

+	/**

+	 * Calculates the range of an axis to apply autoscaling.

+	 */

+	calculateRange: function(axis){

+		var o = axis.options,

+		  min = o.min != null ? o.min : axis.datamin,

+			max = o.max != null ? o.max : axis.datamax,

+			margin;

+

+		if(max - min == 0.0){

+			var widen = (max == 0.0) ? 1.0 : 0.01;

+			min -= widen;

+			max += widen;

+		}

+		axis.tickSize = Flotr.getTickSize(o.noTicks, ((this.options.radarChartMode) ? 0 : min), max, o.tickDecimals);

+

+		// Autoscaling.

+		if(o.min == null){

+			// Add a margin.

+			margin = o.autoscaleMargin;

+			if(margin != 0){

+				min -= axis.tickSize * margin;

+				

+				// Make sure we don't go below zero if all values are positive.

+				if(min < 0 && axis.datamin >= 0) min = 0;

+				min = axis.tickSize * Math.floor(min / axis.tickSize);

+			}

+		}

+		if(o.max == null){

+			margin = o.autoscaleMargin;

+			if(margin != 0){

+				max += axis.tickSize * margin;

+				if(max > 0 && axis.datamax <= 0) max = 0;				

+				max = axis.tickSize * Math.ceil(max / axis.tickSize);

+			}

+		}

+		axis.min = min;

+		axis.max = max;

+	},

+	/**

+	 * Bar series autoscaling in x direction.

+	 */

+	extendXRangeIfNeededByBar: function(axis){

+		if(axis.options.max == null){

+			var newmax = axis.max,

+			    i, s, b, c,

+			    stackedSums = [], 

+			    lastSerie = null;

+

+			for(i = 0; i < this.series.length; ++i){

+				s = this.series[i];

+				b = s.bars;

+				c = s.candles;

+				if(s.axis == axis && (b.show || c.show)) {

+					if (!b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){

+						newmax = axis.max + s.bars.barWidth;

+					}

+					if(b.stacked && b.horizontal){

+						for (j = 0; j < s.data.length; j++) {

+							if (s.bars.show && s.bars.stacked) {

+								var x = s.data[j][0];

+								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];

+								lastSerie = s;

+							}

+						}

+				    

+						for (j = 0; j < stackedSums.length; j++) {

+				    	newmax = Math.max(stackedSums[j], newmax);

+						}

+					}

+				}

+			}

+			axis.lastSerie = lastSerie;

+			axis.max = newmax;

+		}

+	},

+	/**

+	 * Bar series autoscaling in y direction.

+	 */

+	extendYRangeIfNeededByBar: function(axis){

+		if(axis.options.max == null){

+			var newmax = axis.max,

+				  i, s, b, c,

+				  stackedSums = [],

+				  lastSerie = null;

+									

+			for(i = 0; i < this.series.length; ++i){

+				s = this.series[i];

+				b = s.bars;

+				c = s.candles;

+				if (s.yaxis == axis && b.show && !s.hide) {

+					if (b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){

+						newmax = axis.max + b.barWidth;

+					}

+					if(b.stacked && !b.horizontal){

+						for (j = 0; j < s.data.length; j++) {

+							if (s.bars.show && s.bars.stacked) {

+								var x = s.data[j][0];

+								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];

+								lastSerie = s;

+							}

+						}

+						

+						for (j = 0; j < stackedSums.length; j++) {

+							newmax = Math.max(stackedSums[j], newmax);

+						}

+					}

+				}

+			}

+			axis.lastSerie = lastSerie;

+			axis.max = newmax;

+		}

+	},

+	/** 

+	 * Find every values of the x axes

+	 */

+	findXAxesValues: function(){

+		for(i = this.series.length-1; i > -1 ; --i){

+			s = this.series[i];

+			s.xaxis.values = s.xaxis.values || [];

+			for (j = s.data.length-1; j > -1 ; --j){

+				s.xaxis.values[s.data[j][0]] = {};

+			}

+		}

+	},

+	/**

+	 * Calculate axis ticks.

+	 * @param {Object} axis - axis object

+	 * @param {Object} o - axis options

+	 */

+	calculateTicks: function(axis){

+		var o = axis.options, i, v;

+		

+		axis.ticks = [];	

+		if(o.ticks){

+			var ticks = o.ticks, t, label;

+

+			if(Object.isFunction(ticks)){

+				ticks = ticks({min: axis.min, max: axis.max});

+			}

+			

+			// Clean up the user-supplied ticks, copy them over.

+			for(i = 0; i < ticks.length; ++i){

+				t = ticks[i];

+				if(typeof(t) == 'object'){

+					v = t[0];

+					label = (t.length > 1) ? t[1] : o.tickFormatter(v);

+				}else{

+					v = t;

+					label = o.tickFormatter(v);

+				}

+				axis.ticks[i] = { v: v, label: label };

+			}

+		}

+    else {

+			// Round to nearest multiple of tick size.

+			var start = axis.tickSize * Math.ceil(axis.min / axis.tickSize),

+				  decimals;

+			

+			// Then store all possible ticks.

+			for(i = 0; start + i * axis.tickSize <= axis.max; ++i){

+				v = start + i * axis.tickSize;

+				

+				// Round (this is always needed to fix numerical instability).

+				decimals = o.tickDecimals;

+				if(decimals == null) decimals = 1 - Math.floor(Math.log(axis.tickSize) / Math.LN10);

+				if(decimals < 0) decimals = 0;

+				

+				v = v.toFixed(decimals);

+				axis.ticks.push({ v: v, label: o.tickFormatter(v) });

+			}

+		}

+	},

+	/**

+	 * Calculates axis label sizes.

+	 */

+	calculateSpacing: function(){

+		var a = this.axes,

+  			options = this.options,

+  			series = this.series,

+  			margin = options.grid.labelMargin,

+  			x = a.x,

+  			x2 = a.x2,

+  			y = a.y,

+  			y2 = a.y2,

+  			maxOutset = 2,

+  			i, j, l, dim;

+		

+		// Labels width and height

+		[x, x2, y, y2].each(function(axis) {

+			var maxLabel = '';

+			

+		  if (axis.options.showLabels) {

+				for(i = 0; i < axis.ticks.length; ++i){

+					l = axis.ticks[i].label.length;

+					if(l > maxLabel.length){

+						maxLabel = axis.ticks[i].label;

+					}

+				}

+	    }

+		  axis.maxLabel  = this.getTextDimensions(maxLabel, {size:options.fontSize, angle: Flotr.toRad(axis.options.labelsAngle)}, 'font-size:smaller;', 'flotr-grid-label');

+		  axis.titleSize = this.getTextDimensions(axis.options.title, {size: options.fontSize*1.2, angle: Flotr.toRad(axis.options.titleAngle)}, 'font-weight:bold;', 'flotr-axis-title');

+		}, this);

+

+    // Title height

+    dim = this.getTextDimensions(options.title, {size: options.fontSize*1.5}, 'font-size:1em;font-weight:bold;', 'flotr-title');

+    this.titleHeight = dim.height;

+    

+    // Subtitle height

+    dim = this.getTextDimensions(options.subtitle, {size: options.fontSize}, 'font-size:smaller;', 'flotr-subtitle');

+    this.subtitleHeight = dim.height;

+

+		// Grid outline line width.

+		if(options.show){

+			maxOutset = Math.max(maxOutset, options.points.radius + options.points.lineWidth/2);

+		}

+		for(j = 0; j < options.length; ++j){

+			if (series[j].points.show){

+				maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);

+			}

+		}

+		

+		var p = this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};

+		p.left = p.right = p.top = p.bottom = maxOutset;

+		

+		p.bottom += (x.options.showLabels ?  (x.maxLabel.height  + margin) : 0) + 

+		            (x.options.title ?       (x.titleSize.height + margin) : 0);

+		

+    p.top    += (x2.options.showLabels ? (x2.maxLabel.height  + margin) : 0) + 

+                (x2.options.title ?      (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + 

+		this.options.radarChartMode ? (y.options.showLabels ?  (y.maxLabel.height  + margin) : 0) : 0;

+    

+		p.left   += (y.options.showLabels ?  (y.maxLabel.width  + margin) : 0) + 

+                (y.options.title ?       (y.titleSize.width + margin) : 0);

+		

+		p.right  += (y2.options.showLabels ? (y2.maxLabel.width  + margin) : 0) + 

+                (y2.options.title ?      (y2.titleSize.width + margin) : 0) + 

+		this.options.radarChartMode ? (x.options.showLabels ?  (x.maxLabel.width  + margin) : 0) : 0;

+    

+    p.top = Math.floor(p.top); // In order the outline not to be blured

+    

+		this.plotWidth  = this.canvasWidth - p.left - p.right;

+		this.plotHeight = this.canvasHeight - p.bottom - p.top;

+		

+		x.scale  = this.plotWidth / (x.max - x.min);

+		x2.scale = this.plotWidth / (x2.max - x2.min);

+		y.scale  = this.plotHeight / (y.max - y.min);

+		y2.scale = this.plotHeight / (y2.max - y2.min);

+	},

+	/**

+	 * Draws grid, labels and series.

+	 */

+	draw: function() {

+		this.drawGrid();

+		this.drawLabels();

+    this.drawTitles();

+    

+		if(this.series.length){

+			this.el.fire('flotr:beforedraw', [this.series, this]);

+			for(var i = 0; i < this.series.length; i++){

+				if (!this.series[i].hide)

+					this.drawSeries(this.series[i]);

+			}

+		}

+		this.el.fire('flotr:afterdraw', [this.series, this]);

+	},

+	/**

+	 * Translates absolute horizontal x coordinates to relative coordinates.

+	 * @param {Integer} x - absolute integer x coordinate

+	 * @return {Integer} translated relative x coordinate

+	 */

+	tHoz: function(x, axis){

+		axis = axis || this.axes.x;

+		return (x - axis.min) * axis.scale;

+	},

+	/**

+	 * Translates absolute vertical x coordinates to relative coordinates.

+	 * @param {Integer} y - absolute integer y coordinate

+	 * @return {Integer} translated relative y coordinate

+	 */

+	tVert: function(y, axis){

+		axis = axis || this.axes.y;

+		return this.plotHeight - (y - axis.min) * axis.scale;

+	},

+	/**

+	 * Draws a grid for the graph.

+	 */

+	drawGrid: function(){

+		if (this.options.radarChartMode) { // If we are in radar chart mode call drawRadarGrid instead and exit

+			this.drawRadarGrid();

+			return;

+		}

+		var v, o = this.options,

+		    ctx = this.ctx;

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]);

+		}

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+

+		// Draw grid background, if present in options.

+		if(o.grid.backgroundColor != null){

+			ctx.fillStyle = o.grid.backgroundColor;

+			ctx.fillRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		

+		// Draw grid lines in vertical direction.

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		ctx.beginPath();

+		if(o.grid.verticalLines){

+			for(var i = 0; i < this.axes.x.ticks.length; ++i){

+				v = this.axes.x.ticks[i].v;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.x.min || v == this.axes.x.max) && o.grid.outlineWidth != 0)

+					continue;

+	

+				ctx.moveTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, 0);

+				ctx.lineTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, this.plotHeight);

+			}

+		}

+		

+		// Draw grid lines in horizontal direction.

+		if(o.grid.horizontalLines){

+			for(var j = 0; j < this.axes.y.ticks.length; ++j){

+				v = this.axes.y.ticks[j].v;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.y.min || v == this.axes.y.max) && o.grid.outlineWidth != 0)

+					continue;

+	

+				ctx.moveTo(0, Math.floor(this.tVert(v)) + ctx.lineWidth/2);

+				ctx.lineTo(this.plotWidth, Math.floor(this.tVert(v)) + ctx.lineWidth/2);

+			}

+		}

+		ctx.stroke();

+		

+		// Draw axis/grid border.

+		if(o.grid.outlineWidth != 0) {

+			ctx.lineWidth = o.grid.outlineWidth;

+			ctx.strokeStyle = o.grid.color;

+			ctx.lineJoin = 'round';

+			ctx.strokeRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		ctx.restore();

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]);

+		}

+	},

+	/**

+	 * Draws a grid for the graph.

+	 */

+	drawRadarGrid: function(){

+		

+		var v, o = this.options,

+		    ctx = this.ctx;

+		    

+		var sides = this.options.radarChartSides,

+		    degreesInRadiansForAngle = Math.PI * 2 / sides,

+		    nintyDegrees = Math.PI / 2;

+		

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]);

+		}

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'round';

+

+		// Draw grid background, if present in options.

+		if(o.grid.backgroundColor != null){

+			ctx.fillStyle = o.grid.backgroundColor;

+			ctx.fillRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		

+		// Draw grid lines

+		var regPoly = {};

+		regPoly.xaxis = {};

+		regPoly.yaxis = {};

+		regPoly.xaxis.min = regPoly.yaxis.min = this.axes.x.min;

+		regPoly.xaxis.max = regPoly.yaxis.max = this.axes.x.max;

+		regPoly.xaxis.scale = this.plotWidth / (this.axes.x.max - this.axes.x.min);

+		regPoly.yaxis.scale = this.plotHeight / (this.axes.x.max - this.axes.x.min);

+		

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		

+		if(o.grid.horizontalLines){

+			for(var j = 0; j < this.axes.y.ticks.length; ++j){

+				v = this.axes.y.ticks[j].v;

+				if (v < 0) continue;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.y.min || v == this.axes.y.max) && o.grid.outlineWidth != 0)

+					continue;

+				regPoly.data = new Array();

+				for (i = 0; i < sides; i++) {

+					angle = nintyDegrees + (degreesInRadiansForAngle * i);

+					regPoly.data[i] = [v * Math.cos(angle), v * Math.sin(angle)]

+				}

+				regPoly.data[sides] = regPoly.data[0];

+				this.plotLine(regPoly,0);

+			}

+		}

+		

+		// Draw axis/grid border.

+		if(o.grid.outlineWidth != 0) {

+			ctx.lineWidth = o.grid.outlineWidth;

+			ctx.strokeStyle = o.grid.color;

+			regPoly.data = new Array();

+			var radius = this.axes.x.max;

+			for (i = 0; i < sides; i++) {

+				angle = nintyDegrees + (degreesInRadiansForAngle * i);

+				regPoly.data[i] = [radius * Math.cos(angle), radius * Math.sin(angle)]

+				}

+				regPoly.data[sides] = regPoly.data[0];

+				this.plotLine(regPoly,0);

+		}

+		

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		ctx.beginPath();

+		

+		if(o.grid.verticalLines){

+			for(var i = 0; i < sides; ++i){

+				ctx.moveTo(Math.floor(this.tHoz(0)) + ctx.lineWidth/2, 

+						Math.floor(this.tVert(0)) + ctx.lineWidth/2);

+				ctx.lineTo(Math.floor(this.tHoz(regPoly.data[i][0])) + ctx.lineWidth/2, 

+						Math.floor(this.tVert(regPoly.data[i][1])) + ctx.lineWidth/2);

+			}

+		}

+		

+		ctx.stroke();

+		

+		ctx.restore();

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]);

+		}

+	},

+	/**

+	* Draws labels aroung radar chart

+	*/

+	drawRadarLabels:function(){

+		var ctx = this.ctx,

+			options = this.options,

+			axis = this.axes.x,

+			tick, minY = 0, maxY = 0,

+			xOffset, yOffset;

+		var style = {

+		    size: options.fontSize,

+		    adjustAlign: true

+		  };

+		style.color = axis.options.color || options.grid.color;

+		style.angle = Flotr.toRad(axis.options.labelsAngle);

+		var radius = axis.max * 1,

+		      closeTo = axis.max * 0.1,

+		      sides = this.options.radarChartSides,

+		      degreesInRadiansForAngle = Math.PI * 2 / sides,

+		      nintyDegrees = Math.PI / 2,

+		      posdata = new Array();

+		for (i = 0; i < sides; i++) {

+				angle = nintyDegrees + (degreesInRadiansForAngle * i);

+				posdata[i] = [radius * Math.cos(angle), radius * Math.sin(angle)];

+				if (minY > posdata[i][1]) minY = posdata[i][1];

+				if (maxY < posdata[i][1]) maxY = posdata[i][1];

+				}

+		for (i = 0; i < sides; i++) {

+				tick = axis.ticks[i];

+				if(!tick.label || tick.label.length == 0) continue;

+				yOffset = 0;

+				if (posdata[i][0] > 0) {

+					style.halign = 'l';

+					xOffset = options.grid.labelMargin;

+				} else {

+					style.halign = 'r';

+					xOffset = - options.grid.labelMargin;

+				}

+				style.valign = 'm';

+				

+				if ((posdata[i][1] + closeTo) >= minY && (posdata[i][1] - closeTo) <= minY) {

+					style.valign = 't' ; 

+					style.halign = 'c';

+					yOffset = options.grid.labelMargin; 

+				};

+				if (posdata[i][1] == maxY) {

+					style.valign = 'b' ; 

+					style.halign = 'c';

+					yOffset = - options.grid.labelMargin; 

+				}

+				ctx.drawText(

+					tick.label,

+					this.plotOffset.left + this.tHoz(posdata[i][0]) + xOffset, 

+					this.plotOffset.top + this.tVert(posdata[i][1]) + yOffset,

+					style

+				);

+				}

+		

+	},

+	/**

+	 * Draws labels for x and y axis.

+	 */   

+	drawLabels: function(){		

+		// Construct fixed width label boxes, which can be styled easily. 

+		var noLabels = 0, axis,

+			xBoxWidth, i, html, tick,

+			options = this.options,

+      ctx = this.ctx,

+      a = this.axes;

+		

+		for(i = 0; i < a.x.ticks.length; ++i){

+			if (a.x.ticks[i].label) {

+				++noLabels;

+			}

+		}

+		xBoxWidth = this.plotWidth / noLabels;

+    

+		if (!options.HtmlText && this.textEnabled) {

+		  var style = {

+		    size: options.fontSize,

+        adjustAlign: true

+		  };

+

+		  // Add x labels.

+		  if (options.radarChartMode) {

+			this.drawRadarLabels();} else {

+		  axis = a.x;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if(!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'c';

+        style.valign = 't';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.tHoz(tick.v, axis), 

+		      this.plotOffset.top + this.plotHeight + options.grid.labelMargin,

+		      style

+		    );

+		  }}

+		  

+		  // Add x2 labels.

+		  axis = a.x2;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if(!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'c';

+        style.valign = 'b';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.tHoz(tick.v, axis), 

+		      this.plotOffset.top + options.grid.labelMargin,

+		      style

+		    );

+		  }

+		  

+		  // Add y labels.

+		  axis = a.y;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if (!tick.label || tick.label.length == 0 || (tick.v < 0 && this.options.radarChartMode)) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'r';

+        style.valign = 'm';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + (this.options.radarChartMode ? this.tHoz(0) : 0) - options.grid.labelMargin, 

+		      this.plotOffset.top + this.tVert(tick.v, axis),

+		      style

+		    );

+		  }

+		  

+		  // Add y2 labels.

+		  axis = a.y2;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if (!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'l';

+        style.valign = 'm';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.plotWidth + options.grid.labelMargin, 

+		      this.plotOffset.top + this.tVert(tick.v, axis),

+		      style

+		    );

+		    

+				ctx.save();

+				ctx.strokeStyle = style.color;

+				ctx.beginPath();

+				ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis));

+				ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + this.tVert(tick.v, axis));

+				ctx.stroke();

+				ctx.restore();

+		  }

+		} 

+		else if (a.x.options.showLabels || 

+				     a.x2.options.showLabels || 

+				     a.y.options.showLabels || 

+				     a.y2.options.showLabels) {

+			html = ['<div style="font-size:smaller;color:' + options.grid.color + ';" class="flotr-labels">'];

+			

+			// Add x labels.

+			axis = a.x;

+			if (axis.options.showLabels){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if(!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add x2 labels.

+			axis = a.x2;

+			if (axis.options.showLabels && axis.used){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if(!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top - options.grid.labelMargin - axis.maxLabel.height) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add y labels.

+			axis = a.y;

+			if (axis.options.showLabels){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if (!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;left:0;width:' + (this.plotOffset.left - options.grid.labelMargin) + 'px;text-align:right;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add y2 labels.

+			axis = a.y2;

+			if (axis.options.showLabels && axis.used){

+				ctx.save();

+				ctx.strokeStyle = axis.options.color || options.grid.color;

+				ctx.beginPath();

+				

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if (!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;right:0;width:' + (this.plotOffset.right - options.grid.labelMargin) + 'px;text-align:left;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+

+					ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis));

+					ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + this.tVert(tick.v, axis));

+				}

+				ctx.stroke();

+				ctx.restore();

+			}

+			

+			html.push('</div>');

+			this.el.insert(html.join(''));

+		}

+	},

+  /**

+   * Draws the title and the subtitle

+   */   

+  drawTitles: function(){

+    var html,

+        options = this.options,

+        margin = options.grid.labelMargin,

+        ctx = this.ctx,

+        a = this.axes;

+      

+    if (!options.HtmlText && this.textEnabled) {

+      var style = {

+        size: options.fontSize,

+        color: options.grid.color,

+        halign: 'c'

+      };

+

+      // Add subtitle

+      if (options.subtitle){

+        ctx.drawText(

+          options.subtitle,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.titleHeight + this.subtitleHeight - 2,

+          style

+        );

+      }

+      

+			style.weight = 1.5;

+      style.size *= 1.5;

+      

+      // Add title

+      if (options.title){

+        ctx.drawText(

+          options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.titleHeight - 2,

+          style

+        );

+      }

+      

+      style.weight = 1.8;

+      style.size *= 0.8;

+      style.adjustAlign = true;

+      

+			// Add x axis title

+			if (a.x.options.title && a.x.used){

+				style.halign = 'c';

+				style.valign = 't';

+				style.angle = Flotr.toRad(a.x.options.titleAngle);

+        ctx.drawText(

+          a.x.options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,

+          style

+        );

+      }

+			

+			// Add x2 axis title

+			if (a.x2.options.title && a.x2.used){

+				style.halign = 'c';

+				style.valign = 'b';

+				style.angle = Flotr.toRad(a.x2.options.titleAngle);

+        ctx.drawText(

+          a.x2.options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,

+          style

+        );

+      }

+			

+			// Add y axis title

+			if (a.y.options.title && a.y.used){

+				style.halign = 'r';

+				style.valign = 'm';

+				style.angle = Flotr.toRad(a.y.options.titleAngle);

+        ctx.drawText(

+          a.y.options.title,

+          this.plotOffset.left - a.y.maxLabel.width - 2 * margin, 

+          this.plotOffset.top + this.plotHeight / 2,

+          style

+        );

+      }

+			

+			// Add y2 axis title

+			if (a.y2.options.title && a.y2.used){

+				style.halign = 'l';

+				style.valign = 'm';

+				style.angle = Flotr.toRad(a.y2.options.titleAngle);

+        ctx.drawText(

+          a.y2.options.title,

+          this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, 

+          this.plotOffset.top + this.plotHeight / 2,

+          style

+        );

+      }

+    } 

+    else {

+      html = ['<div style="color:'+options.grid.color+';" class="flotr-titles">'];

+      

+      // Add title

+      if (options.title){

+        html.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+'px;font-size:1em;font-weight:bold;text-align:center;width:'+this.plotWidth+'px;" class="flotr-title">'+options.title+'</div>');

+      }

+      

+      // Add subtitle

+      if (options.subtitle){

+        html.push('<div style="position:absolute;top:'+this.titleHeight+'px;left:'+this.plotOffset.left+'px;font-size:smaller;text-align:center;width:'+this.plotWidth+'px;" class="flotr-subtitle">'+options.subtitle+'</div>');

+      }

+      html.push('</div>');

+      

+      

+      html.push('<div class="flotr-axis-title" style="font-weight:bold;">');

+			// Add x axis title

+			if (a.x.options.title && a.x.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height) + 'px;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x.options.title + '</div>');

+			}

+			

+			// Add x2 axis title

+			if (a.x2.options.title && a.x2.used){

+				html.push('<div style="position:absolute;top:0;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x2.options.title + '</div>');

+			}

+			

+			// Add y axis title

+			if (a.y.options.title && a.y.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;left:0;text-align:right;" class="flotr-axis-title">' + a.y.options.title + '</div>');

+			}

+			

+			// Add y2 axis title

+			if (a.y2.options.title && a.y2.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;right:0;text-align:right;" class="flotr-axis-title">' + a.y2.options.title + '</div>');

+			}

+			html.push('</div>');

+      

+      this.el.insert(html.join(''));

+    }

+  },

+	/**

+	 * Actually draws the graph.

+	 * @param {Object} series - series to draw

+	 */

+	drawSeries: function(series){

+		series = series || this.series;

+		

+		var drawn = false;

+		for(var type in Flotr._registeredTypes){

+			if(series[type] && series[type].show){

+				this[Flotr._registeredTypes[type]](series);

+				drawn = true;

+			}

+		}

+		

+		if(!drawn){

+			this[Flotr._registeredTypes[this.options.defaultType]](series);

+		}

+	},

+	

+	plotLine: function(series, offset){

+		var ctx = this.ctx,

+		    xa = series.xaxis,

+		    ya = series.yaxis,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this),

+  			data = series.data;

+			

+		if(data.length < 2) return;

+

+		var prevx = tHoz(data[0][0], xa),

+		    prevy = tVert(data[0][1], ya) + offset;

+		ctx.beginPath();

+		ctx.moveTo(prevx, prevy);

+		for(var i = 0; i < data.length - 1; ++i){

+			var x1 = data[i][0],   y1 = data[i][1],

+			    x2 = data[i+1][0], y2 = data[i+1][1];

+

+      // To allow empty values

+      if (y1 === null || y2 === null) continue;

+      

+			/**

+			 * Clip with ymin.

+			 */

+			if(y1 <= y2 && y1 < ya.min){

+				/**

+				 * Line segment is outside the drawing area.

+				 */

+				if(y2 < ya.min) continue;

+				

+				/**

+				 * Compute new intersection point.

+				 */

+				x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.min;

+			}else if(y2 <= y1 && y2 < ya.min){

+				if(y1 < ya.min) continue;

+				x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.min;

+			}

+

+			/**

+			 * Clip with ymax.

+			 */ 

+			if(y1 >= y2 && y1 > ya.max) {

+				if(y2 > ya.max) continue;

+				x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.max;

+			}

+			else if(y2 >= y1 && y2 > ya.max){

+				if(y1 > ya.max) continue;

+				x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.max;

+			}

+

+			/**

+			 * Clip with xmin.

+			 */

+			if(x1 <= x2 && x1 < xa.min){

+				if(x2 < xa.min) continue;

+				y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.min;

+			}else if(x2 <= x1 && x2 < xa.min){

+				if(x1 < xa.min) continue;

+				y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.min;

+			}

+

+			/**

+			 * Clip with xmax.

+			 */

+			if(x1 >= x2 && x1 > xa.max){

+				if (x2 > xa.max) continue;

+				y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.max;

+			}else if(x2 >= x1 && x2 > xa.max){

+				if(x1 > xa.max) continue;

+				y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.max;

+			}

+

+			if(prevx != tHoz(x1, xa) || prevy != tVert(y1, ya) + offset)

+				ctx.moveTo(tHoz(x1, xa), tVert(y1, ya) + offset);

+			

+			prevx = tHoz(x2, xa);

+			prevy = tVert(y2, ya) + offset;

+			ctx.lineTo(prevx, prevy);

+		}

+		ctx.stroke();

+	},

+	/**

+	 * Function used to fill

+	 * @param {Object} data

+	 */

+	plotLineArea: function(series, offset){

+		var data = series.data;

+		if(data.length < 2) return;

+

+		var top, lastX = 0,

+			ctx = this.ctx,

+	    xa = series.xaxis,

+	    ya = series.yaxis,

+			tHoz = this.tHoz.bind(this),

+			tVert = this.tVert.bind(this),

+			bottom = Math.min(Math.max(0, ya.min), ya.max),

+			first = true;

+		

+		ctx.beginPath();

+		for(var i = 0; i < data.length - 1; ++i){

+			

+			var x1 = data[i][0], y1 = data[i][1],

+			    x2 = data[i+1][0], y2 = data[i+1][1];

+			

+			if(x1 <= x2 && x1 < xa.min){

+				if(x2 < xa.min) continue;

+				y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.min;

+			}else if(x2 <= x1 && x2 < xa.min){

+				if(x1 < xa.min) continue;

+				y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.min;

+			}

+								

+			if(x1 >= x2 && x1 > xa.max){

+				if(x2 > xa.max) continue;

+				y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.max;

+			}else if(x2 >= x1 && x2 > xa.max){

+				if (x1 > xa.max) continue;

+				y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.max;

+			}

+

+			if(first){

+				ctx.moveTo(tHoz(x1, xa), tVert(bottom, ya) + offset);

+				first = false;

+			}

+			

+			/**

+			 * Now check the case where both is outside.

+			 */

+			if(y1 >= ya.max && y2 >= ya.max){

+				ctx.lineTo(tHoz(x1, xa), tVert(ya.max, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(ya.max, ya) + offset);

+				continue;

+			}else if(y1 <= ya.min && y2 <= ya.min){

+				ctx.lineTo(tHoz(x1, xa), tVert(ya.min, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(ya.min, ya) + offset);

+				continue;

+			}

+			

+			/**

+			 * Else it's a bit more complicated, there might

+			 * be two rectangles and two triangles we need to fill

+			 * in; to find these keep track of the current x values.

+			 */

+			var x1old = x1, x2old = x2;

+			

+			/**

+			 * And clip the y values, without shortcutting.

+			 * Clip with ymin.

+			 */

+			if(y1 <= y2 && y1 < ya.min && y2 >= ya.min){

+				x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.min;

+			}else if(y2 <= y1 && y2 < ya.min && y1 >= ya.min){

+				x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.min;

+			}

+

+			/**

+			 * Clip with ymax.

+			 */

+			if(y1 >= y2 && y1 > ya.max && y2 <= ya.max){

+				x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.max;

+			}else if(y2 >= y1 && y2 > ya.max && y1 <= ya.max){

+				x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.max;

+			}

+

+			/**

+			 * If the x value was changed we got a rectangle to fill.

+			 */

+			if(x1 != x1old){

+				top = (y1 <= ya.min) ? top = ya.min : ya.max;

+				ctx.lineTo(tHoz(x1old, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(x1, xa), tVert(top, ya) + offset);

+			}

+		   	

+			/**

+			 * Fill the triangles.

+			 */

+			ctx.lineTo(tHoz(x1, xa), tVert(y1, ya) + offset);

+			ctx.lineTo(tHoz(x2, xa), tVert(y2, ya) + offset);

+

+			/**

+			 * Fill the other rectangle if it's there.

+			 */

+			if(x2 != x2old){

+				top = (y2 <= ya.min) ? ya.min : ya.max;

+				ctx.lineTo(tHoz(x2old, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(top, ya) + offset);

+			}

+

+			lastX = Math.max(x2, x2old);

+		}

+		

+		ctx.lineTo(tHoz(lastX, xa), tVert(bottom, ya) + offset);

+		ctx.closePath();

+		ctx.fill();

+	},

+	/**

+	 * Function: (private) drawSeriesLines

+	 * 

+	 * Function draws lines series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.lines.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesLines: function(series){

+		series = series || this.series;

+		var ctx = this.ctx;

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'round';

+

+		var lw = series.lines.lineWidth;

+		var sw = series.shadowSize;

+

+		if(sw > 0){

+			ctx.lineWidth = sw / 2;

+

+			var offset = lw/2 + ctx.lineWidth/2;

+			

+			ctx.strokeStyle = "rgba(0,0,0,0.1)";

+			this.plotLine(series, offset + sw/2);

+

+			ctx.strokeStyle = "rgba(0,0,0,0.2)";

+			this.plotLine(series, offset);

+

+			if(series.lines.fill) {

+				ctx.fillStyle = "rgba(0,0,0,0.05)";

+				this.plotLineArea(series, offset + sw/2);

+			}

+		}

+

+		ctx.lineWidth = lw;

+		ctx.strokeStyle = series.color;

+		if(series.lines.fill){

+			ctx.fillStyle = series.lines.fillColor != null ? series.lines.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.lines.fillOpacity).toString();

+			this.plotLineArea(series, 0);

+		}

+

+		this.plotLine(series, 0);

+		ctx.restore();

+	},

+	/**

+	 * Function: drawSeriesPoints

+	 * 

+	 * Function draws point series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.points.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesPoints: function(series) {

+		var ctx = this.ctx;

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+

+		var lw = series.lines.lineWidth;

+		var sw = series.shadowSize;

+		

+		if(sw > 0){

+			ctx.lineWidth = sw / 2;

+      

+			ctx.strokeStyle = 'rgba(0,0,0,0.1)';

+			this.plotPointShadows(series, sw/2 + ctx.lineWidth/2, series.points.radius);

+

+			ctx.strokeStyle = 'rgba(0,0,0,0.2)';

+			this.plotPointShadows(series, ctx.lineWidth/2, series.points.radius);

+		}

+

+		ctx.lineWidth = series.points.lineWidth;

+		ctx.strokeStyle = series.color;

+		ctx.fillStyle = series.points.fillColor != null ? series.points.fillColor : series.color;

+		this.plotPoints(series, series.points.radius, series.points.fill);

+		ctx.restore();

+	},

+	plotPoints: function (series, radius, fill) {

+    var xa = series.xaxis,

+        ya = series.yaxis,

+		    ctx = this.ctx, i,

+		    data = series.data;

+			

+		for(i = data.length - 1; i > -1; --i){

+			var x = data[i][0], y = data[i][1];

+			if(x < xa.min || x > xa.max || y < ya.min || y > ya.max)

+				continue;

+			

+			ctx.beginPath();

+			ctx.arc(this.tHoz(x, xa), this.tVert(y, ya), radius, 0, 2 * Math.PI, true);

+			if(fill) ctx.fill();

+			ctx.stroke();

+		}

+	},

+	plotPointShadows: function(series, offset, radius){

+    var xa = series.xaxis,

+        ya = series.yaxis,

+		    ctx = this.ctx, i,

+		    data = series.data;

+			

+		for(i = data.length - 1; i > -1; --i){

+			var x = data[i][0], y = data[i][1];

+			if (x < xa.min || x > xa.max || y < ya.min || y > ya.max)

+				continue;

+			ctx.beginPath();

+			ctx.arc(this.tHoz(x, xa), this.tVert(y, ya) + offset, radius, 0, Math.PI, false);

+			ctx.stroke();

+		}

+	},

+	/**

+	 * Function: drawSeriesBars

+	 * 

+	 * Function draws bar series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.bars.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesBars: function(series) {

+		var ctx = this.ctx,

+			bw = series.bars.barWidth,

+			lw = Math.min(series.bars.lineWidth, bw);

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'miter';

+

+		/**

+		 * @todo linewidth not interpreted the right way.

+		 */

+		ctx.lineWidth = lw;

+		ctx.strokeStyle = series.color;

+    

+		this.plotBarsShadows(series, bw, 0, series.bars.fill);

+

+		if(series.bars.fill){

+			ctx.fillStyle = series.bars.fillColor != null ? series.bars.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.bars.fillOpacity).toString();

+		}

+    

+		this.plotBars(series, bw, 0, series.bars.fill);

+		ctx.restore();

+	},

+	plotBars: function(series, barWidth, offset, fill){

+		var data = series.data;

+		if(data.length < 1) return;

+		

+    var xa = series.xaxis,

+        ya = series.yaxis,

+  			ctx = this.ctx,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this);

+

+		for(var i = 0; i < data.length; i++){

+			var x = data[i][0],

+			    y = data[i][1];

+			var drawLeft = true, drawTop = true, drawRight = true;

+			

+			// Stacked bars

+			var stackOffset = 0;

+			if(series.bars.stacked) {

+			  xa.values.each(function(o, v) {

+			    if (v == x) {

+			      stackOffset = o.stack || 0;

+			      o.stack = stackOffset + y;

+			    }

+			  });

+			}

+

+			// @todo: fix horizontal bars support

+			// Horizontal bars

+			if(series.bars.horizontal)

+				var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth;

+			else 

+				var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset;

+

+			if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+				continue;

+

+			if(left < xa.min){

+				left = xa.min;

+				drawLeft = false;

+			}

+

+			if(right > xa.max){

+				right = xa.max;

+				if (xa.lastSerie != series && series.bars.horizontal)

+					drawTop = false;

+			}

+

+			if(bottom < ya.min)

+				bottom = ya.min;

+

+			if(top > ya.max){

+				top = ya.max;

+				if (ya.lastSerie != series && !series.bars.horizontal)

+					drawTop = false;

+			}

+      

+			/**

+			 * Fill the bar.

+			 */

+			if(fill){

+				ctx.beginPath();

+				ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset);

+				ctx.lineTo(tHoz(left, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(right, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(right, xa), tVert(bottom, ya) + offset);

+				ctx.fill();

+			}

+

+			/**

+			 * Draw bar outline/border.

+			 */

+			if(series.bars.lineWidth != 0 && (drawLeft || drawRight || drawTop)){

+				ctx.beginPath();

+				ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset);

+				

+				ctx[drawLeft ?'lineTo':'moveTo'](tHoz(left, xa), tVert(top, ya) + offset);

+				ctx[drawTop  ?'lineTo':'moveTo'](tHoz(right, xa), tVert(top, ya) + offset);

+				ctx[drawRight?'lineTo':'moveTo'](tHoz(right, xa), tVert(bottom, ya) + offset);

+				         

+				ctx.stroke();

+			}

+		}

+	},

+  plotBarsShadows: function(series, barWidth, offset){

+		var data = series.data;

+    if(data.length < 1) return;

+    

+    var xa = series.xaxis,

+        ya = series.yaxis,

+        ctx = this.ctx,

+        tHoz = this.tHoz.bind(this),

+        tVert = this.tVert.bind(this),

+        sw = this.options.shadowSize;

+

+    for(var i = 0; i < data.length; i++){

+      var x = data[i][0],

+          y = data[i][1];

+      

+      // Stacked bars

+      var stackOffset = 0;

+			if(series.bars.stacked) {

+			  xa.values.each(function(o, v) {

+			    if (v == x) {

+			      stackOffset = o.stackShadow || 0;

+			      o.stackShadow = stackOffset + y;

+			    }

+			  });

+			}

+      

+      // Horizontal bars

+      if(series.bars.horizontal) 

+        var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth;

+      else 

+        var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset;

+

+      if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+        continue;

+

+      if(left < xa.min)   left = xa.min;

+      if(right > xa.max)  right = xa.max;

+      if(bottom < ya.min) bottom = ya.min;

+      if(top > ya.max)    top = ya.max;

+      

+      var width =  tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw);

+      var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw));

+

+      ctx.fillStyle = 'rgba(0,0,0,0.05)';

+      ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height);

+    }

+  },

+	/**

+	 * Function: drawSeriesCandles

+	 * 

+	 * Function draws candles series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.candles.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesCandles: function(series) {

+		var ctx = this.ctx,

+			  bw = series.candles.candleWidth;

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'miter';

+

+		/**

+		 * @todo linewidth not interpreted the right way.

+		 */

+		ctx.lineWidth = series.candles.lineWidth;

+		this.plotCandlesShadows(series, bw/2);

+		this.plotCandles(series, bw/2);

+		

+		ctx.restore();

+	},

+	plotCandles: function(series, offset){

+		var data = series.data;

+		if(data.length < 1) return;

+		

+    var xa = series.xaxis,

+        ya = series.yaxis,

+  			ctx = this.ctx,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this);

+

+		for(var i = 0; i < data.length; i++){

+      var d     = data[i],

+  		    x     = d[0],

+  		    open  = d[1],

+  		    high  = d[2],

+  		    low   = d[3],

+  		    close = d[4];

+

+			var left    = x,

+			    right   = x + series.candles.candleWidth,

+          bottom  = Math.max(ya.min, low),

+	        top     = Math.min(ya.max, high),

+          bottom2 = Math.max(ya.min, Math.min(open, close)),

+	        top2    = Math.min(ya.max, Math.max(open, close));

+

+			if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+				continue;

+

+			var color = series.candles[open>close?'downFillColor':'upFillColor'];

+			/**

+			 * Fill the candle.

+			 */

+			if(series.candles.fill && !series.candles.barcharts){

+				ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.candles.fillOpacity).toString();

+				ctx.fillRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya));

+			}

+

+			/**

+			 * Draw candle outline/border, high, low.

+			 */

+			if(series.candles.lineWidth || series.candles.wickLineWidth){

+				var x, y, pixelOffset = (series.candles.wickLineWidth % 2) / 2;

+

+				x = Math.floor(tHoz((left + right) / 2), xa) + pixelOffset;

+				

+			  ctx.save();

+			  ctx.strokeStyle = color;

+			  ctx.lineWidth = series.candles.wickLineWidth;

+			  ctx.lineCap = 'butt';

+			  

+				if (series.candles.barcharts) {

+					ctx.beginPath();

+					

+					ctx.moveTo(x, Math.floor(tVert(top, ya) + offset));

+					ctx.lineTo(x, Math.floor(tVert(bottom, ya) + offset));

+					

+					y = Math.floor(tVert(open, ya) + offset)+0.5;

+					ctx.moveTo(Math.floor(tHoz(left, xa))+pixelOffset, y);

+					ctx.lineTo(x, y);

+					

+					y = Math.floor(tVert(close, ya) + offset)+0.5;

+					ctx.moveTo(Math.floor(tHoz(right, xa))+pixelOffset, y);

+					ctx.lineTo(x, y);

+				} 

+				else {

+  				ctx.strokeRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya));

+

+  				ctx.beginPath();

+  				ctx.moveTo(x, Math.floor(tVert(top2,    ya) + offset));

+  				ctx.lineTo(x, Math.floor(tVert(top,     ya) + offset));

+  				ctx.moveTo(x, Math.floor(tVert(bottom2, ya) + offset));

+  				ctx.lineTo(x, Math.floor(tVert(bottom,  ya) + offset));

+				}

+				

+				ctx.stroke();

+				ctx.restore();

+			}

+		}

+	},

+  plotCandlesShadows: function(series, offset){

+		var data = series.data;

+    if(data.length < 1 || series.candles.barcharts) return;

+    

+    var xa = series.xaxis,

+        ya = series.yaxis,

+        tHoz = this.tHoz.bind(this),

+        tVert = this.tVert.bind(this),

+        sw = this.options.shadowSize;

+

+    for(var i = 0; i < data.length; i++){

+      var d     = data[i],

+      		x     = d[0],

+	        open  = d[1],

+	        high  = d[2],

+	        low   = d[3],

+	        close = d[4];

+      

+			var left   = x,

+	        right  = x + series.candles.candleWidth,

+          bottom = Math.max(ya.min, Math.min(open, close)),

+	        top    = Math.min(ya.max, Math.max(open, close));

+

+      if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+        continue;

+

+      var width =  tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw);

+      var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw));

+

+      this.ctx.fillStyle = 'rgba(0,0,0,0.05)';

+      this.ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height);

+    }

+  },

+  /**

+   * Function: drawSeriesRadar

+   * 

+   * Function draws a radar chart on the canvas element.

+   * 

+   * Parameters:

+   *    series - Series with options.radar.show = true.

+   * 

+   * Returns:

+   *    void

+   */

+  drawSeriesRadar: function(series) {

+	var ctx = this.ctx,

+		options = this.options, sides= series.data.length;

+		

+	var degreesInRadiansForAngle = Math.PI * 2 / sides,

+	      nintyDegrees = Math.PI / 2;

+	

+	var poly = {};

+	

+	/* 

+	Draw radar grid

+	

+	poly.xaxis = series.xaxis;

+	poly.yaxis = series.yaxis;

+	ctx.save();

+	ctx.translate(this.plotOffset.left, this.plotOffset.top);

+	ctx.lineJoin = 'round';

+	for (radius = 20; radius <= 100; radius += 20) {

+	poly.data = new Array();

+	for (i = 0; i < sides; i++) {

+		angle = nintyDegrees + (degreesInRadiansForAngle * i);

+		poly.data[i] = [radius * Math.cos(angle), radius * Math.sin(angle)]

+	}

+	poly.data[sides] = poly.data[0];

+	this.plotLine(poly,0);}

+	

+	var outside = poly.data;

+	for (i = 0; i < sides; i++) {

+		poly.data = new Array();

+		poly.data[0] = [0,0];

+		poly.data[1] = outside[i];

+		this.plotLine(poly,0);

+	}

+	*/

+	

+	/*

+	Convert Series data into X, Y co-ordinates

+	*/

+	if (!series.dataInRadarFormat) {

+	poly.data = new Array();

+	for (i = 0; i < sides; i++) {

+		angle = nintyDegrees + (degreesInRadiansForAngle * i);

+		poly.data[i] = [series.data[i][1] * Math.cos(angle), series.data[i][1] * Math.sin(angle), series.data[i][0], series.data[i][1]]

+	}

+	poly.data[sides] = poly.data[0];

+	series.data = poly.data;

+	series.lines = series.radar;

+	series.lines.show = false;

+	series.dataInRadarFormat = true;

+	}

+	

+	this.drawSeriesLines(series);

+	

+},

+  

+  

+  /**

+   * Function: drawSeriesPie

+   * 

+   * Function draws a pie in the canvas element.

+   * 

+   * Parameters:

+   *    series - Series with options.pie.show = true.

+   * 

+   * Returns:

+   *    void

+   */

+  drawSeriesPie: function(series) {

+    if (!this.options.pie.drawn) {

+    var ctx = this.ctx,

+        options = this.options,

+        lw = series.pie.lineWidth,

+        sw = series.shadowSize,

+        data = series.data,

+        radius = (Math.min(this.canvasWidth, this.canvasHeight) * series.pie.sizeRatio) / 2,

+        html = [];

+    

+    var vScale = 1;//Math.cos(series.pie.viewAngle);

+    var plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale;

+    

+    var style = {

+      size: options.fontSize*1.2,

+      color: options.grid.color,

+      weight: 1.5

+    };

+    

+    var center = {

+      x: (this.canvasWidth+this.plotOffset.left)/2,

+      y: (this.canvasHeight-this.plotOffset.bottom)/2

+    };

+    

+    // Pie portions

+    var portions = this.series.collect(function(hash, index){

+    	if (hash.pie.show)

+      return {

+        name: (hash.label || hash.data[0][1]),

+        value: [index, hash.data[0][1]],

+        explode: hash.pie.explode

+      };

+    });

+    

+    // Sum of the portions' angles

+    var sum = portions.pluck('value').pluck(1).inject(0, function(acc, n) { return acc + n; });

+    

+    var fraction = 0.0,

+        angle = series.pie.startAngle,

+        value = 0.0;

+    

+    var slices = portions.collect(function(slice){

+      angle += fraction;

+      value = parseFloat(slice.value[1]); // @warning : won't support null values !!

+      fraction = value/sum;

+      return {

+        name:     slice.name,

+        fraction: fraction,

+        x:        slice.value[0],

+        y:        value,

+        explode:  slice.explode,

+        startAngle: 2 * angle * Math.PI,

+        endAngle:   2 * (angle + fraction) * Math.PI

+      };

+    });

+    

+    ctx.save();

+

+    if(sw > 0){

+	    slices.each(function (slice) {

+        var bisection = (slice.startAngle + slice.endAngle) / 2;

+        

+        var xOffset = center.x + Math.cos(bisection) * slice.explode + sw;

+        var yOffset = center.y + Math.sin(bisection) * slice.explode + sw;

+        

+		    this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);

+

+        ctx.fillStyle = 'rgba(0,0,0,0.1)';

+        ctx.fill();

+      }, this);

+    }

+    

+    if (options.HtmlText) {

+      html = ['<div style="color:' + this.options.grid.color + '" class="flotr-labels">'];

+    }

+    

+    slices.each(function (slice, index) {

+      var bisection = (slice.startAngle + slice.endAngle) / 2;

+      var color = options.colors[index];

+      

+      var xOffset = center.x + Math.cos(bisection) * slice.explode;

+      var yOffset = center.y + Math.sin(bisection) * slice.explode;

+      

+      this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);

+      

+      if(series.pie.fill){

+        ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString();

+        ctx.fill();

+      }

+      ctx.lineWidth = lw;

+      ctx.strokeStyle = color;

+      ctx.stroke();

+      

+      /*ctx.save();

+      ctx.scale(1, vScale);

+      

+      ctx.moveTo(xOffset, yOffset);

+      ctx.beginPath();

+      ctx.lineTo(xOffset, yOffset+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius);

+      ctx.lineTo(xOffset, yOffset);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.moveTo(xOffset, yOffset);

+      ctx.beginPath();

+      ctx.lineTo(xOffset, yOffset+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius);

+      ctx.lineTo(xOffset, yOffset);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.moveTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius);

+      ctx.beginPath();

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness);

+      ctx.arc(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius);

+      ctx.arc(xOffset, yOffset, radius, slice.endAngle, slice.startAngle, true);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.scale(1, 1/vScale);

+      this.plotSlice(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false, vScale);

+      ctx.stroke();

+      if(series.pie.fill){

+        ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString();

+        ctx.fill();

+      }

+      

+      ctx.restore();*/

+      

+      var label = options.pie.labelFormatter(slice);

+      

+      var textAlignRight = (Math.cos(bisection) < 0);

+      var distX = xOffset + Math.cos(bisection) * (series.pie.explode + radius);

+      var distY = yOffset + Math.sin(bisection) * (series.pie.explode + radius);

+      

+      if (slice.fraction && label) {

+        if (options.HtmlText) {

+          var divStyle = 'position:absolute;top:' + (distY - 5) + 'px;'; //@todo: change

+          if (textAlignRight) {

+            divStyle += 'right:'+(this.canvasWidth - distX)+'px;text-align:right;';

+          }

+          else {

+            divStyle += 'left:'+distX+'px;text-align:left;';

+          }

+          html.push('<div style="' + divStyle + '" class="flotr-grid-label">' + label + '</div>');

+        }

+        else {

+          style.halign = textAlignRight ? 'r' : 'l';

+          ctx.drawText(

+            label, 

+            distX, 

+            distY + style.size / 2, 

+            style

+          );

+        }

+      }

+    }, this);

+

+    if (options.HtmlText) {

+      html.push('</div>');    

+      this.el.insert(html.join(''));

+    }

+    

+    ctx.restore();

+    options.pie.drawn = true;

+    }

+  },

+  plotSlice: function(x, y, radius, startAngle, endAngle, fill, vScale) {

+    var ctx = this.ctx;

+    vScale = vScale || 1;

+    

+    ctx.save();

+    ctx.scale(1, vScale);

+    ctx.beginPath();

+    ctx.moveTo(x, y);

+    ctx.arc   (x, y, radius, startAngle, endAngle, fill);

+    ctx.lineTo(x, y);

+    ctx.closePath();

+    ctx.restore();

+  },

+  plotPie: function() {}, 

+	/**

+	 * Function: insertLegend

+	 * 

+	 * Function adds a legend div to the canvas container or draws it on the canvas.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	insertLegend: function(){

+		if(!this.options.legend.show)

+			return;

+			

+		var series = this.series,

+			plotOffset = this.plotOffset,

+			options = this.options,

+			fragments = [],

+			rowStarted = false, 

+			ctx = this.ctx,

+			i;

+			

+		var noLegendItems = series.findAll(function(s) {return (s.label && !s.hide)}).size();

+

+    if (noLegendItems) {

+	    if (!options.HtmlText && this.textEnabled) {

+	      var style = {

+	        size: options.fontSize*1.1,

+	        color: options.grid.color

+	      };

+	      

+	      // @todo: take css into account

+	      //var dummyDiv = this.el.insert('<div class="flotr-legend" style="position:absolute;top:-10000px;"></div>');

+	      

+	      var p = options.legend.position, 

+	          m = options.legend.margin,

+	          lbw = options.legend.labelBoxWidth,

+	          lbh = options.legend.labelBoxHeight,

+	          lbm = options.legend.labelBoxMargin,

+	          offsetX = plotOffset.left + m,

+	          offsetY = plotOffset.top + m;

+	      

+	      // We calculate the labels' max width

+	      var labelMaxWidth = 0;

+	      for(i = series.length - 1; i > -1; --i){

+	        if(!series[i].label || series[i].hide) continue;

+	        var label = options.legend.labelFormatter(series[i].label);	

+	        labelMaxWidth = Math.max(labelMaxWidth, ctx.measureText(label, style));

+	      }

+	      

+	      var legendWidth  = Math.round(lbw + lbm*3 + labelMaxWidth),

+	          legendHeight = Math.round(noLegendItems*(lbm+lbh) + lbm);

+	

+	      if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);

+	      if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);

+

+	      // Legend box

+	      var color = Flotr.parseColor(options.legend.backgroundColor || 'rgb(240,240,240)').scale(null, null, null, options.legend.backgroundOpacity || 0.1).toString();

+	      

+	      ctx.fillStyle = color;

+	      ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);

+	      ctx.strokeStyle = options.legend.labelBoxBorderColor;

+	      ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight);

+	      

+	      // Legend labels

+	      var x = offsetX + lbm;

+	      var y = offsetY + lbm;

+	      for(i = 0; i < series.length; i++){

+	        if(!series[i].label || series[i].hide) continue;

+	        var label = options.legend.labelFormatter(series[i].label);

+

+	        ctx.fillStyle = series[i].color;

+	        ctx.fillRect(x, y, lbw-1, lbh-1);

+	        

+	        ctx.strokeStyle = options.legend.labelBoxBorderColor;

+	        ctx.lineWidth = 1;

+	        ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);

+	        

+	        // Legend text

+	        ctx.drawText(

+	          label,

+	          x + lbw + lbm,

+	          y + (lbh + style.size - ctx.fontDescent(style))/2,

+	          style

+	        );

+	        

+	        y += lbh + lbm;

+	      }

+	    }

+	    else {

+	  		for(i = 0; i < series.length; ++i){

+	  			if(!series[i].label || series[i].hide) continue;

+	  			

+	  			if(i % options.legend.noColumns == 0){

+	  				fragments.push(rowStarted ? '</tr><tr>' : '<tr>');

+	  				rowStarted = true;

+	  			}

+	  

+	  			var label = options.legend.labelFormatter(series[i].label);

+	  			

+	  			fragments.push('<td class="flotr-legend-color-box"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:' + options.legend.labelBoxWidth + 'px;height:' + options.legend.labelBoxHeight + 'px;background-color:' + series[i].color + '"></div></div></td>' +

+	  				'<td class="flotr-legend-label">' + label + '</td>');

+	  		}

+	  		if(rowStarted) fragments.push('</tr>');

+	  		

+	  		if(fragments.length > 0){

+	  			var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';

+	  			if(options.legend.container != null){

+	  				$(options.legend.container).update(table);

+	  			}else{

+	  				var pos = '';

+	  				var p = options.legend.position, m = options.legend.margin;

+	  				

+	  				     if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;';

+	  				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;';					

+	  				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;';

+	  				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;';

+	  				     

+	  				var div = this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;' + pos +'">' + table + '</div>').select('div.flotr-legend').first();

+	  				

+	  				if(options.legend.backgroundOpacity != 0.0){

+	  					/**

+	  					 * Put in the transparent background separately to avoid blended labels and

+	  					 * label boxes.

+	  					 */

+	  					var c = options.legend.backgroundColor;

+	  					if(c == null){

+	  						var tmp = (options.grid.backgroundColor != null) ? options.grid.backgroundColor : Flotr.extractColor(div);

+	  						c = Flotr.parseColor(tmp).adjust(null, null, null, 1).toString();

+	  					}

+	  					this.el.insert('<div class="flotr-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.flotr-legend-bg').first().setStyle({

+	  						'opacity': options.legend.backgroundOpacity

+	  					});						

+	  				}

+	  			}

+	  		}

+	    }

+    }

+	},

+	/**

+	 * Function: getEventPosition

+	 * 

+	 * Calculates the coordinates from a mouse event object.

+	 * 

+	 * Parameters:

+	 * 		event - Mouse Event object.

+	 * 

+	 * Returns:

+	 * 		Object with x and y coordinates of the mouse.

+	 */

+	getEventPosition: function (event){

+		var offset = this.overlay.cumulativeOffset(),

+			rx = (event.pageX - offset.left - this.plotOffset.left),

+			ry = (event.pageY - offset.top - this.plotOffset.top),

+			ax = 0, ay = 0

+			

+		if(event.pageX == null && event.clientX != null){

+			var de = document.documentElement, b = document.body;

+			ax = event.clientX + (de && de.scrollLeft || b.scrollLeft || 0);

+			ay = event.clientY + (de && de.scrollTop || b.scrollTop || 0);

+		}else{

+			ax = event.pageX;

+			ay = event.pageY;

+		}

+		

+		return {

+			x:  this.axes.x.min  + rx / this.axes.x.scale,

+			x2: this.axes.x2.min + rx / this.axes.x2.scale,

+			y:  this.axes.y.max  - ry / this.axes.y.scale,

+			y2: this.axes.y2.max - ry / this.axes.y2.scale,

+			relX: rx,

+			relY: ry,

+			absX: ax,

+			absY: ay

+		};

+	},

+	/**

+	 * Function: clickHandler

+	 * 

+	 * Handler observes the 'click' event and fires the 'flotr:click' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'click' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clickHandler: function(event){

+		if(this.ignoreClick){

+			this.ignoreClick = false;

+			return;

+		}

+		this.el.fire('flotr:click', [this.getEventPosition(event), this]);

+	},

+	/**

+	 * Function: mouseMoveHandler

+	 * 

+	 * Handler observes mouse movement over the graph area. Fires the 

+	 * 'flotr:mousemove' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'mousemove' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseMoveHandler: function(event){

+ 		var pos = this.getEventPosition(event);

+    

+		this.lastMousePos.pageX = pos.absX;

+		this.lastMousePos.pageY = pos.absY;	

+		if(this.selectionInterval == null && (this.options.mouse.track || this.series.any(function(s){return s.mouse && s.mouse.track;}))){	

+			this.hit(pos);

+		}

+    

+		this.el.fire('flotr:mousemove', [event, pos, this]);

+	},

+	/**

+	 * Function: mouseDownHandler

+	 * 

+	 * Handler observes the 'mousedown' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'mousedown' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseDownHandler: function (event){

+    if(event.isRightClick()) {

+      event.stop();

+      var overlay = this.overlay;

+      overlay.hide();

+      

+      function cancelContextMenu () {

+        overlay.show();

+        $(document).stopObserving('mousemove', cancelContextMenu);

+      }

+      $(document).observe('mousemove', cancelContextMenu);

+      return;

+    }

+    

+		if(!this.options.selection.mode || !event.isLeftClick()) return;

+		

+		this.setSelectionPos(this.selection.first, event);				

+		if(this.selectionInterval != null){

+			clearInterval(this.selectionInterval);

+		}

+		this.lastMousePos.pageX = null;

+		this.selectionInterval = setInterval(this.updateSelection.bind(this), 1000/this.options.selection.fps);

+		

+		this.mouseUpHandler = this.mouseUpHandler.bind(this);

+		$(document).observe('mouseup', this.mouseUpHandler);

+	},

+	/**

+	 * Function: (private) fireSelectEvent

+	 * 

+	 * Fires the 'flotr:select' event when the user made a selection.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	fireSelectEvent: function(){

+		var a = this.axes, selection = this.selection,

+			x1 = (selection.first.x <= selection.second.x) ? selection.first.x : selection.second.x,

+			x2 = (selection.first.x <= selection.second.x) ? selection.second.x : selection.first.x,

+			y1 = (selection.first.y >= selection.second.y) ? selection.first.y : selection.second.y,

+			y2 = (selection.first.y >= selection.second.y) ? selection.second.y : selection.first.y;

+		

+		x1 = a.x.min + x1 / a.x.scale;

+		x2 = a.x.min + x2 / a.x.scale;

+		y1 = a.y.max - y1 / a.y.scale;

+		y2 = a.y.max - y2 / a.y.scale;

+

+		this.el.fire('flotr:select', [{x1:x1, y1:y1, x2:x2, y2:y2}, this]);

+	},

+	/**

+	 * Function: (private) mouseUpHandler

+	 * 

+	 * Handler observes the mouseup event for the document. 

+	 * 

+	 * Parameters:

+	 * 		event - 'mouseup' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseUpHandler: function(event){

+    $(document).stopObserving('mouseup', this.mouseUpHandler);

+    event.stop();

+    

+		if(this.selectionInterval != null){

+			clearInterval(this.selectionInterval);

+			this.selectionInterval = null;

+		}

+

+		this.setSelectionPos(this.selection.second, event);

+		this.clearSelection();

+		

+		if(this.selectionIsSane()){

+			this.drawSelection();

+			this.fireSelectEvent();

+			this.ignoreClick = true;

+		}

+	},

+	/**

+	 * Function: setSelectionPos

+	 * 

+	 * Calculates the position of the selection.

+	 * 

+	 * Parameters:

+	 * 		pos - Position object.

+	 * 		event - Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	setSelectionPos: function(pos, event) {

+		var options = this.options,

+		    offset = $(this.overlay).cumulativeOffset();

+		

+		if(options.selection.mode.indexOf('x') == -1){

+			pos.x = (pos == this.selection.first) ? 0 : this.plotWidth;			   

+		}else{

+			pos.x = event.pageX - offset.left - this.plotOffset.left;

+			pos.x = Math.min(Math.max(0, pos.x), this.plotWidth);

+		}

+

+		if (options.selection.mode.indexOf('y') == -1){

+			pos.y = (pos == this.selection.first) ? 0 : this.plotHeight;

+		}else{

+			pos.y = event.pageY - offset.top - this.plotOffset.top;

+			pos.y = Math.min(Math.max(0, pos.y), this.plotHeight);

+		}

+	},

+	/**

+	 * Function: updateSelection

+	 * 

+	 * Updates (draws) the selection box.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	updateSelection: function(){

+		if(this.lastMousePos.pageX == null) return;

+		

+		this.setSelectionPos(this.selection.second, this.lastMousePos);

+		this.clearSelection();

+		

+		if(this.selectionIsSane()) this.drawSelection();

+	},

+	/**

+	 * Function: clearSelection

+	 * 

+	 * Removes the selection box from the overlay canvas.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clearSelection: function() {

+		if(this.prevSelection == null) return;

+			

+		var prevSelection = this.prevSelection,

+			octx = this.octx,

+			plotOffset = this.plotOffset,

+			x = Math.min(prevSelection.first.x, prevSelection.second.x),

+			y = Math.min(prevSelection.first.y, prevSelection.second.y),

+			w = Math.abs(prevSelection.second.x - prevSelection.first.x),

+			h = Math.abs(prevSelection.second.y - prevSelection.first.y);

+		

+		octx.clearRect(x + plotOffset.left - octx.lineWidth,

+		               y + plotOffset.top - octx.lineWidth,

+		               w + octx.lineWidth*2,

+		               h + octx.lineWidth*2);

+		

+		this.prevSelection = null;

+	},

+	/**

+	 * Function: setSelection

+	 * 

+	 * Allows the user the manually select an area.

+	 * 

+	 * Parameters:

+	 * 		area - Object with coordinates to select.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	setSelection: function(area){

+		var options = this.options,

+			xa = this.axes.x,

+			ya = this.axes.y,

+			vertScale = yaxis.scale,

+			hozScale = xaxis.scale,

+			selX = options.selection.mode.indexOf('x') != -1,

+			selY = options.selection.mode.indexOf('y') != -1;

+		

+		this.clearSelection();

+

+		this.selection.first.y  = selX ? 0 : (ya.max - area.y1) * vertScale;

+		this.selection.second.y = selX ? this.plotHeight : (ya.max - area.y2) * vertScale;			

+		this.selection.first.x  = selY ? 0 : (area.x1 - xa.min) * hozScale;

+		this.selection.second.x = selY ? this.plotWidth : (area.x2 - xa.min) * hozScale;

+		

+		this.drawSelection();

+		this.fireSelectEvent();

+	},

+	/**

+	 * Function: (private) drawSelection

+	 * 

+	 * Draws the selection box.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSelection: function() {

+		var prevSelection = this.prevSelection,

+			selection = this.selection,

+			octx = this.octx,

+			options = this.options,

+			plotOffset = this.plotOffset;

+		

+		if(prevSelection != null &&

+			selection.first.x == prevSelection.first.x &&

+			selection.first.y == prevSelection.first.y && 

+			selection.second.x == prevSelection.second.x &&

+			selection.second.y == prevSelection.second.y)

+			return;

+		

+		octx.strokeStyle = Flotr.parseColor(options.selection.color).scale(null, null, null, 0.8).toString();

+		octx.lineWidth = 1;

+		octx.lineJoin = 'round';

+		octx.fillStyle = Flotr.parseColor(options.selection.color).scale(null, null, null, 0.4).toString();

+

+		this.prevSelection = {

+			first: { x: selection.first.x, y: selection.first.y },

+			second: { x: selection.second.x, y: selection.second.y }

+		};

+

+		var x = Math.min(selection.first.x, selection.second.x),

+		    y = Math.min(selection.first.y, selection.second.y),

+		    w = Math.abs(selection.second.x - selection.first.x),

+		    h = Math.abs(selection.second.y - selection.first.y);

+		

+		octx.fillRect(x + plotOffset.left, y + plotOffset.top, w, h);

+		octx.strokeRect(x + plotOffset.left, y + plotOffset.top, w, h);

+	},

+	/**

+	 * Function: (private) selectionIsSane

+	 * 

+	 * Determines whether or not the selection is sane and should be drawn.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		boolean - True when sane, false otherwise.

+	 */

+	selectionIsSane: function(){

+		var selection = this.selection;

+		return Math.abs(selection.second.x - selection.first.x) >= 5 &&

+		       Math.abs(selection.second.y - selection.first.y) >= 5;

+	},

+	/**

+	 * Function: clearHit

+	 * 

+	 * Removes the mouse tracking point from the overlay.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clearHit: function(){

+		if(this.prevHit){

+			var options = this.options,

+			    plotOffset = this.plotOffset,

+			    prevHit = this.prevHit;

+					

+			this.octx.clearRect(

+				this.tHoz(prevHit.x) + plotOffset.left - options.points.radius*2,

+				this.tVert(prevHit.y) + plotOffset.top - options.points.radius*2,

+				options.points.radius*3 + options.points.lineWidth*3, 

+				options.points.radius*3 + options.points.lineWidth*3

+			);

+			this.prevHit = null;

+		}		

+	},

+	/**

+	 * Function: hit

+	 * 

+	 * Retrieves the nearest data point from the mouse cursor. If it's within

+	 * a certain range, draw a point on the overlay canvas and display the x and y

+	 * value of the data.

+	 * 

+	 * Parameters:

+	 * 		mouse - Object that holds the relative x and y coordinates of the cursor.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	hit: function(mouse){

+		var series = this.series,

+			options = this.options,

+			prevHit = this.prevHit,

+			plotOffset = this.plotOffset,

+			octx = this.octx, 

+			data, xsens, ysens,

+			/**

+			 * Nearest data element.

+			 */

+			i, n = {

+				dist:Number.MAX_VALUE,

+				x:null,

+				y:null,

+				relX:mouse.relX,

+				relY:mouse.relY,

+				absX:mouse.absX,

+				absY:mouse.absY,

+				mouse:null,

+				radarData:null

+			};

+		

+		for(i = 0; i < series.length; i++){

+			s = series[i];

+			if(!s.mouse.track) continue;

+			data = s.data;

+			xsens = (s.xaxis.scale*s.mouse.sensibility);

+			ysens = (s.yaxis.scale*s.mouse.sensibility);

+

+			for(var j = 0, xpow, ypow; j < data.length; j++){

+				if (data[j][1] === null) continue;

+				xpow = Math.pow(s.xaxis.scale*(data[j][0] - mouse.x), 2);

+				ypow = Math.pow(s.yaxis.scale*(data[j][1] - mouse.y), 2);

+				if(xpow < xsens && ypow < ysens && Math.sqrt(xpow+ypow) < n.dist){

+					n.dist = Math.sqrt(xpow+ypow);

+					n.x = data[j][0];

+					n.y = data[j][1];

+					n.radarLabel = data[j][2];

+					n.radarData = data[j][3];

+					n.mouse = s.mouse;

+				}

+			}

+		}

+		

+		if(n.mouse && n.mouse.track && !prevHit || (prevHit && (n.x != prevHit.x || n.y != prevHit.y))){

+			var mt = this.mouseTrack || this.el.select(".flotr-mouse-value")[0],

+			    pos = '', 

+			    p = options.mouse.position, 

+			    m = options.mouse.margin,

+			    elStyle = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;';

+

+			if (!options.mouse.relative) { // absolute to the canvas

+						 if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;';

+				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;';					

+				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;';

+				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;';

+			}

+			else { // relative to the mouse

+			       if(p.charAt(0) == 'n') pos += 'bottom:' + (m - plotOffset.top - this.tVert(n.y) + this.canvasHeight) + 'px;';

+				else if(p.charAt(0) == 's') pos += 'top:' + (m + plotOffset.top + this.tVert(n.y)) + 'px;';

+				     if(p.charAt(1) == 'e') pos += 'left:' + (m + plotOffset.left + this.tHoz(n.x)) + 'px;';

+				else if(p.charAt(1) == 'w') pos += 'right:' + (m - plotOffset.left - this.tHoz(n.x) + this.canvasWidth) + 'px;';

+			}

+			

+			elStyle += pos;

+				     

+			if(!mt){

+				this.el.insert('<div class="flotr-mouse-value" style="'+elStyle+'"></div>');

+				mt = this.mouseTrack = this.el.select('.flotr-mouse-value').first();

+			}

+			else {

+				this.mouseTrack = mt.setStyle(elStyle);

+			}

+			

+			if(n.x !== null && n.y !== null){

+				mt.show();

+				

+				this.clearHit();

+				if(n.mouse.lineColor != null){

+					octx.save();

+					octx.translate(plotOffset.left, plotOffset.top);

+					octx.lineWidth = options.points.lineWidth;

+					octx.strokeStyle = n.mouse.lineColor;

+					octx.fillStyle = '#ffffff';

+					octx.beginPath();

+					octx.arc(this.tHoz(n.x), this.tVert(n.y), options.mouse.radius, 0, 2 * Math.PI, true);

+					octx.fill();

+					octx.stroke();

+					octx.restore();

+				}

+				this.prevHit = n;

+				

+				var decimals = n.mouse.trackDecimals;

+				if(decimals == null || decimals < 0) decimals = 0;

+				

+				mt.innerHTML = n.mouse.trackFormatter({x: n.x.toFixed(decimals), y: n.y.toFixed(decimals), 

+												radarLabel: n.radarLabel, radarData: n.radarData.toFixed(decimals)});

+				mt.fire('flotr:hit', [n, this]);

+			}

+			else if(prevHit){

+				mt.hide();

+				this.clearHit();

+			}

+		}

+	},

+	saveImage: function (type, width, height, replaceCanvas) {

+		var image = null;

+	  switch (type) {

+	  	case 'jpeg':

+	    case 'jpg': image = Canvas2Image.saveAsJPEG(this.canvas, replaceCanvas, width, height); break;

+      default:

+      case 'png': image = Canvas2Image.saveAsPNG(this.canvas, replaceCanvas, width, height); break;

+      case 'bmp': image = Canvas2Image.saveAsBMP(this.canvas, replaceCanvas, width, height); break;

+	  }

+	  if (Object.isElement(image) && replaceCanvas) {

+	    this.restoreCanvas();

+	    this.canvas.hide();

+	    this.overlay.hide();

+	  	this.el.insert(image.setStyle({position: 'absolute'}));

+	  }

+	},

+	restoreCanvas: function() {

+    this.canvas.show();

+    this.overlay.show();

+    this.el.select('img').invoke('remove');

+	}

+});

+

+Flotr.Color = Class.create({

+	initialize: function(r, g, b, a){

+		this.rgba = ['r','g','b','a'];

+		var x = 4;

+		while(-1<--x){

+			this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);

+		}

+		this.normalize();

+	},

+	

+	adjust: function(rd, gd, bd, ad) {

+		var x = 4;

+		while(-1<--x){

+			if(arguments[x] != null)

+				this[this.rgba[x]] += arguments[x];

+		}

+		return this.normalize();

+	},

+	

+	clone: function(){

+		return new Flotr.Color(this.r, this.b, this.g, this.a);

+	},

+	

+	limit: function(val,minVal,maxVal){

+		return Math.max(Math.min(val, maxVal), minVal);

+	},

+	

+	normalize: function(){

+		var limit = this.limit;

+		this.r = limit(parseInt(this.r), 0, 255);

+		this.g = limit(parseInt(this.g), 0, 255);

+		this.b = limit(parseInt(this.b), 0, 255);

+		this.a = limit(this.a, 0, 1);

+		return this;

+	},

+	

+	scale: function(rf, gf, bf, af){

+		var x = 4;

+		while(-1<--x){

+			if(arguments[x] != null)

+				this[this.rgba[x]] *= arguments[x];

+		}

+		return this.normalize();

+	},

+	

+	distance: function(color){

+		if (!color) return;

+		color = new Flotr.parseColor(color);

+	  var dist = 0;

+		var x = 3;

+		while(-1<--x){

+			dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]);

+		}

+		return dist;

+	},

+	

+	toString: function(){

+		return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')';

+	}

+});

+

+Flotr.Color.lookupColors = {

+	aqua:[0,255,255],

+	azure:[240,255,255],

+	beige:[245,245,220],

+	black:[0,0,0],

+	blue:[0,0,255],

+	brown:[165,42,42],

+	cyan:[0,255,255],

+	darkblue:[0,0,139],

+	darkcyan:[0,139,139],

+	darkgrey:[169,169,169],

+	darkgreen:[0,100,0],

+	darkkhaki:[189,183,107],

+	darkmagenta:[139,0,139],

+	darkolivegreen:[85,107,47],

+	darkorange:[255,140,0],

+	darkorchid:[153,50,204],

+	darkred:[139,0,0],

+	darksalmon:[233,150,122],

+	darkviolet:[148,0,211],

+	fuchsia:[255,0,255],

+	gold:[255,215,0],

+	green:[0,128,0],

+	indigo:[75,0,130],

+	khaki:[240,230,140],

+	lightblue:[173,216,230],

+	lightcyan:[224,255,255],

+	lightgreen:[144,238,144],

+	lightgrey:[211,211,211],

+	lightpink:[255,182,193],

+	lightyellow:[255,255,224],

+	lime:[0,255,0],

+	magenta:[255,0,255],

+	maroon:[128,0,0],

+	navy:[0,0,128],

+	olive:[128,128,0],

+	orange:[255,165,0],

+	pink:[255,192,203],

+	purple:[128,0,128],

+	violet:[128,0,128],

+	red:[255,0,0],

+	silver:[192,192,192],

+	white:[255,255,255],

+	yellow:[255,255,0]

+};

+

+// not used yet

+Flotr.Date = {

+  format: function(d, format) {

+		if (!d) return;

+

+    var leftPad = function(n) {

+      n = n.toString();

+      return n.length == 1 ? "0" + n : n;

+    };

+    

+    var r = [];

+    var escape = false;

+    

+    for (var i = 0; i < format.length; ++i) {

+      var c = format.charAt(i);

+      

+      if (escape) {

+        switch (c) {

+	        case 'h': c = d.getUTCHours().toString(); break;

+	        case 'H': c = leftPad(d.getUTCHours()); break;

+	        case 'M': c = leftPad(d.getUTCMinutes()); break;

+	        case 'S': c = leftPad(d.getUTCSeconds()); break;

+	        case 'd': c = d.getUTCDate().toString(); break;

+	        case 'm': c = (d.getUTCMonth() + 1).toString(); break;

+	        case 'y': c = d.getUTCFullYear().toString(); break;

+	        case 'b': c = Flotr.Date.monthNames[d.getUTCMonth()]; break;

+        }

+        r.push(c);

+        escape = false;

+      }

+      else {

+        if (c == "%")

+          escape = true;

+        else

+          r.push(c);

+      }

+    }

+    return r.join("");

+  },

+  timeUnits: {

+    "second": 1000,

+    "minute": 60 * 1000,

+    "hour": 60 * 60 * 1000,

+    "day": 24 * 60 * 60 * 1000,

+    "month": 30 * 24 * 60 * 60 * 1000,

+    "year": 365.2425 * 24 * 60 * 60 * 1000

+  },

+  // the allowed tick sizes, after 1 year we use an integer algorithm

+  spec: [

+    [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"], 

+    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"], 

+    [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"],

+    [1, "day"], [2, "day"], [3, "day"],

+    [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"],

+    [1, "year"]

+  ],

+  monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

+};

 

--- /dev/null
+++ b/js/flotr/lib/base64.js
@@ -1,1 +1,113 @@
-
+/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>

+ * Version: 1.0

+ * LastModified: Dec 25 1999

+ * This library is free.  You can redistribute it and/or modify it.

+ */

+

+/*

+ * Interfaces:

+ * b64 = base64encode(data);

+ * data = base64decode(b64);

+ */

+

+(function() {

+

+var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

+var base64DecodeChars = [

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,

+    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,

+    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,

+    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,

+    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,

+    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];

+

+function base64encode(str) {

+    var out, i, len;

+    var c1, c2, c3;

+

+    len = str.length;

+    i = 0;

+    out = "";

+    while(i < len) {

+	c1 = str.charCodeAt(i++) & 0xff;

+	if(i == len)

+	{

+	    out += base64EncodeChars.charAt(c1 >> 2);

+	    out += base64EncodeChars.charAt((c1 & 0x3) << 4);

+	    out += "==";

+	    break;

+	}

+	c2 = str.charCodeAt(i++);

+	if(i == len)

+	{

+	    out += base64EncodeChars.charAt(c1 >> 2);

+	    out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));

+	    out += base64EncodeChars.charAt((c2 & 0xF) << 2);

+	    out += "=";

+	    break;

+	}

+	c3 = str.charCodeAt(i++);

+	out += base64EncodeChars.charAt(c1 >> 2);

+	out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));

+	out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6));

+	out += base64EncodeChars.charAt(c3 & 0x3F);

+    }

+    return out;

+}

+

+function base64decode(str) {

+    var c1, c2, c3, c4;

+    var i, len, out;

+

+    len = str.length;

+    i = 0;

+    out = "";

+    while(i < len) {

+	/* c1 */

+	do {

+	    c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];

+	} while(i < len && c1 == -1);

+	if(c1 == -1)

+	    break;

+

+	/* c2 */

+	do {

+	    c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];

+	} while(i < len && c2 == -1);

+	if(c2 == -1)

+	    break;

+

+	out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));

+

+	/* c3 */

+	do {

+	    c3 = str.charCodeAt(i++) & 0xff;

+	    if(c3 == 61)

+		return out;

+	    c3 = base64DecodeChars[c3];

+	} while(i < len && c3 == -1);

+	if(c3 == -1)

+	    break;

+

+	out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));

+

+	/* c4 */

+	do {

+	    c4 = str.charCodeAt(i++) & 0xff;

+	    if(c4 == 61)

+		return out;

+	    c4 = base64DecodeChars[c4];

+	} while(i < len && c4 == -1);

+	if(c4 == -1)

+	    break;

+	out += String.fromCharCode(((c3 & 0x03) << 6) | c4);

+    }

+    return out;

+}

+

+if (!window.btoa) window.btoa = base64encode;

+if (!window.atob) window.atob = base64decode;

+

+})();

--- /dev/null
+++ b/js/flotr/lib/canvas2image.js
@@ -1,1 +1,230 @@
-
+/*

+ * Canvas2Image v0.1

+ * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com

+ * MIT License [http://www.opensource.org/licenses/mit-license.php]

+ */

+

+var Canvas2Image = (function() {

+	// check if we have canvas support

+	var oCanvas = document.createElement("canvas");

+  

+	// no canvas, bail out.

+	if (!oCanvas.getContext) {

+		return {

+			saveAsBMP : function(){},

+			saveAsPNG : function(){},

+			saveAsJPEG : function(){}

+		}

+	}

+

+	var bHasImageData = !!(oCanvas.getContext("2d").getImageData);

+	var bHasDataURL = !!(oCanvas.toDataURL);

+	var bHasBase64 = !!(window.btoa);

+

+	var strDownloadMime = "image/octet-stream";

+

+	// ok, we're good

+	var readCanvasData = function(oCanvas) {

+		var iWidth = parseInt(oCanvas.width);

+		var iHeight = parseInt(oCanvas.height);

+		return oCanvas.getContext("2d").getImageData(0,0,iWidth,iHeight);

+	}

+

+	// base64 encodes either a string or an array of charcodes

+	var encodeData = function(data) {

+		var strData = "";

+		if (typeof data == "string") {

+			strData = data;

+		} else {

+			var aData = data;

+			for (var i = 0; i < aData.length; i++) {

+				strData += String.fromCharCode(aData[i]);

+			}

+		}

+		return btoa(strData);

+	}

+

+	// creates a base64 encoded string containing BMP data

+	// takes an imagedata object as argument

+	var createBMP = function(oData) {

+		var aHeader = [];

+	

+		var iWidth = oData.width;

+		var iHeight = oData.height;

+

+		aHeader.push(0x42); // magic 1

+		aHeader.push(0x4D); 

+	

+		var iFileSize = iWidth*iHeight*3 + 54; // total header size = 54 bytes

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256);

+

+		aHeader.push(0); // reserved

+		aHeader.push(0);

+		aHeader.push(0); // reserved

+		aHeader.push(0);

+

+		aHeader.push(54); // data offset

+		aHeader.push(0);

+		aHeader.push(0);

+		aHeader.push(0);

+

+		var aInfoHeader = [];

+		aInfoHeader.push(40); // info header size

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+

+		var iImageWidth = iWidth;

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256);

+	

+		var iImageHeight = iHeight;

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256);

+	

+		aInfoHeader.push(1); // num of planes

+		aInfoHeader.push(0);

+	

+		aInfoHeader.push(24); // num of bits per pixel

+		aInfoHeader.push(0);

+	

+		aInfoHeader.push(0); // compression = none

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+	

+		var iDataSize = iWidth*iHeight*3; 

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); 

+	

+		for (var i = 0; i < 16; i++) {

+			aInfoHeader.push(0);	// these bytes not used

+		}

+	

+		var iPadding = (4 - ((iWidth * 3) % 4)) % 4;

+

+		var aImgData = oData.data;

+

+		var strPixelData = "";

+		var y = iHeight;

+		do {

+			var iOffsetY = iWidth*(y-1)*4;

+			var strPixelRow = "";

+			for (var x=0;x<iWidth;x++) {

+				var iOffsetX = 4*x;

+

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+2]);

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+1]);

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX]);

+			}

+			for (var c=0;c<iPadding;c++) {

+				strPixelRow += String.fromCharCode(0);

+			}

+			strPixelData += strPixelRow;

+		} while (--y);

+

+		return encodeData(aHeader.concat(aInfoHeader)) + encodeData(strPixelData);

+	}

+

+	// sends the generated file to the client

+	var saveFile = function(strData) {

+    if (!window.open(strData)) {

+      document.location.href = strData;

+    }

+	}

+

+	var makeDataURI = function(strData, strMime) {

+		return "data:" + strMime + ";base64," + strData;

+	}

+

+	// generates a <img> object containing the imagedata

+	var makeImageObject = function(strSource) {

+		var oImgElement = document.createElement("img");

+		oImgElement.src = strSource;

+		return oImgElement;

+	}

+

+	var scaleCanvas = function(oCanvas, iWidth, iHeight) {

+		if (iWidth && iHeight) {

+			var oSaveCanvas = document.createElement("canvas");

+			

+			oSaveCanvas.width = iWidth;

+			oSaveCanvas.height = iHeight;

+			oSaveCanvas.style.width = iWidth+"px";

+			oSaveCanvas.style.height = iHeight+"px";

+

+			var oSaveCtx = oSaveCanvas.getContext("2d");

+

+			oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, iWidth);

+			

+			return oSaveCanvas;

+		}

+		return oCanvas;

+	}

+

+	return {

+		saveAsPNG : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!bHasDataURL) {

+				return false;

+			}

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+			var strData = oScaledCanvas.toDataURL("image/png");

+			if (bReturnImg) {

+				return makeImageObject(strData);

+			} else {

+				saveFile(strData.replace("image/png", strDownloadMime));

+			}

+			return true;

+		},

+

+		saveAsJPEG : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!bHasDataURL) {

+				return false;

+			}

+

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+			var strMime = "image/jpeg";

+			var strData = oScaledCanvas.toDataURL(strMime);

+	

+			// check if browser actually supports jpeg by looking for the mime type in the data uri.

+			// if not, return false

+			if (strData.indexOf(strMime) != 5) {

+				return false;

+			}

+

+			if (bReturnImg) {

+				return makeImageObject(strData);

+			} else {

+				saveFile(strData.replace(strMime, strDownloadMime));

+			}

+			return true;

+		},

+

+		saveAsBMP : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!(bHasImageData && bHasBase64)) {

+				return false;

+			}

+

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+

+			var oData = readCanvasData(oScaledCanvas);

+			var strImgData = createBMP(oData);

+			if (bReturnImg) {

+				return makeImageObject(makeDataURI(strImgData, "image/bmp"));

+			} else {

+				saveFile(makeDataURI(strImgData, strDownloadMime));

+			}

+			return true;

+		}

+	};

+

+})();

--- /dev/null
+++ b/js/flotr/lib/canvastext.js
@@ -1,1 +1,397 @@
-
+/**

+ * This code is released to the public domain by Jim Studt, 2007.

+ * He may keep some sort of up to date copy at http://www.federated.com/~jim/canvastext/


+ * A partial support for accentuated letters as been added too.

+ */

+var CanvasText = {

+	/** The letters definition. It is a list of letters, 

+	 * with their width, and the coordinates of points compositing them.

+	 * The syntax for the points is : [x, y], null value means "pen up"

+	 */

+  letters: {

+		'\n':{ width: -1, points: [] },

+    ' ': { width: 10, points: [] },

+    '!': { width: 10, points: [[5,21],[5,7],null,[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    '"': { width: 16, points: [[4,21],[4,14],null,[12,21],[12,14]] },

+    '#': { width: 21, points: [[11,25],[4,-7],null,[17,25],[10,-7],null,[4,12],[18,12],null,[3,6],[17,6]] },

+    '$': { width: 20, points: [[8,25],[8,-4],null,[12,25],[12,-4],null,[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },

+    '%': { width: 24, points: [[21,21],[3,0],null,[8,21],[10,19],[10,17],[9,15],[7,14],[5,14],[3,16],[3,18],[4,20],[6,21],[8,21],null,[17,7],[15,6],[14,4],[14,2],[16,0],[18,0],[20,1],[21,3],[21,5],[19,7],[17,7]] },

+    '&': { width: 26, points: [[23,12],[23,13],[22,14],[21,14],[20,13],[19,11],[17,6],[15,3],[13,1],[11,0],[7,0],[5,1],[4,2],[3,4],[3,6],[4,8],[5,9],[12,13],[13,14],[14,16],[14,18],[13,20],[11,21],[9,20],[8,18],[8,16],[9,13],[11,10],[16,3],[18,1],[20,0],[22,0],[23,1],[23,2]] },

+    '\'':{ width: 10, points: [[5,19],[4,20],[5,21],[6,20],[6,18],[5,16],[4,15]] },

+    '(': { width: 14, points: [[11,25],[9,23],[7,20],[5,16],[4,11],[4,7],[5,2],[7,-2],[9,-5],[11,-7]] },

+    ')': { width: 14, points: [[3,25],[5,23],[7,20],[9,16],[10,11],[10,7],[9,2],[7,-2],[5,-5],[3,-7]] },

+    '*': { width: 16, points: [[8,21],[8,9],null,[3,18],[13,12],null,[13,18],[3,12]] },

+    '+': { width: 26, points: [[13,18],[13,0],null,[4,9],[22,9]] },

+    ',': { width: 10, points: [[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },

+    '-': { width: 26, points: [[4,9],[22,9]] },

+    '.': { width: 10, points: [[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    '/': { width: 22, points: [[20,25],[2,-7]] },

+    '0': { width: 20, points: [[9,21],[6,20],[4,17],[3,12],[3,9],[4,4],[6,1],[9,0],[11,0],[14,1],[16,4],[17,9],[17,12],[16,17],[14,20],[11,21],[9,21]] },

+    '1': { width: 20, points: [[6,17],[8,18],[11,21],[11,0]] },

+    '2': { width: 20, points: [[4,16],[4,17],[5,19],[6,20],[8,21],[12,21],[14,20],[15,19],[16,17],[16,15],[15,13],[13,10],[3,0],[17,0]] },

+    '3': { width: 20, points: [[5,21],[16,21],[10,13],[13,13],[15,12],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },

+    '4': { width: 20, points: [[13,21],[3,7],[18,7],null,[13,21],[13,0]] },

+    '5': { width: 20, points: [[15,21],[5,21],[4,12],[5,13],[8,14],[11,14],[14,13],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },

+    '6': { width: 20, points: [[16,18],[15,20],[12,21],[10,21],[7,20],[5,17],[4,12],[4,7],[5,3],[7,1],[10,0],[11,0],[14,1],[16,3],[17,6],[17,7],[16,10],[14,12],[11,13],[10,13],[7,12],[5,10],[4,7]] },

+    '7': { width: 20, points: [[17,21],[7,0],null,[3,21],[17,21]] },

+    '8': { width: 20, points: [[8,21],[5,20],[4,18],[4,16],[5,14],[7,13],[11,12],[14,11],[16,9],[17,7],[17,4],[16,2],[15,1],[12,0],[8,0],[5,1],[4,2],[3,4],[3,7],[4,9],[6,11],[9,12],[13,13],[15,14],[16,16],[16,18],[15,20],[12,21],[8,21]] },

+    '9': { width: 20, points: [[16,14],[15,11],[13,9],[10,8],[9,8],[6,9],[4,11],[3,14],[3,15],[4,18],[6,20],[9,21],[10,21],[13,20],[15,18],[16,14],[16,9],[15,4],[13,1],[10,0],[8,0],[5,1],[4,3]] },

+    ':': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],null,[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    ';': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],null,[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },

+    '<': { width: 24, points: [[20,18],[4,9],[20,0]] },

+    '=': { width: 26, points: [[4,12],[22,12],null,[4,6],[22,6]] },

+    '>': { width: 24, points: [[4,18],[20,9],[4,0]] },

+    '?': { width: 18, points: [[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],null,[9,2],[8,1],[9,0],[10,1],[9,2]] },

+    '@': { width: 27, points: [[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],null,[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],null,[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],null,[19,16],[18,8],[18,6],[19,5]] },

+    'A': { width: 18, points: [[9,21],[1,0],null,[9,21],[17,0],null,[4,7],[14,7]] },

+    'B': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],null,[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]] },

+    'C': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]] },

+    'D': { width: 21, points: [[4,21],[4,0],null,[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]] },

+    'E': { width: 19, points: [[4,21],[4,0],null,[4,21],[17,21],null,[4,11],[12,11],null,[4,0],[17,0]] },

+    'F': { width: 18, points: [[4,21],[4,0],null,[4,21],[17,21],null,[4,11],[12,11]] },

+    'G': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],null,[13,8],[18,8]] },

+    'H': { width: 22, points: [[4,21],[4,0],null,[18,21],[18,0],null,[4,11],[18,11]] },

+    'I': { width: 8,  points: [[4,21],[4,0]] },

+    'J': { width: 16, points: [[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]] },

+    'K': { width: 21, points: [[4,21],[4,0],null,[18,21],[4,7],null,[9,12],[18,0]] },

+    'L': { width: 17, points: [[4,21],[4,0],null,[4,0],[16,0]] },

+    'M': { width: 24, points: [[4,21],[4,0],null,[4,21],[12,0],null,[20,21],[12,0],null,[20,21],[20,0]] },

+    'N': { width: 22, points: [[4,21],[4,0],null,[4,21],[18,0],null,[18,21],[18,0]] },

+    'O': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]] },

+    'P': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]] },

+    'Q': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],null,[12,4],[18,-2]] },

+    'R': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],null,[11,11],[18,0]] },

+    'S': { width: 20, points: [[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },

+    'T': { width: 16, points: [[8,21],[8,0],null,[1,21],[15,21]] },

+    'U': { width: 22, points: [[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]] },

+    'V': { width: 18, points: [[1,21],[9,0],null,[17,21],[9,0]] },

+    'W': { width: 24, points: [[2,21],[7,0],null,[12,21],[7,0],null,[12,21],[17,0],null,[22,21],[17,0]] },

+    'X': { width: 20, points: [[3,21],[17,0],null,[17,21],[3,0]] },

+    'Y': { width: 18, points: [[1,21],[9,11],[9,0],null,[17,21],[9,11]] },

+    'Z': { width: 20, points: [[17,21],[3,0],null,[3,21],[17,21],null,[3,0],[17,0]] },

+    '[': { width: 14, points: [[4,25],[4,-7],null,[5,25],[5,-7],null,[4,25],[11,25],null,[4,-7],[11,-7]] },

+    '\\':{ width: 14, points: [[0,21],[14,-3]] },

+    ']': { width: 14, points: [[9,25],[9,-7],null,[10,25],[10,-7],null,[3,25],[10,25],null,[3,-7],[10,-7]] },

+    '^': { width: 14, points: [[3,10],[8,18],[13,10]] },

+    '_': { width: 16, points: [[0,-2],[16,-2]] },

+    '`': { width: 10, points: [[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]] },

+    'a': { width: 19, points: [[15,14],[15,0],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'b': { width: 19, points: [[4,21],[4,0],null,[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },

+    'c': { width: 18, points: [[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'd': { width: 19, points: [[15,21],[15,0],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'e': { width: 18, points: [[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'f': { width: 12, points: [[10,21],[8,21],[6,20],[5,17],[5,0],null,[2,14],[9,14]] },

+    'g': { width: 19, points: [[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'h': { width: 19, points: [[4,21],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },

+    'i': { width: 8,  points: [[3,21],[4,20],[5,21],[4,22],[3,21],null,[4,14],[4,0]] },

+    'j': { width: 10, points: [[5,21],[6,20],[7,21],[6,22],[5,21],null,[6,14],[6,-3],[5,-6],[3,-7],[1,-7]] },

+    'k': { width: 17, points: [[4,21],[4,0],null,[14,14],[4,4],null,[8,8],[15,0]] },

+    'l': { width: 8,  points: [[4,21],[4,0]] },

+    'm': { width: 30, points: [[4,14],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],null,[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]] },

+    'n': { width: 19, points: [[4,14],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },

+    'o': { width: 19, points: [[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]] },

+    'p': { width: 19, points: [[4,14],[4,-7],null,[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },

+    'q': { width: 19, points: [[15,14],[15,-7],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'r': { width: 13, points: [[4,14],[4,0],null,[4,8],[5,11],[7,13],[9,14],[12,14]] },

+    's': { width: 17, points: [[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]] },

+    't': { width: 12, points: [[5,21],[5,4],[6,1],[8,0],[10,0],null,[2,14],[9,14]] },

+    'u': { width: 19, points: [[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],null,[15,14],[15,0]] },

+    'v': { width: 16, points: [[2,14],[8,0],null,[14,14],[8,0]] },

+    'w': { width: 22, points: [[3,14],[7,0],null,[11,14],[7,0],null,[11,14],[15,0],null,[19,14],[15,0]] },

+    'x': { width: 17, points: [[3,14],[14,0],null,[14,14],[3,0]] },

+    'y': { width: 16, points: [[2,14],[8,0],null,[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]] },

+    'z': { width: 17, points: [[14,14],[3,0],null,[3,14],[14,14],null,[3,0],[14,0]] },

+    '{': { width: 14, points: [[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],null,[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],null,[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]] },

+    '|': { width: 8,  points: [[4,25],[4,-7]] },

+    '}': { width: 14, points: [[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],null,[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],null,[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]] },

+    '~': { width: 24, points: [[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],null,[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]] },

















+  },

+  

+  specialchars: {

+  	'pi': { width: 19, points: [[6,14],[6,0],null,[14,14],[14,0],null,[2,13],[6,16],[13,13],[17,16]] }

+  },

+  

+  /** Diacritics, used to draw accentuated letters */

+  diacritics: {



+    '`': { entity: 'grave', points: [[7,22],[12,19]] },

+    '^': { entity: 'circ',  points: [[5.5,19],[9.5,23],[12.5,19]] },


+    '~': { entity: 'tilde', points: [[4,18],[7,22],[10,18],[13,22]] }

+  },

+  

+  /** The default font styling */

+  style: {

+    size: 8,          // font height in pixels

+    font: null,       // not yet implemented

+    color: '#000000', // 

+    weight: 1,        // float, 1 for 'normal'

+    halign: 'l',      // l: left, r: right, c: center

+    valign: 'b',      // t: top, m: middle, b: bottom 

+    adjustAlign: false, // modifies the alignments if the angle is different from 0 to make the spin point always at the good position

+    angle: 0,         // in radians, anticlockwise

+    tracking: 1,      // space between the letters, float, 1 for 'normal'

+    boundingBoxColor: '#ff0000', //null // color of the bounding box (null to hide), can be used for debug and font drawing

+    originPointColor: '#000000' //null // color of the bounding box (null to hide), can be used for debug and font drawing

+  },

+  

+  debug: false,

+  _bufferLexemes: {},

+  

+  /** Get the letter data corresponding to a char

+   * @param {String} ch - The char

+   */

+  letter: function(ch) {

+    return CanvasText.letters[ch];

+  },

+  

+  parseLexemes: function(str) {

+    if (CanvasText._bufferLexemes[str]) 

+      return CanvasText._bufferLexemes[str];

+    

+  	var i, c, matches = str.match(/&[A-Za-z]{2,5};|\s|./g);

+  	var result = [], chars = [];

+  	for (i = 0; i < matches.length; i++) {

+  		c = matches[i];

+  		if (c.length == 1) 

+  			chars.push(c);

+  		else {

+  			var entity = c.substring(1, c.length-1);

+  			if (CanvasText.specialchars[entity]) 

+  				chars.push(entity);

+  			else

+  				chars = chars.concat(c.toArray());

+  		}

+  	}

+  	for (i = 0; i < chars.length; i++) {

+  		c = chars[i];

+  		if (c = CanvasText.letters[c] || CanvasText.specialchars[c])

+  		  result.push(c);

+  	}

+  	return CanvasText._bufferLexemes[str] = result.compact();

+  },

+

+  /** Get the font ascent for a given style

+   * @param {Object} style - The reference style

+   */

+  ascent: function(style) {

+  	style = style || {};

+    return (style.size || CanvasText.style.size);

+  },

+  

+  /** Get the font descent for a given style 

+   * @param {Object} style - The reference style

+   * */

+  descent: function(style) {

+  	style = style || {};

+    return 7.0*(style.size || CanvasText.style.size)/25.0;

+  },

+  

+  /** Measure the text horizontal size 

+   * @param {String} str - The text

+   * @param {Object} style - Text style

+   * */

+  measure: function(str, style) {

+    if (!str) return;

+    style = style || {};

+    

+    var i, width, lexemes = CanvasText.parseLexemes(str),

+        total = 0;

+

+    for (i = lexemes.length-1; i > -1; --i) {

+    	c = lexemes[i];

+    	width = (c.diacritic) ? CanvasText.letter(c.letter).width : c.width;

+      total += width * (style.tracking || CanvasText.style.tracking) * (style.size || CanvasText.style.size) / 25.0;

+    }

+    return total;

+  },

+  

+  getDimensions: function(str, style) {

+    var width = CanvasText.measure(str, style),

+        height = style.size || CanvasText.style.size,

+        angle = style.angle || CanvasText.style.angle;

+

+    if (style.angle == 0) return {width: width, height: height};

+    return {

+      width:  Math.abs(Math.cos(angle) * width) + Math.abs(Math.sin(angle) * height),

+      height: Math.abs(Math.sin(angle) * width) + Math.abs(Math.cos(angle) * height)

+    }

+  },

+  

+  getBestAlign: function(angle, style) {

+    angle += CanvasText.getAngleFromAlign(style.halign, style.valign);

+    var a = {h:'c', v:'m'};

+    if (Math.round(Math.cos(angle)*1000)/1000 != 0) 

+      a.h = (Math.cos(angle) > 0 ? 'r' : 'l');

+    

+    if (Math.round(Math.sin(angle)*1000)/1000 != 0) 

+      a.v = (Math.sin(angle) > 0 ? 't' : 'b');

+    return a;

+  },

+  

+  getAngleFromAlign: function(halign, valign) {

+    var pi = Math.PI, table = {

+      'rm': 0,

+      'rt': pi/4,

+      'ct': pi/2,

+      'lt': 3*(pi/4),

+      'lm': pi,

+      'lb': -3*(pi/4),

+      'cb': -pi/2,

+      'rb': -pi/4,

+      'cm': 0

+    }

+    return table[halign+valign];

+  },

+  

+  /** Draws serie of points at given coordinates 

+   * @param {Canvas context} ctx - The canvas context

+   * @param {Array} points - The points to draw

+   * @param {Number} x - The X coordinate

+   * @param {Number} y - The Y coordinate

+   * @param {Number} mag - The scale 

+   */

+  drawPoints: function (ctx, points, x, y, mag, offset) {

+    var i, a, penUp = true, needStroke = 0;

+    offset = offset || {x:0, y:0};

+    

+    ctx.beginPath();

+    for (i = 0; i < points.length; i++) {

+      a = points[i];

+      if (!a) {

+        penUp = true;

+        continue;

+      }

+      if (penUp) {

+        ctx.moveTo(x + a[0]*mag + offset.x, y - a[1]*mag + offset.y);

+        penUp = false;

+      }

+      else {

+        ctx.lineTo(x + a[0]*mag + offset.x, y - a[1]*mag + offset.y);

+      }

+    }

+    ctx.stroke();

+  },

+  

+  /** Draws a text at given coordinates and with a given style

+   * @param {Canvas context} ctx - The canvas context

+   * @param {String} str - The text to draw

+   * @param {Number} xOrig - The X coordinate

+   * @param {Number} yOrig - The Y coordinate

+   * @param {Object} style - The font style

+   */

+  draw: function(ctx, str, xOrig, yOrig, style) {

+    if (!str) return;

+    style = style || CanvasText.style;

+    style.halign = style.halign || CanvasText.style.halign;

+    style.valign = style.valign || CanvasText.style.valign;

+    style.angle = style.angle || CanvasText.style.angle;

+    style.size = style.size || CanvasText.style.size;

+    style.adjustAlign = style.adjustAlign || CanvasText.style.adjustAlign;

+    

+    var i, c, total = 0,

+        mag = style.size / 25.0,

+        x = 0, y = 0,

+        lexemes = CanvasText.parseLexemes(str);

+    

+    var offset = {x:0, y:0}, 

+        measure = CanvasText.measure(str, style),

+        align;

+        

+    if (style.adjustAlign) {

+      align = CanvasText.getBestAlign(style.angle, style);

+      style.halign = align.h;

+      style.valign = align.v;

+    }

+        

+    switch (style.halign) {

+      case 'l': break;

+      case 'c': offset.x = -measure / 2; break;

+      case 'r': offset.x = -measure; break;

+    }

+    

+    switch (style.valign) {

+      case 'b': break;

+      case 'm': offset.y = style.size / 2; break;

+      case 't': offset.y = style.size; break;

+    }

+    

+    ctx.save();

+    ctx.translate(xOrig, yOrig);

+    ctx.rotate(style.angle);

+    ctx.lineCap = "round";

+    ctx.lineWidth = 2.0 * mag * (style.weight || CanvasText.style.weight);

+    ctx.strokeStyle = style.color || CanvasText.style.color;

+    

+    for (i = 0; i < lexemes.length; i++) {

+    	c = lexemes[i];

+      if (c.width == -1) {

+        x = 0;

+        y = style.size * 1.4;

+        continue;

+      }

+    

+      var points = c.points,

+          width = c.width;

+          

+      if (c.diacritic) {

+        var dia = CanvasText.diacritics[c.diacritic];

+        var char = CanvasText.letter(c.letter);

+

+        CanvasText.drawPoints(ctx, dia.points, x, y - (c.letter.toUpperCase() == c.letter ? 3 : 0), mag, offset);

+        points = char.points;

+        width = char.width;

+      }

+

+      CanvasText.drawPoints(ctx, points, x, y, mag, offset);

+      

+      if (CanvasText.debug) {

+      	ctx.save();

+        ctx.lineJoin = "miter";

+        ctx.lineWidth = 0.5;

+        ctx.strokeStyle = (style.boundingBoxColor || CanvasText.style.boundingBoxColor);

+      	ctx.strokeRect(x+offset.x, y+offset.y, width*mag, -style.size);

+        

+        ctx.fillStyle = (style.originPointColor || CanvasText.style.originPointColor);

+        ctx.beginPath();

+        ctx.arc(0, 0, 1.5, 0, Math.PI*2, true);

+        ctx.fill();

+        

+      	ctx.restore();

+      }

+      

+      x += width*mag*(style.tracking || CanvasText.style.tracking);

+    }

+    ctx.restore();

+    return total;

+  },

+  

+  /** Enables the text function for a Canvas context

+   * @param {Canvas context} ctx - The canvas context

+   */

+  enable: function(ctx) {

+    ctx.drawText    = function(text, x, y, style) { return CanvasText.draw(ctx, text, x, y, style); };

+    ctx.measureText = function(text, style) { return CanvasText.measure(text, style); };

+    ctx.getTextBounds = function(text, style) { return CanvasText.getDimensions(text, style); };

+    ctx.fontAscent  = function(style) { return CanvasText.ascent(style); };

+    ctx.fontDescent = function(style) { return CanvasText.descent(style); };

+  }

+};

--- /dev/null
+++ b/js/flotr/lib/excanvas.js
@@ -1,1 +1,885 @@
-
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+//   different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+//   width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+//   Quirks mode will draw the canvas using border-box. Either change your
+//   doctype to HTML5
+//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+//   or use Box Sizing Behavior from WebFX
+//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+  // alias some functions to make (compiled) code shorter
+  var m = Math;
+  var mr = m.round;
+  var ms = m.sin;
+  var mc = m.cos;
+  var abs = m.abs;
+  var sqrt = m.sqrt;
+
+  // this is used for sub pixel precision
+  var Z = 10;
+  var Z2 = Z / 2;
+
+  /**
+   * This funtion is assigned to the <canvas> elements as element.getContext().
+   * @this {HTMLElement}
+   * @return {CanvasRenderingContext2D_}
+   */
+  function getContext() {
+    return this.context_ ||
+        (this.context_ = new CanvasRenderingContext2D_(this));
+  }
+
+  var slice = Array.prototype.slice;
+
+  /**
+   * Binds a function to an object. The returned function will always use the
+   * passed in {@code obj} as {@code this}.
+   *
+   * Example:
+   *
+   *   g = bind(f, obj, a, b)
+   *   g(c, d) // will do f.call(obj, a, b, c, d)
+   *
+   * @param {Function} f The function to bind the object to
+   * @param {Object} obj The object that should act as this when the function
+   *     is called
+   * @param {*} var_args Rest arguments that will be used as the initial
+   *     arguments when the function is called
+   * @return {Function} A new function that has bound this
+   */
+  function bind(f, obj, var_args) {
+    var a = slice.call(arguments, 2);
+    return function() {
+      return f.apply(obj, a.concat(slice.call(arguments)));
+    };
+  }
+
+  var G_vmlCanvasManager_ = {
+    init: function(opt_doc) {
+      if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+        var doc = opt_doc || document;
+        // Create a dummy element so that IE will allow canvas elements to be
+        // recognized.
+        doc.createElement('canvas');
+        doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+      }
+    },
+
+    init_: function(doc) {
+      // create xmlns
+      if (!doc.namespaces['g_vml_']) {
+        doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+                           '#default#VML');
+
+      }
+      if (!doc.namespaces['g_o_']) {
+        doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+                           '#default#VML');
+      }
+
+      // Setup default CSS.  Only add one style sheet per document
+      if (!doc.styleSheets['ex_canvas_']) {
+        var ss = doc.createStyleSheet();
+        ss.owningElement.id = 'ex_canvas_';
+        ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+            // default size is 300x150 in Gecko and Opera
+            'text-align:left;width:300px;height:150px}' +
+            'g_vml_\\:*{behavior:url(#default#VML)}' +
+            'g_o_\\:*{behavior:url(#default#VML)}';
+
+      }
+
+      // find all canvas elements
+      var els = doc.getElementsByTagName('canvas');
+      for (var i = 0; i < els.length; i++) {
+        this.initElement(els[i]);
+      }
+    },
+
+    /**
+     * Public initializes a canvas element so that it can be used as canvas
+     * element from now on. This is called automatically before the page is
+     * loaded but if you are creating elements using createElement you need to
+     * make sure this is called on the element.
+     * @param {HTMLElement} el The canvas element to initialize.
+     * @return {HTMLElement} the element that was created.
+     */
+    initElement: function(el) {
+      if (!el.getContext) {
+
+        el.getContext = getContext;
+
+        // Remove fallback content. There is no way to hide text nodes so we
+        // just remove all childNodes. We could hide all elements and remove
+        // text nodes but who really cares about the fallback content.
+        el.innerHTML = '';
+
+        // do not use inline function because that will leak memory
+        el.attachEvent('onpropertychange', onPropertyChange);
+        el.attachEvent('onresize', onResize);
+
+        var attrs = el.attributes;
+        if (attrs.width && attrs.width.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setWidth_(attrs.width.nodeValue);
+          el.style.width = attrs.width.nodeValue + 'px';
+        } else {
+          el.width = el.clientWidth;
+        }
+        if (attrs.height && attrs.height.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setHeight_(attrs.height.nodeValue);
+          el.style.height = attrs.height.nodeValue + 'px';
+        } else {
+          el.height = el.clientHeight;
+        }
+        //el.getContext().setCoordsize_()
+      }
+      return el;
+    }
+  };
+
+  function onPropertyChange(e) {
+    var el = e.srcElement;
+
+    switch (e.propertyName) {
+      case 'width':
+        el.style.width = el.attributes.width.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+      case 'height':
+        el.style.height = el.attributes.height.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+    }
+  }
+
+  function onResize(e) {
+    var el = e.srcElement;
+    if (el.firstChild) {
+      el.firstChild.style.width =  el.clientWidth + 'px';
+      el.firstChild.style.height = el.clientHeight + 'px';
+    }
+  }
+
+  G_vmlCanvasManager_.init();
+
+  // precompute "00" to "FF"
+  var dec2hex = [];
+  for (var i = 0; i < 16; i++) {
+    for (var j = 0; j < 16; j++) {
+      dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+    }
+  }
+
+  function createMatrixIdentity() {
+    return [
+      [1, 0, 0],
+      [0, 1, 0],
+      [0, 0, 1]
+    ];
+  }
+
+  function matrixMultiply(m1, m2) {
+    var result = createMatrixIdentity();
+
+    for (var x = 0; x < 3; x++) {
+      for (var y = 0; y < 3; y++) {
+        var sum = 0;
+
+        for (var z = 0; z < 3; z++) {
+          sum += m1[x][z] * m2[z][y];
+        }
+
+        result[x][y] = sum;
+      }
+    }
+    return result;
+  }
+
+  function copyState(o1, o2) {
+    o2.fillStyle     = o1.fillStyle;
+    o2.lineCap       = o1.lineCap;
+    o2.lineJoin      = o1.lineJoin;
+    o2.lineWidth     = o1.lineWidth;
+    o2.miterLimit    = o1.miterLimit;
+    o2.shadowBlur    = o1.shadowBlur;
+    o2.shadowColor   = o1.shadowColor;
+    o2.shadowOffsetX = o1.shadowOffsetX;
+    o2.shadowOffsetY = o1.shadowOffsetY;
+    o2.strokeStyle   = o1.strokeStyle;
+    o2.globalAlpha   = o1.globalAlpha;
+    o2.arcScaleX_    = o1.arcScaleX_;
+    o2.arcScaleY_    = o1.arcScaleY_;
+    o2.lineScale_    = o1.lineScale_;
+  }
+
+  function processStyle(styleString) {
+    var str, alpha = 1;
+
+    styleString = String(styleString);
+    if (styleString.substring(0, 3) == 'rgb') {
+      var start = styleString.indexOf('(', 3);
+      var end = styleString.indexOf(')', start + 1);
+      var guts = styleString.substring(start + 1, end).split(',');
+
+      str = '#';
+      for (var i = 0; i < 3; i++) {
+        str += dec2hex[Number(guts[i])];
+      }
+
+      if (guts.length == 4 && styleString.substr(3, 1) == 'a') {
+        alpha = guts[3];
+      }
+    } else {
+      str = styleString;
+    }
+
+    return {color: str, alpha: alpha};
+  }
+
+  function processLineCap(lineCap) {
+    switch (lineCap) {
+      case 'butt':
+        return 'flat';
+      case 'round':
+        return 'round';
+      case 'square':
+      default:
+        return 'square';
+    }
+  }
+
+  /**
+   * This class implements CanvasRenderingContext2D interface as described by
+   * the WHATWG.
+   * @param {HTMLElement} surfaceElement The element that the 2D context should
+   * be associated with
+   */
+  function CanvasRenderingContext2D_(surfaceElement) {
+    this.m_ = createMatrixIdentity();
+
+    this.mStack_ = [];
+    this.aStack_ = [];
+    this.currentPath_ = [];
+
+    // Canvas context properties
+    this.strokeStyle = '#000';
+    this.fillStyle = '#000';
+
+    this.lineWidth = 1;
+    this.lineJoin = 'miter';
+    this.lineCap = 'butt';
+    this.miterLimit = Z * 1;
+    this.globalAlpha = 1;
+    this.canvas = surfaceElement;
+
+    var el = surfaceElement.ownerDocument.createElement('div');
+    el.style.width =  surfaceElement.clientWidth + 'px';
+    el.style.height = surfaceElement.clientHeight + 'px';
+    el.style.overflow = 'hidden';
+    el.style.position = 'absolute';
+    surfaceElement.appendChild(el);
+
+    this.element_ = el;
+    this.arcScaleX_ = 1;
+    this.arcScaleY_ = 1;
+    this.lineScale_ = 1;
+  }
+
+  var contextPrototype = CanvasRenderingContext2D_.prototype;
+  contextPrototype.clearRect = function() {
+    this.element_.innerHTML = '';
+  };
+
+  contextPrototype.beginPath = function() {
+    // TODO: Branch current matrix so that save/restore has no effect
+    //       as per safari docs.
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.moveTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.lineTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
+
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+                                            aCP2x, aCP2y,
+                                            aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    var cp1 = this.getCoords_(aCP1x, aCP1y);
+    var cp2 = this.getCoords_(aCP2x, aCP2y);
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  // Helper function that takes the already fixed cordinates.
+  function bezierCurveTo(self, cp1, cp2, p) {
+    self.currentPath_.push({
+      type: 'bezierCurveTo',
+      cp1x: cp1.x,
+      cp1y: cp1.y,
+      cp2x: cp2.x,
+      cp2y: cp2.y,
+      x: p.x,
+      y: p.y
+    });
+    self.currentX_ = p.x;
+    self.currentY_ = p.y;
+  }
+
+  contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+    // the following is lifted almost directly from
+    // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+
+    var cp = this.getCoords_(aCPx, aCPy);
+    var p = this.getCoords_(aX, aY);
+
+    var cp1 = {
+      x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
+      y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
+    };
+    var cp2 = {
+      x: cp1.x + (p.x - this.currentX_) / 3.0,
+      y: cp1.y + (p.y - this.currentY_) / 3.0
+    };
+
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  contextPrototype.arc = function(aX, aY, aRadius,
+                                  aStartAngle, aEndAngle, aClockwise) {
+    aRadius *= Z;
+    var arcType = aClockwise ? 'at' : 'wa';
+
+    var xStart = aX + mc(aStartAngle) * aRadius - Z2;
+    var yStart = aY + ms(aStartAngle) * aRadius - Z2;
+
+    var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
+    var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
+
+    // IE won't render arches drawn counter clockwise if xStart == xEnd.
+    if (xStart == xEnd && !aClockwise) {
+      xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+                       // that can be represented in binary
+    }
+
+    var p = this.getCoords_(aX, aY);
+    var pStart = this.getCoords_(xStart, yStart);
+    var pEnd = this.getCoords_(xEnd, yEnd);
+
+    this.currentPath_.push({type: arcType,
+                           x: p.x,
+                           y: p.y,
+                           radius: aRadius,
+                           xStart: pStart.x,
+                           yStart: pStart.y,
+                           xEnd: pEnd.x,
+                           yEnd: pEnd.y});
+
+  };
+
+  contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+  };
+
+  contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+    var oldPath = this.currentPath_;
+    this.beginPath();
+
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.stroke();
+
+    this.currentPath_ = oldPath;
+  };
+
+  contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+    var oldPath = this.currentPath_;
+    this.beginPath();
+
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.fill();
+
+    this.currentPath_ = oldPath;
+  };
+
+  contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+    var gradient = new CanvasGradient_('gradient');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    return gradient;
+  };
+
+  contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
+                                                   aX1, aY1, aR1) {
+    var gradient = new CanvasGradient_('gradientradial');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.r0_ = aR0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    gradient.r1_ = aR1;
+    return gradient;
+  };
+
+  contextPrototype.drawImage = function(image, var_args) {
+    var dx, dy, dw, dh, sx, sy, sw, sh;
+
+    // to find the original width we overide the width and height
+    var oldRuntimeWidth = image.runtimeStyle.width;
+    var oldRuntimeHeight = image.runtimeStyle.height;
+    image.runtimeStyle.width = 'auto';
+    image.runtimeStyle.height = 'auto';
+
+    // get the original size
+    var w = image.width;
+    var h = image.height;
+
+    // and remove overides
+    image.runtimeStyle.width = oldRuntimeWidth;
+    image.runtimeStyle.height = oldRuntimeHeight;
+
+    if (arguments.length == 3) {
+      dx = arguments[1];
+      dy = arguments[2];
+      sx = sy = 0;
+      sw = dw = w;
+      sh = dh = h;
+    } else if (arguments.length == 5) {
+      dx = arguments[1];
+      dy = arguments[2];
+      dw = arguments[3];
+      dh = arguments[4];
+      sx = sy = 0;
+      sw = w;
+      sh = h;
+    } else if (arguments.length == 9) {
+      sx = arguments[1];
+      sy = arguments[2];
+      sw = arguments[3];
+      sh = arguments[4];
+      dx = arguments[5];
+      dy = arguments[6];
+      dw = arguments[7];
+      dh = arguments[8];
+    } else {
+      throw Error('Invalid number of arguments');
+    }
+
+    var d = this.getCoords_(dx, dy);
+
+    var w2 = sw / 2;
+    var h2 = sh / 2;
+
+    var vmlStr = [];
+
+    var W = 10;
+    var H = 10;
+
+    // For some reason that I've now forgotten, using divs didn't work
+    vmlStr.push(' <g_vml_:group',
+                ' coordsize="', Z * W, ',', Z * H, '"',
+                ' coordorigin="0,0"' ,
+                ' style="width:', W, 'px;height:', H, 'px;position:absolute;');
+
+    // If filters are necessary (rotation exists), create them
+    // filters are bog-slow, so only create them if abbsolutely necessary
+    // The following check doesn't account for skews (which don't exist
+    // in the canvas spec (yet) anyway.
+
+    if (this.m_[0][0] != 1 || this.m_[0][1]) {
+      var filter = [];
+
+      // Note the 12/21 reversal
+      filter.push('M11=', this.m_[0][0], ',',
+                  'M12=', this.m_[1][0], ',',
+                  'M21=', this.m_[0][1], ',',
+                  'M22=', this.m_[1][1], ',',
+                  'Dx=', mr(d.x / Z), ',',
+                  'Dy=', mr(d.y / Z), '');
+
+      // Bounding box calculation (need to minimize displayed area so that
+      // filters don't waste time on unused pixels.
+      var max = d;
+      var c2 = this.getCoords_(dx + dw, dy);
+      var c3 = this.getCoords_(dx, dy + dh);
+      var c4 = this.getCoords_(dx + dw, dy + dh);
+
+      max.x = m.max(max.x, c2.x, c3.x, c4.x);
+      max.y = m.max(max.y, c2.y, c3.y, c4.y);
+
+      vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
+                  'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
+                  filter.join(''), ", sizingmethod='clip');")
+    } else {
+      vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
+    }
+
+    vmlStr.push(' ">' ,
+                '<g_vml_:image src="', image.src, '"',
+                ' style="width:', Z * dw, 'px;',
+                ' height:', Z * dh, 'px;"',
+                ' cropleft="', sx / w, '"',
+                ' croptop="', sy / h, '"',
+                ' cropright="', (w - sx - sw) / w, '"',
+                ' cropbottom="', (h - sy - sh) / h, '"',
+                ' />',
+                '</g_vml_:group>');
+
+    this.element_.insertAdjacentHTML('BeforeEnd',
+                                    vmlStr.join(''));
+  };
+
+  contextPrototype.stroke = function(aFill) {
+    var lineStr = [];
+    var lineOpen = false;
+    var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+    var color = a.color;
+    var opacity = a.alpha * this.globalAlpha;
+
+    var W = 10;
+    var H = 10;
+
+    lineStr.push('<g_vml_:shape',
+                 ' filled="', !!aFill, '"',
+                 ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
+                 ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
+                 ' stroked="', !aFill, '"',
+                 ' path="');
+
+    var newSeq = false;
+    var min = {x: null, y: null};
+    var max = {x: null, y: null};
+
+    for (var i = 0; i < this.currentPath_.length; i++) {
+      var p = this.currentPath_[i];
+      var c;
+
+      switch (p.type) {
+        case 'moveTo':
+          c = p;
+          lineStr.push(' m ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'lineTo':
+          lineStr.push(' l ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'close':
+          lineStr.push(' x ');
+          p = null;
+          break;
+        case 'bezierCurveTo':
+          lineStr.push(' c ',
+                       mr(p.cp1x), ',', mr(p.cp1y), ',',
+                       mr(p.cp2x), ',', mr(p.cp2y), ',',
+                       mr(p.x), ',', mr(p.y));
+          break;
+        case 'at':
+        case 'wa':
+          lineStr.push(' ', p.type, ' ',
+                       mr(p.x - this.arcScaleX_ * p.radius), ',',
+                       mr(p.y - this.arcScaleY_ * p.radius), ' ',
+                       mr(p.x + this.arcScaleX_ * p.radius), ',',
+                       mr(p.y + this.arcScaleY_ * p.radius), ' ',
+                       mr(p.xStart), ',', mr(p.yStart), ' ',
+                       mr(p.xEnd), ',', mr(p.yEnd));
+          break;
+      }
+
+
+      // TODO: Following is broken for curves due to
+      //       move to proper paths.
+
+      // Figure out dimensions so we can do gradient fills
+      // properly
+      if (p) {
+        if (min.x == null || p.x < min.x) {
+          min.x = p.x;
+        }
+        if (max.x == null || p.x > max.x) {
+          max.x = p.x;
+        }
+        if (min.y == null || p.y < min.y) {
+          min.y = p.y;
+        }
+        if (max.y == null || p.y > max.y) {
+          max.y = p.y;
+        }
+      }
+    }
+    lineStr.push(' ">');
+
+    if (!aFill) {
+      var lineWidth = this.lineScale_ * this.lineWidth;
+
+      // VML cannot correctly render a line if the width is less than 1px.
+      // In that case, we dilute the color to make the line look thinner.
+      if (lineWidth < 1) {
+        opacity *= lineWidth;
+      }
+
+      lineStr.push(
+        '<g_vml_:stroke',
+        ' opacity="', opacity, '"',
+        ' joinstyle="', this.lineJoin, '"',
+        ' miterlimit="', this.miterLimit, '"',
+        ' endcap="', processLineCap(this.lineCap), '"',
+        ' weight="', lineWidth, 'px"',
+        ' color="', color, '" />'
+      );
+    } else if (typeof this.fillStyle == 'object') {
+      var fillStyle = this.fillStyle;
+      var angle = 0;
+      var focus = {x: 0, y: 0};
+
+      // additional offset
+      var shift = 0;
+      // scale factor for offset
+      var expansion = 1;
+
+      if (fillStyle.type_ == 'gradient') {
+        var x0 = fillStyle.x0_ / this.arcScaleX_;
+        var y0 = fillStyle.y0_ / this.arcScaleY_;
+        var x1 = fillStyle.x1_ / this.arcScaleX_;
+        var y1 = fillStyle.y1_ / this.arcScaleY_;
+        var p0 = this.getCoords_(x0, y0);
+        var p1 = this.getCoords_(x1, y1);
+        var dx = p1.x - p0.x;
+        var dy = p1.y - p0.y;
+        angle = Math.atan2(dx, dy) * 180 / Math.PI;
+
+        // The angle should be a non-negative number.
+        if (angle < 0) {
+          angle += 360;
+        }
+
+        // Very small angles produce an unexpected result because they are
+        // converted to a scientific notation string.
+        if (angle < 1e-6) {
+          angle = 0;
+        }
+      } else {
+        var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_);
+        var width  = max.x - min.x;
+        var height = max.y - min.y;
+        focus = {
+          x: (p0.x - min.x) / width,
+          y: (p0.y - min.y) / height
+        };
+
+        width  /= this.arcScaleX_ * Z;
+        height /= this.arcScaleY_ * Z;
+        var dimension = m.max(width, height);
+        shift = 2 * fillStyle.r0_ / dimension;
+        expansion = 2 * fillStyle.r1_ / dimension - shift;
+      }
+
+      // We need to sort the color stops in ascending order by offset,
+      // otherwise IE won't interpret it correctly.
+      var stops = fillStyle.colors_;
+      stops.sort(function(cs1, cs2) {
+        return cs1.offset - cs2.offset;
+      });
+
+      var length = stops.length;
+      var color1 = stops[0].color;
+      var color2 = stops[length - 1].color;
+      var opacity1 = stops[0].alpha * this.globalAlpha;
+      var opacity2 = stops[length - 1].alpha * this.globalAlpha;
+
+      var colors = [];
+      for (var i = 0; i < length; i++) {
+        var stop = stops[i];
+        colors.push(stop.offset * expansion + shift + ' ' + stop.color);
+      }
+
+      // When colors attribute is used, the meanings of opacity and o:opacity2
+      // are reversed.
+      lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
+                   ' method="none" focus="100%"',
+                   ' color="', color1, '"',
+                   ' color2="', color2, '"',
+                   ' colors="', colors.join(','), '"',
+                   ' opacity="', opacity2, '"',
+                   ' g_o_:opacity2="', opacity1, '"',
+                   ' angle="', angle, '"',
+                   ' focusposition="', focus.x, ',', focus.y, '" />');
+    } else {
+      lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
+                   '" />');
+    }
+
+    lineStr.push('</g_vml_:shape>');
+
+    this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+  };
+
+  contextPrototype.fill = function() {
+    this.stroke(true);
+  }
+
+  contextPrototype.closePath = function() {
+    this.currentPath_.push({type: 'close'});
+  };
+
+  /**
+   * @private
+   */
+  contextPrototype.getCoords_ = function(aX, aY) {
+    var m = this.m_;
+    return {
+      x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
+      y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
+    }
+  };
+
+  contextPrototype.save = function() {
+    var o = {};
+    copyState(this, o);
+    this.aStack_.push(o);
+    this.mStack_.push(this.m_);
+    this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+  };
+
+  contextPrototype.restore = function() {
+    copyState(this.aStack_.pop(), this);
+    this.m_ = this.mStack_.pop();
+  };
+
+  contextPrototype.translate = function(aX, aY) {
+    var m1 = [
+      [1,  0,  0],
+      [0,  1,  0],
+      [aX, aY, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.rotate = function(aRot) {
+    var c = mc(aRot);
+    var s = ms(aRot);
+
+    var m1 = [
+      [c,  s, 0],
+      [-s, c, 0],
+      [0,  0, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.scale = function(aX, aY) {
+    this.arcScaleX_ *= aX;
+    this.arcScaleY_ *= aY;
+    var m1 = [
+      [aX, 0,  0],
+      [0,  aY, 0],
+      [0,  0,  1]
+    ];
+
+    var m = this.m_ = matrixMultiply(m1, this.m_);
+
+    // Get the line scale.
+    // Determinant of this.m_ means how much the area is enlarged by the
+    // transformation. So its square root can be used as a scale factor
+    // for width.
+    var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
+    this.lineScale_ = sqrt(abs(det));
+  };
+
+  /******** STUBS ********/
+  contextPrototype.clip = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.arcTo = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.createPattern = function() {
+    return new CanvasPattern_;
+  };
+
+  // Gradient / Pattern Stubs
+  function CanvasGradient_(aType) {
+    this.type_ = aType;
+    this.x0_ = 0;
+    this.y0_ = 0;
+    this.r0_ = 0;
+    this.x1_ = 0;
+    this.y1_ = 0;
+    this.r1_ = 0;
+    this.colors_ = [];
+  }
+
+  CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+    aColor = processStyle(aColor);
+    this.colors_.push({offset: aOffset,
+                       color: aColor.color,
+                       alpha: aColor.alpha});
+  };
+
+  function CanvasPattern_() {}
+
+  // set up externs
+  G_vmlCanvasManager = G_vmlCanvasManager_;
+  CanvasRenderingContext2D = CanvasRenderingContext2D_;
+  CanvasGradient = CanvasGradient_;
+  CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
+

--- /dev/null
+++ b/js/flotr/lib/prototype-1.6.0.2.js
@@ -1,1 +1,4221 @@
-
+/*  Prototype JavaScript framework, version 1.6.0.2

+ *  (c) 2005-2008 Sam Stephenson

+ *

+ *  Prototype is freely distributable under the terms of an MIT-style license.

+ *  For details, see the Prototype web site: http://www.prototypejs.org/

+ *

+ *--------------------------------------------------------------------------*/

+

+var Prototype = {

+  Version: '1.6.0.2',

+

+  Browser: {

+    IE:     !!(window.attachEvent && !window.opera),

+    Opera:  !!window.opera,

+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,

+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,

+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)

+  },

+

+  BrowserFeatures: {

+    XPath: !!document.evaluate,

+    ElementExtensions: !!window.HTMLElement,

+    SpecificElementExtensions:

+      document.createElement('div').__proto__ &&

+      document.createElement('div').__proto__ !==

+        document.createElement('form').__proto__

+  },

+

+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',

+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

+

+  emptyFunction: function() { },

+  K: function(x) { return x }

+};

+

+if (Prototype.Browser.MobileSafari)

+  Prototype.BrowserFeatures.SpecificElementExtensions = false;

+

+

+/* Based on Alex Arnell's inheritance implementation. */

+var Class = {

+  create: function() {

+    var parent = null, properties = $A(arguments);

+    if (Object.isFunction(properties[0]))

+      parent = properties.shift();

+

+    function klass() {

+      this.initialize.apply(this, arguments);

+    }

+

+    Object.extend(klass, Class.Methods);

+    klass.superclass = parent;

+    klass.subclasses = [];

+

+    if (parent) {

+      var subclass = function() { };

+      subclass.prototype = parent.prototype;

+      klass.prototype = new subclass;

+      parent.subclasses.push(klass);

+    }

+

+    for (var i = 0; i < properties.length; i++)

+      klass.addMethods(properties[i]);

+

+    if (!klass.prototype.initialize)

+      klass.prototype.initialize = Prototype.emptyFunction;

+

+    klass.prototype.constructor = klass;

+

+    return klass;

+  }

+};

+

+Class.Methods = {

+  addMethods: function(source) {

+    var ancestor   = this.superclass && this.superclass.prototype;

+    var properties = Object.keys(source);

+

+    if (!Object.keys({ toString: true }).length)

+      properties.push("toString", "valueOf");

+

+    for (var i = 0, length = properties.length; i < length; i++) {

+      var property = properties[i], value = source[property];

+      if (ancestor && Object.isFunction(value) &&

+          value.argumentNames().first() == "$super") {

+        var method = value, value = Object.extend((function(m) {

+          return function() { return ancestor[m].apply(this, arguments) };

+        })(property).wrap(method), {

+          valueOf:  function() { return method },

+          toString: function() { return method.toString() }

+        });

+      }

+      this.prototype[property] = value;

+    }

+

+    return this;

+  }

+};

+

+var Abstract = { };

+

+Object.extend = function(destination, source) {

+  for (var property in source)

+    destination[property] = source[property];

+  return destination;

+};

+

+Object.extend(Object, {

+  inspect: function(object) {

+    try {

+      if (Object.isUndefined(object)) return 'undefined';

+      if (object === null) return 'null';

+      return object.inspect ? object.inspect() : String(object);

+    } catch (e) {

+      if (e instanceof RangeError) return '...';

+      throw e;

+    }

+  },

+

+  toJSON: function(object) {

+    var type = typeof object;

+    switch (type) {

+      case 'undefined':

+      case 'function':

+      case 'unknown': return;

+      case 'boolean': return object.toString();

+    }

+

+    if (object === null) return 'null';

+    if (object.toJSON) return object.toJSON();

+    if (Object.isElement(object)) return;

+

+    var results = [];

+    for (var property in object) {

+      var value = Object.toJSON(object[property]);

+      if (!Object.isUndefined(value))

+        results.push(property.toJSON() + ': ' + value);

+    }

+

+    return '{' + results.join(', ') + '}';

+  },

+

+  toQueryString: function(object) {

+    return $H(object).toQueryString();

+  },

+

+  toHTML: function(object) {

+    return object && object.toHTML ? object.toHTML() : String.interpret(object);

+  },

+

+  keys: function(object) {

+    var keys = [];

+    for (var property in object)

+      keys.push(property);

+    return keys;

+  },

+

+  values: function(object) {

+    var values = [];

+    for (var property in object)

+      values.push(object[property]);

+    return values;

+  },

+

+  clone: function(object) {

+    return Object.extend({ }, object);

+  },

+

+  isElement: function(object) {

+    return object && object.nodeType == 1;

+  },

+

+  isArray: function(object) {

+    return object != null && typeof object == "object" &&

+      'splice' in object && 'join' in object;

+  },

+

+  isHash: function(object) {

+    return object instanceof Hash;

+  },

+

+  isFunction: function(object) {

+    return typeof object == "function";

+  },

+

+  isString: function(object) {

+    return typeof object == "string";

+  },

+

+  isNumber: function(object) {

+    return typeof object == "number";

+  },

+

+  isUndefined: function(object) {

+    return typeof object == "undefined";

+  }

+});

+

+Object.extend(Function.prototype, {

+  argumentNames: function() {

+    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");

+    return names.length == 1 && !names[0] ? [] : names;

+  },

+

+  bind: function() {

+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;

+    var __method = this, args = $A(arguments), object = args.shift();

+    return function() {

+      return __method.apply(object, args.concat($A(arguments)));

+    }

+  },

+

+  bindAsEventListener: function() {

+    var __method = this, args = $A(arguments), object = args.shift();

+    return function(event) {

+      return __method.apply(object, [event || window.event].concat(args));

+    }

+  },

+

+  curry: function() {

+    if (!arguments.length) return this;

+    var __method = this, args = $A(arguments);

+    return function() {

+      return __method.apply(this, args.concat($A(arguments)));

+    }

+  },

+

+  delay: function() {

+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;

+    return window.setTimeout(function() {

+      return __method.apply(__method, args);

+    }, timeout);

+  },

+

+  wrap: function(wrapper) {

+    var __method = this;

+    return function() {

+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));

+    }

+  },

+

+  methodize: function() {

+    if (this._methodized) return this._methodized;

+    var __method = this;

+    return this._methodized = function() {

+      return __method.apply(null, [this].concat($A(arguments)));

+    };

+  }

+});

+

+Function.prototype.defer = Function.prototype.delay.curry(0.01);

+

+Date.prototype.toJSON = function() {

+  return '"' + this.getUTCFullYear() + '-' +

+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +

+    this.getUTCDate().toPaddedString(2) + 'T' +

+    this.getUTCHours().toPaddedString(2) + ':' +

+    this.getUTCMinutes().toPaddedString(2) + ':' +

+    this.getUTCSeconds().toPaddedString(2) + 'Z"';

+};

+

+var Try = {

+  these: function() {

+    var returnValue;

+

+    for (var i = 0, length = arguments.length; i < length; i++) {

+      var lambda = arguments[i];

+      try {

+        returnValue = lambda();

+        break;

+      } catch (e) { }

+    }

+

+    return returnValue;

+  }

+};

+

+RegExp.prototype.match = RegExp.prototype.test;

+

+RegExp.escape = function(str) {

+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');

+};

+

+/*--------------------------------------------------------------------------*/

+

+var PeriodicalExecuter = Class.create({

+  initialize: function(callback, frequency) {

+    this.callback = callback;

+    this.frequency = frequency;

+    this.currentlyExecuting = false;

+

+    this.registerCallback();

+  },

+

+  registerCallback: function() {

+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);

+  },

+

+  execute: function() {

+    this.callback(this);

+  },

+

+  stop: function() {

+    if (!this.timer) return;

+    clearInterval(this.timer);

+    this.timer = null;

+  },

+

+  onTimerEvent: function() {

+    if (!this.currentlyExecuting) {

+      try {

+        this.currentlyExecuting = true;

+        this.execute();

+      } finally {

+        this.currentlyExecuting = false;

+      }

+    }

+  }

+});

+Object.extend(String, {

+  interpret: function(value) {

+    return value == null ? '' : String(value);

+  },

+  specialChar: {

+    '\b': '\\b',

+    '\t': '\\t',

+    '\n': '\\n',

+    '\f': '\\f',

+    '\r': '\\r',

+    '\\': '\\\\'

+  }

+});

+

+Object.extend(String.prototype, {

+  gsub: function(pattern, replacement) {

+    var result = '', source = this, match;

+    replacement = arguments.callee.prepareReplacement(replacement);

+

+    while (source.length > 0) {

+      if (match = source.match(pattern)) {

+        result += source.slice(0, match.index);

+        result += String.interpret(replacement(match));

+        source  = source.slice(match.index + match[0].length);

+      } else {

+        result += source, source = '';

+      }

+    }

+    return result;

+  },

+

+  sub: function(pattern, replacement, count) {

+    replacement = this.gsub.prepareReplacement(replacement);

+    count = Object.isUndefined(count) ? 1 : count;

+

+    return this.gsub(pattern, function(match) {

+      if (--count < 0) return match[0];

+      return replacement(match);

+    });

+  },

+

+  scan: function(pattern, iterator) {

+    this.gsub(pattern, iterator);

+    return String(this);

+  },

+

+  truncate: function(length, truncation) {

+    length = length || 30;

+    truncation = Object.isUndefined(truncation) ? '...' : truncation;

+    return this.length > length ?

+      this.slice(0, length - truncation.length) + truncation : String(this);

+  },

+

+  strip: function() {

+    return this.replace(/^\s+/, '').replace(/\s+$/, '');

+  },

+

+  stripTags: function() {

+    return this.replace(/<\/?[^>]+>/gi, '');

+  },

+

+  stripScripts: function() {

+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');

+  },

+

+  extractScripts: function() {

+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');

+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');

+    return (this.match(matchAll) || []).map(function(scriptTag) {

+      return (scriptTag.match(matchOne) || ['', ''])[1];

+    });

+  },

+

+  evalScripts: function() {

+    return this.extractScripts().map(function(script) { return eval(script) });

+  },

+

+  escapeHTML: function() {

+    var self = arguments.callee;

+    self.text.data = this;

+    return self.div.innerHTML;

+  },

+

+  unescapeHTML: function() {

+    var div = new Element('div');

+    div.innerHTML = this.stripTags();

+    return div.childNodes[0] ? (div.childNodes.length > 1 ?

+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :

+      div.childNodes[0].nodeValue) : '';

+  },

+

+  toQueryParams: function(separator) {

+    var match = this.strip().match(/([^?#]*)(#.*)?$/);

+    if (!match) return { };

+

+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {

+      if ((pair = pair.split('='))[0]) {

+        var key = decodeURIComponent(pair.shift());

+        var value = pair.length > 1 ? pair.join('=') : pair[0];

+        if (value != undefined) value = decodeURIComponent(value);

+

+        if (key in hash) {

+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];

+          hash[key].push(value);

+        }

+        else hash[key] = value;

+      }

+      return hash;

+    });

+  },

+

+  toArray: function() {

+    return this.split('');

+  },

+

+  succ: function() {

+    return this.slice(0, this.length - 1) +

+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);

+  },

+

+  times: function(count) {

+    return count < 1 ? '' : new Array(count + 1).join(this);

+  },

+

+  camelize: function() {

+    var parts = this.split('-'), len = parts.length;

+    if (len == 1) return parts[0];

+

+    var camelized = this.charAt(0) == '-'

+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)

+      : parts[0];

+

+    for (var i = 1; i < len; i++)

+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

+

+    return camelized;

+  },

+

+  capitalize: function() {

+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();

+  },

+

+  underscore: function() {

+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();

+  },

+

+  dasherize: function() {

+    return this.gsub(/_/,'-');

+  },

+

+  inspect: function(useDoubleQuotes) {

+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {

+      var character = String.specialChar[match[0]];

+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);

+    });

+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';

+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";

+  },

+

+  toJSON: function() {

+    return this.inspect(true);

+  },

+

+  unfilterJSON: function(filter) {

+    return this.sub(filter || Prototype.JSONFilter, '#{1}');

+  },

+

+  isJSON: function() {

+    var str = this;

+    if (str.blank()) return false;

+    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');

+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);

+  },

+

+  evalJSON: function(sanitize) {

+    var json = this.unfilterJSON();

+    try {

+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');

+    } catch (e) { }

+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());

+  },

+

+  include: function(pattern) {

+    return this.indexOf(pattern) > -1;

+  },

+

+  startsWith: function(pattern) {

+    return this.indexOf(pattern) === 0;

+  },

+

+  endsWith: function(pattern) {

+    var d = this.length - pattern.length;

+    return d >= 0 && this.lastIndexOf(pattern) === d;

+  },

+

+  empty: function() {

+    return this == '';

+  },

+

+  blank: function() {

+    return /^\s*$/.test(this);

+  },

+

+  interpolate: function(object, pattern) {

+    return new Template(this, pattern).evaluate(object);

+  }

+});

+

+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {

+  escapeHTML: function() {

+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

+  },

+  unescapeHTML: function() {

+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');

+  }

+});

+

+String.prototype.gsub.prepareReplacement = function(replacement) {

+  if (Object.isFunction(replacement)) return replacement;

+  var template = new Template(replacement);

+  return function(match) { return template.evaluate(match) };

+};

+

+String.prototype.parseQuery = String.prototype.toQueryParams;

+

+Object.extend(String.prototype.escapeHTML, {

+  div:  document.createElement('div'),

+  text: document.createTextNode('')

+});

+

+with (String.prototype.escapeHTML) div.appendChild(text);

+

+var Template = Class.create({

+  initialize: function(template, pattern) {

+    this.template = template.toString();

+    this.pattern = pattern || Template.Pattern;

+  },

+

+  evaluate: function(object) {

+    if (Object.isFunction(object.toTemplateReplacements))

+      object = object.toTemplateReplacements();

+

+    return this.template.gsub(this.pattern, function(match) {

+      if (object == null) return '';

+

+      var before = match[1] || '';

+      if (before == '\\') return match[2];

+

+      var ctx = object, expr = match[3];

+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;

+      match = pattern.exec(expr);

+      if (match == null) return before;

+

+      while (match != null) {

+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];

+        ctx = ctx[comp];

+        if (null == ctx || '' == match[3]) break;

+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);

+        match = pattern.exec(expr);

+      }

+

+      return before + String.interpret(ctx);

+    });

+  }

+});

+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

+

+var $break = { };

+

+var Enumerable = {

+  each: function(iterator, context) {

+    var index = 0;

+    iterator = iterator.bind(context);

+    try {

+      this._each(function(value) {

+        iterator(value, index++);

+      });

+    } catch (e) {

+      if (e != $break) throw e;

+    }

+    return this;

+  },

+

+  eachSlice: function(number, iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var index = -number, slices = [], array = this.toArray();

+    while ((index += number) < array.length)

+      slices.push(array.slice(index, index+number));

+    return slices.collect(iterator, context);

+  },

+

+  all: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result = true;

+    this.each(function(value, index) {

+      result = result && !!iterator(value, index);

+      if (!result) throw $break;

+    });

+    return result;

+  },

+

+  any: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result = false;

+    this.each(function(value, index) {

+      if (result = !!iterator(value, index))

+        throw $break;

+    });

+    return result;

+  },

+

+  collect: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var results = [];

+    this.each(function(value, index) {

+      results.push(iterator(value, index));

+    });

+    return results;

+  },

+

+  detect: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var result;

+    this.each(function(value, index) {

+      if (iterator(value, index)) {

+        result = value;

+        throw $break;

+      }

+    });

+    return result;

+  },

+

+  findAll: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var results = [];

+    this.each(function(value, index) {

+      if (iterator(value, index))

+        results.push(value);

+    });

+    return results;

+  },

+

+  grep: function(filter, iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var results = [];

+

+    if (Object.isString(filter))

+      filter = new RegExp(filter);

+

+    this.each(function(value, index) {

+      if (filter.match(value))

+        results.push(iterator(value, index));

+    });

+    return results;

+  },

+

+  include: function(object) {

+    if (Object.isFunction(this.indexOf))

+      if (this.indexOf(object) != -1) return true;

+

+    var found = false;

+    this.each(function(value) {

+      if (value == object) {

+        found = true;

+        throw $break;

+      }

+    });

+    return found;

+  },

+

+  inGroupsOf: function(number, fillWith) {

+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;

+    return this.eachSlice(number, function(slice) {

+      while(slice.length < number) slice.push(fillWith);

+      return slice;

+    });

+  },

+

+  inject: function(memo, iterator, context) {

+    iterator = iterator.bind(context);

+    this.each(function(value, index) {

+      memo = iterator(memo, value, index);

+    });

+    return memo;

+  },

+

+  invoke: function(method) {

+    var args = $A(arguments).slice(1);

+    return this.map(function(value) {

+      return value[method].apply(value, args);

+    });

+  },

+

+  max: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result;

+    this.each(function(value, index) {

+      value = iterator(value, index);

+      if (result == null || value >= result)

+        result = value;

+    });

+    return result;

+  },

+

+  min: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result;

+    this.each(function(value, index) {

+      value = iterator(value, index);

+      if (result == null || value < result)

+        result = value;

+    });

+    return result;

+  },

+

+  partition: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var trues = [], falses = [];

+    this.each(function(value, index) {

+      (iterator(value, index) ?

+        trues : falses).push(value);

+    });

+    return [trues, falses];

+  },

+

+  pluck: function(property) {

+    var results = [];

+    this.each(function(value) {

+      results.push(value[property]);

+    });

+    return results;

+  },

+

+  reject: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var results = [];

+    this.each(function(value, index) {

+      if (!iterator(value, index))

+        results.push(value);

+    });

+    return results;

+  },

+

+  sortBy: function(iterator, context) {

+    iterator = iterator.bind(context);

+    return this.map(function(value, index) {

+      return {value: value, criteria: iterator(value, index)};

+    }).sort(function(left, right) {

+      var a = left.criteria, b = right.criteria;

+      return a < b ? -1 : a > b ? 1 : 0;

+    }).pluck('value');

+  },

+

+  toArray: function() {

+    return this.map();

+  },

+

+  zip: function() {

+    var iterator = Prototype.K, args = $A(arguments);

+    if (Object.isFunction(args.last()))

+      iterator = args.pop();

+

+    var collections = [this].concat(args).map($A);

+    return this.map(function(value, index) {

+      return iterator(collections.pluck(index));

+    });

+  },

+

+  size: function() {

+    return this.toArray().length;

+  },

+

+  inspect: function() {

+    return '#<Enumerable:' + this.toArray().inspect() + '>';

+  }

+};

+

+Object.extend(Enumerable, {

+  map:     Enumerable.collect,

+  find:    Enumerable.detect,

+  select:  Enumerable.findAll,

+  filter:  Enumerable.findAll,

+  member:  Enumerable.include,

+  entries: Enumerable.toArray,

+  every:   Enumerable.all,

+  some:    Enumerable.any

+});

+function $A(iterable) {

+  if (!iterable) return [];

+  if (iterable.toArray) return iterable.toArray();

+  var length = iterable.length || 0, results = new Array(length);

+  while (length--) results[length] = iterable[length];

+  return results;

+}

+

+if (Prototype.Browser.WebKit) {

+  $A = function(iterable) {

+    if (!iterable) return [];

+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&

+        iterable.toArray) return iterable.toArray();

+    var length = iterable.length || 0, results = new Array(length);

+    while (length--) results[length] = iterable[length];

+    return results;

+  };

+}

+

+Array.from = $A;

+

+Object.extend(Array.prototype, Enumerable);

+

+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

+

+Object.extend(Array.prototype, {

+  _each: function(iterator) {

+    for (var i = 0, length = this.length; i < length; i++)

+      iterator(this[i]);

+  },

+

+  clear: function() {

+    this.length = 0;

+    return this;

+  },

+

+  first: function() {

+    return this[0];

+  },

+

+  last: function() {

+    return this[this.length - 1];

+  },

+

+  compact: function() {

+    return this.select(function(value) {

+      return value != null;

+    });

+  },

+

+  flatten: function() {

+    return this.inject([], function(array, value) {

+      return array.concat(Object.isArray(value) ?

+        value.flatten() : [value]);

+    });

+  },

+

+  without: function() {

+    var values = $A(arguments);

+    return this.select(function(value) {

+      return !values.include(value);

+    });

+  },

+

+  reverse: function(inline) {

+    return (inline !== false ? this : this.toArray())._reverse();

+  },

+

+  reduce: function() {

+    return this.length > 1 ? this : this[0];

+  },

+

+  uniq: function(sorted) {

+    return this.inject([], function(array, value, index) {

+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))

+        array.push(value);

+      return array;

+    });

+  },

+

+  intersect: function(array) {

+    return this.uniq().findAll(function(item) {

+      return array.detect(function(value) { return item === value });

+    });

+  },

+

+  clone: function() {

+    return [].concat(this);

+  },

+

+  size: function() {

+    return this.length;

+  },

+

+  inspect: function() {

+    return '[' + this.map(Object.inspect).join(', ') + ']';

+  },

+

+  toJSON: function() {

+    var results = [];

+    this.each(function(object) {

+      var value = Object.toJSON(object);

+      if (!Object.isUndefined(value)) results.push(value);

+    });

+    return '[' + results.join(', ') + ']';

+  }

+});

+

+// use native browser JS 1.6 implementation if available

+if (Object.isFunction(Array.prototype.forEach))

+  Array.prototype._each = Array.prototype.forEach;

+

+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {

+  i || (i = 0);

+  var length = this.length;

+  if (i < 0) i = length + i;

+  for (; i < length; i++)

+    if (this[i] === item) return i;

+  return -1;

+};

+

+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {

+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;

+  var n = this.slice(0, i).reverse().indexOf(item);

+  return (n < 0) ? n : i - n - 1;

+};

+

+Array.prototype.toArray = Array.prototype.clone;

+

+function $w(string) {

+  if (!Object.isString(string)) return [];

+  string = string.strip();

+  return string ? string.split(/\s+/) : [];

+}

+

+if (Prototype.Browser.Opera){

+  Array.prototype.concat = function() {

+    var array = [];

+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);

+    for (var i = 0, length = arguments.length; i < length; i++) {

+      if (Object.isArray(arguments[i])) {

+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)

+          array.push(arguments[i][j]);

+      } else {

+        array.push(arguments[i]);

+      }

+    }

+    return array;

+  };

+}

+Object.extend(Number.prototype, {

+  toColorPart: function() {

+    return this.toPaddedString(2, 16);

+  },

+

+  succ: function() {

+    return this + 1;

+  },

+

+  times: function(iterator) {

+    $R(0, this, true).each(iterator);

+    return this;

+  },

+

+  toPaddedString: function(length, radix) {

+    var string = this.toString(radix || 10);

+    return '0'.times(length - string.length) + string;

+  },

+

+  toJSON: function() {

+    return isFinite(this) ? this.toString() : 'null';

+  }

+});

+

+$w('abs round ceil floor').each(function(method){

+  Number.prototype[method] = Math[method].methodize();

+});

+function $H(object) {

+  return new Hash(object);

+};

+

+var Hash = Class.create(Enumerable, (function() {

+

+  function toQueryPair(key, value) {

+    if (Object.isUndefined(value)) return key;

+    return key + '=' + encodeURIComponent(String.interpret(value));

+  }

+

+  return {

+    initialize: function(object) {

+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);

+    },

+

+    _each: function(iterator) {

+      for (var key in this._object) {

+        var value = this._object[key], pair = [key, value];

+        pair.key = key;

+        pair.value = value;

+        iterator(pair);

+      }

+    },

+

+    set: function(key, value) {

+      return this._object[key] = value;

+    },

+

+    get: function(key) {

+      return this._object[key];

+    },

+

+    unset: function(key) {

+      var value = this._object[key];

+      delete this._object[key];

+      return value;

+    },

+

+    toObject: function() {

+      return Object.clone(this._object);

+    },

+

+    keys: function() {

+      return this.pluck('key');

+    },

+

+    values: function() {

+      return this.pluck('value');

+    },

+

+    index: function(value) {

+      var match = this.detect(function(pair) {

+        return pair.value === value;

+      });

+      return match && match.key;

+    },

+

+    merge: function(object) {

+      return this.clone().update(object);

+    },

+

+    update: function(object) {

+      return new Hash(object).inject(this, function(result, pair) {

+        result.set(pair.key, pair.value);

+        return result;

+      });

+    },

+

+    toQueryString: function() {

+      return this.map(function(pair) {

+        var key = encodeURIComponent(pair.key), values = pair.value;

+

+        if (values && typeof values == 'object') {

+          if (Object.isArray(values))

+            return values.map(toQueryPair.curry(key)).join('&');

+        }

+        return toQueryPair(key, values);

+      }).join('&');

+    },

+

+    inspect: function() {

+      return '#<Hash:{' + this.map(function(pair) {

+        return pair.map(Object.inspect).join(': ');

+      }).join(', ') + '}>';

+    },

+

+    toJSON: function() {

+      return Object.toJSON(this.toObject());

+    },

+

+    clone: function() {

+      return new Hash(this);

+    }

+  }

+})());

+

+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;

+Hash.from = $H;

+var ObjectRange = Class.create(Enumerable, {

+  initialize: function(start, end, exclusive) {

+    this.start = start;

+    this.end = end;

+    this.exclusive = exclusive;

+  },

+

+  _each: function(iterator) {

+    var value = this.start;

+    while (this.include(value)) {

+      iterator(value);

+      value = value.succ();

+    }

+  },

+

+  include: function(value) {

+    if (value < this.start)

+      return false;

+    if (this.exclusive)

+      return value < this.end;

+    return value <= this.end;

+  }

+});

+

+var $R = function(start, end, exclusive) {

+  return new ObjectRange(start, end, exclusive);

+};

+

+var Ajax = {

+  getTransport: function() {

+    return Try.these(

+      function() {return new XMLHttpRequest()},

+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},

+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}

+    ) || false;

+  },

+

+  activeRequestCount: 0

+};

+

+Ajax.Responders = {

+  responders: [],

+

+  _each: function(iterator) {

+    this.responders._each(iterator);

+  },

+

+  register: function(responder) {

+    if (!this.include(responder))

+      this.responders.push(responder);

+  },

+

+  unregister: function(responder) {

+    this.responders = this.responders.without(responder);

+  },

+

+  dispatch: function(callback, request, transport, json) {

+    this.each(function(responder) {

+      if (Object.isFunction(responder[callback])) {

+        try {

+          responder[callback].apply(responder, [request, transport, json]);

+        } catch (e) { }

+      }

+    });

+  }

+};

+

+Object.extend(Ajax.Responders, Enumerable);

+

+Ajax.Responders.register({

+  onCreate:   function() { Ajax.activeRequestCount++ },

+  onComplete: function() { Ajax.activeRequestCount-- }

+});

+

+Ajax.Base = Class.create({

+  initialize: function(options) {

+    this.options = {

+      method:       'post',

+      asynchronous: true,

+      contentType:  'application/x-www-form-urlencoded',

+      encoding:     'UTF-8',

+      parameters:   '',

+      evalJSON:     true,

+      evalJS:       true

+    };

+    Object.extend(this.options, options || { });

+

+    this.options.method = this.options.method.toLowerCase();

+

+    if (Object.isString(this.options.parameters))

+      this.options.parameters = this.options.parameters.toQueryParams();

+    else if (Object.isHash(this.options.parameters))

+      this.options.parameters = this.options.parameters.toObject();

+  }

+});

+

+Ajax.Request = Class.create(Ajax.Base, {

+  _complete: false,

+

+  initialize: function($super, url, options) {

+    $super(options);

+    this.transport = Ajax.getTransport();

+    this.request(url);

+  },

+

+  request: function(url) {

+    this.url = url;

+    this.method = this.options.method;

+    var params = Object.clone(this.options.parameters);

+

+    if (!['get', 'post'].include(this.method)) {

+      // simulate other verbs over post

+      params['_method'] = this.method;

+      this.method = 'post';

+    }

+

+    this.parameters = params;

+

+    if (params = Object.toQueryString(params)) {

+      // when GET, append parameters to URL

+      if (this.method == 'get')

+        this.url += (this.url.include('?') ? '&' : '?') + params;

+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))

+        params += '&_=';

+    }

+

+    try {

+      var response = new Ajax.Response(this);

+      if (this.options.onCreate) this.options.onCreate(response);

+      Ajax.Responders.dispatch('onCreate', this, response);

+

+      this.transport.open(this.method.toUpperCase(), this.url,

+        this.options.asynchronous);

+

+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

+

+      this.transport.onreadystatechange = this.onStateChange.bind(this);

+      this.setRequestHeaders();

+

+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;

+      this.transport.send(this.body);

+

+      /* Force Firefox to handle ready state 4 for synchronous requests */

+      if (!this.options.asynchronous && this.transport.overrideMimeType)

+        this.onStateChange();

+

+    }

+    catch (e) {

+      this.dispatchException(e);

+    }

+  },

+

+  onStateChange: function() {

+    var readyState = this.transport.readyState;

+    if (readyState > 1 && !((readyState == 4) && this._complete))

+      this.respondToReadyState(this.transport.readyState);

+  },

+

+  setRequestHeaders: function() {

+    var headers = {

+      'X-Requested-With': 'XMLHttpRequest',

+      'X-Prototype-Version': Prototype.Version,

+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'

+    };

+

+    if (this.method == 'post') {

+      headers['Content-type'] = this.options.contentType +

+        (this.options.encoding ? '; charset=' + this.options.encoding : '');

+

+      /* Force "Connection: close" for older Mozilla browsers to work

+       * around a bug where XMLHttpRequest sends an incorrect

+       * Content-length header. See Mozilla Bugzilla #246651.

+       */

+      if (this.transport.overrideMimeType &&

+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)

+            headers['Connection'] = 'close';

+    }

+

+    // user-defined headers

+    if (typeof this.options.requestHeaders == 'object') {

+      var extras = this.options.requestHeaders;

+

+      if (Object.isFunction(extras.push))

+        for (var i = 0, length = extras.length; i < length; i += 2)

+          headers[extras[i]] = extras[i+1];

+      else

+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });

+    }

+

+    for (var name in headers)

+      this.transport.setRequestHeader(name, headers[name]);

+  },

+

+  success: function() {

+    var status = this.getStatus();

+    return !status || (status >= 200 && status < 300);

+  },

+

+  getStatus: function() {

+    try {

+      return this.transport.status || 0;

+    } catch (e) { return 0 }

+  },

+

+  respondToReadyState: function(readyState) {

+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

+

+    if (state == 'Complete') {

+      try {

+        this._complete = true;

+        (this.options['on' + response.status]

+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]

+         || Prototype.emptyFunction)(response, response.headerJSON);

+      } catch (e) {

+        this.dispatchException(e);

+      }

+

+      var contentType = response.getHeader('Content-type');

+      if (this.options.evalJS == 'force'

+          || (this.options.evalJS && this.isSameOrigin() && contentType

+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))

+        this.evalResponse();

+    }

+

+    try {

+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);

+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);

+    } catch (e) {

+      this.dispatchException(e);

+    }

+

+    if (state == 'Complete') {

+      // avoid memory leak in MSIE: clean up

+      this.transport.onreadystatechange = Prototype.emptyFunction;

+    }

+  },

+

+  isSameOrigin: function() {

+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);

+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({

+      protocol: location.protocol,

+      domain: document.domain,

+      port: location.port ? ':' + location.port : ''

+    }));

+  },

+

+  getHeader: function(name) {

+    try {

+      return this.transport.getResponseHeader(name) || null;

+    } catch (e) { return null }

+  },

+

+  evalResponse: function() {

+    try {

+      return eval((this.transport.responseText || '').unfilterJSON());

+    } catch (e) {

+      this.dispatchException(e);

+    }

+  },

+

+  dispatchException: function(exception) {

+    (this.options.onException || Prototype.emptyFunction)(this, exception);

+    Ajax.Responders.dispatch('onException', this, exception);

+  }

+});

+

+Ajax.Request.Events =

+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

+

+Ajax.Response = Class.create({

+  initialize: function(request){

+    this.request = request;

+    var transport  = this.transport  = request.transport,

+        readyState = this.readyState = transport.readyState;

+

+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {

+      this.status       = this.getStatus();

+      this.statusText   = this.getStatusText();

+      this.responseText = String.interpret(transport.responseText);

+      this.headerJSON   = this._getHeaderJSON();

+    }

+

+    if(readyState == 4) {

+      var xml = transport.responseXML;

+      this.responseXML  = Object.isUndefined(xml) ? null : xml;

+      this.responseJSON = this._getResponseJSON();

+    }

+  },

+

+  status:      0,

+  statusText: '',

+

+  getStatus: Ajax.Request.prototype.getStatus,

+

+  getStatusText: function() {

+    try {

+      return this.transport.statusText || '';

+    } catch (e) { return '' }

+  },

+

+  getHeader: Ajax.Request.prototype.getHeader,

+

+  getAllHeaders: function() {

+    try {

+      return this.getAllResponseHeaders();

+    } catch (e) { return null }

+  },

+

+  getResponseHeader: function(name) {

+    return this.transport.getResponseHeader(name);

+  },

+

+  getAllResponseHeaders: function() {

+    return this.transport.getAllResponseHeaders();

+  },

+

+  _getHeaderJSON: function() {

+    var json = this.getHeader('X-JSON');

+    if (!json) return null;

+    json = decodeURIComponent(escape(json));

+    try {

+      return json.evalJSON(this.request.options.sanitizeJSON ||

+        !this.request.isSameOrigin());

+    } catch (e) {

+      this.request.dispatchException(e);

+    }

+  },

+

+  _getResponseJSON: function() {

+    var options = this.request.options;

+    if (!options.evalJSON || (options.evalJSON != 'force' &&

+      !(this.getHeader('Content-type') || '').include('application/json')) ||

+        this.responseText.blank())

+          return null;

+    try {

+      return this.responseText.evalJSON(options.sanitizeJSON ||

+        !this.request.isSameOrigin());

+    } catch (e) {

+      this.request.dispatchException(e);

+    }

+  }

+});

+

+Ajax.Updater = Class.create(Ajax.Request, {

+  initialize: function($super, container, url, options) {

+    this.container = {

+      success: (container.success || container),

+      failure: (container.failure || (container.success ? null : container))

+    };

+

+    options = Object.clone(options);

+    var onComplete = options.onComplete;

+    options.onComplete = (function(response, json) {

+      this.updateContent(response.responseText);

+      if (Object.isFunction(onComplete)) onComplete(response, json);

+    }).bind(this);

+

+    $super(url, options);

+  },

+

+  updateContent: function(responseText) {

+    var receiver = this.container[this.success() ? 'success' : 'failure'],

+        options = this.options;

+

+    if (!options.evalScripts) responseText = responseText.stripScripts();

+

+    if (receiver = $(receiver)) {

+      if (options.insertion) {

+        if (Object.isString(options.insertion)) {

+          var insertion = { }; insertion[options.insertion] = responseText;

+          receiver.insert(insertion);

+        }

+        else options.insertion(receiver, responseText);

+      }

+      else receiver.update(responseText);

+    }

+  }

+});

+

+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {

+  initialize: function($super, container, url, options) {

+    $super(options);

+    this.onComplete = this.options.onComplete;

+

+    this.frequency = (this.options.frequency || 2);

+    this.decay = (this.options.decay || 1);

+

+    this.updater = { };

+    this.container = container;

+    this.url = url;

+

+    this.start();

+  },

+

+  start: function() {

+    this.options.onComplete = this.updateComplete.bind(this);

+    this.onTimerEvent();

+  },

+

+  stop: function() {

+    this.updater.options.onComplete = undefined;

+    clearTimeout(this.timer);

+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);

+  },

+

+  updateComplete: function(response) {

+    if (this.options.decay) {

+      this.decay = (response.responseText == this.lastText ?

+        this.decay * this.options.decay : 1);

+

+      this.lastText = response.responseText;

+    }

+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);

+  },

+

+  onTimerEvent: function() {

+    this.updater = new Ajax.Updater(this.container, this.url, this.options);

+  }

+});

+function $(element) {

+  if (arguments.length > 1) {

+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)

+      elements.push($(arguments[i]));

+    return elements;

+  }

+  if (Object.isString(element))

+    element = document.getElementById(element);

+  return Element.extend(element);

+}

+

+if (Prototype.BrowserFeatures.XPath) {

+  document._getElementsByXPath = function(expression, parentElement) {

+    var results = [];

+    var query = document.evaluate(expression, $(parentElement) || document,

+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

+    for (var i = 0, length = query.snapshotLength; i < length; i++)

+      results.push(Element.extend(query.snapshotItem(i)));

+    return results;

+  };

+}

+

+/*--------------------------------------------------------------------------*/

+

+if (!window.Node) var Node = { };

+

+if (!Node.ELEMENT_NODE) {

+  // DOM level 2 ECMAScript Language Binding

+  Object.extend(Node, {

+    ELEMENT_NODE: 1,

+    ATTRIBUTE_NODE: 2,

+    TEXT_NODE: 3,

+    CDATA_SECTION_NODE: 4,

+    ENTITY_REFERENCE_NODE: 5,

+    ENTITY_NODE: 6,

+    PROCESSING_INSTRUCTION_NODE: 7,

+    COMMENT_NODE: 8,

+    DOCUMENT_NODE: 9,

+    DOCUMENT_TYPE_NODE: 10,

+    DOCUMENT_FRAGMENT_NODE: 11,

+    NOTATION_NODE: 12

+  });

+}

+

+(function() {

+  var element = this.Element;

+  this.Element = function(tagName, attributes) {

+    attributes = attributes || { };

+    tagName = tagName.toLowerCase();

+    var cache = Element.cache;

+    if (Prototype.Browser.IE && attributes.name) {

+      tagName = '<' + tagName + ' name="' + attributes.name + '">';

+      delete attributes.name;

+      return Element.writeAttribute(document.createElement(tagName), attributes);

+    }

+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));

+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);

+  };

+  Object.extend(this.Element, element || { });

+}).call(window);

+

+Element.cache = { };

+

+Element.Methods = {

+  visible: function(element) {

+    return $(element).style.display != 'none';

+  },

+

+  toggle: function(element) {

+    element = $(element);

+    Element[Element.visible(element) ? 'hide' : 'show'](element);

+    return element;

+  },

+

+  hide: function(element) {

+    $(element).style.display = 'none';

+    return element;

+  },

+

+  show: function(element) {

+    $(element).style.display = '';

+    return element;

+  },

+

+  remove: function(element) {

+    element = $(element);

+    element.parentNode.removeChild(element);

+    return element;

+  },

+

+  update: function(element, content) {

+    element = $(element);

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) return element.update().insert(content);

+    content = Object.toHTML(content);

+    element.innerHTML = content.stripScripts();

+    content.evalScripts.bind(content).defer();

+    return element;

+  },

+

+  replace: function(element, content) {

+    element = $(element);

+    if (content && content.toElement) content = content.toElement();

+    else if (!Object.isElement(content)) {

+      content = Object.toHTML(content);

+      var range = element.ownerDocument.createRange();

+      range.selectNode(element);

+      content.evalScripts.bind(content).defer();

+      content = range.createContextualFragment(content.stripScripts());

+    }

+    element.parentNode.replaceChild(content, element);

+    return element;

+  },

+

+  insert: function(element, insertions) {

+    element = $(element);

+

+    if (Object.isString(insertions) || Object.isNumber(insertions) ||

+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))

+          insertions = {bottom:insertions};

+

+    var content, insert, tagName, childNodes;

+

+    for (var position in insertions) {

+      content  = insertions[position];

+      position = position.toLowerCase();

+      insert = Element._insertionTranslations[position];

+

+      if (content && content.toElement) content = content.toElement();

+      if (Object.isElement(content)) {

+        insert(element, content);

+        continue;

+      }

+

+      content = Object.toHTML(content);

+

+      tagName = ((position == 'before' || position == 'after')

+        ? element.parentNode : element).tagName.toUpperCase();

+

+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

+

+      if (position == 'top' || position == 'after') childNodes.reverse();

+      childNodes.each(insert.curry(element));

+

+      content.evalScripts.bind(content).defer();

+    }

+

+    return element;

+  },

+

+  wrap: function(element, wrapper, attributes) {

+    element = $(element);

+    if (Object.isElement(wrapper))

+      $(wrapper).writeAttribute(attributes || { });

+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);

+    else wrapper = new Element('div', wrapper);

+    if (element.parentNode)

+      element.parentNode.replaceChild(wrapper, element);

+    wrapper.appendChild(element);

+    return wrapper;

+  },

+

+  inspect: function(element) {

+    element = $(element);

+    var result = '<' + element.tagName.toLowerCase();

+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {

+      var property = pair.first(), attribute = pair.last();

+      var value = (element[property] || '').toString();

+      if (value) result += ' ' + attribute + '=' + value.inspect(true);

+    });

+    return result + '>';

+  },

+

+  recursivelyCollect: function(element, property) {

+    element = $(element);

+    var elements = [];

+    while (element = element[property])

+      if (element.nodeType == 1)

+        elements.push(Element.extend(element));

+    return elements;

+  },

+

+  ancestors: function(element) {

+    return $(element).recursivelyCollect('parentNode');

+  },

+

+  descendants: function(element) {

+    return $(element).select("*");

+  },

+

+  firstDescendant: function(element) {

+    element = $(element).firstChild;

+    while (element && element.nodeType != 1) element = element.nextSibling;

+    return $(element);

+  },

+

+  immediateDescendants: function(element) {

+    if (!(element = $(element).firstChild)) return [];

+    while (element && element.nodeType != 1) element = element.nextSibling;

+    if (element) return [element].concat($(element).nextSiblings());

+    return [];

+  },

+

+  previousSiblings: function(element) {

+    return $(element).recursivelyCollect('previousSibling');

+  },

+

+  nextSiblings: function(element) {

+    return $(element).recursivelyCollect('nextSibling');

+  },

+

+  siblings: function(element) {

+    element = $(element);

+    return element.previousSiblings().reverse().concat(element.nextSiblings());

+  },

+

+  match: function(element, selector) {

+    if (Object.isString(selector))

+      selector = new Selector(selector);

+    return selector.match($(element));

+  },

+

+  up: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(element.parentNode);

+    var ancestors = element.ancestors();

+    return Object.isNumber(expression) ? ancestors[expression] :

+      Selector.findElement(ancestors, expression, index);

+  },

+

+  down: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return element.firstDescendant();

+    return Object.isNumber(expression) ? element.descendants()[expression] :

+      element.select(expression)[index || 0];

+  },

+

+  previous: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));

+    var previousSiblings = element.previousSiblings();

+    return Object.isNumber(expression) ? previousSiblings[expression] :

+      Selector.findElement(previousSiblings, expression, index);

+  },

+

+  next: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));

+    var nextSiblings = element.nextSiblings();

+    return Object.isNumber(expression) ? nextSiblings[expression] :

+      Selector.findElement(nextSiblings, expression, index);

+  },

+

+  select: function() {

+    var args = $A(arguments), element = $(args.shift());

+    return Selector.findChildElements(element, args);

+  },

+

+  adjacent: function() {

+    var args = $A(arguments), element = $(args.shift());

+    return Selector.findChildElements(element.parentNode, args).without(element);

+  },

+

+  identify: function(element) {

+    element = $(element);

+    var id = element.readAttribute('id'), self = arguments.callee;

+    if (id) return id;

+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));

+    element.writeAttribute('id', id);

+    return id;

+  },

+

+  readAttribute: function(element, name) {

+    element = $(element);

+    if (Prototype.Browser.IE) {

+      var t = Element._attributeTranslations.read;

+      if (t.values[name]) return t.values[name](element, name);

+      if (t.names[name]) name = t.names[name];

+      if (name.include(':')) {

+        return (!element.attributes || !element.attributes[name]) ? null :

+         element.attributes[name].value;

+      }

+    }

+    return element.getAttribute(name);

+  },

+

+  writeAttribute: function(element, name, value) {

+    element = $(element);

+    var attributes = { }, t = Element._attributeTranslations.write;

+

+    if (typeof name == 'object') attributes = name;

+    else attributes[name] = Object.isUndefined(value) ? true : value;

+

+    for (var attr in attributes) {

+      name = t.names[attr] || attr;

+      value = attributes[attr];

+      if (t.values[attr]) name = t.values[attr](element, value);

+      if (value === false || value === null)

+        element.removeAttribute(name);

+      else if (value === true)

+        element.setAttribute(name, name);

+      else element.setAttribute(name, value);

+    }

+    return element;

+  },

+

+  getHeight: function(element) {

+    return $(element).getDimensions().height;

+  },

+

+  getWidth: function(element) {

+    return $(element).getDimensions().width;

+  },

+

+  classNames: function(element) {

+    return new Element.ClassNames(element);

+  },

+

+  hasClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    var elementClassName = element.className;

+    return (elementClassName.length > 0 && (elementClassName == className ||

+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));

+  },

+

+  addClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    if (!element.hasClassName(className))

+      element.className += (element.className ? ' ' : '') + className;

+    return element;

+  },

+

+  removeClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    element.className = element.className.replace(

+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();

+    return element;

+  },

+

+  toggleClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    return element[element.hasClassName(className) ?

+      'removeClassName' : 'addClassName'](className);

+  },

+

+  // removes whitespace-only text node children

+  cleanWhitespace: function(element) {

+    element = $(element);

+    var node = element.firstChild;

+    while (node) {

+      var nextNode = node.nextSibling;

+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))

+        element.removeChild(node);

+      node = nextNode;

+    }

+    return element;

+  },

+

+  empty: function(element) {

+    return $(element).innerHTML.blank();

+  },

+

+  descendantOf: function(element, ancestor) {

+    element = $(element), ancestor = $(ancestor);

+    var originalAncestor = ancestor;

+

+    if (element.compareDocumentPosition)

+      return (element.compareDocumentPosition(ancestor) & 8) === 8;

+

+    if (element.sourceIndex && !Prototype.Browser.Opera) {

+      var e = element.sourceIndex, a = ancestor.sourceIndex,

+       nextAncestor = ancestor.nextSibling;

+      if (!nextAncestor) {

+        do { ancestor = ancestor.parentNode; }

+        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);

+      }

+      if (nextAncestor && nextAncestor.sourceIndex)

+       return (e > a && e < nextAncestor.sourceIndex);

+    }

+

+    while (element = element.parentNode)

+      if (element == originalAncestor) return true;

+    return false;

+  },

+

+  scrollTo: function(element) {

+    element = $(element);

+    var pos = element.cumulativeOffset();

+    window.scrollTo(pos[0], pos[1]);

+    return element;

+  },

+

+  getStyle: function(element, style) {

+    element = $(element);

+    style = style == 'float' ? 'cssFloat' : style.camelize();

+    var value = element.style[style];

+    if (!value) {

+      var css = document.defaultView.getComputedStyle(element, null);

+      value = css ? css[style] : null;

+    }

+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;

+    return value == 'auto' ? null : value;

+  },

+

+  getOpacity: function(element) {

+    return $(element).getStyle('opacity');

+  },

+

+  setStyle: function(element, styles) {

+    element = $(element);

+    var elementStyle = element.style, match;

+    if (Object.isString(styles)) {

+      element.style.cssText += ';' + styles;

+      return styles.include('opacity') ?

+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;

+    }

+    for (var property in styles)

+      if (property == 'opacity') element.setOpacity(styles[property]);

+      else

+        elementStyle[(property == 'float' || property == 'cssFloat') ?

+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :

+            property] = styles[property];

+

+    return element;

+  },

+

+  setOpacity: function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1 || value === '') ? '' :

+      (value < 0.00001) ? 0 : value;

+    return element;

+  },

+

+  getDimensions: function(element) {

+    element = $(element);

+    var display = $(element).getStyle('display');

+    if (display != 'none' && display != null) // Safari bug

+      return {width: element.offsetWidth, height: element.offsetHeight};

+

+    // All *Width and *Height properties give 0 on elements with display none,

+    // so enable the element temporarily

+    var els = element.style;

+    var originalVisibility = els.visibility;

+    var originalPosition = els.position;

+    var originalDisplay = els.display;

+    els.visibility = 'hidden';

+    els.position = 'absolute';

+    els.display = 'block';

+    var originalWidth = element.clientWidth;

+    var originalHeight = element.clientHeight;

+    els.display = originalDisplay;

+    els.position = originalPosition;

+    els.visibility = originalVisibility;

+    return {width: originalWidth, height: originalHeight};

+  },

+

+  makePositioned: function(element) {

+    element = $(element);

+    var pos = Element.getStyle(element, 'position');

+    if (pos == 'static' || !pos) {

+      element._madePositioned = true;

+      element.style.position = 'relative';

+      // Opera returns the offset relative to the positioning context, when an

+      // element is position relative but top and left have not been defined

+      if (window.opera) {

+        element.style.top = 0;

+        element.style.left = 0;

+      }

+    }

+    return element;

+  },

+

+  undoPositioned: function(element) {

+    element = $(element);

+    if (element._madePositioned) {

+      element._madePositioned = undefined;

+      element.style.position =

+        element.style.top =

+        element.style.left =

+        element.style.bottom =

+        element.style.right = '';

+    }

+    return element;

+  },

+

+  makeClipping: function(element) {

+    element = $(element);

+    if (element._overflow) return element;

+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';

+    if (element._overflow !== 'hidden')

+      element.style.overflow = 'hidden';

+    return element;

+  },

+

+  undoClipping: function(element) {

+    element = $(element);

+    if (!element._overflow) return element;

+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;

+    element._overflow = null;

+    return element;

+  },

+

+  cumulativeOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      element = element.offsetParent;

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  positionedOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      element = element.offsetParent;

+      if (element) {

+        if (element.tagName == 'BODY') break;

+        var p = Element.getStyle(element, 'position');

+        if (p !== 'static') break;

+      }

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  absolutize: function(element) {

+    element = $(element);

+    if (element.getStyle('position') == 'absolute') return;

+    // Position.prepare(); // To be done manually by Scripty when it needs it.

+

+    var offsets = element.positionedOffset();

+    var top     = offsets[1];

+    var left    = offsets[0];

+    var width   = element.clientWidth;

+    var height  = element.clientHeight;

+

+    element._originalLeft   = left - parseFloat(element.style.left  || 0);

+    element._originalTop    = top  - parseFloat(element.style.top || 0);

+    element._originalWidth  = element.style.width;

+    element._originalHeight = element.style.height;

+

+    element.style.position = 'absolute';

+    element.style.top    = top + 'px';

+    element.style.left   = left + 'px';

+    element.style.width  = width + 'px';

+    element.style.height = height + 'px';

+    return element;

+  },

+

+  relativize: function(element) {

+    element = $(element);

+    if (element.getStyle('position') == 'relative') return;

+    // Position.prepare(); // To be done manually by Scripty when it needs it.

+

+    element.style.position = 'relative';

+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);

+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

+

+    element.style.top    = top + 'px';

+    element.style.left   = left + 'px';

+    element.style.height = element._originalHeight;

+    element.style.width  = element._originalWidth;

+    return element;

+  },

+

+  cumulativeScrollOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.scrollTop  || 0;

+      valueL += element.scrollLeft || 0;

+      element = element.parentNode;

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  getOffsetParent: function(element) {

+    if (element.offsetParent) return $(element.offsetParent);

+    if (element == document.body) return $(element);

+

+    while ((element = element.parentNode) && element != document.body)

+      if (Element.getStyle(element, 'position') != 'static')

+        return $(element);

+

+    return $(document.body);

+  },

+

+  viewportOffset: function(forElement) {

+    var valueT = 0, valueL = 0;

+

+    var element = forElement;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+

+      // Safari fix

+      if (element.offsetParent == document.body &&

+        Element.getStyle(element, 'position') == 'absolute') break;

+

+    } while (element = element.offsetParent);

+

+    element = forElement;

+    do {

+      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {

+        valueT -= element.scrollTop  || 0;

+        valueL -= element.scrollLeft || 0;

+      }

+    } while (element = element.parentNode);

+

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  clonePosition: function(element, source) {

+    var options = Object.extend({

+      setLeft:    true,

+      setTop:     true,

+      setWidth:   true,

+      setHeight:  true,

+      offsetTop:  0,

+      offsetLeft: 0

+    }, arguments[2] || { });

+

+    // find page position of source

+    source = $(source);

+    var p = source.viewportOffset();

+

+    // find coordinate system to use

+    element = $(element);

+    var delta = [0, 0];

+    var parent = null;

+    // delta [0,0] will do fine with position: fixed elements,

+    // position:absolute needs offsetParent deltas

+    if (Element.getStyle(element, 'position') == 'absolute') {

+      parent = element.getOffsetParent();

+      delta = parent.viewportOffset();

+    }

+

+    // correct by body offsets (fixes Safari)

+    if (parent == document.body) {

+      delta[0] -= document.body.offsetLeft;

+      delta[1] -= document.body.offsetTop;

+    }

+

+    // set position

+    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';

+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';

+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';

+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';

+    return element;

+  }

+};

+

+Element.Methods.identify.counter = 1;

+

+Object.extend(Element.Methods, {

+  getElementsBySelector: Element.Methods.select,

+  childElements: Element.Methods.immediateDescendants

+});

+

+Element._attributeTranslations = {

+  write: {

+    names: {

+      className: 'class',

+      htmlFor:   'for'

+    },

+    values: { }

+  }

+};

+

+if (Prototype.Browser.Opera) {

+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(

+    function(proceed, element, style) {

+      switch (style) {

+        case 'left': case 'top': case 'right': case 'bottom':

+          if (proceed(element, 'position') === 'static') return null;

+        case 'height': case 'width':

+          // returns '0px' for hidden elements; we want it to return null

+          if (!Element.visible(element)) return null;

+

+          // returns the border-box dimensions rather than the content-box

+          // dimensions, so we subtract padding and borders from the value

+          var dim = parseInt(proceed(element, style), 10);

+

+          if (dim !== element['offset' + style.capitalize()])

+            return dim + 'px';

+

+          var properties;

+          if (style === 'height') {

+            properties = ['border-top-width', 'padding-top',

+             'padding-bottom', 'border-bottom-width'];

+          }

+          else {

+            properties = ['border-left-width', 'padding-left',

+             'padding-right', 'border-right-width'];

+          }

+          return properties.inject(dim, function(memo, property) {

+            var val = proceed(element, property);

+            return val === null ? memo : memo - parseInt(val, 10);

+          }) + 'px';

+        default: return proceed(element, style);

+      }

+    }

+  );

+

+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(

+    function(proceed, element, attribute) {

+      if (attribute === 'title') return element.title;

+      return proceed(element, attribute);

+    }

+  );

+}

+

+else if (Prototype.Browser.IE) {

+  // IE doesn't report offsets correctly for static elements, so we change them

+  // to "relative" to get the values, then change them back.

+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(

+    function(proceed, element) {

+      element = $(element);

+      var position = element.getStyle('position');

+      if (position !== 'static') return proceed(element);

+      element.setStyle({ position: 'relative' });

+      var value = proceed(element);

+      element.setStyle({ position: position });

+      return value;

+    }

+  );

+

+  $w('positionedOffset viewportOffset').each(function(method) {

+    Element.Methods[method] = Element.Methods[method].wrap(

+      function(proceed, element) {

+        element = $(element);

+        var position = element.getStyle('position');

+        if (position !== 'static') return proceed(element);

+        // Trigger hasLayout on the offset parent so that IE6 reports

+        // accurate offsetTop and offsetLeft values for position: fixed.

+        var offsetParent = element.getOffsetParent();

+        if (offsetParent && offsetParent.getStyle('position') === 'fixed')

+          offsetParent.setStyle({ zoom: 1 });

+        element.setStyle({ position: 'relative' });

+        var value = proceed(element);

+        element.setStyle({ position: position });

+        return value;

+      }

+    );

+  });

+

+  Element.Methods.getStyle = function(element, style) {

+    element = $(element);

+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();

+    var value = element.style[style];

+    if (!value && element.currentStyle) value = element.currentStyle[style];

+

+    if (style == 'opacity') {

+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))

+        if (value[1]) return parseFloat(value[1]) / 100;

+      return 1.0;

+    }

+

+    if (value == 'auto') {

+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))

+        return element['offset' + style.capitalize()] + 'px';

+      return null;

+    }

+    return value;

+  };

+

+  Element.Methods.setOpacity = function(element, value) {

+    function stripAlpha(filter){

+      return filter.replace(/alpha\([^\)]*\)/gi,'');

+    }

+    element = $(element);

+    var currentStyle = element.currentStyle;

+    if ((currentStyle && !currentStyle.hasLayout) ||

+      (!currentStyle && element.style.zoom == 'normal'))

+        element.style.zoom = 1;

+

+    var filter = element.getStyle('filter'), style = element.style;

+    if (value == 1 || value === '') {

+      (filter = stripAlpha(filter)) ?

+        style.filter = filter : style.removeAttribute('filter');

+      return element;

+    } else if (value < 0.00001) value = 0;

+    style.filter = stripAlpha(filter) +

+      'alpha(opacity=' + (value * 100) + ')';

+    return element;

+  };

+

+  Element._attributeTranslations = {

+    read: {

+      names: {

+        'class': 'className',

+        'for':   'htmlFor'

+      },

+      values: {

+        _getAttr: function(element, attribute) {

+          return element.getAttribute(attribute, 2);

+        },

+        _getAttrNode: function(element, attribute) {

+          var node = element.getAttributeNode(attribute);

+          return node ? node.value : "";

+        },

+        _getEv: function(element, attribute) {

+          attribute = element.getAttribute(attribute);

+          return attribute ? attribute.toString().slice(23, -2) : null;

+        },

+        _flag: function(element, attribute) {

+          return $(element).hasAttribute(attribute) ? attribute : null;

+        },

+        style: function(element) {

+          return element.style.cssText.toLowerCase();

+        },

+        title: function(element) {

+          return element.title;

+        }

+      }

+    }

+  };

+

+  Element._attributeTranslations.write = {

+    names: Object.extend({

+      cellpadding: 'cellPadding',

+      cellspacing: 'cellSpacing'

+    }, Element._attributeTranslations.read.names),

+    values: {

+      checked: function(element, value) {

+        element.checked = !!value;

+      },

+

+      style: function(element, value) {

+        element.style.cssText = value ? value : '';

+      }

+    }

+  };

+

+  Element._attributeTranslations.has = {};

+

+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +

+      'encType maxLength readOnly longDesc').each(function(attr) {

+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;

+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;

+  });

+

+  (function(v) {

+    Object.extend(v, {

+      href:        v._getAttr,

+      src:         v._getAttr,

+      type:        v._getAttr,

+      action:      v._getAttrNode,

+      disabled:    v._flag,

+      checked:     v._flag,

+      readonly:    v._flag,

+      multiple:    v._flag,

+      onload:      v._getEv,

+      onunload:    v._getEv,

+      onclick:     v._getEv,

+      ondblclick:  v._getEv,

+      onmousedown: v._getEv,

+      onmouseup:   v._getEv,

+      onmouseover: v._getEv,

+      onmousemove: v._getEv,

+      onmouseout:  v._getEv,

+      onfocus:     v._getEv,

+      onblur:      v._getEv,

+      onkeypress:  v._getEv,

+      onkeydown:   v._getEv,

+      onkeyup:     v._getEv,

+      onsubmit:    v._getEv,

+      onreset:     v._getEv,

+      onselect:    v._getEv,

+      onchange:    v._getEv

+    });

+  })(Element._attributeTranslations.read.values);

+}

+

+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {

+  Element.Methods.setOpacity = function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1) ? 0.999999 :

+      (value === '') ? '' : (value < 0.00001) ? 0 : value;

+    return element;

+  };

+}

+

+else if (Prototype.Browser.WebKit) {

+  Element.Methods.setOpacity = function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1 || value === '') ? '' :

+      (value < 0.00001) ? 0 : value;

+

+    if (value == 1)

+      if(element.tagName == 'IMG' && element.width) {

+        element.width++; element.width--;

+      } else try {

+        var n = document.createTextNode(' ');

+        element.appendChild(n);

+        element.removeChild(n);

+      } catch (e) { }

+

+    return element;

+  };

+

+  // Safari returns margins on body which is incorrect if the child is absolutely

+  // positioned.  For performance reasons, redefine Element#cumulativeOffset for

+  // KHTML/WebKit only.

+  Element.Methods.cumulativeOffset = function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      if (element.offsetParent == document.body)

+        if (Element.getStyle(element, 'position') == 'absolute') break;

+

+      element = element.offsetParent;

+    } while (element);

+

+    return Element._returnOffset(valueL, valueT);

+  };

+}

+

+if (Prototype.Browser.IE || Prototype.Browser.Opera) {

+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements

+  Element.Methods.update = function(element, content) {

+    element = $(element);

+

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) return element.update().insert(content);

+

+    content = Object.toHTML(content);

+    var tagName = element.tagName.toUpperCase();

+

+    if (tagName in Element._insertionTranslations.tags) {

+      $A(element.childNodes).each(function(node) { element.removeChild(node) });

+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())

+        .each(function(node) { element.appendChild(node) });

+    }

+    else element.innerHTML = content.stripScripts();

+

+    content.evalScripts.bind(content).defer();

+    return element;

+  };

+}

+

+if ('outerHTML' in document.createElement('div')) {

+  Element.Methods.replace = function(element, content) {

+    element = $(element);

+

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) {

+      element.parentNode.replaceChild(content, element);

+      return element;

+    }

+

+    content = Object.toHTML(content);

+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

+

+    if (Element._insertionTranslations.tags[tagName]) {

+      var nextSibling = element.next();

+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

+      parent.removeChild(element);

+      if (nextSibling)

+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });

+      else

+        fragments.each(function(node) { parent.appendChild(node) });

+    }

+    else element.outerHTML = content.stripScripts();

+

+    content.evalScripts.bind(content).defer();

+    return element;

+  };

+}

+

+Element._returnOffset = function(l, t) {

+  var result = [l, t];

+  result.left = l;

+  result.top = t;

+  return result;

+};

+

+Element._getContentFromAnonymousElement = function(tagName, html) {

+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];

+  if (t) {

+    div.innerHTML = t[0] + html + t[1];

+    t[2].times(function() { div = div.firstChild });

+  } else div.innerHTML = html;

+  return $A(div.childNodes);

+};

+

+Element._insertionTranslations = {

+  before: function(element, node) {

+    element.parentNode.insertBefore(node, element);

+  },

+  top: function(element, node) {

+    element.insertBefore(node, element.firstChild);

+  },

+  bottom: function(element, node) {

+    element.appendChild(node);

+  },

+  after: function(element, node) {

+    element.parentNode.insertBefore(node, element.nextSibling);

+  },

+  tags: {

+    TABLE:  ['<table>',                '</table>',                   1],

+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],

+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],

+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],

+    SELECT: ['<select>',               '</select>',                  1]

+  }

+};

+

+(function() {

+  Object.extend(this.tags, {

+    THEAD: this.tags.TBODY,

+    TFOOT: this.tags.TBODY,

+    TH:    this.tags.TD

+  });

+}).call(Element._insertionTranslations);

+

+Element.Methods.Simulated = {

+  hasAttribute: function(element, attribute) {

+    attribute = Element._attributeTranslations.has[attribute] || attribute;

+    var node = $(element).getAttributeNode(attribute);

+    return node && node.specified;

+  }

+};

+

+Element.Methods.ByTag = { };

+

+Object.extend(Element, Element.Methods);

+

+if (!Prototype.BrowserFeatures.ElementExtensions &&

+    document.createElement('div').__proto__) {

+  window.HTMLElement = { };

+  window.HTMLElement.prototype = document.createElement('div').__proto__;

+  Prototype.BrowserFeatures.ElementExtensions = true;

+}

+

+Element.extend = (function() {

+  if (Prototype.BrowserFeatures.SpecificElementExtensions)

+    return Prototype.K;

+

+  var Methods = { }, ByTag = Element.Methods.ByTag;

+

+  var extend = Object.extend(function(element) {

+    if (!element || element._extendedByPrototype ||

+        element.nodeType != 1 || element == window) return element;

+

+    var methods = Object.clone(Methods),

+      tagName = element.tagName, property, value;

+

+    // extend methods for specific tags

+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

+

+    for (property in methods) {

+      value = methods[property];

+      if (Object.isFunction(value) && !(property in element))

+        element[property] = value.methodize();

+    }

+

+    element._extendedByPrototype = Prototype.emptyFunction;

+    return element;

+

+  }, {

+    refresh: function() {

+      // extend methods for all tags (Safari doesn't need this)

+      if (!Prototype.BrowserFeatures.ElementExtensions) {

+        Object.extend(Methods, Element.Methods);

+        Object.extend(Methods, Element.Methods.Simulated);

+      }

+    }

+  });

+

+  extend.refresh();

+  return extend;

+})();

+

+Element.hasAttribute = function(element, attribute) {

+  if (element.hasAttribute) return element.hasAttribute(attribute);

+  return Element.Methods.Simulated.hasAttribute(element, attribute);

+};

+

+Element.addMethods = function(methods) {

+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

+

+  if (!methods) {

+    Object.extend(Form, Form.Methods);

+    Object.extend(Form.Element, Form.Element.Methods);

+    Object.extend(Element.Methods.ByTag, {

+      "FORM":     Object.clone(Form.Methods),

+      "INPUT":    Object.clone(Form.Element.Methods),

+      "SELECT":   Object.clone(Form.Element.Methods),

+      "TEXTAREA": Object.clone(Form.Element.Methods)

+    });

+  }

+

+  if (arguments.length == 2) {

+    var tagName = methods;

+    methods = arguments[1];

+  }

+

+  if (!tagName) Object.extend(Element.Methods, methods || { });

+  else {

+    if (Object.isArray(tagName)) tagName.each(extend);

+    else extend(tagName);

+  }

+

+  function extend(tagName) {

+    tagName = tagName.toUpperCase();

+    if (!Element.Methods.ByTag[tagName])

+      Element.Methods.ByTag[tagName] = { };

+    Object.extend(Element.Methods.ByTag[tagName], methods);

+  }

+

+  function copy(methods, destination, onlyIfAbsent) {

+    onlyIfAbsent = onlyIfAbsent || false;

+    for (var property in methods) {

+      var value = methods[property];

+      if (!Object.isFunction(value)) continue;

+      if (!onlyIfAbsent || !(property in destination))

+        destination[property] = value.methodize();

+    }

+  }

+

+  function findDOMClass(tagName) {

+    var klass;

+    var trans = {

+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",

+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",

+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",

+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",

+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":

+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":

+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":

+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":

+      "FrameSet", "IFRAME": "IFrame"

+    };

+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';

+    if (window[klass]) return window[klass];

+    klass = 'HTML' + tagName + 'Element';

+    if (window[klass]) return window[klass];

+    klass = 'HTML' + tagName.capitalize() + 'Element';

+    if (window[klass]) return window[klass];

+

+    window[klass] = { };

+    window[klass].prototype = document.createElement(tagName).__proto__;

+    return window[klass];

+  }

+

+  if (F.ElementExtensions) {

+    copy(Element.Methods, HTMLElement.prototype);

+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);

+  }

+

+  if (F.SpecificElementExtensions) {

+    for (var tag in Element.Methods.ByTag) {

+      var klass = findDOMClass(tag);

+      if (Object.isUndefined(klass)) continue;

+      copy(T[tag], klass.prototype);

+    }

+  }

+

+  Object.extend(Element, Element.Methods);

+  delete Element.ByTag;

+

+  if (Element.extend.refresh) Element.extend.refresh();

+  Element.cache = { };

+};

+

+document.viewport = {

+  getDimensions: function() {

+    var dimensions = { };

+    var B = Prototype.Browser;

+    $w('width height').each(function(d) {

+      var D = d.capitalize();

+      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :

+        (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];

+    });

+    return dimensions;

+  },

+

+  getWidth: function() {

+    return this.getDimensions().width;

+  },

+

+  getHeight: function() {

+    return this.getDimensions().height;

+  },

+

+  getScrollOffsets: function() {

+    return Element._returnOffset(

+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,

+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);

+  }

+};

+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,

+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style

+ * license.  Please see http://www.yui-ext.com/ for more information. */

+

+var Selector = Class.create({

+  initialize: function(expression) {

+    this.expression = expression.strip();

+    this.compileMatcher();

+  },

+

+  shouldUseXPath: function() {

+    if (!Prototype.BrowserFeatures.XPath) return false;

+

+    var e = this.expression;

+

+    // Safari 3 chokes on :*-of-type and :empty

+    if (Prototype.Browser.WebKit &&

+     (e.include("-of-type") || e.include(":empty")))

+      return false;

+

+    // XPath can't do namespaced attributes, nor can it read

+    // the "checked" property from DOM nodes

+    if ((/(\[[\w-]*?:|:checked)/).test(this.expression))

+      return false;

+

+    return true;

+  },

+

+  compileMatcher: function() {

+    if (this.shouldUseXPath())

+      return this.compileXPathMatcher();

+

+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,

+        c = Selector.criteria, le, p, m;

+

+    if (Selector._cache[e]) {

+      this.matcher = Selector._cache[e];

+      return;

+    }

+

+    this.matcher = ["this.matcher = function(root) {",

+                    "var r = root, h = Selector.handlers, c = false, n;"];

+

+    while (e && le != e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        p = ps[i];

+        if (m = e.match(p)) {

+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :

+    	      new Template(c[i]).evaluate(m));

+          e = e.replace(m[0], '');

+          break;

+        }

+      }

+    }

+

+    this.matcher.push("return h.unique(n);\n}");

+    eval(this.matcher.join('\n'));

+    Selector._cache[this.expression] = this.matcher;

+  },

+

+  compileXPathMatcher: function() {

+    var e = this.expression, ps = Selector.patterns,

+        x = Selector.xpath, le, m;

+

+    if (Selector._cache[e]) {

+      this.xpath = Selector._cache[e]; return;

+    }

+

+    this.matcher = ['.//*'];

+    while (e && le != e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        if (m = e.match(ps[i])) {

+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :

+            new Template(x[i]).evaluate(m));

+          e = e.replace(m[0], '');

+          break;

+        }

+      }

+    }

+

+    this.xpath = this.matcher.join('');

+    Selector._cache[this.expression] = this.xpath;

+  },

+

+  findElements: function(root) {

+    root = root || document;

+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);

+    return this.matcher(root);

+  },

+

+  match: function(element) {

+    this.tokens = [];

+

+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;

+    var le, p, m;

+

+    while (e && le !== e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        p = ps[i];

+        if (m = e.match(p)) {

+          // use the Selector.assertions methods unless the selector

+          // is too complex.

+          if (as[i]) {

+            this.tokens.push([i, Object.clone(m)]);

+            e = e.replace(m[0], '');

+          } else {

+            // reluctantly do a document-wide search

+            // and look for a match in the array

+            return this.findElements(document).include(element);

+          }

+        }

+      }

+    }

+

+    var match = true, name, matches;

+    for (var i = 0, token; token = this.tokens[i]; i++) {

+      name = token[0], matches = token[1];

+      if (!Selector.assertions[name](element, matches)) {

+        match = false; break;

+      }

+    }

+

+    return match;

+  },

+

+  toString: function() {

+    return this.expression;

+  },

+

+  inspect: function() {

+    return "#<Selector:" + this.expression.inspect() + ">";

+  }

+});

+

+Object.extend(Selector, {

+  _cache: { },

+

+  xpath: {

+    descendant:   "//*",

+    child:        "/*",

+    adjacent:     "/following-sibling::*[1]",

+    laterSibling: '/following-sibling::*',

+    tagName:      function(m) {

+      if (m[1] == '*') return '';

+      return "[local-name()='" + m[1].toLowerCase() +

+             "' or local-name()='" + m[1].toUpperCase() + "']";

+    },

+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",

+    id:           "[@id='#{1}']",

+    attrPresence: function(m) {

+      m[1] = m[1].toLowerCase();

+      return new Template("[@#{1}]").evaluate(m);

+    },

+    attr: function(m) {

+      m[1] = m[1].toLowerCase();

+      m[3] = m[5] || m[6];

+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);

+    },

+    pseudo: function(m) {

+      var h = Selector.xpath.pseudos[m[1]];

+      if (!h) return '';

+      if (Object.isFunction(h)) return h(m);

+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);

+    },

+    operators: {

+      '=':  "[@#{1}='#{3}']",

+      '!=': "[@#{1}!='#{3}']",

+      '^=': "[starts-with(@#{1}, '#{3}')]",

+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",

+      '*=': "[contains(@#{1}, '#{3}')]",

+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",

+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"

+    },

+    pseudos: {

+      'first-child': '[not(preceding-sibling::*)]',

+      'last-child':  '[not(following-sibling::*)]',

+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',

+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",

+      'checked':     "[@checked]",

+      'disabled':    "[@disabled]",

+      'enabled':     "[not(@disabled)]",

+      'not': function(m) {

+        var e = m[6], p = Selector.patterns,

+            x = Selector.xpath, le, v;

+

+        var exclusion = [];

+        while (e && le != e && (/\S/).test(e)) {

+          le = e;

+          for (var i in p) {

+            if (m = e.match(p[i])) {

+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);

+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");

+              e = e.replace(m[0], '');

+              break;

+            }

+          }

+        }

+        return "[not(" + exclusion.join(" and ") + ")]";

+      },

+      'nth-child':      function(m) {

+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);

+      },

+      'nth-last-child': function(m) {

+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);

+      },

+      'nth-of-type':    function(m) {

+        return Selector.xpath.pseudos.nth("position() ", m);

+      },

+      'nth-last-of-type': function(m) {

+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);

+      },

+      'first-of-type':  function(m) {

+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);

+      },

+      'last-of-type':   function(m) {

+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);

+      },

+      'only-of-type':   function(m) {

+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);

+      },

+      nth: function(fragment, m) {

+        var mm, formula = m[6], predicate;

+        if (formula == 'even') formula = '2n+0';

+        if (formula == 'odd')  formula = '2n+1';

+        if (mm = formula.match(/^(\d+)$/)) // digit only

+          return '[' + fragment + "= " + mm[1] + ']';

+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b

+          if (mm[1] == "-") mm[1] = -1;

+          var a = mm[1] ? Number(mm[1]) : 1;

+          var b = mm[2] ? Number(mm[2]) : 0;

+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +

+          "((#{fragment} - #{b}) div #{a} >= 0)]";

+          return new Template(predicate).evaluate({

+            fragment: fragment, a: a, b: b });

+        }

+      }

+    }

+  },

+

+  criteria: {

+    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',

+    className:    'n = h.className(n, r, "#{1}", c);    c = false;',

+    id:           'n = h.id(n, r, "#{1}", c);           c = false;',

+    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',

+    attr: function(m) {

+      m[3] = (m[5] || m[6]);

+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);

+    },

+    pseudo: function(m) {

+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');

+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);

+    },

+    descendant:   'c = "descendant";',

+    child:        'c = "child";',

+    adjacent:     'c = "adjacent";',

+    laterSibling: 'c = "laterSibling";'

+  },

+

+  patterns: {

+    // combinators must be listed first

+    // (and descendant needs to be last combinator)

+    laterSibling: /^\s*~\s*/,

+    child:        /^\s*>\s*/,

+    adjacent:     /^\s*\+\s*/,

+    descendant:   /^\s/,

+

+    // selectors follow

+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,

+    id:           /^#([\w\-\*]+)(\b|$)/,

+    className:    /^\.([\w\-\*]+)(\b|$)/,

+    pseudo:

+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,

+    attrPresence: /^\[([\w]+)\]/,

+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/

+  },

+

+  // for Selector.match and Element#match

+  assertions: {

+    tagName: function(element, matches) {

+      return matches[1].toUpperCase() == element.tagName.toUpperCase();

+    },

+

+    className: function(element, matches) {

+      return Element.hasClassName(element, matches[1]);

+    },

+

+    id: function(element, matches) {

+      return element.id === matches[1];

+    },

+

+    attrPresence: function(element, matches) {

+      return Element.hasAttribute(element, matches[1]);

+    },

+

+    attr: function(element, matches) {

+      var nodeValue = Element.readAttribute(element, matches[1]);

+      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);

+    }

+  },

+

+  handlers: {

+    // UTILITY FUNCTIONS

+    // joins two collections

+    concat: function(a, b) {

+      for (var i = 0, node; node = b[i]; i++)

+        a.push(node);

+      return a;

+    },

+

+    // marks an array of nodes for counting

+    mark: function(nodes) {

+      var _true = Prototype.emptyFunction;

+      for (var i = 0, node; node = nodes[i]; i++)

+        node._countedByPrototype = _true;

+      return nodes;

+    },

+

+    unmark: function(nodes) {

+      for (var i = 0, node; node = nodes[i]; i++)

+        node._countedByPrototype = undefined;

+      return nodes;

+    },

+

+    // mark each child node with its position (for nth calls)

+    // "ofType" flag indicates whether we're indexing for nth-of-type

+    // rather than nth-child

+    index: function(parentNode, reverse, ofType) {

+      parentNode._countedByPrototype = Prototype.emptyFunction;

+      if (reverse) {

+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {

+          var node = nodes[i];

+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;

+        }

+      } else {

+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)

+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;

+      }

+    },

+

+    // filters out duplicates and extends all nodes

+    unique: function(nodes) {

+      if (nodes.length == 0) return nodes;

+      var results = [], n;

+      for (var i = 0, l = nodes.length; i < l; i++)

+        if (!(n = nodes[i])._countedByPrototype) {

+          n._countedByPrototype = Prototype.emptyFunction;

+          results.push(Element.extend(n));

+        }

+      return Selector.handlers.unmark(results);

+    },

+

+    // COMBINATOR FUNCTIONS

+    descendant: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        h.concat(results, node.getElementsByTagName('*'));

+      return results;

+    },

+

+    child: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        for (var j = 0, child; child = node.childNodes[j]; j++)

+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);

+      }

+      return results;

+    },

+

+    adjacent: function(nodes) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        var next = this.nextElementSibling(node);

+        if (next) results.push(next);

+      }

+      return results;

+    },

+

+    laterSibling: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        h.concat(results, Element.nextSiblings(node));

+      return results;

+    },

+

+    nextElementSibling: function(node) {

+      while (node = node.nextSibling)

+	      if (node.nodeType == 1) return node;

+      return null;

+    },

+

+    previousElementSibling: function(node) {

+      while (node = node.previousSibling)

+        if (node.nodeType == 1) return node;

+      return null;

+    },

+

+    // TOKEN FUNCTIONS

+    tagName: function(nodes, root, tagName, combinator) {

+      var uTagName = tagName.toUpperCase();

+      var results = [], h = Selector.handlers;

+      if (nodes) {

+        if (combinator) {

+          // fastlane for ordinary descendant combinators

+          if (combinator == "descendant") {

+            for (var i = 0, node; node = nodes[i]; i++)

+              h.concat(results, node.getElementsByTagName(tagName));

+            return results;

+          } else nodes = this[combinator](nodes);

+          if (tagName == "*") return nodes;

+        }

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node.tagName.toUpperCase() === uTagName) results.push(node);

+        return results;

+      } else return root.getElementsByTagName(tagName);

+    },

+

+    id: function(nodes, root, id, combinator) {

+      var targetNode = $(id), h = Selector.handlers;

+      if (!targetNode) return [];

+      if (!nodes && root == document) return [targetNode];

+      if (nodes) {

+        if (combinator) {

+          if (combinator == 'child') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (targetNode.parentNode == node) return [targetNode];

+          } else if (combinator == 'descendant') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (Element.descendantOf(targetNode, node)) return [targetNode];

+          } else if (combinator == 'adjacent') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (Selector.handlers.previousElementSibling(targetNode) == node)

+                return [targetNode];

+          } else nodes = h[combinator](nodes);

+        }

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node == targetNode) return [targetNode];

+        return [];

+      }

+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];

+    },

+

+    className: function(nodes, root, className, combinator) {

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      return Selector.handlers.byClassName(nodes, root, className);

+    },

+

+    byClassName: function(nodes, root, className) {

+      if (!nodes) nodes = Selector.handlers.descendant([root]);

+      var needle = ' ' + className + ' ';

+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {

+        nodeClassName = node.className;

+        if (nodeClassName.length == 0) continue;

+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))

+          results.push(node);

+      }

+      return results;

+    },

+

+    attrPresence: function(nodes, root, attr, combinator) {

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      var results = [];

+      for (var i = 0, node; node = nodes[i]; i++)

+        if (Element.hasAttribute(node, attr)) results.push(node);

+      return results;

+    },

+

+    attr: function(nodes, root, attr, value, operator, combinator) {

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      var handler = Selector.operators[operator], results = [];

+      for (var i = 0, node; node = nodes[i]; i++) {

+        var nodeValue = Element.readAttribute(node, attr);

+        if (nodeValue === null) continue;

+        if (handler(nodeValue, value)) results.push(node);

+      }

+      return results;

+    },

+

+    pseudo: function(nodes, name, value, root, combinator) {

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      return Selector.pseudos[name](nodes, value, root);

+    }

+  },

+

+  pseudos: {

+    'first-child': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        if (Selector.handlers.previousElementSibling(node)) continue;

+          results.push(node);

+      }

+      return results;

+    },

+    'last-child': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        if (Selector.handlers.nextElementSibling(node)) continue;

+          results.push(node);

+      }

+      return results;

+    },

+    'only-child': function(nodes, value, root) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))

+          results.push(node);

+      return results;

+    },

+    'nth-child':        function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root);

+    },

+    'nth-last-child':   function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, true);

+    },

+    'nth-of-type':      function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, false, true);

+    },

+    'nth-last-of-type': function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, true, true);

+    },

+    'first-of-type':    function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, "1", root, false, true);

+    },

+    'last-of-type':     function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, "1", root, true, true);

+    },

+    'only-of-type':     function(nodes, formula, root) {

+      var p = Selector.pseudos;

+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);

+    },

+

+    // handles the an+b logic

+    getIndices: function(a, b, total) {

+      if (a == 0) return b > 0 ? [b] : [];

+      return $R(1, total).inject([], function(memo, i) {

+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);

+        return memo;

+      });

+    },

+

+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type

+    nth: function(nodes, formula, root, reverse, ofType) {

+      if (nodes.length == 0) return [];

+      if (formula == 'even') formula = '2n+0';

+      if (formula == 'odd')  formula = '2n+1';

+      var h = Selector.handlers, results = [], indexed = [], m;

+      h.mark(nodes);

+      for (var i = 0, node; node = nodes[i]; i++) {

+        if (!node.parentNode._countedByPrototype) {

+          h.index(node.parentNode, reverse, ofType);

+          indexed.push(node.parentNode);

+        }

+      }

+      if (formula.match(/^\d+$/)) { // just a number

+        formula = Number(formula);

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node.nodeIndex == formula) results.push(node);

+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b

+        if (m[1] == "-") m[1] = -1;

+        var a = m[1] ? Number(m[1]) : 1;

+        var b = m[2] ? Number(m[2]) : 0;

+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);

+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {

+          for (var j = 0; j < l; j++)

+            if (node.nodeIndex == indices[j]) results.push(node);

+        }

+      }

+      h.unmark(nodes);

+      h.unmark(indexed);

+      return results;

+    },

+

+    'empty': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        // IE treats comments as element nodes

+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;

+        results.push(node);

+      }

+      return results;

+    },

+

+    'not': function(nodes, selector, root) {

+      var h = Selector.handlers, selectorType, m;

+      var exclusions = new Selector(selector).findElements(root);

+      h.mark(exclusions);

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!node._countedByPrototype) results.push(node);

+      h.unmark(exclusions);

+      return results;

+    },

+

+    'enabled': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!node.disabled) results.push(node);

+      return results;

+    },

+

+    'disabled': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (node.disabled) results.push(node);

+      return results;

+    },

+

+    'checked': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (node.checked) results.push(node);

+      return results;

+    }

+  },

+

+  operators: {

+    '=':  function(nv, v) { return nv == v; },

+    '!=': function(nv, v) { return nv != v; },

+    '^=': function(nv, v) { return nv.startsWith(v); },

+    '$=': function(nv, v) { return nv.endsWith(v); },

+    '*=': function(nv, v) { return nv.include(v); },

+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },

+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }

+  },

+

+  split: function(expression) {

+    var expressions = [];

+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {

+      expressions.push(m[1].strip());

+    });

+    return expressions;

+  },

+

+  matchElements: function(elements, expression) {

+    var matches = $$(expression), h = Selector.handlers;

+    h.mark(matches);

+    for (var i = 0, results = [], element; element = elements[i]; i++)

+      if (element._countedByPrototype) results.push(element);

+    h.unmark(matches);

+    return results;

+  },

+

+  findElement: function(elements, expression, index) {

+    if (Object.isNumber(expression)) {

+      index = expression; expression = false;

+    }

+    return Selector.matchElements(elements, expression || '*')[index || 0];

+  },

+

+  findChildElements: function(element, expressions) {

+    expressions = Selector.split(expressions.join(','));

+    var results = [], h = Selector.handlers;

+    for (var i = 0, l = expressions.length, selector; i < l; i++) {

+      selector = new Selector(expressions[i].strip());

+      h.concat(results, selector.findElements(element));

+    }

+    return (l > 1) ? h.unique(results) : results;

+  }

+});

+

+if (Prototype.Browser.IE) {

+  Object.extend(Selector.handlers, {

+    // IE returns comment nodes on getElementsByTagName("*").

+    // Filter them out.

+    concat: function(a, b) {

+      for (var i = 0, node; node = b[i]; i++)

+        if (node.tagName !== "!") a.push(node);

+      return a;

+    },

+

+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.

+    unmark: function(nodes) {

+      for (var i = 0, node; node = nodes[i]; i++)

+        node.removeAttribute('_countedByPrototype');

+      return nodes;

+    }

+  });

+}

+

+function $$() {

+  return Selector.findChildElements(document, $A(arguments));

+}

+var Form = {

+  reset: function(form) {

+    $(form).reset();

+    return form;

+  },

+

+  serializeElements: function(elements, options) {

+    if (typeof options != 'object') options = { hash: !!options };

+    else if (Object.isUndefined(options.hash)) options.hash = true;

+    var key, value, submitted = false, submit = options.submit;

+

+    var data = elements.inject({ }, function(result, element) {

+      if (!element.disabled && element.name) {

+        key = element.name; value = $(element).getValue();

+        if (value != null && (element.type != 'submit' || (!submitted &&

+            submit !== false && (!submit || key == submit) && (submitted = true)))) {

+          if (key in result) {

+            // a key is already present; construct an array of values

+            if (!Object.isArray(result[key])) result[key] = [result[key]];

+            result[key].push(value);

+          }

+          else result[key] = value;

+        }

+      }

+      return result;

+    });

+

+    return options.hash ? data : Object.toQueryString(data);

+  }

+};

+

+Form.Methods = {

+  serialize: function(form, options) {

+    return Form.serializeElements(Form.getElements(form), options);

+  },

+

+  getElements: function(form) {

+    return $A($(form).getElementsByTagName('*')).inject([],

+      function(elements, child) {

+        if (Form.Element.Serializers[child.tagName.toLowerCase()])

+          elements.push(Element.extend(child));

+        return elements;

+      }

+    );

+  },

+

+  getInputs: function(form, typeName, name) {

+    form = $(form);

+    var inputs = form.getElementsByTagName('input');

+

+    if (!typeName && !name) return $A(inputs).map(Element.extend);

+

+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {

+      var input = inputs[i];

+      if ((typeName && input.type != typeName) || (name && input.name != name))

+        continue;

+      matchingInputs.push(Element.extend(input));

+    }

+

+    return matchingInputs;

+  },

+

+  disable: function(form) {

+    form = $(form);

+    Form.getElements(form).invoke('disable');

+    return form;

+  },

+

+  enable: function(form) {

+    form = $(form);

+    Form.getElements(form).invoke('enable');

+    return form;

+  },

+

+  findFirstElement: function(form) {

+    var elements = $(form).getElements().findAll(function(element) {

+      return 'hidden' != element.type && !element.disabled;

+    });

+    var firstByIndex = elements.findAll(function(element) {

+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;

+    }).sortBy(function(element) { return element.tabIndex }).first();

+

+    return firstByIndex ? firstByIndex : elements.find(function(element) {

+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());

+    });

+  },

+

+  focusFirstElement: function(form) {

+    form = $(form);

+    form.findFirstElement().activate();

+    return form;

+  },

+

+  request: function(form, options) {

+    form = $(form), options = Object.clone(options || { });

+

+    var params = options.parameters, action = form.readAttribute('action') || '';

+    if (action.blank()) action = window.location.href;

+    options.parameters = form.serialize(true);

+

+    if (params) {

+      if (Object.isString(params)) params = params.toQueryParams();

+      Object.extend(options.parameters, params);

+    }

+

+    if (form.hasAttribute('method') && !options.method)

+      options.method = form.method;

+

+    return new Ajax.Request(action, options);

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+Form.Element = {

+  focus: function(element) {

+    $(element).focus();

+    return element;

+  },

+

+  select: function(element) {

+    $(element).select();

+    return element;

+  }

+};

+

+Form.Element.Methods = {

+  serialize: function(element) {

+    element = $(element);

+    if (!element.disabled && element.name) {

+      var value = element.getValue();

+      if (value != undefined) {

+        var pair = { };

+        pair[element.name] = value;

+        return Object.toQueryString(pair);

+      }

+    }

+    return '';

+  },

+

+  getValue: function(element) {

+    element = $(element);

+    var method = element.tagName.toLowerCase();

+    return Form.Element.Serializers[method](element);

+  },

+

+  setValue: function(element, value) {

+    element = $(element);

+    var method = element.tagName.toLowerCase();

+    Form.Element.Serializers[method](element, value);

+    return element;

+  },

+

+  clear: function(element) {

+    $(element).value = '';

+    return element;

+  },

+

+  present: function(element) {

+    return $(element).value != '';

+  },

+

+  activate: function(element) {

+    element = $(element);

+    try {

+      element.focus();

+      if (element.select && (element.tagName.toLowerCase() != 'input' ||

+          !['button', 'reset', 'submit'].include(element.type)))

+        element.select();

+    } catch (e) { }

+    return element;

+  },

+

+  disable: function(element) {

+    element = $(element);

+    element.blur();

+    element.disabled = true;

+    return element;

+  },

+

+  enable: function(element) {

+    element = $(element);

+    element.disabled = false;

+    return element;

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+var Field = Form.Element;

+var $F = Form.Element.Methods.getValue;

+

+/*--------------------------------------------------------------------------*/

+

+Form.Element.Serializers = {

+  input: function(element, value) {

+    switch (element.type.toLowerCase()) {

+      case 'checkbox':

+      case 'radio':

+        return Form.Element.Serializers.inputSelector(element, value);

+      default:

+        return Form.Element.Serializers.textarea(element, value);

+    }

+  },

+

+  inputSelector: function(element, value) {

+    if (Object.isUndefined(value)) return element.checked ? element.value : null;

+    else element.checked = !!value;

+  },

+

+  textarea: function(element, value) {

+    if (Object.isUndefined(value)) return element.value;

+    else element.value = value;

+  },

+

+  select: function(element, index) {

+    if (Object.isUndefined(index))

+      return this[element.type == 'select-one' ?

+        'selectOne' : 'selectMany'](element);

+    else {

+      var opt, value, single = !Object.isArray(index);

+      for (var i = 0, length = element.length; i < length; i++) {

+        opt = element.options[i];

+        value = this.optionValue(opt);

+        if (single) {

+          if (value == index) {

+            opt.selected = true;

+            return;

+          }

+        }

+        else opt.selected = index.include(value);

+      }

+    }

+  },

+

+  selectOne: function(element) {

+    var index = element.selectedIndex;

+    return index >= 0 ? this.optionValue(element.options[index]) : null;

+  },

+

+  selectMany: function(element) {

+    var values, length = element.length;

+    if (!length) return null;

+

+    for (var i = 0, values = []; i < length; i++) {

+      var opt = element.options[i];

+      if (opt.selected) values.push(this.optionValue(opt));

+    }

+    return values;

+  },

+

+  optionValue: function(opt) {

+    // extend element because hasAttribute may not be native

+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {

+  initialize: function($super, element, frequency, callback) {

+    $super(callback, frequency);

+    this.element   = $(element);

+    this.lastValue = this.getValue();

+  },

+

+  execute: function() {

+    var value = this.getValue();

+    if (Object.isString(this.lastValue) && Object.isString(value) ?

+        this.lastValue != value : String(this.lastValue) != String(value)) {

+      this.callback(this.element, value);

+      this.lastValue = value;

+    }

+  }

+});

+

+Form.Element.Observer = Class.create(Abstract.TimedObserver, {

+  getValue: function() {

+    return Form.Element.getValue(this.element);

+  }

+});

+

+Form.Observer = Class.create(Abstract.TimedObserver, {

+  getValue: function() {

+    return Form.serialize(this.element);

+  }

+});

+

+/*--------------------------------------------------------------------------*/

+

+Abstract.EventObserver = Class.create({

+  initialize: function(element, callback) {

+    this.element  = $(element);

+    this.callback = callback;

+

+    this.lastValue = this.getValue();

+    if (this.element.tagName.toLowerCase() == 'form')

+      this.registerFormCallbacks();

+    else

+      this.registerCallback(this.element);

+  },

+

+  onElementEvent: function() {

+    var value = this.getValue();

+    if (this.lastValue != value) {

+      this.callback(this.element, value);

+      this.lastValue = value;

+    }

+  },

+

+  registerFormCallbacks: function() {

+    Form.getElements(this.element).each(this.registerCallback, this);

+  },

+

+  registerCallback: function(element) {

+    if (element.type) {

+      switch (element.type.toLowerCase()) {

+        case 'checkbox':

+        case 'radio':

+          Event.observe(element, 'click', this.onElementEvent.bind(this));

+          break;

+        default:

+          Event.observe(element, 'change', this.onElementEvent.bind(this));

+          break;

+      }

+    }

+  }

+});

+

+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {

+  getValue: function() {

+    return Form.Element.getValue(this.element);

+  }

+});

+

+Form.EventObserver = Class.create(Abstract.EventObserver, {

+  getValue: function() {

+    return Form.serialize(this.element);

+  }

+});

+if (!window.Event) var Event = { };

+

+Object.extend(Event, {

+  KEY_BACKSPACE: 8,

+  KEY_TAB:       9,

+  KEY_RETURN:   13,

+  KEY_ESC:      27,

+  KEY_LEFT:     37,

+  KEY_UP:       38,

+  KEY_RIGHT:    39,

+  KEY_DOWN:     40,

+  KEY_DELETE:   46,

+  KEY_HOME:     36,

+  KEY_END:      35,

+  KEY_PAGEUP:   33,

+  KEY_PAGEDOWN: 34,

+  KEY_INSERT:   45,

+

+  cache: { },

+

+  relatedTarget: function(event) {

+    var element;

+    switch(event.type) {

+      case 'mouseover': element = event.fromElement; break;

+      case 'mouseout':  element = event.toElement;   break;

+      default: return null;

+    }

+    return Element.extend(element);

+  }

+});

+

+Event.Methods = (function() {

+  var isButton;

+

+  if (Prototype.Browser.IE) {

+    var buttonMap = { 0: 1, 1: 4, 2: 2 };

+    isButton = function(event, code) {

+      return event.button == buttonMap[code];

+    };

+

+  } else if (Prototype.Browser.WebKit) {

+    isButton = function(event, code) {

+      switch (code) {

+        case 0: return event.which == 1 && !event.metaKey;

+        case 1: return event.which == 1 && event.metaKey;

+        default: return false;

+      }

+    };

+

+  } else {

+    isButton = function(event, code) {

+      return event.which ? (event.which === code + 1) : (event.button === code);

+    };

+  }

+

+  return {

+    isLeftClick:   function(event) { return isButton(event, 0) },

+    isMiddleClick: function(event) { return isButton(event, 1) },

+    isRightClick:  function(event) { return isButton(event, 2) },

+

+    element: function(event) {

+      var node = Event.extend(event).target;

+      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);

+    },

+

+    findElement: function(event, expression) {

+      var element = Event.element(event);

+      if (!expression) return element;

+      var elements = [element].concat(element.ancestors());

+      return Selector.findElement(elements, expression, 0);

+    },

+

+    pointer: function(event) {

+      return {

+        x: event.pageX || (event.clientX +

+          (document.documentElement.scrollLeft || document.body.scrollLeft)),

+        y: event.pageY || (event.clientY +

+          (document.documentElement.scrollTop || document.body.scrollTop))

+      };

+    },

+

+    pointerX: function(event) { return Event.pointer(event).x },

+    pointerY: function(event) { return Event.pointer(event).y },

+

+    stop: function(event) {

+      Event.extend(event);

+      event.preventDefault();

+      event.stopPropagation();

+      event.stopped = true;

+    }

+  };

+})();

+

+Event.extend = (function() {

+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {

+    m[name] = Event.Methods[name].methodize();

+    return m;

+  });

+

+  if (Prototype.Browser.IE) {

+    Object.extend(methods, {

+      stopPropagation: function() { this.cancelBubble = true },

+      preventDefault:  function() { this.returnValue = false },

+      inspect: function() { return "[object Event]" }

+    });

+

+    return function(event) {

+      if (!event) return false;

+      if (event._extendedByPrototype) return event;

+

+      event._extendedByPrototype = Prototype.emptyFunction;

+      var pointer = Event.pointer(event);

+      Object.extend(event, {

+        target: event.srcElement,

+        relatedTarget: Event.relatedTarget(event),

+        pageX:  pointer.x,

+        pageY:  pointer.y

+      });

+      return Object.extend(event, methods);

+    };

+

+  } else {

+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;

+    Object.extend(Event.prototype, methods);

+    return Prototype.K;

+  }

+})();

+

+Object.extend(Event, (function() {

+  var cache = Event.cache;

+

+  function getEventID(element) {

+    if (element._prototypeEventID) return element._prototypeEventID[0];

+    arguments.callee.id = arguments.callee.id || 1;

+    return element._prototypeEventID = [++arguments.callee.id];

+  }

+

+  function getDOMEventName(eventName) {

+    if (eventName && eventName.include(':')) return "dataavailable";

+    return eventName;

+  }

+

+  function getCacheForID(id) {

+    return cache[id] = cache[id] || { };

+  }

+

+  function getWrappersForEventName(id, eventName) {

+    var c = getCacheForID(id);

+    return c[eventName] = c[eventName] || [];

+  }

+

+  function createWrapper(element, eventName, handler) {

+    var id = getEventID(element);

+    var c = getWrappersForEventName(id, eventName);

+    if (c.pluck("handler").include(handler)) return false;

+

+    var wrapper = function(event) {

+      if (!Event || !Event.extend ||

+        (event.eventName && event.eventName != eventName))

+          return false;

+

+      Event.extend(event);

+      handler.call(element, event);

+    };

+

+    wrapper.handler = handler;

+    c.push(wrapper);

+    return wrapper;

+  }

+

+  function findWrapper(id, eventName, handler) {

+    var c = getWrappersForEventName(id, eventName);

+    return c.find(function(wrapper) { return wrapper.handler == handler });

+  }

+

+  function destroyWrapper(id, eventName, handler) {

+    var c = getCacheForID(id);

+    if (!c[eventName]) return false;

+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));

+  }

+

+  function destroyCache() {

+    for (var id in cache)

+      for (var eventName in cache[id])

+        cache[id][eventName] = null;

+  }

+

+  if (window.attachEvent) {

+    window.attachEvent("onunload", destroyCache);

+  }

+

+  return {

+    observe: function(element, eventName, handler) {

+      element = $(element);

+      var name = getDOMEventName(eventName);

+

+      var wrapper = createWrapper(element, eventName, handler);

+      if (!wrapper) return element;

+

+      if (element.addEventListener) {

+        element.addEventListener(name, wrapper, false);

+      } else {

+        element.attachEvent("on" + name, wrapper);

+      }

+

+      return element;

+    },

+

+    stopObserving: function(element, eventName, handler) {

+      element = $(element);

+      var id = getEventID(element), name = getDOMEventName(eventName);

+

+      if (!handler && eventName) {

+        getWrappersForEventName(id, eventName).each(function(wrapper) {

+          element.stopObserving(eventName, wrapper.handler);

+        });

+        return element;

+

+      } else if (!eventName) {

+        Object.keys(getCacheForID(id)).each(function(eventName) {

+          element.stopObserving(eventName);

+        });

+        return element;

+      }

+

+      var wrapper = findWrapper(id, eventName, handler);

+      if (!wrapper) return element;

+

+      if (element.removeEventListener) {

+        element.removeEventListener(name, wrapper, false);

+      } else {

+        element.detachEvent("on" + name, wrapper);

+      }

+

+      destroyWrapper(id, eventName, handler);

+

+      return element;

+    },

+

+    fire: function(element, eventName, memo) {

+      element = $(element);

+      if (element == document && document.createEvent && !element.dispatchEvent)

+        element = document.documentElement;

+

+      var event;

+      if (document.createEvent) {

+        event = document.createEvent("HTMLEvents");

+        event.initEvent("dataavailable", true, true);

+      } else {

+        event = document.createEventObject();

+        event.eventType = "ondataavailable";

+      }

+

+      event.eventName = eventName;

+      event.memo = memo || { };

+

+      if (document.createEvent) {

+        element.dispatchEvent(event);

+      } else {

+        element.fireEvent(event.eventType, event);

+      }

+

+      return Event.extend(event);

+    }

+  };

+})());

+

+Object.extend(Event, Event.Methods);

+

+Element.addMethods({

+  fire:          Event.fire,

+  observe:       Event.observe,

+  stopObserving: Event.stopObserving

+});

+

+Object.extend(document, {

+  fire:          Element.Methods.fire.methodize(),

+  observe:       Element.Methods.observe.methodize(),

+  stopObserving: Element.Methods.stopObserving.methodize(),

+  loaded:        false

+});

+

+(function() {

+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,

+     Matthias Miller, Dean Edwards and John Resig. */

+

+  var timer;

+

+  function fireContentLoadedEvent() {

+    if (document.loaded) return;

+    if (timer) window.clearInterval(timer);

+    document.fire("dom:loaded");

+    document.loaded = true;

+  }

+

+  if (document.addEventListener) {

+    if (Prototype.Browser.WebKit) {

+      timer = window.setInterval(function() {

+        if (/loaded|complete/.test(document.readyState))

+          fireContentLoadedEvent();

+      }, 0);

+

+      Event.observe(window, "load", fireContentLoadedEvent);

+

+    } else {

+      document.addEventListener("DOMContentLoaded",

+        fireContentLoadedEvent, false);

+    }

+

+  } else {

+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");

+    $("__onDOMContentLoaded").onreadystatechange = function() {

+      if (this.readyState == "complete") {

+        this.onreadystatechange = null;

+        fireContentLoadedEvent();

+      }

+    };

+  }

+})();

+/*------------------------------- DEPRECATED -------------------------------*/

+

+Hash.toQueryString = Object.toQueryString;

+

+var Toggle = { display: Element.toggle };

+

+Element.Methods.childOf = Element.Methods.descendantOf;

+

+var Insertion = {

+  Before: function(element, content) {

+    return Element.insert(element, {before:content});

+  },

+

+  Top: function(element, content) {

+    return Element.insert(element, {top:content});

+  },

+

+  Bottom: function(element, content) {

+    return Element.insert(element, {bottom:content});

+  },

+

+  After: function(element, content) {

+    return Element.insert(element, {after:content});

+  }

+};

+

+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

+

+// This should be moved to script.aculo.us; notice the deprecated methods

+// further below, that map to the newer Element methods.

+var Position = {

+  // set to true if needed, warning: firefox performance problems

+  // NOT neeeded for page scrolling, only if draggable contained in

+  // scrollable elements

+  includeScrollOffsets: false,

+

+  // must be called before calling withinIncludingScrolloffset, every time the

+  // page is scrolled

+  prepare: function() {

+    this.deltaX =  window.pageXOffset

+                || document.documentElement.scrollLeft

+                || document.body.scrollLeft

+                || 0;

+    this.deltaY =  window.pageYOffset

+                || document.documentElement.scrollTop

+                || document.body.scrollTop

+                || 0;

+  },

+

+  // caches x/y coordinate pair to use with overlap

+  within: function(element, x, y) {

+    if (this.includeScrollOffsets)

+      return this.withinIncludingScrolloffsets(element, x, y);

+    this.xcomp = x;

+    this.ycomp = y;

+    this.offset = Element.cumulativeOffset(element);

+

+    return (y >= this.offset[1] &&

+            y <  this.offset[1] + element.offsetHeight &&

+            x >= this.offset[0] &&

+            x <  this.offset[0] + element.offsetWidth);

+  },

+

+  withinIncludingScrolloffsets: function(element, x, y) {

+    var offsetcache = Element.cumulativeScrollOffset(element);

+

+    this.xcomp = x + offsetcache[0] - this.deltaX;

+    this.ycomp = y + offsetcache[1] - this.deltaY;

+    this.offset = Element.cumulativeOffset(element);

+

+    return (this.ycomp >= this.offset[1] &&

+            this.ycomp <  this.offset[1] + element.offsetHeight &&

+            this.xcomp >= this.offset[0] &&

+            this.xcomp <  this.offset[0] + element.offsetWidth);

+  },

+

+  // within must be called directly before

+  overlap: function(mode, element) {

+    if (!mode) return 0;

+    if (mode == 'vertical')

+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /

+        element.offsetHeight;

+    if (mode == 'horizontal')

+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /

+        element.offsetWidth;

+  },

+

+  // Deprecation layer -- use newer Element methods now (1.5.2).

+

+  cumulativeOffset: Element.Methods.cumulativeOffset,

+

+  positionedOffset: Element.Methods.positionedOffset,

+

+  absolutize: function(element) {

+    Position.prepare();

+    return Element.absolutize(element);

+  },

+

+  relativize: function(element) {

+    Position.prepare();

+    return Element.relativize(element);

+  },

+

+  realOffset: Element.Methods.cumulativeScrollOffset,

+

+  offsetParent: Element.Methods.getOffsetParent,

+

+  page: Element.Methods.viewportOffset,

+

+  clone: function(source, target, options) {

+    options = options || { };

+    return Element.clonePosition(target, source, options);

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){

+  function iter(name) {

+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";

+  }

+

+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?

+  function(element, className) {

+    className = className.toString().strip();

+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);

+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];

+  } : function(element, className) {

+    className = className.toString().strip();

+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);

+    if (!classNames && !className) return elements;

+

+    var nodes = $(element).getElementsByTagName('*');

+    className = ' ' + className + ' ';

+

+    for (var i = 0, child, cn; child = nodes[i]; i++) {

+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||

+          (classNames && classNames.all(function(name) {

+            return !name.toString().blank() && cn.include(' ' + name + ' ');

+          }))))

+        elements.push(Element.extend(child));

+    }

+    return elements;

+  };

+

+  return function(className, parentElement) {

+    return $(parentElement || document.body).getElementsByClassName(className);

+  };

+}(Element.Methods);

+

+/*--------------------------------------------------------------------------*/

+

+Element.ClassNames = Class.create();

+Element.ClassNames.prototype = {

+  initialize: function(element) {

+    this.element = $(element);

+  },

+

+  _each: function(iterator) {

+    this.element.className.split(/\s+/).select(function(name) {

+      return name.length > 0;

+    })._each(iterator);

+  },

+

+  set: function(className) {

+    this.element.className = className;

+  },

+

+  add: function(classNameToAdd) {

+    if (this.include(classNameToAdd)) return;

+    this.set($A(this).concat(classNameToAdd).join(' '));

+  },

+

+  remove: function(classNameToRemove) {

+    if (!this.include(classNameToRemove)) return;

+    this.set($A(this).without(classNameToRemove).join(' '));

+  },

+

+  toString: function() {

+    return $A(this).join(' ');

+  }

+};

+

+Object.extend(Element.ClassNames.prototype, Enumerable);

+

+/*--------------------------------------------------------------------------*/

+

+Element.addMethods();

symlink:b/labs/about.php (new)
--- /dev/null
+++ b/labs/about.php
@@ -1,1 +1,1 @@
-
+../about.php

--- /dev/null
+++ b/labs/busstopdensity.php
@@ -1,1 +1,78 @@
+<?php
+include ('../include/common.inc.php');
+//include_header("Bus Stop Density", "busstopdensity")
+?>
 
+<style type="text/css">
+
+			#map_container{
+				width:100%;
+				height:100%;
+			}
+			#status, #legend {
+				background-color: #eeeeee;
+				padding: 3px 5px 3px 5px;
+				opacity: .9;
+			}
+		</style>
+	
+		<div id="map_container"></div>
+			<div id="status">
+			Status:&nbsp; <span id="log"> <img alt="progess bar" src="progress_bar.gif" width="150" height="16"/></span>
+		</div>
+		<script type="text/javascript" src="http://www.google.com/jsapi?autoload={%22modules%22:[{%22name%22:%22maps%22,version:3,other_params:%22sensor=false%22},{%22name%22:%22jquery%22,%22version%22:%221.4.2%22}]}"></script>
+		<script type="text/javascript">
+		//<![CDATA[
+		//Google Map API v3
+		
+		var googleMap = null;
+		var previousPos = null;
+
+		$(function($){//Called when page is loaded
+			googleMap = new google.maps.Map(document.getElementById("map_container"), {
+				zoom: 17, 
+				minZoom: 12, 
+				center: new google.maps.LatLng(-35.25,149.125), 
+				mapTypeId: google.maps.MapTypeId.SATELLITE});
+			//Set status bar
+			googleMap.controls[google.maps.ControlPosition.TOP_LEFT].push($("#status").get(0));
+			//Set legend
+			googleMap.controls[google.maps.ControlPosition.BOTTOM_LEFT].push($("#legend").get(0));
+
+			google.maps.event.addListener(googleMap, "zoom_changed", function(){
+				google.maps.event.trigger(googleMap, "mousemove", previousPos);
+			});//onzoomend
+				
+			//Add a listener when mouse moves
+			google.maps.event.addListener(googleMap, "mousemove", function(event){
+				var latLng = event.latLng;
+				var xy = googleMap.getProjection().fromLatLngToPoint(latLng);
+				var ratio = Math.pow(2,googleMap.getZoom());
+				$("#log").html("Zoom:" + googleMap.getZoom() + " WGS84:(" + latLng.lat().toFixed(5) + ", " + latLng.lng().toFixed(5) + ") Px:(" + Math.floor(xy.x * ratio)  + "," + Math.floor(xy.y *ratio) + ")");
+				previousPos = event;
+			});//onmouseover
+
+			//Add a listener when mouse leaves the map area
+			google.maps.event.addListener(googleMap, "mouseout", function(event){
+				$("#log").html("");
+			});//onmouseout
+
+			//Add tile overlay
+			var myOverlay = new google.maps.ImageMapType({
+				getTileUrl: function(coord, zoom) {
+					return 'busstopdensity.tile.php?x=' + coord.x + '&y=' + coord.y + '&zoom=' +zoom;
+				},
+				tileSize: new google.maps.Size(256, 256),
+				isPng: true,
+				opacity:1.0
+			});
+			googleMap.overlayMapTypes.insertAt(0, myOverlay);
+			$("#log").html("Map loaded!");
+		});//onload
+		//]]>
+		</script>
+<?php
+include_footer()
+?>
+        
+

--- /dev/null
+++ b/labs/busstopdensity.tile.php
@@ -1,1 +1,124 @@
+<?php
+include ('../include/common.inc.php');
+$debugOkay = Array();
 
+/*
+*DISCLAIMER
+*  http://blog.gmapify.fr/create-beautiful-tiled-heat-maps-with-php-and-gd
+*THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES *OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, *INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF *USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+*	@author: Olivier G. <olbibigo_AT_gmail_DOT_com>
+*	@version: 1.0
+*	@history:
+*		1.0	creation
+*/
+	set_time_limit(120);//2mn
+	ini_set('memory_limit', '256M');
+error_reporting(E_ALL ^ E_DEPRECATED);
+	require_once ('lib/GoogleMapUtility.php');
+	require_once ('lib/HeatMap.php');
+
+	//Root folder to store generated tiles
+	define('TILE_DIR', 'tiles/');
+	//Covered geographic areas
+	define('MIN_LAT', -35.48);
+	define('MAX_LAT', -35.15);
+	define('MIN_LNG', 148.98);
+	define('MAX_LNG', 149.25);
+	define('TILE_SIZE_FACTOR', 0.5);
+	define('SPOT_RADIUS', 30);
+	define('SPOT_DIMMING_LEVEL', 50);
+	
+	//Input parameters
+	if(isset($_GET['x']))
+		$X = (int)$_GET['x'];
+	else
+		exit("x missing");
+	if(isset($_GET['y']))
+		$Y = (int)$_GET['y'];
+	else
+		exit("y missing");
+	if(isset($_GET['zoom']))
+		$zoom = (int)$_GET['zoom'];
+	else
+		exit("zoom missing");
+if ($zoom < 12) { //enforce minimum zoom
+			header('Content-type: image/png');
+			echo file_get_contents(TILE_DIR.'empty.png');
+}
+	$dir = TILE_DIR.$zoom;
+	$tilename = $dir.'/'.$X.'_'.$Y.'.png';
+	//HTTP headers  (data type and caching rule)
+	header("Cache-Control: must-revalidate");
+	header("Expires: " . gmdate("D, d M Y H:i:s", time() + 86400) . " GMT");
+	if(!file_exists($tilename)){
+		$rect = GoogleMapUtility::getTileRect($X, $Y, $zoom);
+		//A tile can contain part of a spot with center in an adjacent tile (overlaps).
+		//Knowing the spot radius (in pixels) and zoom level, a smart way to process tiles would be to compute the box (in decimal degrees) containing only spots that can be drawn on current tile. We choose a simpler solution by increeasing  geo bounds by 2*TILE_SIZE_FACTOR whatever the zoom level and spot radius.
+		$extend_X = $rect->width * TILE_SIZE_FACTOR;//in decimal degrees
+		$extend_Y = $rect->height * TILE_SIZE_FACTOR;//in decimal degrees
+		$swlat = $rect->y - $extend_Y;
+		$swlng = $rect->x - $extend_X;
+		$nelat = $swlat + $rect->height + 2 * $extend_Y;
+		$nelng = $swlng + $rect->width + 2 * $extend_X;
+
+		if( ($nelat <= MIN_LAT) || ($swlat >= MAX_LAT) || ($nelng <= MIN_LNG) || ($swlng >= MAX_LNG)){
+			//No geodata so return generic empty tile
+			echo file_get_contents(TILE_DIR.'empty.png');
+			exit();
+		}
+
+		//Get McDonald's spots
+		$spots = fGetPOI('Select * from stops where
+				(stop_lon > '.$swlng.' AND stop_lon < '.$nelng.')
+			AND (stop_lat < '.$nelat.' AND stop_lat > '.$swlat.')', $im, $X, $Y, $zoom, SPOT_RADIUS);
+
+		
+		if(empty($spots)){
+			//No geodata so return generic empty tile
+			header('Content-type: image/png');
+			echo file_get_contents(TILE_DIR.'empty.png');
+		}else{
+			if(!file_exists($dir)){
+				mkdir($dir, 0705);
+			}
+			//All the magics is in HeatMap class :)
+			$im = HeatMap::createImage($spots, GoogleMapUtility::TILE_SIZE, GoogleMapUtility::TILE_SIZE, heatMap::$WITH_ALPHA, SPOT_RADIUS, SPOT_DIMMING_LEVEL, HeatMap::$GRADIENT_FIRE);
+			//Store tile for reuse and output it
+			header('content-type:image/png;');
+			imagepng($im, $tilename);
+			echo file_get_contents($tilename);
+			imagedestroy($im);
+			unset($im);
+		}
+	}else{
+		//Output stored tile
+		header('content-type:image/png;');
+		echo file_get_contents($tilename);
+	}
+	/////////////
+	//Functions//
+	/////////////
+	function fGetPOI($query, &$im, $X, $Y, $zoom, $offset){
+            global $conn;
+		$nbPOIInsideTile = 0;
+
+	$result = pg_query($conn, $query);
+        $spots = Array();
+	if (!$result) {
+		databaseError(pg_result_error($result));
+		return Array();
+	}
+	foreach( pg_fetch_all($result) as $row){
+				$point = GoogleMapUtility::getOffsetPixelCoords($row['stop_lat'], $row['stop_lon'], $zoom, $X, $Y);
+				//Count result only in the tile
+				if( ($point->x > -$offset) && ($point->x < (GoogleMapUtility::TILE_SIZE+$offset)) && ($point->y > -$offset) && ($point->y < (GoogleMapUtility::TILE_SIZE+$offset))){
+					$spots[] = new HeatMapPoint($point->x, $point->y);
+				}
+				
+			}//while
+		return $spots;
+	}//fAddPOI
+?>
+
+

symlink:b/labs/css (new)
--- /dev/null
+++ b/labs/css
@@ -1,1 +1,1 @@
-
+../css/

symlink:b/labs/feedback.php (new)
--- /dev/null
+++ b/labs/feedback.php
@@ -1,1 +1,1 @@
-
+../feedback.php

 Binary files /dev/null and b/labs/gradient/classic.png differ
 Binary files /dev/null and b/labs/gradient/fire.png differ
 Binary files /dev/null and b/labs/gradient/pgaitch.png differ
file:b/labs/index.php (new)
--- /dev/null
+++ b/labs/index.php
@@ -1,1 +1,20 @@
+<?php
+include ('../include/common.inc.php');
+include_header("Busness R&amp;D", "index")
+?>
+	    <ul data-role="listview" data-theme="e" data-groupingtheme="e">
+		<li data-role="list-divider" > Experimental Features </li>
+		<li><a href="mywaybalance.php"><h3>MyWay Balance for mobile</h3>
+		<p>Mobile viewer for MyWay balance. Warning! No HTTPS security.</p></a></li>
+		<li><a href="networkstats.php"><h3>Route Statistics</h3>
+		<p>Analysis of route timing points</p></a></li>
+		<li><a href="busstopdensity.php"><h3>Bus Stop Density Map</h3>
+		<p>Analysis of bus stop coverage</p></a></li>
+		<li>More coming soon!</li>
+            </ul>
+	    </div>
+<?php
+include_footer()
+?>
+        
 

symlink:b/labs/js (new)
--- /dev/null
+++ b/labs/js
@@ -1,1 +1,1 @@
-
+../js

symlink:b/labs/lib (new)
--- /dev/null
+++ b/labs/lib
@@ -1,1 +1,1 @@
-
+../lib

--- a/labs/myway_api.json.php
+++ b/labs/myway_api.json.php
@@ -74,7 +74,7 @@
 }
 
 if (!isset($return['error'])) {
-	include_once ('simple_html_dom.php');
+	include_once ('lib/simple_html_dom.php');
 	$page = str_get_html($pageHTML);
 	$pageAlerts = $page->find(".smartCardAlert");
 	if (sizeof($pageAlerts) > 0) {

--- a/labs/mywaybalance.php
+++ b/labs/mywaybalance.php
@@ -1,22 +1,40 @@
 <?php
 include ('../include/common.inc.php');
-include_header("MyWay Balance", "mywayBalance", true, false, true);
+include_header("MyWay Balance", "mywayBalance", false, false, true);
+		echo '<div data-role="page"> 
+	<div data-role="header" data-position="inline">
+	<a href="' . $_SERVER["HTTP_REFERER"] . '" data-icon="arrow-l" data-rel="back" class="ui-btn-left">Back</a> 
+		<h1>MyWay Balance</h1>
+		<a href="mywaybalance.php?logout=yes" data-icon="delete" class="ui-btn-right">Logout</a>
+	</div><!-- /header -->
+        <a name="maincontent" id="maincontent"></a>
+        <div data-role="content"> ';
+	
 $return = Array();
+function logout() {
+	setcookie("card_number", "", time() - 60 * 60 * 24 * 100, "/");
+	setcookie("date", "", time() - 60 * 60 * 24 * 100, "/");
+	setcookie("secret_answer", "", time() - 60 * 60 * 24 * 100, "/");
+}
 function printBalance($cardNumber, $date, $pwrd)
 {
 	global $return;
-	$return = json_decode(getPage(curPageURL() . "/myway_api.json.php?card_number=$cardNumber&DOBday={$date[0]}&DOBmonth={$date[1]}&DOByear={$date[2]}&secret_answer=$pwrd"), true);
-    
-        if (isset($return['error'])) {
-            echo "<font color=red>" . var_dump($return['error']) . "</font>";
-        } else {
+	$return = json_decode(getPage(curPageURL() . "/myway_api.json.php?card_number=$cardNumber&DOBday={$date[0]}&DOBmonth={$date[1]}&DOByear={$date[2]}&secret_answer=$pwrd") , true);
+	if (isset($return['error'])) {
+		logout();
+		echo '<h3><font color="red">' . $return['error'][0] . "</font></h3>";
+	}
+	else {
 		echo "<h2>Balance: " . $return['myway_carddetails']['Card Balance'] . "</h2>";
 		echo '<ul data-role="listview" data-inset="true"><li data-role="list-divider"> Recent Transactions </li>';
+		$txCount=0;
 		foreach ($return['myway_transactions'] as $transaction) {
 			echo "<li><b>" . $transaction["Date / Time"] . "</b>";
-                        echo "<br><small>" . $transaction["TX Reference No / Type"]. "</small>";
-                        echo '<p class="ui-li-aside">'.$transaction["TX Amount"].'</p>';
+			echo "<br><small>" . $transaction["TX Reference No / Type"] . "</small>";
+			echo '<p class="ui-li-aside">' . $transaction["TX Amount"] . '</p>';
 			echo "</li>";
+			$txCount++;
+			if ($txCount > 10) break;
 		}
 		echo "</ul>";
 	}
@@ -25,12 +43,15 @@
 	$cardNumber = $_REQUEST['card_number'];
 	$date = explode("/", $_REQUEST['date']);
 	$pwrd = $_REQUEST['secret_answer'];
-	if ($_REQUEST['remember'] == true) {
-		$_COOKIE['card_number'] = $cardNumber;
-		$_COOKIE['date'] = $date;
-		$_COOKIE['secret_answer'] = $pwrd;
+	if ($_REQUEST['remember'] == "on") {
+		setcookie("card_number", $cardNumber, time() + 60 * 60 * 24 * 100, "/");
+		setcookie("date", $_REQUEST['date'], time() + 60 * 60 * 24 * 100, "/");
+		setcookie("secret_answer", $pwrd, time() + 60 * 60 * 24 * 100, "/");
 	}
 	printBalance($cardNumber, $date, $pwrd);
+}
+else if (isset($_REQUEST['logout'])) {
+	echo '<center><h3> Logged out of MyWay balance </h3><a href="/index.php">Back to main menu...</a><center>';
 }
 else if (isset($_COOKIE['card_number']) && isset($_COOKIE['date']) && isset($_COOKIE['secret_answer'])) {
 	$cardNumber = $_COOKIE['card_number'];

--- /dev/null
+++ b/labs/networkstats.php
@@ -1,1 +1,147 @@
+<?php
+include ('../include/common.inc.php');
+include_header("Route Statistics", "networkstats")
+?>
+<script type="text/javascript" src="js/flotr/lib/prototype-1.6.0.2.js"></script>
 
+		<!--[if IE]>
+
+			<script type="text/javascript" src="js/flotr/lib/excanvas.js"></script>
+
+			<script type="text/javascript" src="js/flotr/lib/base64.js"></script>
+
+		<![endif]-->
+
+		<script type="text/javascript" src="js/flotr/lib/canvas2image.js"></script>
+
+		<script type="text/javascript" src="js/flotr/lib/canvastext.js"></script>
+
+		<script type="text/javascript" src="js/flotr/flotr.debug-0.2.0-alpha_radar1.js"></script>
+		<form method="get" action="networkstats.php">
+			<select id="routeid" name="routeid">
+				<?php
+				foreach (getRoutes() as $route) {
+				echo "<option value=\"{$route['route_id']}\">{$route['route_short_name']} {$route['route_long_name']}</option>";
+				}
+				?>
+			</select>
+			<input type="submit" value="View"/>
+		</form>
+
+<?php
+// middle of graph = 6am
+$adjustFactor = 0;
+$route = getRoute($routeid);
+echo "<h1>{$route['route_short_name']} {$route['route_long_name']}</h1>";
+foreach (getRouteTrips($routeid) as $key => $trip) {
+	$dLabel[$key] = $trip['arrival_time'];
+	if ($key == 0) {
+		$time = strtotime($trip['arrival_time']);
+		$adjustFactor = (date("G", $time) * 3600);
+	}
+	$tripStops = viaPoints($trip['trip_id']);
+	foreach ($tripStops as $i => $stop) {
+		if ($key == 0) {
+			$dTicks[$i] = $stop['stop_name'];
+		}
+		$time = strtotime($stop['arrival_time']);
+		$d[$key][$i] = 	(date("G", $time) * 3600) + (date("i", $time) * 60) + date("s", $time) - $adjustFactor;
+
+	}
+}
+
+?>
+<div id="container" style="width:100%;height:900px;"></div>
+<script type="text/javascript">
+
+			/**
+
+			 * Wait till dom's finished loading.
+
+			 */
+
+			document.observe('dom:loaded', function(){
+
+				/**
+
+				 * Fill series d1 and d2.
+
+				 */
+<?php
+foreach ($d as $key => $dataseries) {
+	
+	echo "var d$key =[";
+	foreach ($dataseries as $i => $datapoint) {
+		echo "[$i, $datapoint],";
+	}
+	echo "];\n";
+}
+
+?>
+
+			    
+
+			    var f = Flotr.draw($('container'), 
+
+					[
+						<?php
+foreach ($d as $key => $dataseries) {
+	
+	echo '{data:d'.$key.", label:'{$dLabel[$key]}'".', radar:{fill:false}},'."\n";
+	
+}
+
+?>
+					 ],
+
+					{defaultType: 'radar',
+
+					 radarChartMode: true,
+
+					 HtmlText: false,
+
+					 fontSize: 9,
+
+					 xaxis:{
+
+						ticks: [
+							<?php
+foreach ($dTicks as $key => $tickName) {
+		echo '['.$key.', "'.$tickName.'"],';
+}
+
+?>
+							
+							]},
+
+					 mouse:{ // Setup point tracking
+
+						track: true,
+
+						lineColor: 'black',
+
+						relative: true,
+
+						sensibility: 70,
+
+						trackFormatter: function(obj){
+						var d = new Date();
+						d.setMinutes(0);
+						d.setHours(0);
+d.setTime(d.getTime() + Math.floor(obj.radarData*1000) + <?php echo $adjustFactor*1000 ?>);
+return d.getHours() +':'+ (d.getMinutes().toString().length == 1 ? '0'+ d.getMinutes():  d.getMinutes());
+}}});
+
+			});
+
+		</script>
+
+	    </div>
+	    
+
+
+<?php
+include_footer()
+?>
+        
+

 Binary files /dev/null and b/labs/tiles/empty.png differ
--- a/labs/tripPlannerTester.kml.php
+++ b/labs/tripPlannerTester.kml.php
@@ -8,6 +8,50 @@
 	}
 	else {
 		return (($pBegin - $pEnd) * (1 - ($pStep / $pMax))) + $pEnd;
+	}
+}
+require ("../lib/rolling-curl/RollingCurl.php");
+function processResult_cb($response, $info, $request)
+{
+	global $testRegions, $regionTimes,$csv,$kml, $latdeltasize,$londeltasize;
+	$md = $request->metadata;
+	$tripplan = json_decode($response);
+	$plans = Array();
+	//var_dump(Array($info, $request));
+	if (is_array($tripplan->plan->itineraries->itinerary)) {
+		foreach ($tripplan->plan->itineraries->itinerary as $itineraryNumber => $itinerary) {
+			$plans[floor($itinerary->duration / 60000) ] = $itinerary;
+		}
+	}
+	else {
+		$plans[floor($tripplan->plan->itineraries->itinerary->duration / 60000) ] = $tripplan->plan->itineraries->itinerary;
+	}
+	if ($csv) echo "{$md['i']},{$md['j']}," . min(array_keys($plans)) . ",$latdeltasize, $londeltasize,{$md['key']}\n";
+	if ($kml) {
+		$time = min(array_keys($plans));
+		$plan = "";
+		if (is_array($plans[min(array_keys($plans)) ]->legs->leg)) {
+			foreach ($plans[min(array_keys($plans)) ]->legs->leg as $legNumber => $leg) {
+				$plan.= processLeg($legNumber, $leg) . ",";
+			}
+		}
+		else {
+			$plan.= processLeg(0, $plans[min(array_keys($plans)) ]->legs->leg);
+		}
+		if (isset($tripplan->error) && $tripplan->error->id == 404) {
+			$time = 999;
+			$plan = "Trip not possible without excessive walking from nearest bus stop";
+		}
+		$testRegions[] = Array(
+			"lat" => $md['i'],
+			"lon" => $md['j'],
+			"time" => $time,
+			"latdeltasize" => $latdeltasize,
+			"londeltasize" => $londeltasize,
+			"regionname" => $md['key'],
+			"plan" => $plan . '<br/><a href="' . htmlspecialchars($md['url']) . '">original plan</a>'
+		);
+		$regionTimes[] = $time;
 	}
 }
 function Gradient($HexFrom, $HexTo, $ColorSteps)
@@ -49,18 +93,18 @@
 		//}
 		//$walkingstep.= floor($step->distance) . "m";
 		//return $walkingstep;
+		
 	}
 }
 $csv = false;
 $kml = true;
+$gearthcolors = false;
 if ($kml) {
-	//header('Content-Type: application/vnd.google-earth.kml+xml');
+	header('Content-Type: application/vnd.google-earth.kml+xml');
 	echo '<?xml version="1.0" encoding="UTF-8"?>
 <kml xmlns="http://www.opengis.net/kml/2.2"><Document>';
 }
 include ('../include/common.inc.php');
-//Test code to grab transit times
-// make sure to sleep(10);
 $boundingBoxes = Array(
 	"belconnen" => Array(
 		"startlat" => - 35.1928,
@@ -93,8 +137,8 @@
 		"finishlon" => 149.1243,
 	)
 );
-$latdeltasize = 0.01;
-$londeltasize = 0.01;
+$latdeltasize = 0.005;
+$londeltasize = 0.005;
 $from = "Wattle Street";
 $fromPlace = (startsWith($from, "-") ? $from : geocode($from, false));
 $startTime = "9:00 am";
@@ -103,81 +147,40 @@
 $regionTimes = Array();
 $testRegions = Array();
 $useragent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1";
+if ($kml) echo "<name> $from at $startTime on $startDate </name>";
 if ($csv) echo "<pre>";
 if ($csv) echo "lat,lon,time,latdeltasize, londeltasize, region key name\n";
+$rc = new RollingCurl("processResult_cb");
+$rc->window_size = 2;
 foreach ($boundingBoxes as $key => $boundingBox) {
 	for ($i = $boundingBox['startlat']; $i >= $boundingBox['finishlat']; $i-= $latdeltasize) {
 		for ($j = $boundingBox['startlon']; $j <= $boundingBox['finishlon']; $j+= $londeltasize) {
 			$url = $otpAPIurl . "ws/plan?date=" . urlencode($startDate) . "&time=" . urlencode($startTime) . "&mode=TRANSIT%2CWALK&optimize=QUICK&maxWalkDistance=440&wheelchair=false&toPlace=" . $i . "," . $j . "&fromPlace=$fromPlace";
-			$ch = curl_init($url);
-			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-			curl_setopt($ch, CURLOPT_HEADER, 0);
-			curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+			//debug($url);
+			$request = new RollingCurlRequest($url);
+			$request->headers = Array(
 				"Accept: application/json"
-			));
-			curl_setopt($ch, CURLOPT_TIMEOUT, 5);
-			$page = curl_exec($ch);
-			if (curl_errno($ch)) {
-				if ($csv) echo "Trip planner temporarily unavailable: " . curl_errno($ch) . " " . curl_error($ch);
-			}
-			else {
-				$tripplan = json_decode($page); 
-				$plans = Array();
-				if (is_array($tripplan->plan->itineraries->itinerary)) {
-					foreach ($tripplan->plan->itineraries->itinerary as $itineraryNumber => $itinerary) {
-						$plans[floor($itinerary->duration / 60000) ] = $itinerary;
-					}
-				}
-				else {
-					$plans[floor($tripplan->plan->itineraries->itinerary->duration / 60000) ] = $tripplan->plan->itineraries->itinerary;
-				}
-				if ($csv) echo "$i,$j," . min(array_keys($plans)) . ",$latdeltasize, $londeltasize,$key\n";
-				if ($kml) {
-					$time = min(array_keys($plans));
-					$plan = "";
-					if (is_array($plans[min(array_keys($plans)) ]->legs->leg)) {
-						foreach ($plans[min(array_keys($plans)) ]->legs->leg as $legNumber => $leg) {
-							$plan .= processLeg($legNumber, $leg).",";
-						}
-					}
-					else {
-						$plan .= processLeg(0, $plans[min(array_keys($plans)) ]->legs->leg);
-					}
-						if (isset($tripplan->error) && $tripplan->error->id == 404) {
-							$time = 999;
-							$plan = "Trip not possible without excessive walking from nearest bus stop";
-						}
-					$testRegions[] = Array(
-						"lat" => $i,
-						"lon" => $j,
-						"time" => $time,
-						"latdeltasize" => $latdeltasize,
-						"londeltasize" => $londeltasize,
-						"regionname" => $key,
-						"plan" => $plan . "<br/><a href='". htmlspecialchars($url)."'>original plan</a>"
-					);
-					$regionTimes[] = $time;
-				}
-			}
-			flush(); @ob_flush();
-			curl_close($ch);
-		}
-	}
-}
+			);
+			$request->metadata = Array( "i" => $i, "j" => $j, "key" => $key, "url" => $url);
+			$rc->add($request);
+		}
+	}
+}
+$rc->execute();
 if ($kml) {
 	$colorSteps = 9;
 	//$minTime = min($regionTimes);
 	//$maxTime = max($regionTimes);
 	//$rangeTime = $maxTime - $minTime;
 	//$deltaTime = $rangeTime / $colorSteps;
-	$Gradients = Gradient(strrev("66FF00"), strrev("FF0000"), $colorSteps); // KML is BGR not RGB so strrev
+	$Gradients = Gradient(strrev("66FF00") , strrev("FF0000") , $colorSteps); // KML is BGR not RGB so strrev
 	foreach ($testRegions as $testRegion) {
 		//$band = (floor(($testRegion[time] - $minTime) / $deltaTime));
 		$band = (floor($testRegion[time] / 10));
 		if ($band > $colorSteps) $band = $colorSteps;
 		echo "<Placemark>
   <name>" . $testRegion['regionname'] . " time {$testRegion['time']} band $band</name>
-  <description> {$testRegion['plan']} </description>
+  <description> <![CDATA[ {$testRegion['plan']}  ]]> </description>
     <Style>
         <PolyStyle>
             <color>c7" . $Gradients[$band] . "</color>" . // 7f = 50% alpha, c7=78%

--- a/labs/tripPlannerTester.php
+++ b/labs/tripPlannerTester.php
@@ -3,7 +3,9 @@
     <script src="openlayers/OpenLayers.js"></script>
  <SCRIPT TYPE="text/javascript" SRC="OpenStreetMap.js"></SCRIPT> 
     <script type="text/javascript">
-
+        var map,select;
+       
+	
 function init()
 {
     var extent = new OpenLayers.Bounds(148.98, -35.48, 149.25, -35.15);
@@ -16,13 +18,13 @@
 		}; 
  
 		// create the ol map object
-		var map = new OpenLayers.Map('map', options);
+		map = new OpenLayers.Map('map', options);
     
 var osmtiles = new OpenLayers.Layer.OSM("OSM");
 
 var nearmap = new OpenLayers.Layer.OSM.NearMap("NearMap");
 
-    var tripplantest = new OpenLayers.Layer.GML("tripplantest", "tripPlannerTester.kml.php", {
+    var tripplantest = new OpenLayers.Layer.GML("tripplantest", "tripPlannerTester.kml", {
         format: OpenLayers.Format.KML,
         formatOptions: {
             extractStyles: true,
@@ -44,9 +46,45 @@
     {
         displayProjection: new OpenLayers.Projection("EPSG:900913")
     }));
+    
+  select = new OpenLayers.Control.SelectFeature(tripplantest);
+            
+            tripplantest.events.on({
+                "featureselected": onFeatureSelect,
+                "featureunselected": onFeatureUnselect
+            });
+ 
+            map.addControl(select);
+            select.activate();   
 
 }
- 
+ function onPopupClose(evt) {
+            select.unselectAll();
+        }
+        function onFeatureSelect(event) {
+            var feature = event.feature;
+            // Since KML is user-generated, do naive protection against
+            // Javascript.
+            var content = "<h2>"+feature.attributes.name + "</h2>" + feature.attributes.description;
+            if (content.search("<script") != -1) {
+                content = "Content contained Javascript! Escaped content below.<br />" + content.replace(/</g, "&lt;");
+            }
+            popup = new OpenLayers.Popup.FramedCloud("chicken", 
+                                     feature.geometry.getBounds().getCenterLonLat(),
+                                     new OpenLayers.Size(100,100),
+                                     content,
+                                     null, true, onPopupClose);
+            feature.popup = popup;
+            map.addPopup(popup);
+        }
+        function onFeatureUnselect(event) {
+            var feature = event.feature;
+            if(feature.popup) {
+                map.removePopup(feature.popup);
+                feature.popup.destroy();
+                delete feature.popup;
+            }
+        }
     </script>
 
   </head>

--- a/layar_api.php
+++ b/layar_api.php
@@ -5,36 +5,31 @@
 $output['layer'] = "canberrabusstops";
 $max_page = 10;
 $max_results = 50;
-$page_start = 0 + filter_var($_REQUEST['pageKey'], FILTER_SANITIZE_NUMBER_INT);
-$page_end = $max_page + filter_var($_REQUEST['pageKey'], FILTER_SANITIZE_NUMBER_INT);
-$lat = filter_var($_REQUEST['lat'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
-$lon = filter_var($_REQUEST['lon'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
-
-$contents = getStopsNearby($lat, $lon, 50);
-
+$page_start = 0 + $pageKey;
+$page_end = $max_page + $pageKey;
+$contents = getNearbyStops($lat, $lon, 50, $max_distance);
 $stopNum = 0;
 foreach ($contents as $stop) {
 	$stopNum++;
 	if ($stopNum > $page_start && $stopNum <= $page_end) {
 		$hotspot = Array();
-		$hotspot['id'] = $stop[id];
-		$hotspot['title'] = $stop[name];
+		$hotspot['id'] = $stop['stop_id'];
+		$hotspot['title'] = $stop['stop_name'];
 		$hotspot['type'] = 0;
-		$hotspot['lat'] = floor($stop[lat] * 1000000);
-		$hotspot['lon'] = floor($stop[lon] * 1000000);
-		$hotspot['distance'] = distance($stop[lat], $stop[lon], $_REQUEST['lat'], $_REQUEST['lon']);
+		$hotspot['lat'] = floor($stop['stop_lat'] * 1000000);
+		$hotspot['lon'] = floor($stop['stop_lon'] * 1000000);
+		$hotspot['distance'] = floor($stop['distance']);
+		$hotspot['attribution'] = "ACTION Buses";
 		$hotspot['actions'] = Array(
 			Array(
 				"label" => 'View more trips/information',
-				'uri' => 'http://bus.lambdacomplex.org/' . 'stop.php?stopid=' . $stop[id]
+				'uri' => 'http://bus.lambdacomplex.org/' . 'stop.php?stopid=' . $stop['stop_id']
 			)
 		);
-
-		$url = $APIurl . "/json/stoptrips?stop=" . $row[0] . "&time=" . midnight_seconds() . "&service_period=" . service_period() . "&limit=4&time_range=" . strval(90 * 60);
-		$trips = getStopTrips($stopID);
+		$trips = getStopTripsWithTimes($stop['stop_id'], "", "", "", 3);
 		foreach ($trips as $key => $row) {
 			if ($key < 3) {
-				$hotspot['line' . strval($key + 2) ] = $row[1][1] . ' @ ' . midnight_seconds_to_time($row[0]);
+				$hotspot['line' . strval($key + 2) ] = $row['route_short_name'] . ' ' . $row['route_long_name'] . ' @ ' . $row['arrival_time'];
 			}
 		}
 		if (sizeof($trips) == 0) $hotspot['line2'] = 'No trips in the near future.';
@@ -49,7 +44,7 @@
 	$output['errorString'] = 'no results, try increasing range';
 	$output['errorCode'] = 21;
 }
-if ($page_end >= $max_results || sizeof($hotspot) < $max_page) {
+if ($page_end >= $max_results || sizeof($contents) < $page_start+$max_page) {
 	$output["morePages"] = false;
 	$output["nextPageKey"] = null;
 }

--- /dev/null
+++ b/lib/GoogleMapUtility.php
@@ -1,1 +1,97 @@
+<?php
+/*
+*DISCLAIMER
+* 
+*THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES *OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, *INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF *USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+*	@author: Olivier G. <olbibigo_AT_gmail_DOT_com>
+*	@version: 1.1
+*	@history:
+*		1.0	creation
+		1.1	disclaimer added
+*/
+class GoogleMapUtility {
+	const TILE_SIZE = 256;
+	
+	//(lat, lng, z) -> parent tile (X,Y)
+	public static function getTileXY($lat, $lng, $zoom) {
+		$normalised = GoogleMapUtility::_toNormalisedMercatorCoords(GoogleMapUtility::_toMercatorCoords($lat, $lng));
+		$scale = 1 << ($zoom);
+		return new Point(
+			(int)($normalised->x * $scale), 
+			(int)($normalised->y * $scale)
+		);
+	}//toTileXY
+	
+	//(lat, lng, z) -> (x,y) with (0,0) in the upper left corner of the MAP
+	public static function getPixelCoords($lat, $lng, $zoom) {
+		$normalised = GoogleMapUtility::_toNormalisedMercatorCoords(GoogleMapUtility::_toMercatorCoords($lat, $lng));
+		$scale = (1 << ($zoom)) * GoogleMapUtility::TILE_SIZE;
+		return new Point(
+			(int)($normalised->x * $scale), 
+			(int)($normalised->y * $scale)
+		);
+	}//getPixelCoords
 
+	//(lat, lng, z) -> (x,y) in the upper left corner of the TILE ($X, $Y)
+	public static function getOffsetPixelCoords($lat,$lng,$zoom, $X, $Y) {
+		$pixelCoords = GoogleMapUtility::getPixelCoords($lat, $lng, $zoom);
+		return new Point(
+			$pixelCoords->x - $X * GoogleMapUtility::TILE_SIZE, 
+			$pixelCoords->y - $Y * GoogleMapUtility::TILE_SIZE
+		);
+	}//getPixelOffsetInTile
+	
+	public static function getTileRect($X,$Y,$zoom) {
+		$tilesAtThisZoom = 1 << $zoom;
+		$lngWidth = 360.0 / $tilesAtThisZoom;
+		$lng = -180 + ($X * $lngWidth);	
+		$latHeightMerc = 1.0 / $tilesAtThisZoom;
+		$topLatMerc = $Y * $latHeightMerc;
+		$bottomLatMerc = $topLatMerc + $latHeightMerc;
+		$bottomLat = (180 / M_PI) * ((2 * atan(exp(M_PI * (1 - (2 * $bottomLatMerc))))) - (M_PI / 2));
+		$topLat = (180 / M_PI) * ((2 * atan(exp(M_PI * (1 - (2 * $topLatMerc))))) - (M_PI / 2));
+		$latHeight = $topLat - $bottomLat;
+		return new Boundary($lng, $bottomLat, $lngWidth, $latHeight);
+	}//getTileRect	
+
+	private static function _toMercatorCoords($lat, $lng) {
+		if ($lng > 180) {
+			$lng -= 360;
+		}
+		$lng /= 360;
+		$lat = asinh(tan(deg2rad($lat)))/M_PI/2;
+		return new Point($lng, $lat);
+	}//_toMercatorCoords
+
+	private static function _toNormalisedMercatorCoords($point) {
+		$point->x += 0.5;
+		$point->y = abs($point->y-0.5);
+		return $point;
+	}//_toNormalisedMercatorCoords
+}//GoogleMapUtility
+
+class Point {
+	public $x,$y;
+	function __construct($x,$y) {
+		$this->x = $x;
+		$this->y = $y;
+	}
+	function __toString() {
+		return "({$this->x},{$this->y})";
+	}
+}//Point
+
+class Boundary {
+	public $x,$y,$width,$height;
+	function __construct($x,$y,$width,$height) {
+		$this->x = $x;
+		$this->y = $y;
+		$this->width = $width;
+		$this->height = $height;
+	}
+	function __toString() {
+		return "({$this->x} x {$this->y},{$this->width},{$this->height})";
+	}
+}//Boundary
+?>

file:b/lib/HeatMap.php (new)
--- /dev/null
+++ b/lib/HeatMap.php
@@ -1,1 +1,275 @@
-
+<?php
+/*
+*DISCLAIMER
+* http://blog.gmapify.fr/create-beautiful-tiled-heat-maps-with-php-and-gd
+*THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES *OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, *INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF *USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+*	@author: Olivier G. <olbibigo_AT_gmail_DOT_com>
+*	@version: 1.0
+*	@history:
+*		1.0	creation
+*/
+	define('PI2', 2*M_PI);
+
+	class HeatMapPoint{
+		public $x,$y;
+		function __construct($x,$y) {
+			$this->x = $x;
+			$this->y = $y;
+		}
+		function __toString() {
+			return "({$this->x},{$this->y})";
+		}
+	}//Point
+
+	class HeatMap{
+		//TRANSPARENCY
+		public static $WITH_ALPHA = 0;
+		public static $WITH_TRANSPARENCY = 1;
+		//GRADIENT STYLE
+		public static $GRADIENT_CLASSIC = 'classic';
+		public static $GRADIENT_FIRE = 'fire';
+		public static $GRADIENT_PGAITCH = 'pgaitch';
+		//GRADIENT MODE (for heatImage)
+		public static $GRADIENT_NO_NEGATE_NO_INTERPOLATE = 0;
+		public static $GRADIENT_NO_NEGATE_INTERPOLATE = 1;
+		public static $GRADIENT_NEGATE_NO_INTERPOLATE = 2;
+		public static $GRADIENT_NEGATE_INTERPOLATE = 3;
+		//NOT PROCESSED PIXEL (for heatImage)
+		public static $KEEP_VALUE = 0;
+		public static $NO_KEEP_VALUE = 1;
+		//CONSTRAINTS
+		private static $MIN_RADIUS = 2;//in px
+		private static $MAX_RADIUS = 50;//in px
+		private static $MAX_IMAGE_SIZE = 10000;//in px
+		
+		//generate an $image_width by $image_height pixels heatmap image of $points
+		public static function createImage($data, $image_width, $image_height, $mode=0, $spot_radius = 30, $dimming = 75, $gradient_name = 'classic'){
+			$_gradient_name = $gradient_name;
+			if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
+				$_gradient_name = self::$GRADIENT_CLASSIC;
+			}
+			$_image_width = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_width)));
+			$_image_height = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_height)));
+			$_spot_radius = min(self::$MAX_RADIUS, max(self::$MIN_RADIUS, intval($spot_radius)));
+			$_dimming = min(255, max(0, intval($dimming)));
+			if(!is_array($data)){
+				return false;
+			}
+			$im = imagecreatetruecolor($_image_width, $_image_height);
+			$white = imagecolorallocate($im, 255, 255, 255);
+			imagefill($im, 0, 0, $white);
+			if(self::$WITH_ALPHA == $mode){
+				imagealphablending($im, false);
+				imagesavealpha($im,true);
+			}
+			//Step 1: create grayscale image
+			foreach($data as $datum){
+				if( (is_array($datum) && (count($datum)==1)) || (!is_array($datum) && ('HeatMapPoint' == get_class($datum)))){//Plot points
+					if('HeatMapPoint' != get_class($datum)){
+						$datum = $datum[0];
+					}
+					self::_drawCircularGradient($im, $datum->x, $datum->y, $_spot_radius, $_dimming);
+				}else if(is_array($datum)){//Draw lines
+					$length = count($datum)-1;
+					for($i=0; $i < $length; ++$i){//Loop through points
+						//Bresenham's algorithm to plot from from $datum[$i] to $datum[$i+1];
+						self::_drawBilinearGradient($im, $datum[$i], $datum[$i+1], $_spot_radius, $_dimming);
+					}
+				}
+			}
+			//Gaussian filter
+			if($_spot_radius >= 30){
+				imagefilter($im, IMG_FILTER_GAUSSIAN_BLUR);
+			}
+			//Step 2: create colored image
+			if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
+				return FALSE;
+			}
+			$grad_size = count($grad_rgba);
+			for($x=0; $x <$_image_width; ++$x){
+				for($y=0; $y <$_image_height; ++$y){
+					$level = imagecolorat($im, $x, $y) & 0xFF;
+					if( ($level >= 0) && ($level < $grad_size) ){
+						imagesetpixel($im, $x, $y, $grad_rgba[imagecolorat($im, $x, $y) & 0xFF]);
+					}
+				}
+			}
+			if(self::$WITH_TRANSPARENCY == $mode){
+				imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
+			}
+			return $im;
+		}//createImage
+
+		//Heat an image
+		public static function heatImage($filepath, $gradient_name = 'classic', $mode= 0, $min_level=0, $max_level=255, $gradient_interpolate=0, $keep_value=0){
+			$_gradient_name = $gradient_name;
+			if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
+				$_gradient_name = self::$GRADIENT_CLASSIC;
+			}
+			$_min_level = min(255, max(0, intval($min_level)));
+			$_max_level = min(255, max(0, intval($max_level)));
+
+			//try opening jpg first then png then gif format
+			if(FALSE === ($im = @imagecreatefromjpeg($filepath))){
+				if(FALSE === ($im = @imagecreatefrompng($filepath))){
+					if(FALSE === ($im = @imagecreatefromgif($filepath))){
+						return FALSE;
+					}
+				}
+			}
+			if(self::$WITH_ALPHA == $mode){
+				imagealphablending($im, false);
+				imagesavealpha($im,true);
+			}
+			$width = imagesx($im);
+			$height = imagesy($im);	
+			if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
+				return FALSE;
+			}
+			//Convert to grayscale
+			$grad_size = count($grad_rgba);
+			$level_range = $_max_level - $_min_level;
+			for($x=0; $x <$width; ++$x){
+				for($y=0; $y <$height; ++$y){
+					$rgb = imagecolorat($im, $x, $y);
+					$r = ($rgb >> 16) & 0xFF;
+					$g = ($rgb >> 8) & 0xFF;
+					$b = $rgb & 0xFF;
+					$gray_level = Min(255, Max(0, floor(0.33 * $r + 0.5 * $g + 0.16 * $b)));//between 0 and 255				
+					if( ($gray_level >= $_min_level) && ($gray_level <= $_max_level) ){
+						switch($gradient_interpolate){
+							case self::$GRADIENT_NO_NEGATE_NO_INTERPOLATE:
+								//$_max_level takes related lowest gradient color
+								//$_min_level takes related highest gradient color
+								$value = 255 - $gray_level;
+								break;
+							case self::$GRADIENT_NEGATE_NO_INTERPOLATE:
+								//$_max_level takes related highest gradient color
+								//$_min_level takes related lowest gradient color
+								$value = $gray_level;
+								break;
+							case self::$GRADIENT_NO_NEGATE_INTERPOLATE:
+								//$_max_level takes lowest gradient color
+								//$_min_level takes highest gradient color
+								$value = 255- floor(($gray_level - $_min_level) * $grad_size / $level_range);
+								break;
+							case self::$GRADIENT_NEGATE_INTERPOLATE:
+								//$_max_level takes highest gradient color
+								//$_min_level takes lowest gradient color
+								$value = floor(($gray_level - $_min_level) * $grad_size / $level_range);
+								break;
+							default:
+						}
+						imagesetpixel($im, $x, $y, $grad_rgba[$value]);
+					}else{
+						if(self::$KEEP_VALUE == $keep_value){
+							//Do nothing
+						}else{//self::$NO_KEEP_VALUE
+							imagesetpixel($im, $x, $y, imagecolorallocatealpha($im,0,0,0,0));
+						}
+					}
+				}
+			}			
+			if(self::$WITH_TRANSPARENCY == $mode){
+				imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
+			}
+			return $im;
+		}//heatImage
+		
+		private static function _drawCircularGradient(&$im, $center_x, $center_y, $spot_radius, $dimming){
+			$dirty = array();
+			$ratio = (255 - $dimming) / $spot_radius;
+			for($r=$spot_radius; $r > 0; --$r){
+				$channel = $dimming + $r * $ratio;
+				$angle_step = 0.45/$r; //0.01;
+				//Process pixel by pixel to draw a radial grayscale radient
+				for($angle=0; $angle <= PI2; $angle += $angle_step){
+					$x = floor($center_x + $r*cos($angle));
+					$y = floor($center_y + $r*sin($angle));
+					if(!isset($dirty[$x][$y])){
+						$previous_channel = @imagecolorat($im, $x, $y) & 0xFF;//grayscale so same value
+						$new_channel = Max(0, Min(255,($previous_channel * $channel)/255));
+						imagesetpixel($im, $x, $y, imagecolorallocate($im, $new_channel, $new_channel, $new_channel));
+						$dirty[$x][$y] = 0;
+					}
+				}
+			}
+		}//_drawCircularGradient
+		
+		private static function _drawBilinearGradient(&$im, $point0, $point1, $spot_radius, $dimming){
+			if($point0->x < $point1->x){
+				$x0 = $point0->x;
+				$y0 = $point0->y;
+				$x1 = $point1->x;
+				$y1 = $point1->y;
+			}else{
+				$x0 = $point1->x;
+				$y0 = $point1->y;
+				$x1 = $point0->x;
+				$y1 = $point0->y;
+			}
+
+			if( ($x0==$x1) && ($y0==$y1)){//check if same coordinates
+				return false;
+			}
+			$steep = (abs($y1 - $y0) > abs($x1 - $x0))? true: false;
+			if($steep){
+				list($x0, $y0) = array($y0, $x0);//swap
+				list($x1, $y1) = array($y1, $x1);//swap
+			}
+			if($x0>$x1){
+				list($x0, $x1) = array($x1, $x0);//swap
+				list($y0, $y1) = array($y1, $y0);//swap
+			}
+			$deltax = $x1 - $x0;
+			$deltay = abs($y1 - $y0);
+			$error = $deltax / 2;
+			$y = $y0;
+			if( $y0 < $y1){
+				$ystep = 1; 
+			}else{
+				$ystep = -1;
+			}
+			$step = max(1, floor($spot_radius/ 3));
+			for($x=$x0; $x<=$x1; ++$x){//Loop through x value
+				if(0==(($x-$x0) % $step)){
+					if($steep){
+						self::_drawCircularGradient(&$im, $y, $x, $spot_radius, $dimming);
+					}else{ 
+						self::_drawCircularGradient(&$im, $x, $y, $spot_radius, $dimming);
+					}
+				}
+				$error -= $deltay;
+				if($error<0){
+						$y = $y + $ystep;
+						$error = $error + $deltax;
+				}
+			}		
+		}//_drawBilinearGradient
+		
+		private static function _createGradient($im, $mode, $gradient_name){
+			//create the gradient from an image
+			if(FALSE === ($grad_im = imagecreatefrompng('gradient/'.$gradient_name.'.png'))){
+				return FALSE;
+			}
+			$width_g = imagesx($grad_im);
+			$height_g = imagesy($grad_im);
+			//Get colors along the longest dimension
+			//Max density is for lower channel value
+			for($y=$height_g-1; $y >= 0 ; --$y){
+					$rgb = imagecolorat($grad_im, 1, $y);
+					//Linear function
+					$alpha = Min(127, Max(0, floor(127 - $y/2)));
+					if(self::$WITH_ALPHA == $mode){
+						$grad_rgba[] = imagecolorallocatealpha($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF, $alpha);
+					}else{
+						$grad_rgba[] = imagecolorallocate($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF);
+					}
+			}
+			imagedestroy($grad_im);
+			unset($grad_im);
+			return($grad_rgba);
+		}//_createGradient
+	}//Heatmap
+?>

--- /dev/null
+++ b/lib/rolling-curl/.svn/all-wcprops
@@ -1,1 +1,42 @@
+K 25
+svn:wc:ra_dav:version-url
+V 22
+/svn/!svn/ver/20/trunk
+END
+RollingCurlGroup.php
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/!svn/ver/20/trunk/RollingCurlGroup.php
+END
+example_groups.php
+K 25
+svn:wc:ra_dav:version-url
+V 41
+/svn/!svn/ver/20/trunk/example_groups.php
+END
+example.php
+K 25
+svn:wc:ra_dav:version-url
+V 34
+/svn/!svn/ver/20/trunk/example.php
+END
+RollingCurl.php
+K 25
+svn:wc:ra_dav:version-url
+V 38
+/svn/!svn/ver/20/trunk/RollingCurl.php
+END
+CHANGELOG.txt
+K 25
+svn:wc:ra_dav:version-url
+V 36
+/svn/!svn/ver/20/trunk/CHANGELOG.txt
+END
+README.txt
+K 25
+svn:wc:ra_dav:version-url
+V 33
+/svn/!svn/ver/20/trunk/README.txt
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/entries
@@ -1,1 +1,233 @@
-
+10
+
+dir
+20
+http://rolling-curl.googlecode.com/svn/trunk
+http://rolling-curl.googlecode.com/svn
+
+
+
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+74aa2acc-2e27-11de-b2a4-4f96ceaaac44
+
+RollingCurlGroup.php
+file
+
+
+
+
+2011-04-10T08:32:48.081650Z
+73c08d9e9e24b4adc89816624c7aca30
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+5152
+
+example_groups.php
+file
+
+
+
+
+2011-04-10T08:32:48.082650Z
+907ed82a47d346c39acbd5578e1d0230
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1367
+
+example.php
+file
+
+
+
+
+2011-04-10T08:32:48.083650Z
+87aa845abfaffc09ed4eca024f2a8b8a
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1860
+
+RollingCurl.php
+file
+
+
+
+
+2011-04-10T08:32:48.084650Z
+205391c449f3f3ee050004dadc374dc8
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+10444
+
+CHANGELOG.txt
+file
+
+
+
+
+2011-04-10T08:32:48.085650Z
+d0452f6f9530ed04580159121d0fd5f7
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+662
+
+README.txt
+file
+
+
+
+
+2011-04-10T08:32:48.085650Z
+60dd357081431c0f2b82989cdbce8615
+2010-09-12T20:39:22.711474Z
+20
+alexander.makarow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+6355
+
+

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/CHANGELOG.txt.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/RollingCurlGroup.php.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/prop-base/example_groups.php.svn-base
@@ -1,1 +1,6 @@
+K 13
+svn:eol-style
+V 6
+native
+END
 

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/CHANGELOG.txt.svn-base
@@ -1,1 +1,15 @@
+Rolling Curl changelog
+======================
 
+September 13, 2010
+------------------
+- Bug #12, #14: Fixed default options overriding (LionsAd)
+- Bug #10: Added use of curl_multi_select to avoid burning CPU (LionsAd)
+- Enh #6, #9: Added $request as parameter to callback function (LionsAd)
+- Chg: Request renamed to RollingCurlRequest (LionsAd)
+- Added RollingCurlGroup class that allows processing groups of requests (LionsAd)
+- More cleanup at unsetting a class (LionsAd)
+- Timeout parameter for curl_multi_select is now configurable (LionsAd)
+- single_curl now returns true (LionsAd)
+- Readme corrections (Alexander Makarov)
+- Code cleanup (Alexander Makarov)

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/README.txt.svn-base
@@ -1,1 +1,210 @@
-
+Rolling Curl
+============
+
+RollingCurl allows you to process multiple HTTP requests in parallel using CURL PHP library.
+
+Released under the Apache License 2.0.
+
+Authors
+-------
+- Was originally written by [Josh Fraser](joshfraser.com).
+- Currently maintained by [Alexander Makarov](http://rmcreative.ru/).
+- Received significant updates and patched from [LionsAd](http://github.com/LionsAd/rolling-curl).
+
+Overview
+--------
+RollingCurl is a more efficient implementation of curl_multi() curl_multi is a great way to process multiple HTTP requests in parallel in PHP.
+curl_multi is particularly handy when working with large data sets (like fetching thousands of RSS feeds at one time). Unfortunately there is
+very little documentation on the best way to implement curl_multi. As a result, most of the examples around the web are either inefficient or
+fail entirely when asked to handle more than a few hundred requests.
+
+The problem is that most implementations of curl_multi wait for each set of requests to complete before processing them. If there are too many requests
+to process at once, they usually get broken into groups that are then processed one at a time. The problem with this is that each group has to wait for
+the slowest request to download. In a group of 100 requests, all it takes is one slow one to delay the processing of 99 others. The larger the number of
+requests you are dealing with, the more noticeable this latency becomes.
+
+The solution is to process each request as soon as it completes. This eliminates the wasted CPU cycles from busy waiting. Also there is a queue of
+cURL requests to allow for maximum throughput. Each time a request is completed, a new one is added from the queue. By dynamically adding and removing
+links, we keep a constant number of links downloading at all times. This gives us a way to throttle the amount of simultaneous requests we are sending.
+The result is a faster and more efficient way of processing large quantities of cURL requests in parallel.
+
+Callbacks
+---------
+
+Each of requests usually do have a callback to process results that is being executed when request is done
+(both successfully or not).
+
+Callback accepts three parameters and can look like the following one:
+~~~
+[php]
+function request_callback($response, $info, $request){
+    // doing something with the data received
+}
+~~~
+
+- $response contains received page body.
+- $info is an associative array that holds various information about response such as HTTP response code, content type,
+time taken to make request etc.
+- $request contains RollingCurlRequest that was used to make request.
+
+Examples
+--------
+### Hello world
+
+~~~
+[php]
+// an array of URL's to fetch
+$urls = array("http://www.google.com",
+              "http://www.facebook.com",
+              "http://www.yahoo.com");
+
+// a function that will process the returned responses
+function request_callback($response, $info, $request) {
+	// parse the page title out of the returned HTML
+	if (preg_match("~<title>(.*?)</title>~i", $response, $out)) {
+		$title = $out[1];
+	}
+	echo "<b>$title</b><br />";
+	print_r($info);
+	echo "<hr>";
+}
+
+// create a new RollingCurl object and pass it the name of your custom callback function
+$rc = new RollingCurl("request_callback");
+// the window size determines how many simultaneous requests to allow.
+$rc->window_size = 20;
+foreach ($urls as $url) {
+    // add each request to the RollingCurl object
+    $request = new RollingCurlRequest($url);
+    $rc->add($request);
+}
+$rc->execute();
+~~~
+
+
+### Setting custom options
+
+Set custom options for EVERY request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->execute();
+~~~
+
+Set custom options for A SINGLE request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$request = new RollingCurlRequest($url);
+$request->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->add($request);
+$rc->execute();
+~~~
+
+### Shortcuts
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->get("http://www.google.com");
+$rc->get("http://www.yahoo.com");
+$rc->execute();
+~~~
+
+### Class callbacks
+
+~~~
+[php]
+class MyInfoCollector {
+    private $rc;
+
+    function __construct(){
+        $this->rc = new RollingCurl(array($this, 'processPage'));
+    }
+
+    function processPage($response, $info, $request){
+      //...
+    }
+
+    function run($urls){
+        foreach ($urls as $url){
+            $request = new RollingCurlRequest($url);
+            $this->rc->add($request);
+        }
+        $this->rc->execute();
+    }
+}
+
+$collector = new MyInfoCollector();
+$collector->run(array(
+    'http://google.com/',
+    'http://yahoo.com/'
+));
+~~~
+
+### Using RollingCurlGroup
+
+~~~
+[php]
+class TestCurlRequest extends RollingCurlGroupRequest {
+    public $test_verbose = true;
+
+    function process($output, $info) {
+        echo "Processing " . $this->url . "\n";
+        if ($this->test_verbose)
+            print_r($info);
+
+        parent::process($output, $info);
+    }
+}
+
+class TestCurlGroup extends RollingCurlGroup {
+    function process($output, $info, $request) {
+        echo "Group CB: Progress " . $this->name . " (" . ($this->finished_requests + 1) . "/" . $this->num_requests . ")\n";
+        parent::process($output, $info, $request);
+    }
+
+    function finished() {
+        echo "Group CB: Finished" . $this->name . "\n";
+        parent::finished();
+    }
+}
+
+$group = new TestCurlGroup("High");
+$group->add(new TestCurlRequest("www.google.de"));
+$group->add(new TestCurlRequest("www.yahoo.de"));
+$group->add(new TestCurlRequest("www.newyorktimes.com"));
+$reqs[] = $group;
+
+$group = new TestCurlGroup("Normal");
+$group->add(new TestCurlRequest("twitter.com"));
+$group->add(new TestCurlRequest("www.bing.com"));
+$group->add(new TestCurlRequest("m.facebook.com"));
+$reqs[] = $group;
+
+$reqs[] = new TestCurlRequest("www.kernel.org");
+
+// No callback here, as its done in Request class
+$rc = new GroupRollingCurl();
+
+foreach ($reqs as $req)
+$rc->add($req);
+
+$rc->execute();
+~~~
+
+The same function (add) can be used both for adding requests and groups of requests.
+The "callback" in request and groups is:
+
+process($output, $info)
+
+and
+
+process($output, $info, $request)
+
+Also you can override RollingCurlGroup::finished() that will be executed right after finishing group processing.
+
+$Id$

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/RollingCurl.php.svn-base
@@ -1,1 +1,375 @@
-
+<?php
+/*
+Authored by Josh Fraser (www.joshfraser.com)
+Released under Apache License 2.0
+
+Maintained by Alexander Makarov, http://rmcreative.ru/
+
+$Id$
+*/
+
+/**
+ * Class that represent a single curl request
+ */
+class RollingCurlRequest {
+    public $url = false;
+    public $method = 'GET';
+    public $post_data = null;
+    public $headers = null;
+    public $options = null;
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return void
+     */
+    function __construct($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->url = $url;
+        $this->method = $method;
+        $this->post_data = $post_data;
+        $this->headers = $headers;
+        $this->options = $options;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->url, $this->method, $this->post_data, $this->headers, $this->options);
+    }
+}
+
+/**
+ * RollingCurl custom exception
+ */
+class RollingCurlException extends Exception {
+}
+
+/**
+ * Class that holds a rolling queue of curl requests.
+ *
+ * @throws RollingCurlException
+ */
+class RollingCurl {
+    /**
+     * @var int
+     *
+     * Window size is the max number of simultaneous connections allowed.
+     *
+     * REMEMBER TO RESPECT THE SERVERS:
+     * Sending too many requests at one time can easily be perceived
+     * as a DOS attack. Increase this window_size if you are making requests
+     * to multiple servers or have permission from the receving server admins.
+     */
+    private $window_size = 5;
+
+    /**
+     * @var float
+     *
+     * Timeout is the timeout used for curl_multi_select.
+     */
+    private $timeout = 10;
+
+    /**
+     * @var string|array
+     *
+     * Callback function to be applied to each result.
+     */
+    private $callback;
+
+    /**
+     * @var array
+     *
+     * Set your base options that you want to be used with EVERY request.
+     */
+    protected $options = array(
+        CURLOPT_SSL_VERIFYPEER => 0,
+        CURLOPT_RETURNTRANSFER => 1,
+        CURLOPT_CONNECTTIMEOUT => 30,
+        CURLOPT_TIMEOUT => 30
+    );
+
+    /**
+     * @var array
+     */
+    private $headers = array();
+
+    /**
+     * @var Request[]
+     *
+     * The request queue
+     */
+    private $requests = array();
+
+    /**
+     * @var RequestMap[]
+     *
+     * Maps handles to request indexes
+     */
+    private $requestMap = array();
+
+    /**
+     * @param  $callback
+     * Callback function to be applied to each result.
+     *
+     * Can be specified as 'my_callback_function'
+     * or array($object, 'my_callback_method').
+     *
+     * Function should take three parameters: $response, $info, $request.
+     * $response is response body, $info is additional curl info.
+     * $request is the original request
+     *
+     * @return void
+     */
+    function __construct($callback = null) {
+        $this->callback = $callback;
+    }
+
+    /**
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        return (isset($this->{$name})) ? $this->{$name} : null;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $value
+     * @return bool
+     */
+    public function __set($name, $value) {
+        // append the base options & headers
+        if ($name == "options" || $name == "headers") {
+            $this->{$name} = $value + $this->{$name};
+        } else {
+            $this->{$name} = $value;
+        }
+        return true;
+    }
+
+    /**
+     * Add a request to the request queue
+     *
+     * @param Request $request
+     * @return bool
+     */
+    public function add($request) {
+        $this->requests[] = $request;
+        return true;
+    }
+
+    /**
+     * Create new Request and add it to the request queue
+     *
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function request($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->requests[] = new RollingCurlRequest($url, $method, $post_data, $headers, $options);
+        return true;
+    }
+
+    /**
+     * Perform GET request
+     *
+     * @param string $url
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function get($url, $headers = null, $options = null) {
+        return $this->request($url, "GET", null, $headers, $options);
+    }
+
+    /**
+     * Perform POST request
+     *
+     * @param string $url
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function post($url, $post_data = null, $headers = null, $options = null) {
+        return $this->request($url, "POST", $post_data, $headers, $options);
+    }
+
+    /**
+     * Execute processing
+     *
+     * @param int $window_size Max number of simultaneous connections
+     * @return string|bool
+     */
+    public function execute($window_size = null) {
+        // rolling curl window must always be greater than 1
+        if (sizeof($this->requests) == 1) {
+            return $this->single_curl();
+        } else {
+            // start the rolling curl. window_size is the max number of simultaneous connections
+            return $this->rolling_curl($window_size);
+        }
+    }
+
+    /**
+     * Performs a single curl request
+     *
+     * @access private
+     * @return string
+     */
+    private function single_curl() {
+        $ch = curl_init();
+        $request = array_shift($this->requests);
+        $options = $this->get_options($request);
+        curl_setopt_array($ch, $options);
+        $output = curl_exec($ch);
+        $info = curl_getinfo($ch);
+
+        // it's not neccesary to set a callback for one-off requests
+        if ($this->callback) {
+            $callback = $this->callback;
+            if (is_callable($this->callback)) {
+                call_user_func($callback, $output, $info, $request);
+            }
+        }
+        else
+            return $output;
+        return true;
+    }
+
+    /**
+     * Performs multiple curl requests
+     *
+     * @access private
+     * @throws RollingCurlException
+     * @param int $window_size Max number of simultaneous connections
+     * @return bool
+     */
+    private function rolling_curl($window_size = null) {
+        if ($window_size)
+            $this->window_size = $window_size;
+
+        // make sure the rolling window isn't greater than the # of urls
+        if (sizeof($this->requests) < $this->window_size)
+            $this->window_size = sizeof($this->requests);
+
+        if ($this->window_size < 2) {
+            throw new RollingCurlException("Window size must be greater than 1");
+        }
+
+        $master = curl_multi_init();
+
+        // start the first batch of requests
+        for ($i = 0; $i < $this->window_size; $i++) {
+            $ch = curl_init();
+
+            $options = $this->get_options($this->requests[$i]);
+
+            curl_setopt_array($ch, $options);
+            curl_multi_add_handle($master, $ch);
+
+            // Add to our request Maps
+            $key = (string) $ch;
+            $this->requestMap[$key] = $i;
+        }
+
+        do {
+            while (($execrun = curl_multi_exec($master, $running)) == CURLM_CALL_MULTI_PERFORM) ;
+            if ($execrun != CURLM_OK)
+                break;
+            // a request was just completed -- find out which one
+            while ($done = curl_multi_info_read($master)) {
+
+                // get the info and content returned on the request
+                $info = curl_getinfo($done['handle']);
+                $output = curl_multi_getcontent($done['handle']);
+
+                // send the return values to the callback function.
+                $callback = $this->callback;
+                if (is_callable($callback)) {
+                    $key = (string) $done['handle'];
+                    $request = $this->requests[$this->requestMap[$key]];
+                    unset($this->requestMap[$key]);
+                    call_user_func($callback, $output, $info, $request);
+                }
+
+                // start a new request (it's important to do this before removing the old one)
+                if ($i < sizeof($this->requests) && isset($this->requests[$i]) && $i < count($this->requests)) {
+                    $ch = curl_init();
+                    $options = $this->get_options($this->requests[$i]);
+                    curl_setopt_array($ch, $options);
+                    curl_multi_add_handle($master, $ch);
+
+                    // Add to our request Maps
+                    $key = (string) $ch;
+                    $this->requestMap[$key] = $i;
+                    $i++;
+                }
+
+                // remove the curl handle that just completed
+                curl_multi_remove_handle($master, $done['handle']);
+
+            }
+
+            // Block for data in / output; error handling is done by curl_multi_exec
+            if ($running)
+                curl_multi_select($master, $this->timeout);
+
+        } while ($running);
+        curl_multi_close($master);
+        return true;
+    }
+
+
+    /**
+     * Helper function to set up a new request by setting the appropriate options
+     *
+     * @access private
+     * @param Request $request
+     * @return array
+     */
+    private function get_options($request) {
+        // options for this entire curl object
+        $options = $this->__get('options');
+        if (ini_get('safe_mode') == 'Off' || !ini_get('safe_mode')) {
+            $options[CURLOPT_FOLLOWLOCATION] = 1;
+            $options[CURLOPT_MAXREDIRS] = 5;
+        }
+        $headers = $this->__get('headers');
+
+        // append custom options for this specific request
+        if ($request->options) {
+            $options = $request->options + $options;
+        }
+
+        // set the request URL
+        $options[CURLOPT_URL] = $request->url;
+
+        // posting data w/ this request?
+        if ($request->post_data) {
+            $options[CURLOPT_POST] = 1;
+            $options[CURLOPT_POSTFIELDS] = $request->post_data;
+        }
+        if ($headers) {
+            $options[CURLOPT_HEADER] = 0;
+            $options[CURLOPT_HTTPHEADER] = $headers;
+        }
+
+        return $options;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->window_size, $this->callback, $this->options, $this->headers, $this->requests);
+    }
+}
+

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/RollingCurlGroup.php.svn-base
@@ -1,1 +1,218 @@
-
+<?php
+/*
+
+  Authored by Fabian Franz (www.lionsad.de)
+  Released under Apache License 2.0
+
+$Id$
+*/
+
+class RollingCurlGroupException extends Exception {}
+
+/**
+ * @throws RollingCurlGroupException
+ */
+abstract class RollingCurlGroupRequest extends RollingCurlRequest {
+    private $group = null;
+
+    /**
+     * Set group for this request
+     *
+     * @param group The group to be set
+     */
+    function setGroup($group) {
+        if (!($group instanceof RollingCurlGroup))
+            throw new RollingCurlGroupException("setGroup: group needs to be of instance RollingCurlGroup");
+
+        $this->group = $group;
+    }
+
+    /**
+     * Process the request
+     *
+     *
+     */
+    function process($output, $info) {
+        if ($this->group)
+            $this->group->process($output, $info, $this);
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->group);
+        parent::__destruct();
+    }
+
+}
+
+/**
+ * A group of curl requests.
+ *
+ * @throws RollingCurlGroupException *
+ */
+class RollingCurlGroup {
+    /**
+     * @var string group name
+     */
+    protected $name;
+
+    /**
+     * @var int total number of requests in a group
+     */
+    protected $num_requests = 0;
+
+    /**
+     * @var int total number of finished requests in a group
+     */
+    protected $finished_requests = 0;
+
+    /**
+     * @var array requests array
+     */
+    private $requests = array();
+
+    /**
+     * @param string $name group name
+     * @return void
+     */
+    function __construct($name) {
+        $this->name = $name;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->name, $this->num_requests, $this->finished_requests, $this->requests);
+    }
+
+    /**
+     * Adds request to a group
+     *
+     * @throws RollingCurlGroupException
+     * @param RollingCurlGroupRequest|array $request
+     * @return bool
+     */
+    function add($request) {
+        if ($request instanceof RollingCurlGroupRequest) {
+            $request->setGroup($this);
+            $this->num_requests++;
+            $this->requests[] = $request;
+        }
+        else if (is_array($request)) {
+            foreach ($request as $req)
+            $this->add($req);
+        }
+        else
+            throw new RollingCurlGroupException("add: Request needs to be of instance RollingCurlGroupRequest");
+
+        return true;
+    }
+
+    /**
+     * @throws RollingCurlGroupException
+     * @param RollingCurl $rc
+     * @return bool
+     */
+    function addToRC(RollingCurl $rc){
+        $ret = true;
+
+        while (count($this->requests) > 0){
+            $ret1 = $rc->add(array_shift($this->requests));
+            if (!$ret1)
+                $ret = false;
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Override to implement custom response processing.
+     *
+     * Don't forget to call parent::process().
+     *
+     * @param string $output received page body
+     * @param array $info holds various information about response such as HTTP response code, content type, time taken to make request etc.
+     * @param RollingCurlRequest $request request used
+     * @return void
+     */
+    function process($output, $info, $request) {
+        $this->finished_requests++;
+
+        if ($this->finished_requests >= $this->num_requests)
+            $this->finished();
+    }
+
+    /**
+     * Override to execute code after all requests in a group are processed.
+     *
+     * @return void
+     */
+    function finished() {
+    }
+
+}
+
+/**
+ * Group version of rolling curl
+ */
+class GroupRollingCurl extends RollingCurl {
+
+    /**
+     * @var mixed common callback for all groups
+     */
+    private $group_callback = null;
+
+    /**
+     * @param string $output received page body
+     * @param array $info holds various information about response such as HTTP response code, content type, time taken to make request etc.
+     * @param RollingCurlRequest $request request used
+     * @return void
+     */
+    protected function process($output, $info, $request) {
+        if ($request instanceof RollingCurlGroupRequest)
+            $request->process($output, $info);
+
+        if (is_callable($this->group_callback))
+            call_user_func($this->group_callback, $output, $info, $request);
+    }
+
+    /**
+     * @param mixed $callback common callback for all groups
+     * @return void
+     */
+    function __construct($callback = null) {
+        $this->group_callback = $callback;
+
+        parent::__construct(array(&$this, "process"));
+    }
+
+    /**
+     * Adds a group to processing queue
+     *
+     * @param RollingCurlGroup|Request $request
+     * @return bool
+     */
+    public function add($request) {
+        if ($request instanceof RollingCurlGroup)
+            return $request->addToRC($this);
+        else
+            return parent::add($request);
+    }
+
+    /**
+     * Execute processing
+     *
+     * @param int $window_size Max number of simultaneous connections
+     * @return bool|string
+     */
+    public function execute($window_size = null) {
+        if (count($this->requests) == 0)
+            return false;
+
+        return parent::execute($window_size);
+    }
+}
+

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/example.php.svn-base
@@ -1,1 +1,66 @@
+<?php
+/*
+authored by Josh Fraser (www.joshfraser.com)
+released under Apache License 2.0
 
+Maintained by Alexander Makarov, http://rmcreative.ru/
+
+$Id$
+*/
+
+require("RollingCurl.php");
+
+// a little example that fetches a bunch of sites in parallel and echos the page title and response info for each request
+function request_callback($response, $info, $request) {
+	// parse the page title out of the returned HTML
+	if (preg_match("~<title>(.*?)</title>~i", $response, $out)) {
+		$title = $out[1];
+	}
+	echo "<b>$title</b><br />";
+	print_r($info);
+    print_r($request);
+	echo "<hr>";
+}
+
+// single curl request
+$rc = new RollingCurl("request_callback");
+$rc->request("http://www.msn.com");
+$rc->execute();
+
+// another single curl request
+$rc = new RollingCurl("request_callback");
+$rc->request("http://www.google.com");
+$rc->execute();
+
+echo "<hr>";
+
+// top 20 sites according to alexa (11/5/09)
+$urls = array("http://www.google.com",
+              "http://www.facebook.com",
+              "http://www.yahoo.com",
+              "http://www.youtube.com",
+              "http://www.live.com",
+              "http://www.wikipedia.com",
+              "http://www.blogger.com",
+              "http://www.msn.com",
+              "http://www.baidu.com",
+              "http://www.yahoo.co.jp",
+              "http://www.myspace.com",
+              "http://www.qq.com",
+              "http://www.google.co.in",
+              "http://www.twitter.com",
+              "http://www.google.de",
+              "http://www.microsoft.com",
+              "http://www.google.cn",
+              "http://www.sina.com.cn",
+              "http://www.wordpress.com",
+              "http://www.google.co.uk");
+
+$rc = new RollingCurl("request_callback");
+$rc->window_size = 20;
+foreach ($urls as $url) {
+    $request = new RollingCurlRequest($url);
+    $rc->add($request);
+}
+$rc->execute();
+

--- /dev/null
+++ b/lib/rolling-curl/.svn/text-base/example_groups.php.svn-base
@@ -1,1 +1,49 @@
+<?php
+require 'RollingCurl.php';
+require 'RollingCurlGroup.php';
 
+class TestCurlRequest extends RollingCurlGroupRequest {
+    public $test_verbose = true;
+
+    function process($output, $info) {
+        echo "Processing " . $this->url . "\n";
+        if ($this->test_verbose)
+            print_r($info);
+
+        parent::process($output, $info);
+    }
+}
+
+class TestCurlGroup extends RollingCurlGroup {
+    function process($output, $info, $request) {
+        echo "Group CB: Progress " . $this->name . " (" . ($this->finished_requests + 1) . "/" . $this->num_requests . ")\n";
+        parent::process($output, $info, $request);
+    }
+
+    function finished() {
+        echo "Group CB: Finished" . $this->name . "\n";
+        parent::finished();
+    }
+}
+
+$group = new TestCurlGroup("High");
+$group->add(new TestCurlRequest("www.google.de"));
+$group->add(new TestCurlRequest("www.yahoo.de"));
+$group->add(new TestCurlRequest("www.newyorktimes.com"));
+$reqs[] = $group;
+
+$group = new TestCurlGroup("Normal");
+$group->add(new TestCurlRequest("twitter.com"));
+$group->add(new TestCurlRequest("www.bing.com"));
+$group->add(new TestCurlRequest("m.facebook.com"));
+$reqs[] = $group;
+
+$reqs[] = new TestCurlRequest("www.kernel.org");
+
+// No callback here, as its done in Request class
+$rc = new GroupRollingCurl();
+
+foreach ($reqs as $req)
+    $rc->add($req);
+
+$rc->execute();

--- /dev/null
+++ b/lib/rolling-curl/CHANGELOG.txt
@@ -1,1 +1,15 @@
+Rolling Curl changelog
+======================
 
+September 13, 2010
+------------------
+- Bug #12, #14: Fixed default options overriding (LionsAd)
+- Bug #10: Added use of curl_multi_select to avoid burning CPU (LionsAd)
+- Enh #6, #9: Added $request as parameter to callback function (LionsAd)
+- Chg: Request renamed to RollingCurlRequest (LionsAd)
+- Added RollingCurlGroup class that allows processing groups of requests (LionsAd)
+- More cleanup at unsetting a class (LionsAd)
+- Timeout parameter for curl_multi_select is now configurable (LionsAd)
+- single_curl now returns true (LionsAd)
+- Readme corrections (Alexander Makarov)
+- Code cleanup (Alexander Makarov)

--- /dev/null
+++ b/lib/rolling-curl/README.txt
@@ -1,1 +1,210 @@
-
+Rolling Curl
+============
+
+RollingCurl allows you to process multiple HTTP requests in parallel using CURL PHP library.
+
+Released under the Apache License 2.0.
+
+Authors
+-------
+- Was originally written by [Josh Fraser](joshfraser.com).
+- Currently maintained by [Alexander Makarov](http://rmcreative.ru/).
+- Received significant updates and patched from [LionsAd](http://github.com/LionsAd/rolling-curl).
+
+Overview
+--------
+RollingCurl is a more efficient implementation of curl_multi() curl_multi is a great way to process multiple HTTP requests in parallel in PHP.
+curl_multi is particularly handy when working with large data sets (like fetching thousands of RSS feeds at one time). Unfortunately there is
+very little documentation on the best way to implement curl_multi. As a result, most of the examples around the web are either inefficient or
+fail entirely when asked to handle more than a few hundred requests.
+
+The problem is that most implementations of curl_multi wait for each set of requests to complete before processing them. If there are too many requests
+to process at once, they usually get broken into groups that are then processed one at a time. The problem with this is that each group has to wait for
+the slowest request to download. In a group of 100 requests, all it takes is one slow one to delay the processing of 99 others. The larger the number of
+requests you are dealing with, the more noticeable this latency becomes.
+
+The solution is to process each request as soon as it completes. This eliminates the wasted CPU cycles from busy waiting. Also there is a queue of
+cURL requests to allow for maximum throughput. Each time a request is completed, a new one is added from the queue. By dynamically adding and removing
+links, we keep a constant number of links downloading at all times. This gives us a way to throttle the amount of simultaneous requests we are sending.
+The result is a faster and more efficient way of processing large quantities of cURL requests in parallel.
+
+Callbacks
+---------
+
+Each of requests usually do have a callback to process results that is being executed when request is done
+(both successfully or not).
+
+Callback accepts three parameters and can look like the following one:
+~~~
+[php]
+function request_callback($response, $info, $request){
+    // doing something with the data received
+}
+~~~
+
+- $response contains received page body.
+- $info is an associative array that holds various information about response such as HTTP response code, content type,
+time taken to make request etc.
+- $request contains RollingCurlRequest that was used to make request.
+
+Examples
+--------
+### Hello world
+
+~~~
+[php]
+// an array of URL's to fetch
+$urls = array("http://www.google.com",
+              "http://www.facebook.com",
+              "http://www.yahoo.com");
+
+// a function that will process the returned responses
+function request_callback($response, $info, $request) {
+	// parse the page title out of the returned HTML
+	if (preg_match("~<title>(.*?)</title>~i", $response, $out)) {
+		$title = $out[1];
+	}
+	echo "<b>$title</b><br />";
+	print_r($info);
+	echo "<hr>";
+}
+
+// create a new RollingCurl object and pass it the name of your custom callback function
+$rc = new RollingCurl("request_callback");
+// the window size determines how many simultaneous requests to allow.
+$rc->window_size = 20;
+foreach ($urls as $url) {
+    // add each request to the RollingCurl object
+    $request = new RollingCurlRequest($url);
+    $rc->add($request);
+}
+$rc->execute();
+~~~
+
+
+### Setting custom options
+
+Set custom options for EVERY request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->execute();
+~~~
+
+Set custom options for A SINGLE request:
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$request = new RollingCurlRequest($url);
+$request->options = array(CURLOPT_HEADER => true, CURLOPT_NOBODY => true);
+$rc->add($request);
+$rc->execute();
+~~~
+
+### Shortcuts
+
+~~~
+[php]
+$rc = new RollingCurl("request_callback");
+$rc->get("http://www.google.com");
+$rc->get("http://www.yahoo.com");
+$rc->execute();
+~~~
+
+### Class callbacks
+
+~~~
+[php]
+class MyInfoCollector {
+    private $rc;
+
+    function __construct(){
+        $this->rc = new RollingCurl(array($this, 'processPage'));
+    }
+
+    function processPage($response, $info, $request){
+      //...
+    }
+
+    function run($urls){
+        foreach ($urls as $url){
+            $request = new RollingCurlRequest($url);
+            $this->rc->add($request);
+        }
+        $this->rc->execute();
+    }
+}
+
+$collector = new MyInfoCollector();
+$collector->run(array(
+    'http://google.com/',
+    'http://yahoo.com/'
+));
+~~~
+
+### Using RollingCurlGroup
+
+~~~
+[php]
+class TestCurlRequest extends RollingCurlGroupRequest {
+    public $test_verbose = true;
+
+    function process($output, $info) {
+        echo "Processing " . $this->url . "\n";
+        if ($this->test_verbose)
+            print_r($info);
+
+        parent::process($output, $info);
+    }
+}
+
+class TestCurlGroup extends RollingCurlGroup {
+    function process($output, $info, $request) {
+        echo "Group CB: Progress " . $this->name . " (" . ($this->finished_requests + 1) . "/" . $this->num_requests . ")\n";
+        parent::process($output, $info, $request);
+    }
+
+    function finished() {
+        echo "Group CB: Finished" . $this->name . "\n";
+        parent::finished();
+    }
+}
+
+$group = new TestCurlGroup("High");
+$group->add(new TestCurlRequest("www.google.de"));
+$group->add(new TestCurlRequest("www.yahoo.de"));
+$group->add(new TestCurlRequest("www.newyorktimes.com"));
+$reqs[] = $group;
+
+$group = new TestCurlGroup("Normal");
+$group->add(new TestCurlRequest("twitter.com"));
+$group->add(new TestCurlRequest("www.bing.com"));
+$group->add(new TestCurlRequest("m.facebook.com"));
+$reqs[] = $group;
+
+$reqs[] = new TestCurlRequest("www.kernel.org");
+
+// No callback here, as its done in Request class
+$rc = new GroupRollingCurl();
+
+foreach ($reqs as $req)
+$rc->add($req);
+
+$rc->execute();
+~~~
+
+The same function (add) can be used both for adding requests and groups of requests.
+The "callback" in request and groups is:
+
+process($output, $info)
+
+and
+
+process($output, $info, $request)
+
+Also you can override RollingCurlGroup::finished() that will be executed right after finishing group processing.
+
+$Id$

--- /dev/null
+++ b/lib/rolling-curl/RollingCurl.php
@@ -1,1 +1,376 @@
-
+<?php
+/*
+Authored by Josh Fraser (www.joshfraser.com)
+Released under Apache License 2.0
+
+Maintained by Alexander Makarov, http://rmcreative.ru/
+
+$Id$
+*/
+
+/**
+ * Class that represent a single curl request
+ */
+class RollingCurlRequest {
+    public $url = false;
+    public $method = 'GET';
+    public $post_data = null;
+    public $headers = null;
+    public $options = null;
+    public $metadata = Array();
+    
+    /**
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return void
+     */
+    function __construct($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->url = $url;
+        $this->method = $method;
+        $this->post_data = $post_data;
+        $this->headers = $headers;
+        $this->options = $options;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->url, $this->method, $this->post_data, $this->headers, $this->options);
+    }
+}
+
+/**
+ * RollingCurl custom exception
+ */
+class RollingCurlException extends Exception {
+}
+
+/**
+ * Class that holds a rolling queue of curl requests.
+ *
+ * @throws RollingCurlException
+ */
+class RollingCurl {
+    /**
+     * @var int
+     *
+     * Window size is the max number of simultaneous connections allowed.
+     *
+     * REMEMBER TO RESPECT THE SERVERS:
+     * Sending too many requests at one time can easily be perceived
+     * as a DOS attack. Increase this window_size if you are making requests
+     * to multiple servers or have permission from the receving server admins.
+     */
+    private $window_size = 5;
+
+    /**
+     * @var float
+     *
+     * Timeout is the timeout used for curl_multi_select.
+     */
+    private $timeout = 10;
+
+    /**
+     * @var string|array
+     *
+     * Callback function to be applied to each result.
+     */
+    private $callback;
+
+    /**
+     * @var array
+     *
+     * Set your base options that you want to be used with EVERY request.
+     */
+    protected $options = array(
+        CURLOPT_SSL_VERIFYPEER => 0,
+        CURLOPT_RETURNTRANSFER => 1,
+        CURLOPT_CONNECTTIMEOUT => 60,
+        CURLOPT_TIMEOUT => 60
+    );
+
+    /**
+     * @var array
+     */
+    private $headers = array();
+
+    /**
+     * @var Request[]
+     *
+     * The request queue
+     */
+    private $requests = array();
+
+    /**
+     * @var RequestMap[]
+     *
+     * Maps handles to request indexes
+     */
+    private $requestMap = array();
+
+    /**
+     * @param  $callback
+     * Callback function to be applied to each result.
+     *
+     * Can be specified as 'my_callback_function'
+     * or array($object, 'my_callback_method').
+     *
+     * Function should take three parameters: $response, $info, $request.
+     * $response is response body, $info is additional curl info.
+     * $request is the original request
+     *
+     * @return void
+     */
+    function __construct($callback = null) {
+        $this->callback = $callback;
+    }
+
+    /**
+     * @param string $name
+     * @return mixed
+     */
+    public function __get($name) {
+        return (isset($this->{$name})) ? $this->{$name} : null;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $value
+     * @return bool
+     */
+    public function __set($name, $value) {
+        // append the base options & headers
+        if ($name == "options" || $name == "headers") {
+            $this->{$name} = $value + $this->{$name};
+        } else {
+            $this->{$name} = $value;
+        }
+        return true;
+    }
+
+    /**
+     * Add a request to the request queue
+     *
+     * @param Request $request
+     * @return bool
+     */
+    public function add($request) {
+        $this->requests[] = $request;
+        return true;
+    }
+
+    /**
+     * Create new Request and add it to the request queue
+     *
+     * @param string $url
+     * @param string $method
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function request($url, $method = "GET", $post_data = null, $headers = null, $options = null) {
+        $this->requests[] = new RollingCurlRequest($url, $method, $post_data, $headers, $options);
+        return true;
+    }
+
+    /**
+     * Perform GET request
+     *
+     * @param string $url
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function get($url, $headers = null, $options = null) {
+        return $this->request($url, "GET", null, $headers, $options);
+    }
+
+    /**
+     * Perform POST request
+     *
+     * @param string $url
+     * @param  $post_data
+     * @param  $headers
+     * @param  $options
+     * @return bool
+     */
+    public function post($url, $post_data = null, $headers = null, $options = null) {
+        return $this->request($url, "POST", $post_data, $headers, $options);
+    }
+
+    /**
+     * Execute processing
+     *
+     * @param int $window_size Max number of simultaneous connections
+     * @return string|bool
+     */
+    public function execute($window_size = null) {
+        // rolling curl window must always be greater than 1
+        if (sizeof($this->requests) == 1) {
+            return $this->single_curl();
+        } else {
+            // start the rolling curl. window_size is the max number of simultaneous connections
+            return $this->rolling_curl($window_size);
+        }
+    }
+
+    /**
+     * Performs a single curl request
+     *
+     * @access private
+     * @return string
+     */
+    private function single_curl() {
+        $ch = curl_init();
+        $request = array_shift($this->requests);
+        $options = $this->get_options($request);
+        curl_setopt_array($ch, $options);
+        $output = curl_exec($ch);
+        $info = curl_getinfo($ch);
+
+        // it's not neccesary to set a callback for one-off requests
+        if ($this->callback) {
+            $callback = $this->callback;
+            if (is_callable($this->callback)) {
+                call_user_func($callback, $output, $info, $request);
+            }
+        }
+        else
+            return $output;
+        return true;
+    }
+
+    /**
+     * Performs multiple curl requests
+     *
+     * @access private
+     * @throws RollingCurlException
+     * @param int $window_size Max number of simultaneous connections
+     * @return bool
+     */
+    private function rolling_curl($window_size = null) {
+        if ($window_size)
+            $this->window_size = $window_size;
+
+        // make sure the rolling window isn't greater than the # of urls
+        if (sizeof($this->requests) < $this->window_size)
+            $this->window_size = sizeof($this->requests);
+
+        if ($this->window_size < 2) {
+            throw new RollingCurlException("Window size must be greater than 1");
+        }
+
+        $master = curl_multi_init();
+
+        // start the first batch of requests
+        for ($i = 0; $i < $this->window_size; $i++) {
+            $ch = curl_init();
+
+            $options = $this->get_options($this->requests[$i]);
+
+            curl_setopt_array($ch, $options);
+            curl_multi_add_handle($master, $ch);
+
+            // Add to our request Maps
+            $key = (string) $ch;
+            $this->requestMap[$key] = $i;
+        }
+
+        do {
+            while (($execrun = curl_multi_exec($master, $running)) == CURLM_CALL_MULTI_PERFORM) ;
+            if ($execrun != CURLM_OK)
+                break;
+            // a request was just completed -- find out which one
+            while ($done = curl_multi_info_read($master)) {
+
+                // get the info and content returned on the request
+                $info = curl_getinfo($done['handle']);
+                $output = curl_multi_getcontent($done['handle']);
+
+                // send the return values to the callback function.
+                $callback = $this->callback;
+                if (is_callable($callback)) {
+                    $key = (string) $done['handle'];
+                    $request = $this->requests[$this->requestMap[$key]];
+                    unset($this->requestMap[$key]);
+                    call_user_func($callback, $output, $info, $request);
+                }
+
+                // start a new request (it's important to do this before removing the old one)
+                if ($i < sizeof($this->requests) && isset($this->requests[$i]) && $i < count($this->requests)) {
+                    $ch = curl_init();
+                    $options = $this->get_options($this->requests[$i]);
+                    curl_setopt_array($ch, $options);
+                    curl_multi_add_handle($master, $ch);
+
+                    // Add to our request Maps
+                    $key = (string) $ch;
+                    $this->requestMap[$key] = $i;
+                    $i++;
+                }
+
+                // remove the curl handle that just completed
+                curl_multi_remove_handle($master, $done['handle']);
+
+            }
+
+            // Block for data in / output; error handling is done by curl_multi_exec
+            if ($running)
+                curl_multi_select($master, $this->timeout);
+
+        } while ($running);
+        curl_multi_close($master);
+        return true;
+    }
+
+
+    /**
+     * Helper function to set up a new request by setting the appropriate options
+     *
+     * @access private
+     * @param Request $request
+     * @return array
+     */
+    private function get_options($request) {
+        // options for this entire curl object
+        $options = $this->__get('options');
+        if (ini_get('safe_mode') == 'Off' || !ini_get('safe_mode')) {
+            $options[CURLOPT_FOLLOWLOCATION] = 1;
+            $options[CURLOPT_MAXREDIRS] = 5;
+        }
+        $headers = $this->__get('headers');
+
+        // append custom options for this specific request
+        if ($request->options) {
+            $options = $request->options + $options;
+        }
+
+        // set the request URL
+        $options[CURLOPT_URL] = $request->url;
+
+        // posting data w/ this request?
+        if ($request->post_data) {
+            $options[CURLOPT_POST] = 1;
+            $options[CURLOPT_POSTFIELDS] = $request->post_data;
+        }
+        if ($headers) {
+            $options[CURLOPT_HEADER] = 0;
+            $options[CURLOPT_HTTPHEADER] = $headers;
+        }
+
+        return $options;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->window_size, $this->callback, $this->options, $this->headers, $this->requests);
+    }
+}
+

--- /dev/null
+++ b/lib/rolling-curl/RollingCurlGroup.php
@@ -1,1 +1,218 @@
-
+<?php
+/*
+
+  Authored by Fabian Franz (www.lionsad.de)
+  Released under Apache License 2.0
+
+$Id$
+*/
+
+class RollingCurlGroupException extends Exception {}
+
+/**
+ * @throws RollingCurlGroupException
+ */
+abstract class RollingCurlGroupRequest extends RollingCurlRequest {
+    private $group = null;
+
+    /**
+     * Set group for this request
+     *
+     * @param group The group to be set
+     */
+    function setGroup($group) {
+        if (!($group instanceof RollingCurlGroup))
+            throw new RollingCurlGroupException("setGroup: group needs to be of instance RollingCurlGroup");
+
+        $this->group = $group;
+    }
+
+    /**
+     * Process the request
+     *
+     *
+     */
+    function process($output, $info) {
+        if ($this->group)
+            $this->group->process($output, $info, $this);
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->group);
+        parent::__destruct();
+    }
+
+}
+
+/**
+ * A group of curl requests.
+ *
+ * @throws RollingCurlGroupException *
+ */
+class RollingCurlGroup {
+    /**
+     * @var string group name
+     */
+    protected $name;
+
+    /**
+     * @var int total number of requests in a group
+     */
+    protected $num_requests = 0;
+
+    /**
+     * @var int total number of finished requests in a group
+     */
+    protected $finished_requests = 0;
+
+    /**
+     * @var array requests array
+     */
+    private $requests = array();
+
+    /**
+     * @param string $name group name
+     * @return void
+     */
+    function __construct($name) {
+        $this->name = $name;
+    }
+
+    /**
+     * @return void
+     */
+    public function __destruct() {
+        unset($this->name, $this->num_requests, $this->finished_requests, $this->requests);
+    }
+
+    /**
+     * Adds request to a group
+     *
+     * @throws RollingCurlGroupException
+     * @param RollingCurlGroupRequest|array $request
+     * @return bool
+     */
+    function add($request) {
+        if ($request instanceof RollingCurlGroupRequest) {
+            $request->setGroup($this);
+            $this->num_requests++;
+            $this->requests[] = $request;
+        }
+        else if (is_array($request)) {
+            foreach ($request as $req)
+            $this->add($req);
+        }
+        else
+            throw new RollingCurlGroupException("add: Request needs to be of instance RollingCurlGroupRequest");
+
+        return true;
+    }
+
+    /**
+     * @throws RollingCurlGroupException
+     * @param RollingCurl $rc
+     * @return bool
+     */
+    function addToRC(RollingCurl $rc){
+        $ret = true;
+
+        while (count($this->requests) > 0){
+            $ret1 = $rc->add(array_shift($this->requests));
+            if (!$ret1)
+                $ret = false;
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Override to implement custom response processing.
+     *
+     * Don't forget to call parent::process().
+     *
+     * @param string $output received page body
+     * @param array $info holds various information about response such as HTTP response code, content type, time taken to make request etc.
+     * @param RollingCurlRequest $request request used
+     * @return void
+     */
+    function process($output, $info, $request) {
+        $this->finished_requests++;
+
+        if ($this->finished_requests >= $this->num_requests)
+            $this->finished();
+    }
+
+    /**
+     * Override to execute code after all requests in a group are processed.
+     *
+     * @return void
+     */
+    function finished() {
+    }
+
+}
+
+/**
+ * Group version of rolling curl
+ */
+class GroupRollingCurl extends RollingCurl {
+
+    /**
+     * @var mixed common callback for all groups
+     */
+    private $group_callback = null;
+
+    /**
+     * @param string $output received page body
+     * @param array $info holds various information about response such as HTTP response code, content type, time taken to make request etc.
+     * @param RollingCurlRequest $request request used
+     * @return void
+     */
+    protected function process($output, $info, $request) {
+        if ($request instanceof RollingCurlGroupRequest)
+            $request->process($output, $info);
+
+        if (is_callable($this->group_callback))
+            call_user_func($this->group_callback, $output, $info, $request);
+    }
+
+    /**
+     * @param mixed $callback common callback for all groups
+     * @return void
+     */
+    function __construct($callback = null) {
+        $this->group_callback = $callback;
+
+        parent::__construct(array(&$this, "process"));
+    }
+
+    /**
+     * Adds a group to processing queue
+     *
+     * @param RollingCurlGroup|Request $request
+     * @return bool
+     */
+    public function add($request) {
+        if ($request instanceof RollingCurlGroup)
+            return $request->addToRC($this);
+        else
+            return parent::add($request);
+    }
+
+    /**
+     * Execute processing
+     *
+     * @param int $window_size Max number of simultaneous connections
+     * @return bool|string
+     */
+    public function execute($window_size = null) {
+        if (count($this->requests) == 0)
+            return false;
+
+        return parent::execute($window_size);
+    }
+}
+

--- /dev/null
+++ b/lib/rolling-curl/example.php
@@ -1,1 +1,66 @@
+<?php
+/*
+authored by Josh Fraser (www.joshfraser.com)
+released under Apache License 2.0
 
+Maintained by Alexander Makarov, http://rmcreative.ru/
+
+$Id$
+*/
+
+require("RollingCurl.php");
+
+// a little example that fetches a bunch of sites in parallel and echos the page title and response info for each request
+function request_callback($response, $info, $request) {
+	// parse the page title out of the returned HTML
+	if (preg_match("~<title>(.*?)</title>~i", $response, $out)) {
+		$title = $out[1];
+	}
+	echo "<b>$title</b><br />";
+	print_r($info);
+    print_r($request);
+	echo "<hr>";
+}
+
+// single curl request
+$rc = new RollingCurl("request_callback");
+$rc->request("http://www.msn.com");
+$rc->execute();
+
+// another single curl request
+$rc = new RollingCurl("request_callback");
+$rc->request("http://www.google.com");
+$rc->execute();
+
+echo "<hr>";
+
+// top 20 sites according to alexa (11/5/09)
+$urls = array("http://www.google.com",
+              "http://www.facebook.com",
+              "http://www.yahoo.com",
+              "http://www.youtube.com",
+              "http://www.live.com",
+              "http://www.wikipedia.com",
+              "http://www.blogger.com",
+              "http://www.msn.com",
+              "http://www.baidu.com",
+              "http://www.yahoo.co.jp",
+              "http://www.myspace.com",
+              "http://www.qq.com",
+              "http://www.google.co.in",
+              "http://www.twitter.com",
+              "http://www.google.de",
+              "http://www.microsoft.com",
+              "http://www.google.cn",
+              "http://www.sina.com.cn",
+              "http://www.wordpress.com",
+              "http://www.google.co.uk");
+
+$rc = new RollingCurl("request_callback");
+$rc->window_size = 20;
+foreach ($urls as $url) {
+    $request = new RollingCurlRequest($url);
+    $rc->add($request);
+}
+$rc->execute();
+

--- /dev/null
+++ b/lib/rolling-curl/example_groups.php
@@ -1,1 +1,49 @@
+<?php
+require 'RollingCurl.php';
+require 'RollingCurlGroup.php';
 
+class TestCurlRequest extends RollingCurlGroupRequest {
+    public $test_verbose = true;
+
+    function process($output, $info) {
+        echo "Processing " . $this->url . "\n";
+        if ($this->test_verbose)
+            print_r($info);
+
+        parent::process($output, $info);
+    }
+}
+
+class TestCurlGroup extends RollingCurlGroup {
+    function process($output, $info, $request) {
+        echo "Group CB: Progress " . $this->name . " (" . ($this->finished_requests + 1) . "/" . $this->num_requests . ")\n";
+        parent::process($output, $info, $request);
+    }
+
+    function finished() {
+        echo "Group CB: Finished" . $this->name . "\n";
+        parent::finished();
+    }
+}
+
+$group = new TestCurlGroup("High");
+$group->add(new TestCurlRequest("www.google.de"));
+$group->add(new TestCurlRequest("www.yahoo.de"));
+$group->add(new TestCurlRequest("www.newyorktimes.com"));
+$reqs[] = $group;
+
+$group = new TestCurlGroup("Normal");
+$group->add(new TestCurlRequest("twitter.com"));
+$group->add(new TestCurlRequest("www.bing.com"));
+$group->add(new TestCurlRequest("m.facebook.com"));
+$reqs[] = $group;
+
+$reqs[] = new TestCurlRequest("www.kernel.org");
+
+// No callback here, as its done in Request class
+$rc = new GroupRollingCurl();
+
+foreach ($reqs as $req)
+    $rc->add($req);
+
+$rc->execute();

file:a/postinstall (deleted)
--- a/postinstall
+++ /dev/null
@@ -1,32 +1,1 @@
-#!/bin/bash
-#this script should be run from a fresh git checkout from github
-#ami base must have yum install lighttpd-fastcgi, git, tomcat6 
-#screen php-cli php-gd tomcat6-webapps tomcat6-admin-webapps svn maven2
-#http://www.how2forge.org/installing-lighttpd-with-php5-and-mysql-support-on-fedora-12
 
-cp /root/aws.php /tmp/
-mkdir /var/www/lib/staticmaplite/cache 
-chcon -h system_u:object_r:httpd_sys_content_t /var/www
-chcon -R -h root:object_r:httpd_sys_content_t /var/www/*
-chcon -R -t httpd_sys_content_rw_t /var/www/lib/staticmaplite/cache
-chmod -R 777 /var/www/lib/staticmaplite/cache 
-wget http://s3-ap-southeast-1.amazonaws.com/busresources/cbrfeed.zip \
--O /var/www/cbrfeed.zip
-
-createdb transitdata
-#made with pg_dump transitdata | gzip -c >  transitdata.cbrfeed.sql.gz
-gunzip /var/www/transitdata.cbrfeed.sql.gz
-psql -d transitdata -f /var/www/transitdata.cbrfeed.sql
-createlang -d transitdata plpgsql
-psql -d transitdata -f /var/www/lib/postgis.sql
-php /var/www/updatedb.php
-
-wget http://s3-ap-southeast-1.amazonaws.com/busresources/Graph.obj \
--O /tmp/Graph.obj
-rm -rfv /usr/share/tomcat6/webapps/opentripplanner*
-wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-webapp.war \
--O /usr/share/tomcat6/webapps/opentripplanner-webapp.war
-wget http://s3-ap-southeast-1.amazonaws.com/busresources/opentripplanner-api-webapp.war \
--O /usr/share/tomcat6/webapps/opentripplanner-api-webapp.war
-/etc/init.d/tomcat6 restart
-

file:a/readme.txt -> file:b/readme.txt
--- a/readme.txt
+++ b/readme.txt
@@ -2,14 +2,32 @@
 Based on the maxious-canberra-transit-feed @ http://s3-ap-southeast-1.amazonaws.com/busresources/cbrfeed.zip
 Source code for the https://github.com/maxious/ACTBus-data transit 
 feed and https://github.com/maxious/ACTBus-ui this site available from github.
-Uses jQuery Mobile, PHP, Ruby, Python, Google Transit Feed Specification 
-tools, OpenTripPlanner, OpenLayers, OpenStreetMap, Cloudmade Geocoder 
+Uses jQuery Mobile, PHP, PostgreSQL, OpenTripPlanner, OpenLayers, OpenStreetMap, Cloudmade Geocoder 
 and Tile Service
 
-Must have view.sh running on port 8765 for this webapp to work
+See aws/awsStartup.sh for example startup steps. You need to load the included database dump; 
+for other transit networks you can use the updatedb.php script to load.
 
-For static maps, may have to do
+For openstreetmap static maps, may have to do
 /usr/sbin/setsebool -P httpd_can_network_connect=1
-on fedora
+on Fedora and other SELinux systems.
+
+To enter a service override, you can use the psql tool. eg.
+transitdata=# COPY calendar_dates (service_id, date, exception_type) FROM stdin;
+Enter data to be copied [spaced with tabs] followed by a newline.
+End with a backslash and a period on a line by itself.
+>> saturday	20110416	2 
+>> sunday	20110416	1
+>> saturday	20110423 	2
+>> sunday	20110423 	1
+>> weekday	20110425        2
+>> sunday	20110425        1
+>> weekday	20110422        2
+>> noservice    20110422        1
+>> weekday	20110426        2
+>> noservice    20110426        1
+>> sunday	20110424 	2
+>> noservice    20110424 	1
+>> \.
 
 

--- a/routeList.php
+++ b/routeList.php
@@ -7,87 +7,89 @@
 			<ul> 
 				<li><a href="routeList.php">By Final Destination...</a></li> 
 				<li><a href="routeList.php?bynumber=yes">By Number... </a></li>
-				<li><a href="routeList.php?bysuburb=yes">By Suburb... </a></li>
+				<li><a href="routeList.php?bysuburbs=yes">By Suburb... </a></li>
 				<li><a href="routeList.php?nearby=yes">Nearby... </a></li>
 			</ul>
                 </div>
 	';
 }
-if ($_REQUEST['bysuburb']) {
+if (isset($bysuburbs)) {
 	include_header("Routes by Suburb", "routeList");
 	navbar();
 	echo '  <ul data-role="listview" data-filter="true" data-inset="true" >';
-	if (!isset($_REQUEST['firstLetter'])) {
+	if (!isset($firstLetter)) {
 		foreach (range('A', 'Z') as $letter) {
-			echo "<li><a href=\"routeList.php?firstLetter=$letter&bysuburb=yes\">$letter...</a></li>\n";
+			echo "<li><a href=\"routeList.php?firstLetter=$letter&amp;bysuburbs=yes\">$letter...</a></li>\n";
 		}
 	}
 	else {
 		foreach ($suburbs as $suburb) {
-			if (startsWith($suburb, $_REQUEST['firstLetter'])) {
+			if (startsWith($suburb, $firstLetter)) {
 				echo '<li><a href="routeList.php?suburb=' . urlencode($suburb) . '">' . $suburb . '</a></li>';
 			}
 		}
 	}
 	echo '</ul>';
 }
-else if ($_REQUEST['nearby'] || $_REQUEST['suburb']) {
-	if ($_REQUEST['suburb']) {
-		$suburb = filter_var($_REQUEST['suburb'], FILTER_SANITIZE_STRING);
-		$url = $APIurl . "/json/stopzonesearch?q=" . $suburb;
-		include_header("Routes by Suburb", "routeList");
+else if (isset($nearby) || isset($suburb)) {
+	$routes = Array();
+	if ($suburb) {
+		include_header($suburb . " - " . ucwords(service_period()) , "routeList");
+		navbar();
+		timePlaceSettings();
 		trackEvent("Route Lists", "Routes By Suburb", $suburb);
+		$routes = getRoutesbysuburbs($suburb);
 	}
-	if ($_REQUEST['nearby']) {
-		$url = $APIurl . "/json/neareststops?lat={$_SESSION['lat']}&lon={$_SESSION['lon']}&limit=15";
+	if (isset($nearby)) {
 		include_header("Routes Nearby", "routeList", true, true);
+		trackEvent("Route Lists", "Routes Nearby", $_SESSION['lat'] . "," . $_SESSION['lon']);
+		navbar();
 		timePlaceSettings(true);
 		if (!isset($_SESSION['lat']) || !isset($_SESSION['lat']) || $_SESSION['lat'] == "" || $_SESSION['lon'] == "") {
 			include_footer();
 			die();
 		}
+		$routes = getRoutesNearby($_SESSION['lat'], $_SESSION['lon']);
 	}
-	$stops = json_decode(getPage($url));
-	$routes = Array();
-	foreach ($stops as $stop) {
-		$url = $APIurl . "/json/stoproutes?stop=" . $stop[0];
-		$stoproutes = json_decode(getPage($url));
-		foreach ($stoproutes as $route) {
-			if (!isset($routes[$route[0]])) $routes[$route[0]] = $route;
+	echo '  <ul data-role="listview" data-filter="true" data-inset="true" >';
+	if ($routes) {
+		foreach ($routes as $route) {
+			echo '<li><a href="trip.php?routeid=' . $route['route_id'] . '"><h3>' . $route['route_short_name'] . "</h3><p>" . $route['route_long_name'] . " (" . ucwords($route['service_id']) . ")</p>";
+			if (isset($nearby)) {
+				$time = getTimeInterpolatedRouteAtStop($route['route_id'], $route['stop_id']);
+				echo '<span class="ui-li-count">' . ($time['arrival_time'] ? $time['arrival_time'] : "No more trips today") . "<br>" . floor($route['distance']) . 'm away</span>';
+			}
+			echo "</a></li>\n";
 		}
 	}
-	navbar();
-	echo '  <ul data-role="listview" data-filter="true" data-inset="true" >';
-	sksort($routes, 1, true);
-	foreach ($routes as $row) {
-		echo '<li><a href="trip.php?routeid=' . $row[0] . '"><h3>'. $row[1] . "</h3><p>". $row[2] . " (" . ucwords($row[4]) . ")</p></a></li>\n";
+	else {
+		echo "<li style='text-align: center;'> No routes nearby.</li>";
 	}
 }
-else if ($_REQUEST['bynumber'] || $_REQUEST['numberSeries']) {
+else if (isset($bynumber) || isset($numberSeries)) {
 	include_header("Routes by Number", "routeList");
 	navbar();
 	echo ' <ul data-role="listview"  data-inset="true">';
-	$url = $APIurl . "/json/routes";
-	$contents = json_decode(getPage($url));
-	$routeSeries = Array();
-	$seriesRange = Array();
-	foreach ($contents as $key => $row) {
-		foreach (explode(" ", $row[1]) as $routeNumber) {
-			$seriesNum = substr($routeNumber, 0, -1) . "0";
-			if ($seriesNum == "0") $seriesNum = $routeNumber;
-			$finalDigit = substr($routeNumber, sizeof($routeNumber) - 1, 1);
-			if (isset($seriesRange[$seriesNum])) {
-				if ($finalDigit < $seriesRange[$seriesNum]['max']) $seriesRange[$seriesNum]['max'] = $routeNumber;
-				if ($finalDigit > $seriesRange[$seriesNum]['min']) $seriesRange[$seriesNum]['min'] = $routeNumber;
+	if (isset($bynumber)) {
+		$routes = getRoutesByNumber();
+		$routeSeries = Array();
+		$seriesRange = Array();
+		foreach ($routes as $key => $routeNumber) {
+			foreach (explode(" ", $routeNumber['route_short_name']) as $routeNumber) {
+				$seriesNum = substr($routeNumber, 0, -1) . "0";
+				if ($seriesNum == "0") $seriesNum = $routeNumber;
+				$finalDigit = substr($routeNumber, sizeof($routeNumber) - 1, 1);
+				if (isset($seriesRange[$seriesNum])) {
+					if ($finalDigit < $seriesRange[$seriesNum]['max']) $seriesRange[$seriesNum]['max'] = $routeNumber;
+					if ($finalDigit > $seriesRange[$seriesNum]['min']) $seriesRange[$seriesNum]['min'] = $routeNumber;
+				}
+				else {
+					$seriesRange[$seriesNum]['max'] = $routeNumber;
+					$seriesRange[$seriesNum]['min'] = $routeNumber;
+				}
+				$routeSeries[$seriesNum][$seriesNum . "-" . $row[1] . "-" . $row[0]] = $row;
 			}
-			else {
-				$seriesRange[$seriesNum]['max'] = $routeNumber;
-				$seriesRange[$seriesNum]['min'] = $routeNumber;
-			}
-			$routeSeries[$seriesNum][$seriesNum . "-" . $row[1] . "-" . $row[0]] = $row;
 		}
-	}
-	if ($_REQUEST['bynumber']) {
 		ksort($routeSeries);
 		ksort($seriesRange);
 		foreach ($routeSeries as $series => $routes) {
@@ -97,9 +99,10 @@
 			echo "</a></li>\n";
 		}
 	}
-	else if ($_REQUEST['numberSeries']) {
-		foreach ($routeSeries[$_REQUEST['numberSeries']] as $row) {
-			echo '<li> <a href="trip.php?routeid=' . $row[0] . '"><h3>' . $row[1] . "</h3><p>".  $row[2] . " (" . ucwords($row[3]) . ")</p></a></li>\n";
+	else if ($numberSeries) {
+		$routes = getRoutesByNumber($numberSeries);
+		foreach ($routes as $route) {
+			echo '<li> <a href="trip.php?routeid=' . $route['route_id'] . '"><h3>' . $route['route_short_name'] . "</h3><p>" . $route['route_long_name'] . " (" . ucwords($route['service_id']) . ")</p></a></li>\n";
 		}
 	}
 }
@@ -107,20 +110,14 @@
 	include_header("Routes by Destination", "routeList");
 	navbar();
 	echo ' <ul data-role="listview"  data-inset="true">';
-	$url = $APIurl . "/json/routes";
-	$contents = json_decode(getPage($url));
-	// by destination!
-	foreach ($contents as $row) {
-		$routeDestinations[$row[2]][] = $row;
-	}
-	if ($_REQUEST['routeDestination']) {
-		foreach ($routeDestinations[urldecode($_REQUEST['routeDestination'])] as $row) {
-			echo '<li><a href="trip.php?routeid=' . $row[0] . '"><h3>' . $row[1] . '</h3><p>'  . $row[2] . " (" . ucwords($row[3]) . ")</p></a></li>\n";
+	if (isset($routeDestination)) {
+		foreach (getRoutesByDestination($routeDestination) as $route) {
+			echo '<li><a href="trip.php?routeid=' . $route["route_id"] . '"><h3>' . $route["route_short_name"] . '</h3><p>' . $route["route_long_name"] . " (" . ucwords($route['service_id']) . ")</p></a></li>\n";
 		}
 	}
 	else {
-		foreach ($routeDestinations as $destination => $routes) {
-			echo '<li><a href="' . curPageURL() . '/routeList.php?routeDestination=' . urlencode($destination) . '">' . $destination . "... </a></li>\n";
+		foreach (getRoutesByDestination() as $destination) {
+			echo '<li><a href="' . curPageURL() . '/routeList.php?routeDestination=' . urlencode($destination['route_long_name']) . '">' . $destination['route_long_name'] . "... </a></li>\n";
 		}
 	}
 }

file:a/stop.php -> file:b/stop.php
--- a/stop.php
+++ b/stop.php
@@ -1,10 +1,7 @@
 <?php
 include ('include/common.inc.php');
-$stopid = filter_var($_REQUEST['stopid'], FILTER_SANITIZE_NUMBER_INT);
-$stopcode = filter_var($_REQUEST['stopcode'], FILTER_SANITIZE_STRING);
-$url = $APIurl . "/json/stop?stop_id=" . $stopid;
-$stop = json_decode(getPage($url));
-if ($stopcode != "" && $stop[5] != $stopcode) {
+if ($stopid) $stop = getStop($stopid);
+/*if ($stopcode != "" && $stop[5] != $stopcode) {
 	$url = $APIurl . "/json/stopcodesearch?q=" . $stopcode;
 	$stopsearch = json_decode(getPage($url));
 	$stopid = $stopsearch[0][0];
@@ -14,7 +11,7 @@
 if (!startsWith($stop[5], "Wj") && strpos($stop[1], "Platform") === false) {
 	// expand out to all platforms
 	
-}
+}*/
 $stops = Array();
 $stopPositions = Array();
 $stopNames = Array();
@@ -22,97 +19,94 @@
 $allStopsTrips = Array();
 $fetchedTripSequences = Array();
 $stopLinks = "";
-if (isset($_REQUEST['stopids'])) {
-	$stopids = explode(",", filter_var($_REQUEST['stopids'], FILTER_SANITIZE_STRING));
+if (isset($stopids)) {
 	foreach ($stopids as $sub_stopid) {
-		$url = $APIurl . "/json/stop?stop_id=" . $sub_stopid;
-		$stop = json_decode(getPage($url));
-		$stops[] = $stop;
+		$stops[] = getStop($sub_stopid);
 	}
 	$stop = $stops[0];
-	$stopid = $stops[0][0];
+	$stopid = $stops[0]["stop_id"];
 	$stopLinks.= "Individual stop pages: ";
 	foreach ($stops as $key => $sub_stop) {
-	//	$stopNames[$key] = $sub_stop[1] . ' Stop #' . ($key + 1);
-        if (strpos($stop[1],
-                   "Station")) {
-		$stopNames[$key] = 'Platform ' . ($key + 1);
-		$stopLinks.= '<a href="stop.php?stopid=' . $sub_stop[0] . '&stopcode=' . $sub_stop[5] . '">' . $sub_stop[1] . '</a> ';  
-        }         else {
-		$stopNames[$key] = '#' . ($key + 1);
-		$stopLinks.= '<a href="stop.php?stopid=' . $sub_stop[0] . '&stopcode=' . $sub_stop[5] . '">' . $sub_stop[1] . ' Stop #' . ($key + 1) . '</a> ';
-        }
+		//	$stopNames[$key] = $sub_stop[1] . ' Stop #' . ($key + 1);
+		if (strpos($stop["stop_name"], "Station")) {
+			$stopNames[$key] = 'Platform ' . ($key + 1);
+			$stopLinks.= '<a href="stop.php?stopid=' . $sub_stop["stop_id"] . '&amp;stopcode=' . $sub_stop["stop_code"] . '">' . $sub_stop["stop_name"] . '</a> ';
+		}
+		else {
+			$stopNames[$key] = '#' . ($key + 1);
+			$stopLinks.= '<a href="stop.php?stopid=' . $sub_stop["stop_id"] . '&amp;stopcode=' . $sub_stop["stop_code"] . '">' . $sub_stop["stop_name"] . ' Stop #' . ($key + 1) . '</a> ';
+		}
 		$stopPositions[$key] = Array(
-			$sub_stop[2],
-			$sub_stop[3]
+			$sub_stop["stop_lat"],
+			$sub_stop["stop_lon"]
 		);
-                
-                $url = $APIurl . "/json/stopalltrips?stop=" . $sub_stop[0];		$trips = json_decode(getPage($url));
-                $tripSequence = "";
+		$trips = getStopTrips($sub_stop["stop_id"]);
+		$tripSequence = "";
 		foreach ($trips as $trip) {
-                        $tripSequence .= "$trip[0],";
-			$tripStopNumbers[$trip[0]][] = $key;
+			$tripSequence.= "{$trip['trip_id']},";
+			$tripStopNumbers[$trip['trip_id']][] = $key;
 		}
-                
-                if (!in_array($tripSequence,$fetchedTripSequences)) {
-                    // only fetch new trip sequences
-                    $fetchedTripSequences[] = $tripSequence;
-                    $url = $APIurl . "/json/stoptrips?stop=" . $sub_stop[0] . "&time=" . midnight_seconds() . "&service_period=" . service_period();
-                    $trips = json_decode(getPage($url));
-                    foreach ($trips as $trip) {
-                            if (!isset($allStopsTrips[$trip[1][0]])) $allStopsTrips[$trip[1][0]] = $trip;
-                    }
-                } else {
-                    echo "skipped sequence $tripSequence";
-                }
+		if (!in_array($tripSequence, $fetchedTripSequences)) {
+			// only fetch new trip sequences
+			$fetchedTripSequences[] = $tripSequence;
+			$trips = getStopTripsWithTimes($sub_stop["stop_id"]);
+			foreach ($trips as $trip) {
+				if (!isset($allStopsTrips[$trip["trip_id"]])) $allStopsTrips[$trip["trip_id"]] = $trip;
+			}
+		}
+		//else {
+		//	echo "skipped sequence $tripSequence";
+		//}
 	}
 }
-include_header($stop[1], "stop");
+include_header($stop['stop_name'], "stop");
 timePlaceSettings();
-echo '<div data-role="content" class="ui-content" role="main">        <a name="maincontent" id="maincontent"></a>';
 echo $stopLinks;
 if (sizeof($stops) > 0) {
-    trackEvent("View Stops","View Combined Stops", $stop[1], $stop[0]);
-
-	echo '<p>' . staticmap($stopPositions) . '</p>';
+	trackEvent("View Stops", "View Combined Stops", $stop["stop_name"], $stop["stop_id"]);
+	echo staticmap($stopPositions);
 }
 else {
-        trackEvent("View Stops","View Single Stop", $stop[1], $stop[0]);
-	echo '<p>' . staticmap(Array(
+	trackEvent("View Stops", "View Single Stop", $stop["stop_name"], $stop["stop_id"]);
+	echo staticmap(Array(
 		0 => Array(
-			$stop[2],
-			$stop[3]
+			$stop["stop_lat"],
+			$stop["stop_lon"]
 		)
-	)) . '</p>';
+	)) ;
 }
 echo '  <ul data-role="listview"  data-inset="true">';
 if (sizeof($allStopsTrips) > 0) {
-    sksort($allStopsTrips,0, $true);
+    sktimesort($allStopsTrips,"arrival_time", true);
 	$trips = $allStopsTrips;
 }
 else {
-	$url = $APIurl . "/json/stoptrips?stop=" . $stopid . "&time=" . midnight_seconds() . "&service_period=" . service_period();
-	$trips = json_decode(getPage($url));
+	$trips = getStopTripsWithTimes($stopid);
 }
-foreach ($trips as $row) {
-	echo '<li>';
-	echo '<a href="trip.php?stopid=' . $stopid . '&tripid=' . $row[1][0] . '"><h3>' . $row[1][1]."</h3><p>";
-        $viaPoints = viaPointNames($row[1][0], $stopid);
-        if ($viaPoints != "") echo '<br><span class="viaPoints">Via: ' . $viaPoints . '</span>';
-	if (sizeof($tripStopNumbers) > 0) {
-            echo '<br><small>Boarding At: ';
-            foreach ($tripStopNumbers[$row[1][0]] as $key) {
-                echo $stopNames[$key] .' ';
-            }
-            echo '</small>';
-        }
-	echo '</p>';
-	echo '<p class="ui-li-aside"><strong>' . midnight_seconds_to_time($row[0]) . '</strong></p>';
-	echo '</a></li>';
-        flush(); @ob_flush();
+if (sizeof($trips) == 0) {
+	echo "<li style='text-align: center;'>No trips in the near future.</li>";
 }
-if (sizeof($trips) == 0) echo "<li> <center>No trips in the near future.</center> </li>";
-echo '</ul></div>';
+else {
+	foreach ($trips as $trip) {
+		echo '<li>';
+		echo '<a href="trip.php?stopid=' . $stopid . '&amp;tripid=' . $trip['trip_id'] . '"><h3>' . $trip['route_short_name'] . " " . $trip['route_long_name'] . "</h3><p>";
+		$viaPoints = viaPointNames($trip['trip_id'], $trip['stop_sequence']);
+		if ($viaPoints != "") echo '<br><span class="viaPoints">Via: ' . $viaPoints . '</span>';
+		if (sizeof($tripStopNumbers) > 0) {
+			echo '<br><small>Boarding At: ';
+			foreach ($tripStopNumbers[$trip['trip_id']] as $key) {
+				echo $stopNames[$key] . ' ';
+			}
+			echo '</small>';
+		}
+		echo '</p>';
+		echo '<p class="ui-li-aside"><strong>' . $trip['arrival_time'] . '</strong></p>';
+		echo '</a></li>';
+		flush();
+		@ob_flush();
+	}
+}
+echo '</ul>';
 include_footer();
 ?>
 

--- a/stopList.php
+++ b/stopList.php
@@ -1,16 +1,13 @@
 <?php
 include ('include/common.inc.php');
-function filterByFirstLetter($var)
-{
-	return $var[1][0] == $_REQUEST['firstLetter'];
-}
+$stops = Array();
 function navbar()
 {
 	echo '
 		<div data-role="navbar">
 			<ul> 
 				<li><a href="stopList.php">Timing Points</a></li>
-				<li><a href="stopList.php?suburbs=yes">By Suburb</a></li>
+				<li><a href="stopList.php?bysuburbs=yes">By Suburb</a></li>
 				<li><a href="stopList.php?nearby=yes">Nearby Stops</a></li>
 				<li><a href="stopList.php?allstops=yes">All Stops</a></li> 
 			</ul>
@@ -18,20 +15,19 @@
 	';
 }
 // By suburb
-if (isset($_REQUEST['suburbs'])) {
+if (isset($bysuburbs)) {
 	include_header("Stops by Suburb", "stopList");
 	navbar();
 	echo '  <ul data-role="listview" data-filter="true" data-inset="true" >';
-	if (!isset($_REQUEST['firstLetter'])) {
+	if (!isset($firstLetter)) {
 		foreach (range('A', 'Z') as $letter) {
-			echo "<li><a href=\"stopList.php?firstLetter=$letter&suburbs=yes\">$letter...</a></li>\n";
+			echo "<li><a href=\"stopList.php?firstLetter=$letter&amp;bysuburbs=yes\">$letter...</a></li>\n";
 		}
 	}
 	else {
 		foreach ($suburbs as $suburb) {
-			if (startsWith($suburb, $_REQUEST['firstLetter'])) {
+			if (startsWith($suburb, $firstLetter)) {
 				echo '<li><a href="stopList.php?suburb=' . urlencode($suburb) . '">' . $suburb . '</a></li>';
-				flush(); @ob_flush();
 			}
 		}
 	}
@@ -39,102 +35,95 @@
 }
 else {
 	// Timing Points / All stops
-	if ($_REQUEST['allstops']) {
+	if (isset($allstops)) {
 		$listType = 'allstops=yes';
-		$url = $APIurl . "/json/stops";
+		$stops = getStops();
 		include_header("All Stops", "stopList");
 		navbar();
 		timePlaceSettings();
 	}
-	else if ($_REQUEST['nearby']) {
+	else if (isset($nearby)) {
 		$listType = 'nearby=yes';
-		$url = $APIurl . "/json/neareststops?lat={$_SESSION['lat']}&lon={$_SESSION['lon']}&limit=15";
 		include_header("Nearby Stops", "stopList", true, true);
+		trackEvent("Stop Lists", "Stops Nearby", $_SESSION['lat'] . "," . $_SESSION['lon']);
 		navbar();
 		timePlaceSettings(true);
 		if (!isset($_SESSION['lat']) || !isset($_SESSION['lat']) || $_SESSION['lat'] == "" || $_SESSION['lon'] == "") {
 			include_footer();
 			die();
 		}
+		$stops = getNearbyStops($_SESSION['lat'], $_SESSION['lon'], 15);
 	}
-	else if ($_REQUEST['suburb']) {
-		$suburb = filter_var($_REQUEST['suburb'], FILTER_SANITIZE_STRING);
-		$listType = "suburb=$suburb";
-		$url = $APIurl . "/json/stopzonesearch?q=" . $suburb;
+	else if (isset($suburb)) {
+		$stops = getStopsBySuburb($suburb);
 		include_header("Stops in " . ucwords($suburb) , "stopList");
 		navbar();
-	       trackEvent("Stop Lists","Stops By Suburb", $suburb);
+		trackEvent("Stop Lists", "Stops By Suburb", $suburb);
 	}
 	else {
-		$url = $APIurl . "/json/timingpoints";
+		$stops = getStops(true, $firstLetter);
 		include_header("Timing Points / Major Stops", "stopList");
 		navbar();
 		timePlaceSettings();
 	}
 	echo '  <ul data-role="listview" data-filter="true" data-inset="true" >';
-	if (!isset($_REQUEST['firstLetter']) && !$_REQUEST['suburb'] && !$_REQUEST['nearby']) {
+	if (!isset($firstLetter) && !isset($suburb) && !isset($nearby)) {
 		foreach (range('A', 'Z') as $letter) {
-			echo "<li><a href=\"stopList.php?firstLetter=$letter&$listType\">$letter...</a></li>\n";
+			echo "<li><a href=\"stopList.php?firstLetter=$letter&amp;$listType\">$letter...</a></li>\n";
 		}
 	}
 	else {
-		$stops = json_decode(getPage($url));
-		foreach ($stops as $key => $row) {
-			$stopName[$key] = $row[1];
-		}
-		// Sort the stops by name
-		array_multisort($stopName, SORT_ASC, $stops);
-		if (!isset($_REQUEST['suburb']) && !isset($_REQUEST['nearby'])) {
-			$stops = array_filter($stops, "filterByFirstLetter");
-		}
+		//var_dump($stops);
 		$stopsGrouped = Array();
-		foreach ($stops as $key => $row) {
-			if ((trim(preg_replace("/\(Platform.*/", "", $stops[$key][1])) != trim(preg_replace("/\(Platform.*/", "", $stops[$key + 1][1]))) || $key + 1 >= sizeof($stops)) {
+		foreach ($stops as $key => $stop) {
+			if ((trim(preg_replace("/\(Platform.*/", "", $stops[$key]["stop_name"])) != trim(preg_replace("/\(Platform.*/", "", $stops[$key + 1]["stop_name"]))) || $key + 1 >= sizeof($stops)) {
 				if (sizeof($stopsGrouped) > 0) {
 					// print and empty grouped stops
 					// subsequent duplicates
-					$stopsGrouped["stop_ids"][] = $row[0];
+					$stopsGrouped["stop_ids"][] = $stop['stop_id'];
 					echo '<li>';
 					if (!startsWith($stopsGrouped['stop_codes'][0], "Wj")) echo '<img src="css/images/time.png" alt="Timing Point: " class="ui-li-icon">';
 					echo '<a href="stop.php?stopids=' . implode(",", $stopsGrouped['stop_ids']) . '">';
 					if (isset($_SESSION['lat']) && isset($_SESSION['lon'])) {
-						echo '<span class="ui-li-count">' . distance($row[2], $row[3], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
+						echo '<span class="ui-li-count">' . distance($stop['stop_lat'], $stop['stop_lon'], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
 					}
-					echo bracketsMeanNewLine(trim(preg_replace("/\(Platform.*/", "", $row[1])) . '(' . sizeof($stopsGrouped["stop_ids"]) . ' stops)');
+					echo bracketsMeanNewLine(trim(preg_replace("/\(Platform.*/", "", $stop['stop_name'])) . '(' . sizeof($stopsGrouped["stop_ids"]) . ' stops)');
 					echo "</a></li>\n";
-					flush(); @ob_flush();
+					flush();
+					@ob_flush();
 					$stopsGrouped = Array();
 				}
 				else {
 					// just a normal stop
 					echo '<li>';
-					if (!startsWith($row[5], "Wj")) echo '<img src="css/images/time.png" alt="Timing Point" class="ui-li-icon">';
-					echo '<a href="stop.php?stopid=' . $row[0] . (startsWith($row[5], "Wj") ? '&stopcode=' . $row[5] : "") . '">';
+					if (!startsWith($stop['stop_code'], "Wj")) echo '<img src="css/images/time.png" alt="Timing Point" class="ui-li-icon">';
+					echo '<a href="stop.php?stopid=' . $stop['stop_id'] . (startsWith($stop['stop_code'], "Wj") ? '&amp;stopcode=' . $stop['stop_code'] : "") . '">';
 					if (isset($_SESSION['lat']) && isset($_SESSION['lon'])) {
-						echo '<span class="ui-li-count">' . distance($row[2], $row[3], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
+						echo '<span class="ui-li-count">' . distance($stop['stop_lat'], $stop['stop_lon'], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
 					}
-					echo bracketsMeanNewLine($row[1]);
+					echo bracketsMeanNewLine($stop['stop_name']);
 					echo "</a></li>\n";
-					flush(); @ob_flush();
+					flush();
+					@ob_flush();
 				}
 			}
 			else {
 				// this is a duplicated line item
-				if ($key - 1 <= 0 || (trim(preg_replace("/\(Platform.*/", "", $stops[$key][1])) != trim(preg_replace("/\(Platform.*/", "", $stops[$key - 1][1])))) {
+				if ($key - 1 <= 0 || (trim(preg_replace("/\(Platform.*/", "", $stops[$key]['stop_name'])) != trim(preg_replace("/\(Platform.*/", "", $stops[$key - 1]['stop_name'])))) {
 					// first duplicate
 					$stopsGrouped = Array(
-						"name" => trim(preg_replace("/\(Platform.*/", "", $row[1])) ,
+						"name" => trim(preg_replace("/\(Platform.*/", "", $stop['stop_name'])) ,
 						"stop_ids" => Array(
-							$row[0]
+							$stop['stop_id']
 						) ,
 						"stop_codes" => Array(
-							$row[5]
+							$stop['stop_code']
 						)
 					);
 				}
 				else {
 					// subsequent duplicates
-					$stopsGrouped["stop_ids"][] = $row[0];
+					$stopsGrouped["stop_ids"][] = $stop['stop_id'];;
 				}
 			}
 		}

 Binary files a/transitdata.cbrfeed.sql.gz and /dev/null differ
file:a/transitdata.sql (deleted)
--- a/transitdata.sql
+++ /dev/null
@@ -1,59 +1,1 @@
---
--- PostgreSQL database dump
---
 
-SET statement_timeout = 0;
-SET client_encoding = 'UTF8';
-SET standard_conforming_strings = off;
-SET check_function_bodies = false;
-SET client_min_messages = warning;
-SET escape_string_warning = off;
-
-SET search_path = public, pg_catalog;
-
-SET default_tablespace = '';
-
-SET default_with_oids = false;
-
---
--- Name: trips; Type: TABLE; Schema: public; Owner: postgres; Tablespace: 
---
-
-CREATE TABLE trips (
-    route_id integer,
-    trip_id integer NOT NULL,
-    trip_headsign text,
-    service_id text
-);
-
-
-ALTER TABLE public.trips OWNER TO postgres;
-
---
--- Data for Name: trips; Type: TABLE DATA; Schema: public; Owner: postgres
---
-
-COPY trips (route_id, trip_id, trip_headsign, service_id) FROM stdin;
-\.
-
-
---
--- Name: trips_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace: 
---
-
-ALTER TABLE ONLY trips
-    ADD CONSTRAINT trips_pkey PRIMARY KEY (trip_id);
-
-
---
--- Name: routetrips; Type: INDEX; Schema: public; Owner: postgres; Tablespace: 
---
-
-CREATE INDEX routetrips ON trips USING btree (route_id);
-
-
---
--- PostgreSQL database dump complete
---
-
-

file:a/trip.php -> file:b/trip.php
--- a/trip.php
+++ b/trip.php
@@ -1,97 +1,86 @@
 <?php
 include ('include/common.inc.php');
-$tripid = filter_var($_REQUEST['tripid'], FILTER_SANITIZE_NUMBER_INT);
-$stopid = filter_var($_REQUEST['stopid'], FILTER_SANITIZE_NUMBER_INT);
-$routeid = filter_var($_REQUEST['routeid'], FILTER_SANITIZE_NUMBER_INT);
+
 $routetrips = Array();
-if ($_REQUEST['routeid'] && !$_REQUEST['tripid']) {
-	$tripid = 0;
-	$url = $APIurl . "/json/routetrips?route_id=" . $routeid;
-	$routetrips = json_decode(getPage($url));
-	foreach ($routetrips as $trip) {
-		if ($trip[2] > midnight_seconds()) {
-			$tripid = $trip[1];
-			break;
-		}
-	}
-	if ($tripid == 0) $tripid = $routetrips[0][1];
+
+if (isset($routeid) && !isset($tripid)) {
+    $trip = getRouteNextTrip($routeid);
+    $tripid = $trip['trip_id'];
+} else {
+    $trip = getTrip($tripid);
+    $routeid = $trip["route_id"];
 }
-$url = $APIurl . "/json/triprows?trip=" . $tripid;
-$trips = array_flatten(json_decode(getPage($url)));
-if (sizeof($routetrips) == 0) {
-	$routeid = $trips[1]->route_id;
-	$url = $APIurl . "/json/routetrips?route_id=" . $trips[1]->route_id;
-	$routetrips = json_decode(getPage($url));
-}
-include_header("Stops on " . $trips[1]->route_short_name . ' ' . $trips[1]->route_long_name, "trip");
-trackEvent("Route/Trip View","View Route", $trips[1]->route_short_name . ' ' . $trips[1]->route_long_name, $trips[1]->route_id);
-$url = $APIurl . "/json/tripstoptimes?trip=" . $tripid;
-$json = json_decode(getPage($url));
-$stops = $json[0];
-$times = $json[1];
-$viaPoints = Array();
-foreach ($stops as $stop) {
-	if (!startsWith($stop[5], "Wj")) {
-		$viaPoints[] = $stop[1];
-	}
-}
-echo '<p><h2>Via:</h2> ' . implode(", ", $viaPoints) . '</small></p>';
-echo '<p><h2>Other Trips:</h2> ';
-foreach ($routetrips as $othertrip) {
-	echo '<a href="trip.php?tripid=' . $othertrip[1] . "&routeid=" . $routeid . '">' . midnight_seconds_to_time($othertrip[0]) . '</a> ';
+
+$routetrips = getRouteTrips($routeid);
+    
+include_header("Stops on " . $trip['route_short_name'] . ' ' . $trip['route_long_name'], "trip");
+trackEvent("Route/Trip View","View Route",  $trip['route_short_name'] . ' ' . $trip['route_long_name'], $routeid);
+
+
+echo '<h2>Via:</h2> <small>' . viaPointNames($tripid) . '</small>';
+echo '<h2>Other Trips:</h2> ';
+foreach (getRouteTrips($routeid) as $othertrip) {
+	echo '<a href="trip.php?tripid=' . $othertrip['trip_id'] . "&amp;routeid=" . $routeid . '">' . str_replace("  ",":00",str_replace(":00"," ",$othertrip['arrival_time'])). '</a> ';
 }
 flush(); @ob_flush();
-echo '</p><p><h2>Other directions/timing periods:</h2> ';
-$url = $APIurl . "/json/routesearch?routeshortname=" . rawurlencode($trips[1]->route_short_name);
-$json = json_decode(getPage($url));
-foreach ($json as $row) {
-	if ($row[0] != $routeid) echo '<a href="trip.php?routeid=' . $row[0] . '">' . $row[2] . ' (' . ucwords($row[3]) . ')</a> ';
+echo '<h2>Other directions/timing periods:</h2> ';
+foreach (getRoutesByNumber($trip['route_short_name']) as $row) {
+	if ($row['route_id'] != $routeid) echo '<a href="trip.php?routeid=' . $row['route_id'] . '">' . $row['route_long_name'] . ' (' . ucwords($row['service_id']) . ')</a> ';
 }
 flush(); @ob_flush();
 echo '  <ul data-role="listview"  data-inset="true">';
-echo '<li data-role="list-divider">' . midnight_seconds_to_time($times[0]) . '-' . midnight_seconds_to_time($times[sizeof($times) - 1]) . ' ' . $trips[1]->route_long_name . '</li>';
 $stopsGrouped = Array();
-foreach ($stops as $key => $row) {
-	if (($stops[$key][1] != $stops[$key + 1][1]) || $key + 1 >= sizeof($stops)) {
+$tripStopTimes = getTimeInterpolatedTrip($tripid);
+echo '<li data-role="list-divider">' . $tripStopTimes[0]['arrival_time'] . ' to ' . $tripStopTimes[sizeof($tripStopTimes) - 1]['arrival_time'] . ' ' . $trips[1]->route_long_name . '</li>';
+
+foreach ($tripStopTimes as $key => $tripStopTime) {
+	if (($tripStopTimes[$key]["stop_name"] != $tripStopTimes[$key + 1]["stop_name"]) || $key + 1 >= sizeof($tripStopTimes)) {
 		echo '<li>';
-		if (!startsWith($row[5], "Wj")) echo '<img src="css/images/time.png" alt="Timing Point" class="ui-li-icon">';
+		if (!startsWith($tripStopTime['stop_code'], "Wj")) echo '<img src="css/images/time.png" alt="Timing Point" class="ui-li-icon">';
 		if (sizeof($stopsGrouped) > 0) {
 			// print and empty grouped stops
 			// subsequent duplicates
-			$stopsGrouped["stop_ids"][] = $row[0];
-			$stopsGrouped["endTime"] = $times[$key];
+			$stopsGrouped["stop_ids"][] = $tripStopTime['stop_id'];
+			$stopsGrouped["endTime"] = $tripStopTime['arrival_time'];
 			echo '<a href="stop.php?stopids=' . implode(",", $stopsGrouped['stop_ids']) . '">';
-			echo '<p class="ui-li-aside">' . midnight_seconds_to_time($stopsGrouped['startTime']) . ' to ' . midnight_seconds_to_time($stopsGrouped['endTime']) . '</p>';
-			echo bracketsMeanNewLine($row[1]);
+			echo '<p class="ui-li-aside">' . $stopsGrouped['startTime'] . ' to ' . $stopsGrouped['endTime'];
+                        echo '</p>';
+                        if (isset($_SESSION['lat']) && isset($_SESSION['lon'])) {
+						echo '<span class="ui-li-count">' . distance($stop['stop_lat'],$stop['stop_lon'], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
+					}
+			echo bracketsMeanNewLine($tripStopTime["stop_name"]);
 			echo '</a></li>';
                         flush(); @ob_flush();
 			$stopsGrouped = Array();
 		}
 		else {
 			// just a normal stop
-			echo '<a href="stop.php?stopid=' . $row[0] . (startsWith($row[5], "Wj") ? '&stopcode=' . $row[5] : "") . '">';
-			echo '<p class="ui-li-aside">' . midnight_seconds_to_time($times[$key]) . '</p>';
-			echo bracketsMeanNewLine($row[1]);
+			echo '<a href="stop.php?stopid=' . $tripStopTime['stop_id'] . (startsWith($tripStopTime['stop_code'], "Wj") ? '&amp;stopcode=' . $tripStopTime['stop_code'] : "") . '">';
+			echo '<p class="ui-li-aside">' . $tripStopTime['arrival_time'] . '</p>';
+			if (isset($_SESSION['lat']) && isset($_SESSION['lon'])) {
+						echo '<span class="ui-li-count">' . distance($stop['stop_lat'],$stop['stop_lon'], $_SESSION['lat'], $_SESSION['lon'], true) . 'm away</span>';
+					}
+                                        echo bracketsMeanNewLine($tripStopTime['stop_name']);
 			echo '</a></li>';
                         flush(); @ob_flush();
 		}
 	}
 	else {
 		// this is a duplicated line item
-		if ($key - 1 <= 0 || ($stops[$key][1] != $stops[$key - 1][1])) {
+		if ($key - 1 <= 0 || ($tripStopTimes[$key]['stop_name'] != $tripStopTimes[$key - 1]['stop_name'])) {
 			// first duplicate
 			$stopsGrouped = Array(
-				"name" => $row[1],
-				"startTime" => $times[$key],
+				"name" => $tripStopTime['stop_name'],
+				"startTime" => $tripStopTime['arrival_time'],
 				"stop_ids" => Array(
-					$row[0]
+					$tripStopTime['stop_id']
 				)
 			);
 		}
 		else {
 			// subsequent duplicates
-			$stopsGrouped["stop_ids"][] = $row[0];
-			$stopsGrouped["endTime"] = $times[$key];
+			$stopsGrouped["stop_ids"][] = $tripStopTime['stop_id'];
+			$stopsGrouped["endTime"] = $tripStopTime['arrival_time'];
 		}
 	}
 }

--- a/tripPlanner.php
+++ b/tripPlanner.php
@@ -1,8 +1,8 @@
 <?php
 include ('include/common.inc.php');
 include_header("Trip Planner", "tripPlanner", true, true, true);
-$from = (isset($_REQUEST['from']) ? filter_var($_REQUEST['from'], FILTER_SANITIZE_STRING) : "Brigalow");
-$to = (isset($_REQUEST['to']) ? filter_var($_REQUEST['to'], FILTER_SANITIZE_STRING) : "Barry");
+$from = (isset($_REQUEST['from']) ? filter_var($_REQUEST['from'], FILTER_SANITIZE_STRING) : "");
+$to = (isset($_REQUEST['to']) ? filter_var($_REQUEST['to'], FILTER_SANITIZE_STRING) : "");
 $date = (isset($_REQUEST['date']) ? filter_var($_REQUEST['date'], FILTER_SANITIZE_STRING) : date("m/d/Y"));
 $time = (isset($_REQUEST['time']) ? filter_var($_REQUEST['time'], FILTER_SANITIZE_STRING) : date("H:i"));
 function formatTime($timeString) {
@@ -125,23 +125,24 @@
 	}
 	else {
 		$url = $otpAPIurl . "ws/plan?date=" . urlencode($_REQUEST['date']) . "&time=" . urlencode($_REQUEST['time']) . "&mode=TRANSIT%2CWALK&optimize=QUICK&maxWalkDistance=840&wheelchair=false&toPlace=$toPlace&fromPlace=$fromPlace&intermediatePlaces=";
-		$ch = curl_init($url);
+		debug($url);
+                $ch = curl_init($url);
 		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 		curl_setopt($ch, CURLOPT_HEADER, 0);
 		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
 			"Accept: application/json"
 		));
-		curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+		curl_setopt($ch, CURLOPT_TIMEOUT, 10);
 		$page = curl_exec($ch);
-		if (curl_errno($ch)) {
-			tripPlanForm("Trip planner temporarily unavailable: " . curl_errno($ch) . " " . curl_error($ch) .(isDebug() ? $url : ""));
+		if (curl_errno($ch) || curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) {
+			tripPlanForm("Trip planner temporarily unavailable: " . curl_errno($ch) . " " . curl_error($ch) . " ". curl_getinfo($ch, CURLINFO_HTTP_CODE) .(isDebug() ? "<br>".$url : ""));
                         trackEvent("Trip Planner","Trip Planner Failed", $url);
                 }
 		else {
                   	trackEvent("Trip Planner","Plan Trip From", $from);
                         trackEvent("Trip Planner","Plan Trip To", $to);
 			$tripplan = json_decode($page);
-			debug(print_r($triplan, true));
+			debug(print_r($tripplan, true));
 			echo "<h1> From: {$tripplan->plan->from->name} To: {$tripplan->plan->to->name} </h1>";
 			echo "<h1> At: ".formatTime($tripplan->plan->date)." </h1>";
 			if (is_array($tripplan->plan->itineraries->itinerary)) {

--- a/updatedb.php
+++ b/updatedb.php
@@ -1,9 +1,12 @@
 <?php
+include('include/common-db.inc.php');
 // Unzip cbrfeed.zip, import all csv files to database
+$unzip = true;
 $zip = zip_open(dirname(__FILE__) . "/cbrfeed.zip");
 $tmpdir = "/tmp/cbrfeed/";
 mkdir($tmpdir);
-/*if (is_resource($zip)) {
+if ($unzip) {
+if (is_resource($zip)) {
 	while ($zip_entry = zip_read($zip)) {
 		$fp = fopen($tmpdir . zip_entry_name($zip_entry) , "w");
 		if (zip_entry_open($zip, $zip_entry, "r")) {
@@ -15,12 +18,8 @@
 		}
 	}
 	zip_close($zip);
-}*/
-$conn = pg_connect("dbname=transitdata user=postgres password=snmc");
-  if (!$conn) {
-      echo "An error occured.\n";
-      exit;
-  }
+}
+}
                         
 foreach (scandir($tmpdir) as $file) {
 	if (!strpos($file, ".txt") === false) {