Ver Fonte

* Updates the sim stats module. Cleans out some of the rot.
* Adds a prototype web stats module which is disabled by default. It's functional with one report right now, however, the database structure may change, so I don't recommend enabling this to keep actual stats right now. I'll let you know when it's safe.
* Adds Prototype for ajaxy web content
* removed a warning or two.

Teravus Ovares há 15 anos atrás
pai
commit
e9cef70f89

+ 1 - 0
CONTRIBUTORS.txt

@@ -113,6 +113,7 @@ This software uses components from the following developers:
 * Prebuild (http://sourceforge.net/projects/dnpb/)
 * LibSecondLife (http://www.libsecondlife.org/wiki/Main_Page)
 * DotNetOpenMail v0.5.8b (http://dotnetopenmail.sourceforge.net)
+* Prototype JavaScript Framework ajax (http://www.prototypejs.org/)
 
 
 In addition, we would like to thank:

+ 2 - 1
OpenSim/Framework/Servers/BaseHttpServer.cs

@@ -1091,6 +1091,7 @@ namespace OpenSim.Framework.Servers
             {
                 host = (string)headervals["Host"];
             }
+            keysvals.Add("headers",headervals);
 
             if (keysvals.Contains("method"))
             {
@@ -1238,7 +1239,7 @@ namespace OpenSim.Framework.Servers
 
             byte[] buffer;
 
-            if (!contentType.Contains("image"))
+            if (!(contentType.Contains("image") || contentType.Contains("x-shockwave-flash")))
             {
                 buffer = Encoding.UTF8.GetBytes(responseString);
             }

+ 8 - 1
OpenSim/Framework/SimStats.cs

@@ -72,11 +72,18 @@ namespace OpenSim.Framework
             get { return m_objectCapacity; }
         }
         private uint m_objectCapacity;
+
+        public OpenMetaverse.UUID RegionUUID
+        {
+            get { return regionUUID;}   
+        }
+        private OpenMetaverse.UUID regionUUID;
                 
         public SimStats(
             uint regionX, uint regionY, uint regionFlags, uint objectCapacity, 
-            SimStatsPacket.RegionBlock regionBlock, SimStatsPacket.StatBlock[] statsBlock)
+            SimStatsPacket.RegionBlock regionBlock, SimStatsPacket.StatBlock[] statsBlock, OpenMetaverse.UUID pRUUID)
         {
+            regionUUID = pRUUID;
             m_regionX = regionX;
             m_regionY = regionY;
             m_regionFlags = regionFlags;

+ 43 - 22
OpenSim/Framework/Statistics/SimExtraStatsCollector.cs

@@ -51,27 +51,27 @@ namespace OpenSim.Framework.Statistics
         private long assetServiceRequestFailures;
         private long inventoryServiceRetrievalFailures;
 
-        private float timeDilation;
-        private float simFps;
-        private float physicsFps;
-        private float agentUpdates;
-        private float rootAgents;
-        private float childAgents;
-        private float totalPrims;
-        private float activePrims;
-        private float totalFrameTime;
-        private float netFrameTime;
-        private float physicsFrameTime;
-        private float otherFrameTime;
-        private float imageFrameTime;
-        private float inPacketsPerSecond;
-        private float outPacketsPerSecond;
-        private float unackedBytes;
-        private float agentFrameTime;
-        private float pendingDownloads;
-        private float pendingUploads;
-        private float activeScripts;
-        private float scriptLinesPerSecond;
+        private volatile float timeDilation;
+        private volatile float simFps;
+        private volatile float physicsFps;
+        private volatile float agentUpdates;
+        private volatile float rootAgents;
+        private volatile float childAgents;
+        private volatile float totalPrims;
+        private volatile float activePrims;
+        private volatile float totalFrameTime;
+        private volatile float netFrameTime;
+        private volatile float physicsFrameTime;
+        private volatile float otherFrameTime;
+        private volatile float imageFrameTime;
+        private volatile float inPacketsPerSecond;
+        private volatile float outPacketsPerSecond;
+        private volatile float unackedBytes;
+        private volatile float agentFrameTime;
+        private volatile float pendingDownloads;
+        private volatile float pendingUploads;
+        private volatile float activeScripts;
+        private volatile float scriptLinesPerSecond;
 
         /// <summary>
         /// Number of times that a client thread terminated because of an exception
@@ -87,7 +87,28 @@ namespace OpenSim.Framework.Statistics
         public long TexturesInCache { get { return texturesInCache; } }
         public long AssetCacheMemoryUsage { get { return assetCacheMemoryUsage; } }
         public long TextureCacheMemoryUsage { get { return textureCacheMemoryUsage; } }
-        
+
+        public float TimeDilation { get { return timeDilation; } }
+        public float SimFps { get { return simFps; } }
+        public float PhysicsFps { get { return physicsFps; } }
+        public float AgentUpdates { get { return agentUpdates; } }
+        public float RootAgents { get { return rootAgents; } }
+        public float ChildAgents { get { return childAgents; } }
+        public float TotalPrims { get { return totalPrims; } }
+        public float ActivePrims { get { return activePrims; } }
+        public float TotalFrameTime { get { return totalFrameTime; } }
+        public float NetFrameTime { get { return netFrameTime; } }
+        public float PhysicsFrameTime { get { return physicsFrameTime; } }
+        public float OtherFrameTime { get { return otherFrameTime; } }
+        public float ImageFrameTime { get { return imageFrameTime; } }
+        public float InPacketsPerSecond { get { return inPacketsPerSecond; } }
+        public float OutPacketsPerSecond { get { return outPacketsPerSecond; } }
+        public float UnackedBytes { get { return unackedBytes; } }
+        public float AgentFrameTime { get { return agentFrameTime; } }
+        public float PendingDownloads { get { return pendingDownloads; } }
+        public float PendingUploads { get { return pendingUploads; } }
+        public float ActiveScripts { get { return activeScripts; } }
+        public float ScriptLinesPerSecond { get { return scriptLinesPerSecond; } }
         /// <summary>
         /// This is the time it took for the last asset request made in response to a cache miss.
         /// </summary>

+ 4 - 0
OpenSim/Region/ClientStack/LindenUDP/LLPacketHandler.cs

@@ -603,10 +603,14 @@ namespace OpenSim.Region.ClientStack.LindenUDP
         public void Flush()
         {
             m_PacketQueue.Flush();
+            m_UnackedBytes = (-1 * m_UnackedBytes);
+            SendPacketStats();
         }
 
         public void Clear()
         {
+            m_UnackedBytes = (-1 * m_UnackedBytes);
+            SendPacketStats();
             m_NeedAck.Clear();
             m_PendingAcks.Clear();
             m_Sequence += 1000000;

+ 39 - 39
OpenSim/Region/Environment/Scenes/Scene.cs

@@ -69,7 +69,7 @@ namespace OpenSim.Region.Environment.Scenes
 
         protected Timer m_restartWaitTimer = new Timer();
 
-        protected SimStatsReporter m_statsReporter;
+        public SimStatsReporter StatsReporter;
 
         protected List<RegionInfo> m_regionRestartNotifyList = new List<RegionInfo>();
         protected List<RegionInfo> m_neighbours = new List<RegionInfo>();
@@ -244,7 +244,7 @@ namespace OpenSim.Region.Environment.Scenes
         /// </summary>
         public float SimulatorFPS
         {
-            get { return m_statsReporter.getLastReportedSimFPS(); }
+            get { return StatsReporter.getLastReportedSimFPS(); }
         }
 
         public int TimePhase
@@ -358,10 +358,10 @@ namespace OpenSim.Region.Environment.Scenes
 
             m_physics_enabled = !RegionInfo.RegionSettings.DisablePhysics;
 
-            m_statsReporter = new SimStatsReporter(this);
-            m_statsReporter.OnSendStatsResult += SendSimStatsPackets;
+            StatsReporter = new SimStatsReporter(this);
+            StatsReporter.OnSendStatsResult += SendSimStatsPackets;
 
-            m_statsReporter.SetObjectCapacity(objectCapacity);
+            StatsReporter.SetObjectCapacity(objectCapacity);
 
             m_simulatorVersion = simulatorVersion
                 + " (OS " + Util.GetOperatingSystemInformation() + ")"
@@ -814,7 +814,7 @@ namespace OpenSim.Region.Environment.Scenes
                     if (m_update_entities == 1)
                     {
                         m_update_entities = 5;
-                        m_statsReporter.SetUpdateMS(6000);
+                        StatsReporter.SetUpdateMS(6000);
                     }
                 }
                 else
@@ -822,7 +822,7 @@ namespace OpenSim.Region.Environment.Scenes
                     if (m_update_entities == 5)
                     {
                         m_update_entities = 1;
-                        m_statsReporter.SetUpdateMS(3000);
+                        StatsReporter.SetUpdateMS(3000);
                     }
                 }
 
@@ -891,20 +891,20 @@ namespace OpenSim.Region.Environment.Scenes
                         otherMS = System.Environment.TickCount - otherMS;
                         // if (m_frame%m_update_avatars == 0)
                         //   UpdateInWorldTime();
-                        m_statsReporter.AddPhysicsFPS(physicsFPS);
-                        m_statsReporter.AddTimeDilation(m_timedilation);
-                        m_statsReporter.AddFPS(1);
-                        m_statsReporter.AddInPackets(0);
-                        m_statsReporter.SetRootAgents(m_sceneGraph.GetRootAgentCount());
-                        m_statsReporter.SetChildAgents(m_sceneGraph.GetChildAgentCount());
-                        m_statsReporter.SetObjects(m_sceneGraph.GetTotalObjectsCount());
-                        m_statsReporter.SetActiveObjects(m_sceneGraph.GetActiveObjectsCount());
+                        StatsReporter.AddPhysicsFPS(physicsFPS);
+                        StatsReporter.AddTimeDilation(m_timedilation);
+                        StatsReporter.AddFPS(1);
+                        StatsReporter.AddInPackets(0);
+                        StatsReporter.SetRootAgents(m_sceneGraph.GetRootAgentCount());
+                        StatsReporter.SetChildAgents(m_sceneGraph.GetChildAgentCount());
+                        StatsReporter.SetObjects(m_sceneGraph.GetTotalObjectsCount());
+                        StatsReporter.SetActiveObjects(m_sceneGraph.GetActiveObjectsCount());
                         frameMS = System.Environment.TickCount - frameMS;
-                        m_statsReporter.addFrameMS(frameMS);
-                        m_statsReporter.addPhysicsMS(physicsMS);
-                        m_statsReporter.addOtherMS(otherMS);
-                        m_statsReporter.SetActiveScripts(m_sceneGraph.GetActiveScriptsCount());
-                        m_statsReporter.addScriptLines(m_sceneGraph.GetScriptLPS());
+                        StatsReporter.addFrameMS(frameMS);
+                        StatsReporter.addPhysicsMS(physicsMS);
+                        StatsReporter.addOtherMS(otherMS);
+                        StatsReporter.SetActiveScripts(m_sceneGraph.GetActiveScriptsCount());
+                        StatsReporter.addScriptLines(m_sceneGraph.GetScriptLPS());
                     }
                 }
                 catch (NotImplementedException)
@@ -2380,8 +2380,8 @@ namespace OpenSim.Region.Environment.Scenes
 
             client.OnObjectOwner += ObjectOwner;
             
-            if (m_statsReporter != null)
-                client.OnNetworkStatsUpdate += m_statsReporter.AddPacketsFromClientStats;
+            if (StatsReporter != null)
+                client.OnNetworkStatsUpdate += StatsReporter.AddPacketsFromClientStats;
 
             // EventManager.TriggerOnNewClient(client);
         }
@@ -3027,14 +3027,14 @@ namespace OpenSim.Region.Environment.Scenes
             if (presence != null)
             {
                 // Nothing is removed here, so down count it as such
-                // if (presence.IsChildAgent)
-                // {
-                //    m_sceneGraph.removeUserCount(false);
-                // }
-                // else
-                // {
-                //    m_sceneGraph.removeUserCount(true);
-                // }
+                if (presence.IsChildAgent)
+                {
+                   m_sceneGraph.removeUserCount(false);
+                }
+                else
+                {
+                   m_sceneGraph.removeUserCount(true);
+                }
 
                 // Don't do this to root agents on logout, it's not nice for the viewer
                 if (presence.IsChildAgent) 
@@ -3322,9 +3322,9 @@ namespace OpenSim.Region.Environment.Scenes
             if (RegionInfo.ObjectCapacity != 0)
                 objects = RegionInfo.ObjectCapacity;
 
-            if (m_statsReporter != null)
+            if (StatsReporter != null)
             {
-                m_statsReporter.SetObjectCapacity(objects);
+                StatsReporter.SetObjectCapacity(objects);
             }
             objectCapacity = objects;
         }
@@ -3430,25 +3430,25 @@ namespace OpenSim.Region.Environment.Scenes
 
         public void AddPacketStats(int inPackets, int outPackets, int unAckedBytes)
         {
-            m_statsReporter.AddInPackets(inPackets);
-            m_statsReporter.AddOutPackets(outPackets);
-            m_statsReporter.AddunAckedBytes(unAckedBytes);
+            StatsReporter.AddInPackets(inPackets);
+            StatsReporter.AddOutPackets(outPackets);
+            StatsReporter.AddunAckedBytes(unAckedBytes);
         }
 
         public void AddAgentTime(int ms)
         {
-            m_statsReporter.addFrameMS(ms);
-            m_statsReporter.addAgentMS(ms);
+            StatsReporter.addFrameMS(ms);
+            StatsReporter.addAgentMS(ms);
         }
 
         public void AddAgentUpdates(int count)
         {
-            m_statsReporter.AddAgentUpdates(count);
+            StatsReporter.AddAgentUpdates(count);
         }
 
         public void AddPendingDownloads(int count)
         {
-            m_statsReporter.addPendingDownload(count);
+            StatsReporter.addPendingDownload(count);
         }
 
         #endregion

+ 11 - 0
OpenSim/Region/Environment/Scenes/SceneGraph.cs

@@ -306,8 +306,19 @@ namespace OpenSim.Region.Environment.Scenes
                 if (!resultOfObjectLinked)
                 {
                     m_numPrim -= ((SceneObjectGroup) Entities[uuid]).Children.Count;
+
+                    if ((((SceneObjectGroup)Entities[uuid]).RootPart.Flags & PrimFlags.Physics) == PrimFlags.Physics)
+                    {
+                        RemovePhysicalPrim(((SceneObjectGroup)Entities[uuid]).Children.Count);
+                    }
                 }
+
+                
+
                 Entities.Remove(uuid);
+                //SceneObjectGroup part;
+                //((part.RootPart.Flags & PrimFlags.Physics) == PrimFlags.Physics)
+                
 
                 return true;
             }

+ 10 - 3
OpenSim/Region/Environment/Scenes/SimStatsReporter.cs

@@ -26,6 +26,7 @@
  */
 
 using System;
+//using System.Collections.Generic;
 using System.Timers;
 using OpenMetaverse.Packets;
 using OpenSim.Framework;
@@ -189,7 +190,7 @@ namespace OpenSim.Region.Environment.Scenes
                 }
                 
                 sb[0].StatID = (uint) Stats.TimeDilation;
-                sb[0].StatValue = m_timeDilation ; //((((m_timeDilation + (0.10f * statsUpdateFactor)) /10)  / statsUpdateFactor));
+                sb[0].StatValue = (Single.IsNaN(m_timeDilation)) ? 0.1f : m_timeDilation ; //((((m_timeDilation + (0.10f * statsUpdateFactor)) /10)  / statsUpdateFactor));
 
                 sb[1].StatID = (uint) Stats.SimFPS;
                 sb[1].StatValue = simfps/statsUpdateFactor;
@@ -253,7 +254,7 @@ namespace OpenSim.Region.Environment.Scenes
               
                 SimStats simStats 
                     = new SimStats(
-                        ReportingRegion.RegionLocX, ReportingRegion.RegionLocY, regionFlags, (uint)objectCapacity, rb, sb);
+                        ReportingRegion.RegionLocX, ReportingRegion.RegionLocY, regionFlags, (uint)objectCapacity, rb, sb, m_scene.RegionInfo.originRegionID);
 
                 handlerSendStatResult = OnSendStatsResult;
                 if (handlerSendStatResult != null)
@@ -309,7 +310,11 @@ namespace OpenSim.Region.Environment.Scenes
 
         public void SetChildAgents(int childAgents)
         {
-            m_childAgents = childAgents;
+            m_childAgents = (childAgents > 0) ? childAgents : 0;
+            if (childAgents < 0)
+            {
+               //List<ScenePresence> avs=  m_scene.GetScenePresences();
+            }
         }
 
         public void SetObjects(int objects)
@@ -350,6 +355,7 @@ namespace OpenSim.Region.Environment.Scenes
         public void AddunAckedBytes(int numBytes)
         {
             m_unAckedBytes += numBytes;
+            if (m_unAckedBytes < 0) m_unAckedBytes = 0;
         }
 
         public void addFrameMS(int ms)
@@ -383,6 +389,7 @@ namespace OpenSim.Region.Environment.Scenes
         public void addPendingDownload(int count)
         {
             m_pendingDownloads += count;
+            if (m_pendingDownloads < 0) m_pendingDownloads = 0;
             //m_log.InfoFormat("[stats]: Adding {0} to pending downloads to make {1}", count, m_pendingDownloads);
         }
 

+ 185 - 0
OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs

@@ -0,0 +1,185 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Mono.Data.SqliteClient;
+using OpenMetaverse;
+using OpenSim.Region.Environment.Scenes;
+using OpenSim.Framework.Statistics;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class ActiveConnectionsAJAX : IStatsController
+    {
+        #region IStatsController Members
+
+        public Hashtable ProcessModel(Hashtable pParams)
+        {
+            
+            List<Scene> m_scene = (List<Scene>)pParams["Scenes"];
+
+            Hashtable nh = new Hashtable();
+            nh.Add("hdata", m_scene);
+
+            return nh;
+        }
+
+        public string RenderView(Hashtable pModelResult)
+        {
+            List<Scene> all_scenes = (List<Scene>) pModelResult["hdata"];
+
+            StringBuilder output = new StringBuilder();
+            HTMLUtil.OL_O(ref output, "");
+            foreach (Scene scene in all_scenes)
+            {
+                List<ScenePresence> avatarInScene = scene.GetScenePresences();
+
+                HTMLUtil.LI_O(ref output, "");
+                output.Append(scene.RegionInfo.RegionName);
+                HTMLUtil.OL_O(ref output, "");
+                foreach (ScenePresence av in avatarInScene)
+                {
+                    Dictionary<string,string> queues = new Dictionary<string, string>();
+                    if (av.ControllingClient is IStatsCollector)
+                    {
+                        IStatsCollector isClient = (IStatsCollector) av.ControllingClient;
+                        queues = decodeQueueReport(isClient.Report());
+                    }
+                    HTMLUtil.LI_O(ref output, "");
+                    output.Append(av.Name);
+                    output.Append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
+                    output.Append((av.IsChildAgent ? "Child" : "Root"));
+                    
+                    Dictionary<string, int> throttles = DecodeClientThrottles(av.ControllingClient.GetThrottlesPacked(1));
+                    
+                    HTMLUtil.UL_O(ref output, "");
+                    foreach (string throttlename in throttles.Keys)
+                    {
+                        HTMLUtil.LI_O(ref output, "");
+                        output.Append(throttlename);
+                        output.Append(":");
+                        output.Append(throttles[throttlename].ToString());
+                        if (queues.ContainsKey(throttlename))
+                        {
+                            output.Append("/");
+                            output.Append(queues[throttlename]);
+                        }
+                        HTMLUtil.LI_C(ref output);
+                    }
+                    if (queues.ContainsKey("Incoming") && queues.ContainsKey("Outgoing"))
+                    {
+                        HTMLUtil.LI_O(ref output, "red");
+                        output.Append("SEND:");
+                        output.Append(queues["Outgoing"]);
+                        output.Append("/");
+                        output.Append(queues["Incoming"]);
+                        HTMLUtil.LI_C(ref output);
+                    }
+
+                    HTMLUtil.UL_C(ref output);
+                    HTMLUtil.LI_C(ref output);
+                }
+                HTMLUtil.OL_C(ref output);
+            }
+            HTMLUtil.OL_C(ref output);
+            return output.ToString();
+        }
+
+        public Dictionary<string, int> DecodeClientThrottles(byte[] throttle)
+        {
+            Dictionary<string, int> returndict = new Dictionary<string, int>();
+            // From mantis http://opensimulator.org/mantis/view.php?id=1374
+            // it appears that sometimes we are receiving empty throttle byte arrays.
+            // TODO: Investigate this behaviour
+            if (throttle.Length == 0)
+            {
+                return new Dictionary<string, int>();
+            }
+
+            int tResend = -1;
+            int tLand = -1;
+            int tWind = -1;
+            int tCloud = -1;
+            int tTask = -1;
+            int tTexture = -1;
+            int tAsset = -1;
+            int tall = -1;
+            const int singlefloat = 4;
+
+            //Agent Throttle Block contains 7 single floatingpoint values.
+            int j = 0;
+
+            // Some Systems may be big endian...
+            // it might be smart to do this check more often...
+            if (!BitConverter.IsLittleEndian)
+                for (int i = 0; i < 7; i++)
+                    Array.Reverse(throttle, j + i * singlefloat, singlefloat);
+
+            // values gotten from OpenMetaverse.org/wiki/Throttle.  Thanks MW_
+            // bytes
+            // Convert to integer, since..   the full fp space isn't used.
+            tResend = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Resend", tResend);
+            j += singlefloat;
+            tLand = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Land", tLand);
+            j += singlefloat;
+            tWind = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Wind", tWind);
+            j += singlefloat;
+            tCloud = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Cloud", tCloud);
+            j += singlefloat;
+            tTask = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Task", tTask);
+            j += singlefloat;
+            tTexture = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Texture", tTexture);
+            j += singlefloat;
+            tAsset = (int)BitConverter.ToSingle(throttle, j);
+            returndict.Add("Asset", tAsset);
+
+            tall = tResend + tLand + tWind + tCloud + tTask + tTexture + tAsset;
+            returndict.Add("All", tall);
+
+            return returndict;
+        }
+        public Dictionary<string,string> decodeQueueReport(string rep)
+        {
+            Dictionary<string, string> returndic = new Dictionary<string, string>();
+            if (rep.Length == 79)
+            {
+                int pos = 1;
+                returndic.Add("All", rep.Substring((6 * pos), 8)); pos++;
+                returndic.Add("Incoming", rep.Substring((7 * pos), 8)); pos++;
+                returndic.Add("Outgoing", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Resend", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Land", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Wind", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Cloud", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Task", rep.Substring((7 * pos) , 8)); pos++;
+                returndic.Add("Texture", rep.Substring((7 * pos), 8)); pos++;
+                returndic.Add("Asset", rep.Substring((7 * pos), 8)); 
+                /*
+                 * return string.Format("{0,7} {1,7} {2,7} {3,7} {4,7} {5,7} {6,7} {7,7} {8,7} {9,7}",
+                                 SendQueue.Count(),
+                                 IncomingPacketQueue.Count,
+                                 OutgoingPacketQueue.Count,
+                                 ResendOutgoingPacketQueue.Count,
+                                 LandOutgoingPacketQueue.Count,
+                                 WindOutgoingPacketQueue.Count,
+                                 CloudOutgoingPacketQueue.Count,
+                                 TaskOutgoingPacketQueue.Count,
+                                 TextureOutgoingPacketQueue.Count,
+                                 AssetOutgoingPacketQueue.Count);
+                 */
+            }
+
+
+
+            return returndic;
+        }
+        #endregion
+    }
+}

+ 215 - 0
OpenSim/Region/UserStatistics/Default_Report.cs

@@ -0,0 +1,215 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Mono.Data.SqliteClient;
+using OpenMetaverse;
+using OpenSim.Region.Environment.Scenes;
+using OpenSim.Framework.Statistics;
+
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class Default_Report : IStatsController
+    {
+
+        public Default_Report()
+        {
+
+        }
+
+        #region IStatsController Members
+
+        public Hashtable ProcessModel(Hashtable pParams)
+        {
+            SqliteConnection conn = (SqliteConnection)pParams["DatabaseConnection"];
+            List<Scene> m_scene = (List<Scene>)pParams["Scenes"];
+
+            stats_default_page_values mData = rep_DefaultReport_data(conn, m_scene);
+            mData.sim_stat_data = (Dictionary<UUID,USimStatsData>)pParams["SimStats"];
+
+            Hashtable nh = new Hashtable();
+            nh.Add("hdata", mData);
+            
+            return nh;
+        }
+
+        public string RenderView(Hashtable pModelResult)
+        {
+            stats_default_page_values mData = (stats_default_page_values) pModelResult["hdata"];
+            return rep_Default_report_view(mData);
+        }
+
+        #endregion
+
+        public string rep_Default_report_view(stats_default_page_values values)
+        {
+
+            StringBuilder output = new StringBuilder();
+
+
+
+            const string TableClass = "defaultr";
+            const string TRClass = "defaultr";
+            const string TDHeaderClass = "header";
+            const string TDDataClass = "content";
+            //const string TDDataClassRight = "contentright";
+            const string TDDataClassCenter = "contentcenter";
+
+            const string STYLESHEET =
+                @"
+<STYLE>
+body
+{
+	font-size:15px; font-family:Helvetica, Verdana; color:Black;
+}
+TABLE.defaultr { }
+TR.defaultr { padding: 5px; }
+TD.header { font-weight:bold; padding:5px; }
+TD.content {}
+TD.contentright { text-align: right; }
+TD.contentcenter { text-align: center; }
+TD.align_top { vertical-align: top; }
+</STYLE>
+";
+            HTMLUtil.HtmlHeaders_O(ref output);
+            
+            HTMLUtil.InsertProtoTypeAJAX(ref output);
+            string[] ajaxUpdaterDivs = new string[2];
+            int[] ajaxUpdaterSeconds = new int[2];
+            string[] ajaxUpdaterReportFragments = new string[2];
+
+            ajaxUpdaterDivs[0] = "activeconnections";
+            ajaxUpdaterSeconds[0] = 10;
+            ajaxUpdaterReportFragments[0] = "activeconnectionsajax.ajax";
+
+            ajaxUpdaterDivs[1] = "activesimstats";
+            ajaxUpdaterSeconds[1] = 20;
+            ajaxUpdaterReportFragments[1] = "simstatsajax.ajax";
+
+            HTMLUtil.InsertPeriodicUpdaters(ref output, ajaxUpdaterDivs, ajaxUpdaterSeconds, ajaxUpdaterReportFragments);
+            
+            output.Append(STYLESHEET);
+            HTMLUtil.HtmlHeaders_C(ref output);
+            
+            HTMLUtil.TABLE_O(ref output, TableClass);
+            HTMLUtil.TR_O(ref output, TRClass);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("# Users Total");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("# Sessions Total");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("Avg Client FPS");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("Avg Client Mem Use");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("Avg Sim FPS");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("Avg Ping");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("KB Out Total");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDHeaderClass);
+            output.Append("KB In Total");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TR_C(ref output);
+            HTMLUtil.TR_O(ref output, TRClass);
+            HTMLUtil.TD_O(ref output, TDDataClass);
+            output.Append(values.total_num_users);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClass);
+            output.Append(values.total_num_sessions);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.avg_client_fps);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.avg_client_mem_use);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.avg_sim_fps);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.avg_ping);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.total_kb_out);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, TDDataClassCenter);
+            output.Append(values.total_kb_in);
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TR_C(ref output);
+            HTMLUtil.TABLE_C(ref output);
+
+            HTMLUtil.HR(ref output, "");
+            HTMLUtil.TABLE_O(ref output, "");
+            HTMLUtil.TR_O(ref output, "");
+            HTMLUtil.TD_O(ref output, "align_top");
+            output.Append("<DIV id=\"activeconnections\">loading...</DIV>");
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TD_O(ref output, "align_top");
+            output.Append("<DIV id=\"activesimstats\">loading...</DIV>");
+
+            HTMLUtil.TD_C(ref output);
+            HTMLUtil.TR_C(ref output);
+            HTMLUtil.TABLE_C(ref output);
+            output.Append("</BODY></HTML>");
+            // TODO: FIXME: template
+            return output.ToString();
+        }
+
+       
+
+        public stats_default_page_values rep_DefaultReport_data(SqliteConnection db, List<Scene> m_scene)
+        {
+            stats_default_page_values returnstruct = new stats_default_page_values();
+            returnstruct.all_scenes = m_scene.ToArray();
+            lock (db)
+            {
+                string SQL = @"SELECT COUNT(DISTINCT agent_id) as agents, COUNT(*) as sessions, AVG(avg_fps) as client_fps, 
+                                AVG(avg_sim_fps) as savg_sim_fps, AVG(avg_ping) as sav_ping, SUM(n_out_kb) as num_in_kb, 
+                                SUM(n_out_pk) as num_in_packets, SUM(n_in_kb) as num_out_kb, SUM(n_in_pk) as num_out_packets, AVG(mem_use) as sav_mem_use
+                                FROM stats_session_data;";
+                SqliteCommand cmd = new SqliteCommand(SQL, db);
+                SqliteDataReader sdr = cmd.ExecuteReader();
+                if (sdr.HasRows)
+                {
+                    sdr.Read();
+                    returnstruct.total_num_users = Convert.ToInt32(sdr["agents"]);
+                    returnstruct.total_num_sessions = Convert.ToInt32(sdr["sessions"]);
+                    returnstruct.avg_client_fps = Convert.ToSingle(sdr["client_fps"]);
+                    returnstruct.avg_sim_fps = Convert.ToSingle(sdr["savg_sim_fps"]);
+                    returnstruct.avg_ping = Convert.ToSingle(sdr["sav_ping"]);
+                    returnstruct.total_kb_out = Convert.ToSingle(sdr["num_out_kb"]);
+                    returnstruct.total_kb_in = Convert.ToSingle(sdr["num_in_kb"]);
+                    returnstruct.avg_client_mem_use = Convert.ToSingle(sdr["sav_mem_use"]);
+
+                }
+            }
+            return returnstruct;
+        }
+
+    }
+
+    public struct stats_default_page_values
+    {
+        public int total_num_users;
+        public int total_num_sessions;
+        public float avg_client_fps;
+        public float avg_client_mem_use;
+        public float avg_sim_fps;
+        public float avg_ping;
+        public float total_kb_out;
+        public float total_kb_in;
+        public float avg_client_resends;
+        public Scene[] all_scenes;
+        public Dictionary<UUID, USimStatsData> sim_stat_data;
+    }
+}

+ 185 - 0
OpenSim/Region/UserStatistics/HTMLUtil.cs

@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public static class HTMLUtil
+    {
+
+        public static void TR_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<tr");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(">\n\t");
+        }
+        public static void TR_C(ref StringBuilder o)
+        {
+            o.Append("</tr>\n");
+        }
+
+        public static void TD_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<td");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(">");
+        }
+        public static void TD_C(ref StringBuilder o)
+        {
+            o.Append("</td>");
+        }
+        public static void TABLE_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<table");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(">\n\t");
+        }
+        public static void TABLE_C(ref StringBuilder o)
+        {
+            o.Append("</table>\n");
+        }
+        
+        public static void BLOCKQUOTE_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<blockquote");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(" />\n");
+        }
+
+        public static void BLOCKQUOTE_C(ref StringBuilder o)
+        {
+            o.Append("</blockquote>\n");
+        }
+
+        public static void BR(ref StringBuilder o)
+        {
+            o.Append("<br />\n");
+        }
+
+        public static void HR(ref StringBuilder o, string pclass)
+        {
+            o.Append("<hr");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(" />\n");
+        }
+
+        public static void UL_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<ul");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(" />\n");
+        }
+
+        public static void UL_C(ref StringBuilder o)
+        {
+            o.Append("</ul>\n");
+        }
+
+        public static void OL_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<ol");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(" />\n");
+        }
+
+        public static void OL_C(ref StringBuilder o)
+        {
+            o.Append("</ol>\n");
+        }
+
+        public static void LI_O(ref StringBuilder o, string pclass)
+        {
+            o.Append("<li");
+            if (pclass.Length > 0)
+            {
+                GenericClass(ref o, pclass);
+            }
+            o.Append(" />\n");
+        }
+
+        public static void LI_C(ref StringBuilder o)
+        {
+            o.Append("</li>\n");
+        }
+
+        public static void GenericClass(ref StringBuilder o, string pclass)
+        {
+            o.Append(" class=\"");
+            o.Append(pclass);
+            o.Append("\"");
+        }
+
+        public static void InsertProtoTypeAJAX(ref StringBuilder o)
+        {
+            o.Append("<script type=\"text/javascript\" src=\"prototype.js\"></script>\n");
+            o.Append("<script type=\"text/javascript\" src=\"updater.js\"></script>\n");
+        }
+
+        public static void InsertPeriodicUpdaters(ref StringBuilder o, string[] divID, int[] seconds, string[] reportfrag)
+        {
+            o.Append("<script type=\"text/javascript\">\n");
+            o.Append(
+                @"
+				// <![CDATA[
+				document.observe('dom:loaded', function() {
+					/*
+					first arg	: div to update
+					second arg	: interval to poll in seconds
+					third arg	: file to get data
+					*/
+");
+            for (int i = 0; i < divID.Length; i++)
+            {
+            
+                o.Append("new updater('");
+                o.Append(divID[i]);
+                o.Append("', ");
+                o.Append(seconds[i]);
+                o.Append(", '");
+                o.Append(reportfrag[i]);
+                o.Append("');\n");
+            }
+
+            o.Append(@"
+				});
+				// ]]>
+				</script>");
+
+        }
+
+        public static void HtmlHeaders_O ( ref StringBuilder o)
+        {
+            o.Append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n");
+		    o.Append("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"nl\">");
+            o.Append("<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\" />");
+
+
+        }
+        public static void HtmlHeaders_C ( ref StringBuilder o)
+        {
+            o.Append("</HEAD>");
+            o.Append("<BODY>");
+        }
+    }
+}

+ 10 - 0
OpenSim/Region/UserStatistics/IStatsReport.cs

@@ -0,0 +1,10 @@
+using System.Collections;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public interface IStatsController
+    {
+        Hashtable ProcessModel(Hashtable pParams);
+        string RenderView(Hashtable pModelResult);
+    }
+}

+ 33 - 0
OpenSim/Region/UserStatistics/Prototype_distributor.cs

@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using OpenSim.Framework;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class Prototype_distributor : IStatsController
+    {
+        private string prototypejs=string.Empty;
+
+
+        public Hashtable ProcessModel(Hashtable pParams)
+        {
+            Hashtable pResult = new Hashtable();
+            if (prototypejs.Length == 0)
+            {
+                StreamReader fs = new StreamReader(new FileStream(Util.dataDir() + "/data/prototype.js", FileMode.Open));
+                prototypejs = fs.ReadToEnd();
+            }
+            pResult["js"] = prototypejs;
+            return pResult;
+        }
+
+        public string RenderView(Hashtable pModelResult)
+        {
+            return pModelResult["js"].ToString();
+        }
+
+    }
+}

+ 190 - 0
OpenSim/Region/UserStatistics/SimStatsAJAX.cs

@@ -0,0 +1,190 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Mono.Data.SqliteClient;
+using OpenMetaverse;
+using OpenSim.Region.Environment.Scenes;
+using OpenSim.Framework.Statistics;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class SimStatsAJAX : IStatsController
+    {
+        #region IStatsController Members
+
+        public Hashtable ProcessModel(Hashtable pParams)
+        {
+            List<Scene> m_scene = (List<Scene>)pParams["Scenes"];
+            
+            Hashtable nh = new Hashtable();
+            nh.Add("hdata", m_scene);
+            nh.Add("simstats", pParams["SimStats"]);
+
+            return nh;
+        }
+
+        public string RenderView(Hashtable pModelResult)
+        {
+            StringBuilder output = new StringBuilder();
+            List<Scene> all_scenes = (List<Scene>) pModelResult["hdata"];
+            Dictionary<UUID, USimStatsData> sdatadic = (Dictionary<UUID,USimStatsData>)pModelResult["simstats"];
+
+            const string TableClass = "defaultr";
+            const string TRClass = "defaultr";
+            const string TDHeaderClass = "header";
+            const string TDDataClass = "content";
+            //const string TDDataClassRight = "contentright";
+            const string TDDataClassCenter = "contentcenter";
+
+            foreach (USimStatsData sdata in sdatadic.Values)
+            {
+
+
+                foreach (Scene sn in all_scenes)
+                {
+                    if (sn.RegionInfo.RegionID == sdata.RegionId)
+                    {
+                        output.Append("<H2>");
+                        output.Append(sn.RegionInfo.RegionName);
+                        output.Append("</H2>");
+                    }
+                }
+                HTMLUtil.TABLE_O(ref output, TableClass);
+                HTMLUtil.TR_O(ref output, TRClass);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("Dilatn");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("SimFPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("PhysFPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("AgntUp");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("RootAg");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("ChldAg");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("Prims");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("ATvPrm");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("AtvScr");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("ScrLPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TR_C(ref output);
+                HTMLUtil.TR_O(ref output, TRClass);
+                HTMLUtil.TD_O(ref output, TDDataClass);
+                output.Append(sdata.TimeDilation);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClass);
+                output.Append(sdata.SimFps);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.PhysicsFps);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.AgentUpdates);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.RootAgents);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.ChildAgents);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.TotalPrims);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.ActivePrims);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.ActiveScripts);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.ScriptLinesPerSecond);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TR_C(ref output);
+                HTMLUtil.TR_O(ref output, TRClass);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("FrmMS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("AgtMS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("PhysMS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("OthrMS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("ScrLPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("OutPPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("InPPS");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("NoAckKB");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("PndDWN");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDHeaderClass);
+                output.Append("PndUP");
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TR_C(ref output);
+                HTMLUtil.TR_O(ref output, TRClass);
+                HTMLUtil.TD_O(ref output, TDDataClass);
+                output.Append(sdata.TotalFrameTime);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClass);
+                output.Append(sdata.AgentFrameTime);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.PhysicsFrameTime);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.OtherFrameTime);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.ScriptLinesPerSecond);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.OutPacketsPerSecond);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.InPacketsPerSecond);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.UnackedBytes);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.PendingDownloads);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TD_O(ref output, TDDataClassCenter);
+                output.Append(sdata.PendingUploads);
+                HTMLUtil.TD_C(ref output);
+                HTMLUtil.TR_C(ref output);
+                HTMLUtil.TABLE_C(ref output);
+            }
+            return output.ToString();
+        }
+
+        #endregion
+    }
+}

+ 35 - 0
OpenSim/Region/UserStatistics/Updater_distributor.cs

@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using OpenSim.Framework;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class Updater_distributor : IStatsController
+    {
+        private string updaterjs = string.Empty;
+
+
+        public Hashtable ProcessModel(Hashtable pParams)
+        {
+            Hashtable pResult = new Hashtable();
+            if (updaterjs.Length == 0)
+            {
+                StreamReader fs = new StreamReader(new FileStream(Util.dataDir() + "/data/updater.js", FileMode.Open));
+                updaterjs = fs.ReadToEnd();
+                fs.Close();
+                fs.Dispose();
+            }
+            pResult["js"] = updaterjs;
+            return pResult;
+        }
+
+        public string RenderView(Hashtable pModelResult)
+        {
+            return pModelResult["js"].ToString();
+        }
+
+    }
+}

+ 1083 - 0
OpenSim/Region/UserStatistics/WebStatsModule.cs

@@ -0,0 +1,1083 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Net; // to be used for REST-->Grid shortly
+using System.Reflection;
+using log4net;
+using Nini.Config;
+using OpenMetaverse;
+using OpenMetaverse.StructuredData;
+using OpenSim.Framework;
+using OpenSim.Framework.Servers;
+using OpenSim.Region.Environment.Interfaces;
+using OpenSim.Region.Environment.Scenes;
+using Mono.Data.SqliteClient;
+
+
+using Caps = OpenSim.Framework.Communications.Capabilities.Caps;
+
+using OSD = OpenMetaverse.StructuredData.OSD;
+using OSDMap = OpenMetaverse.StructuredData.OSDMap;
+
+namespace OpenSim.Region.UserStatistics
+{
+    public class WebStatsModule : IRegionModule
+    {
+        private static readonly ILog m_log =
+            LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
+        private static SqliteConnection dbConn;
+        private Dictionary<UUID, UserSessionID> m_sessions = new Dictionary<UUID, UserSessionID>();
+        private List<Scene> m_scene = new List<Scene>();
+        private Dictionary<string, IStatsController> reports = new Dictionary<string, IStatsController>();
+        private Dictionary<UUID, USimStatsData> m_simstatsCounters = new Dictionary<UUID, USimStatsData>(); 
+        private const int updateStatsMod = 6;
+        private volatile int concurrencyCounter = 0;
+        private bool enabled = false;
+
+
+        public virtual void Initialise(Scene scene, IConfigSource config)
+        {
+            IConfig cnfg;
+            try
+            {
+                cnfg = config.Configs["WebStats"];
+                enabled = cnfg.GetBoolean("enabled", false);
+
+            } 
+            catch (Exception)
+            {
+                enabled = false;
+            }
+            if (!enabled)
+            {
+                return;
+            }
+
+            lock (m_scene)
+            {
+                if (m_scene.Count == 0)
+                {
+                    IConfig startupConfig = config.Configs["Startup"];
+
+                    dbConn = new SqliteConnection("URI=file:LocalUserStatistics.db,version=3");
+                    dbConn.Open();
+                    CheckAndUpdateDatabase(dbConn);
+
+                    Default_Report rep = new Default_Report();
+                    Prototype_distributor protodep = new Prototype_distributor();
+                    Updater_distributor updatedep = new Updater_distributor();
+                    ActiveConnectionsAJAX ajConnections = new ActiveConnectionsAJAX();
+                    SimStatsAJAX ajSimStats = new SimStatsAJAX();
+                    reports.Add("", rep);
+                    reports.Add("index.aspx", rep);
+                    reports.Add("prototype.js", protodep);
+                    reports.Add("updater.js", updatedep);
+                    reports.Add("activeconnectionsajax.ajax", ajConnections);
+                    reports.Add("simstatsajax.ajax", ajSimStats);
+
+                    scene.CommsManager.HttpServer.AddHTTPHandler("/SStats/", HandleStatsRequest);
+
+
+                }
+                m_scene.Add(scene);
+                m_simstatsCounters.Add(scene.RegionInfo.RegionID, new USimStatsData(scene.RegionInfo.RegionID));
+                scene.StatsReporter.OnSendStatsResult += ReceiveClassicSimStatsPacket;
+            }
+            
+        }
+
+        public void ReceiveClassicSimStatsPacket(SimStats stats)
+        {
+            if (!enabled)
+            {
+                return;
+            }
+
+            try
+            {
+                
+                if (concurrencyCounter > 0)
+                    return;
+
+                USimStatsData ss = m_simstatsCounters[stats.RegionUUID];
+
+                if ((++ss.StatsCounter % updateStatsMod) == 0)
+                {
+                    ss.ConsumeSimStats(stats);
+                }
+            } 
+            catch (KeyNotFoundException)
+            {
+               
+            }
+        }
+
+        public Hashtable HandleStatsRequest(Hashtable request)
+        {
+            Hashtable responsedata = new Hashtable();
+            string regpath = request["uri"].ToString();
+            int response_code = 404;
+            string contenttype = "text/html";
+            
+            string strOut = string.Empty;
+
+            regpath = regpath.Remove(0, 8);
+            if (reports.ContainsKey(regpath))
+            {
+                IStatsController rep = reports[regpath];
+                Hashtable repParams = new Hashtable();
+
+                repParams["DatabaseConnection"] = dbConn;
+                repParams["Scenes"] = m_scene;
+                repParams["SimStats"] = m_simstatsCounters;
+                
+                concurrencyCounter++;
+
+                strOut = rep.RenderView(rep.ProcessModel(repParams));
+
+                if (regpath.EndsWith("js"))
+                {
+                    contenttype = "text/javascript";
+                }
+
+                concurrencyCounter--;
+                
+                response_code = 200;
+
+            }
+            else
+            {
+                strOut = m_scene[0].CommsManager.HttpServer.GetHTTP404("");
+            }
+            
+
+            responsedata["int_response_code"] = response_code;
+            responsedata["content_type"] = contenttype;
+            responsedata["keepalive"] = false;
+            responsedata["str_response_string"] = strOut;
+
+            return responsedata;
+        }
+
+       
+
+        public void CheckAndUpdateDatabase(SqliteConnection db)
+        {
+            lock (db)
+            {
+                // TODO: FIXME: implement stats migrations
+                const string SQL = @"SELECT * FROM migrations LIMIT 1";
+
+                SqliteCommand cmd = new SqliteCommand(SQL, db);
+
+                try
+                {
+                    cmd.ExecuteNonQuery();
+                }
+                catch (SqliteSyntaxException)
+                {
+                    CreateTables(db);
+                }
+            }
+
+        }
+
+        public void CreateTables(SqliteConnection db)
+        {
+            SqliteCommand createcmd = new SqliteCommand(SQL_STATS_TABLE_CREATE, db);
+            createcmd.ExecuteNonQuery();
+
+            createcmd.CommandText = SQL_MIGRA_TABLE_CREATE;
+            createcmd.ExecuteNonQuery();
+        }
+
+
+
+        public virtual void PostInitialise()
+        {
+            if (!enabled)
+            {
+                return;
+            }
+            AddHandlers();
+        }
+
+        public virtual void Close()
+        {
+            if (!enabled)
+            {
+                return;
+            }
+            dbConn.Close();
+            dbConn.Dispose();
+        }
+
+        public virtual string Name
+        {
+            get { return "ViewerStatsModule"; }
+        }
+
+        public bool IsSharedModule
+        {
+            get { return true; }
+        }
+
+        public void OnRegisterCaps(UUID agentID, Caps caps)
+        {
+            m_log.DebugFormat("[VC]: OnRegisterCaps: agentID {0} caps {1}", agentID, caps);
+            string capsPath = "/CAPS/VS/" + UUID.Random();
+            caps.RegisterHandler("ViewerStats",
+                                 new RestStreamHandler("POST", capsPath,
+                                                       delegate(string request, string path, string param,
+                                                                OSHttpRequest httpRequest, OSHttpResponse httpResponse)
+                                                       {
+                                                           return ViewerStatsReport(request, path, param,
+                                                                                  agentID, caps);
+                                                       }));
+        }
+
+        public void OnDeRegisterCaps(UUID agentID, Caps caps)
+        {
+
+        }
+
+        protected virtual void AddHandlers()
+        {
+            lock (m_scene)
+            {
+                foreach (Scene scene in m_scene)
+                {
+                    scene.EventManager.OnRegisterCaps += OnRegisterCaps;
+                    scene.EventManager.OnDeregisterCaps += OnDeRegisterCaps;
+                    scene.EventManager.OnClientClosed += OnClientClosed;
+                    scene.EventManager.OnMakeRootAgent += OnMakeRootAgent;
+                    scene.EventManager.OnMakeChildAgent += OnMakeChildAgent;
+                }
+            }
+        }
+
+        public void OnMakeRootAgent(ScenePresence agent)
+        {
+            UUID regionUUID = GetRegionUUIDFromHandle(agent.RegionHandle);
+
+            lock (m_sessions)
+            {
+                if (!m_sessions.ContainsKey(agent.UUID))
+                {
+                    UserSessionData usd = UserSessionUtil.newUserSessionData();
+
+                    UserSessionID uid = new UserSessionID();
+                    uid.name_f = agent.Firstname;
+                    uid.name_l = agent.Lastname;
+                    uid.region_id = regionUUID;
+                    uid.session_id = agent.ControllingClient.SessionId;
+                    uid.session_data = usd;
+
+                    m_sessions.Add(agent.UUID, uid);
+                }
+                else
+                {
+                    UserSessionID uid = m_sessions[agent.UUID];
+                    uid.region_id = regionUUID;
+                    uid.session_id = agent.ControllingClient.SessionId;
+                    m_sessions[agent.UUID] = uid;
+                }
+            }
+        }
+
+
+
+
+        public void OnMakeChildAgent(ScenePresence agent)
+        {
+            lock (m_sessions)
+            {
+                if (m_sessions.ContainsKey(agent.UUID))
+                {
+                    if (m_sessions[agent.UUID].region_id == GetRegionUUIDFromHandle(agent.RegionHandle))
+                    {
+                        m_sessions.Remove(agent.UUID);
+                    }
+                }
+            }
+        }
+
+
+        public void OnClientClosed(UUID agentID)
+        {
+            lock (m_sessions)
+            {
+                if (m_sessions.ContainsKey(agentID))
+                {
+                    m_sessions.Remove(agentID);
+                }
+            }
+
+        }
+
+        public UUID GetRegionUUIDFromHandle(ulong regionhandle)
+        {
+            lock (m_scene)
+            {
+                foreach (Scene scene in m_scene)
+                {
+                    if (scene.RegionInfo.RegionHandle == regionhandle)
+                        return scene.RegionInfo.RegionID;
+                }
+            }
+            return UUID.Zero;
+        }
+        /// <summary>
+        /// Callback for a viewerstats cap
+        /// </summary>
+        /// <param name="request"></param>
+        /// <param name="path"></param>
+        /// <param name="param"></param>
+        /// <param name="agentID"></param>
+        /// <param name="caps"></param>
+        /// <returns></returns>
+        public string ViewerStatsReport(string request, string path, string param,
+                                      UUID agentID, Caps caps)
+        {
+            m_log.Debug(request);
+            UserSessionID uid;
+            UserSessionData usd;
+
+            lock (m_sessions)
+            {
+                if (!m_sessions.ContainsKey(agentID))
+                {
+                    m_log.Warn("[VS]: no session for stat disclosure");
+                    return string.Empty;
+                }
+                uid = m_sessions[agentID];
+            }
+
+            usd = uid.session_data;
+
+            OSD message = OSDParser.DeserializeLLSDXml(request);
+            if (message.Type != OSDType.Map)
+                return String.Empty;
+
+            OSDMap mmap = (OSDMap) message;
+            {
+                if (mmap["agent"].Type != OSDType.Map)
+                    return String.Empty;
+                OSDMap agent_map = (OSDMap) mmap["agent"];
+                usd.agent_id = agentID;
+                usd.name_f = uid.name_f;
+                usd.name_l = uid.name_l;
+                usd.region_id = uid.region_id;
+                usd.a_language = agent_map["language"].AsString();
+                usd.mem_use = (float) agent_map["mem_use"].AsReal();
+                usd.meters_traveled = (float) agent_map["meters_traveled"].AsReal();
+                usd.regions_visited = agent_map["regions_visited"].AsInteger();
+                usd.run_time = (float) agent_map["run_time"].AsReal();
+                usd.start_time = (float) agent_map["start_time"].AsReal();
+                usd.client_version = agent_map["version"].AsString();
+
+                UserSessionUtil.UpdateMultiItems(ref usd, agent_map["agents_in_view"].AsInteger(),
+                                                 (float) agent_map["ping"].AsReal(),
+                                                 (float) agent_map["sim_fps"].AsReal(),
+                                                 (float) agent_map["fps"].AsReal());
+
+                if (mmap["downloads"].Type != OSDType.Map)
+                    return String.Empty;
+                OSDMap downloads_map = (OSDMap) mmap["downloads"];
+                usd.d_object_kb = (float) downloads_map["object_kbytes"].AsReal();
+                usd.d_texture_kb = (float) downloads_map["texture_kbytes"].AsReal();
+                usd.d_world_kb = (float) downloads_map["workd_kbytes"].AsReal();
+
+
+                usd.session_id = mmap["session_id"].AsUUID();
+
+                if (mmap["system"].Type != OSDType.Map)
+                    return String.Empty;
+                OSDMap system_map = (OSDMap) mmap["system"];
+
+                usd.s_cpu = system_map["cpu"].AsString();
+                usd.s_gpu = system_map["gpu"].AsString();
+                usd.s_os = system_map["os"].AsString();
+                usd.s_ram = system_map["ram"].AsInteger();
+
+                if (mmap["stats"].Type != OSDType.Map)
+                    return String.Empty;
+
+                OSDMap stats_map = (OSDMap) mmap["stats"];
+                {
+                    if (mmap["failures"].Type != OSDType.Map)
+                        return String.Empty;
+                    OSDMap stats_failures = (OSDMap) stats_map["failures"];
+                    usd.f_dropped = stats_failures["dropped"].AsInteger();
+                    usd.f_failed_resends = stats_failures["failed_resends"].AsInteger();
+                    usd.f_invalid = stats_failures["invalid"].AsInteger();
+                    usd.f_resent = stats_failures["resent"].AsInteger();
+                    usd.f_send_packet = stats_failures["send_packet"].AsInteger();
+
+                    if (mmap["net"].Type != OSDType.Map)
+                        return String.Empty;
+                    OSDMap stats_net = (OSDMap) stats_map["net"];
+                    {
+                        if (mmap["in"].Type != OSDType.Map)
+                            return String.Empty;
+
+                        OSDMap net_in = (OSDMap) stats_net["in"];
+                        usd.n_in_kb = (float) net_in["kbytes"].AsReal();
+                        usd.n_in_pk = net_in["packets"].AsInteger();
+
+                        if (mmap["out"].Type != OSDType.Map)
+                            return String.Empty;
+                        OSDMap net_out = (OSDMap) stats_net["out"];
+
+                        usd.n_out_kb = (float) net_out["kbytes"].AsReal();
+                        usd.n_out_pk = net_out["packets"].AsInteger();
+                    }
+
+
+                }
+            }
+            
+            uid.session_data = usd;
+            m_sessions[agentID] = uid;
+            UpdateUserStats(uid, dbConn);
+
+            return String.Empty;
+        }
+
+        public void UpdateUserStats(UserSessionID uid, SqliteConnection db)
+        {
+            lock (db)
+            {
+                SqliteCommand updatecmd = new SqliteCommand(SQL_STATS_TABLE_UPDATE, db);
+                updatecmd.Parameters.Add(new SqliteParameter(":session_id", uid.session_data.session_id.ToString()));
+                updatecmd.Parameters.Add(new SqliteParameter(":agent_id", uid.session_data.agent_id.ToString()));
+                updatecmd.Parameters.Add(new SqliteParameter(":region_id", uid.session_data.region_id.ToString()));
+                updatecmd.Parameters.Add(new SqliteParameter(":last_updated", (int) uid.session_data.last_updated));
+                updatecmd.Parameters.Add(new SqliteParameter(":remote_ip", uid.session_data.remote_ip));
+                updatecmd.Parameters.Add(new SqliteParameter(":name_f", uid.session_data.name_f));
+                updatecmd.Parameters.Add(new SqliteParameter(":name_l", uid.session_data.name_l));
+                updatecmd.Parameters.Add(new SqliteParameter(":avg_agents_in_view", uid.session_data.avg_agents_in_view));
+                updatecmd.Parameters.Add(new SqliteParameter(":min_agents_in_view",
+                                                             (int) uid.session_data.min_agents_in_view));
+                updatecmd.Parameters.Add(new SqliteParameter(":max_agents_in_view",
+                                                             (int) uid.session_data.max_agents_in_view));
+                updatecmd.Parameters.Add(new SqliteParameter(":mode_agents_in_view",
+                                                             (int) uid.session_data.mode_agents_in_view));
+                updatecmd.Parameters.Add(new SqliteParameter(":avg_fps", uid.session_data.avg_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":min_fps", uid.session_data.min_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":max_fps", uid.session_data.max_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":mode_fps", uid.session_data.mode_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":a_language", uid.session_data.a_language));
+                updatecmd.Parameters.Add(new SqliteParameter(":mem_use", uid.session_data.mem_use));
+                updatecmd.Parameters.Add(new SqliteParameter(":meters_traveled", uid.session_data.meters_traveled));
+                updatecmd.Parameters.Add(new SqliteParameter(":avg_ping", uid.session_data.avg_ping));
+                updatecmd.Parameters.Add(new SqliteParameter(":min_ping", uid.session_data.min_ping));
+                updatecmd.Parameters.Add(new SqliteParameter(":max_ping", uid.session_data.max_ping));
+                updatecmd.Parameters.Add(new SqliteParameter(":mode_ping", uid.session_data.mode_ping));
+                updatecmd.Parameters.Add(new SqliteParameter(":regions_visited", uid.session_data.regions_visited));
+                updatecmd.Parameters.Add(new SqliteParameter(":run_time", uid.session_data.run_time));
+                updatecmd.Parameters.Add(new SqliteParameter(":avg_sim_fps", uid.session_data.avg_sim_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":min_sim_fps", uid.session_data.min_sim_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":max_sim_fps", uid.session_data.max_sim_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":mode_sim_fps", uid.session_data.mode_sim_fps));
+                updatecmd.Parameters.Add(new SqliteParameter(":start_time", uid.session_data.start_time));
+                updatecmd.Parameters.Add(new SqliteParameter(":client_version", uid.session_data.client_version));
+                updatecmd.Parameters.Add(new SqliteParameter(":s_cpu", uid.session_data.s_cpu));
+                updatecmd.Parameters.Add(new SqliteParameter(":s_gpu", uid.session_data.s_gpu));
+                updatecmd.Parameters.Add(new SqliteParameter(":s_os", uid.session_data.s_os));
+                updatecmd.Parameters.Add(new SqliteParameter(":s_ram", uid.session_data.s_ram));
+                updatecmd.Parameters.Add(new SqliteParameter(":d_object_kb", uid.session_data.d_object_kb));
+                updatecmd.Parameters.Add(new SqliteParameter(":d_texture_kb", uid.session_data.d_texture_kb));
+                updatecmd.Parameters.Add(new SqliteParameter(":d_world_kb", uid.session_data.d_world_kb));
+                updatecmd.Parameters.Add(new SqliteParameter(":n_in_kb", uid.session_data.n_in_kb));
+                updatecmd.Parameters.Add(new SqliteParameter(":n_in_pk", uid.session_data.n_in_pk));
+                updatecmd.Parameters.Add(new SqliteParameter(":n_out_kb", uid.session_data.n_out_kb));
+                updatecmd.Parameters.Add(new SqliteParameter(":n_out_pk", uid.session_data.n_out_pk));
+                updatecmd.Parameters.Add(new SqliteParameter(":f_dropped", uid.session_data.f_dropped));
+                updatecmd.Parameters.Add(new SqliteParameter(":f_failed_resends", uid.session_data.f_failed_resends));
+                updatecmd.Parameters.Add(new SqliteParameter(":f_invalid", uid.session_data.f_invalid));
+
+                updatecmd.Parameters.Add(new SqliteParameter(":f_off_circuit", uid.session_data.f_off_circuit));
+                updatecmd.Parameters.Add(new SqliteParameter(":f_resent", uid.session_data.f_resent));
+                updatecmd.Parameters.Add(new SqliteParameter(":f_send_packet", uid.session_data.f_send_packet));
+
+                updatecmd.Parameters.Add(new SqliteParameter(":session_key", uid.session_data.session_id.ToString()));
+                updatecmd.Parameters.Add(new SqliteParameter(":agent_key", uid.session_data.agent_id.ToString()));
+                updatecmd.Parameters.Add(new SqliteParameter(":region_key", uid.session_data.region_id.ToString()));
+                m_log.Debug("UPDATE");
+
+                int result = updatecmd.ExecuteNonQuery();
+
+                if (result == 0)
+                {
+                    m_log.Debug("INSERT");
+                    updatecmd.CommandText = SQL_STATS_TABLE_INSERT;
+                    updatecmd.ExecuteNonQuery();
+                }
+            }
+
+        }
+
+        #region SQL
+        private const string SQL_MIGRA_TABLE_CREATE = @"create table migrations(name varchar(100), version int)";
+
+        private const string SQL_STATS_TABLE_CREATE = @"CREATE TABLE stats_session_data (
+               session_id VARCHAR(36) NOT NULL PRIMARY KEY,
+               agent_id VARCHAR(36) NOT NULL DEFAULT '',
+               region_id VARCHAR(36) NOT NULL DEFAULT '',
+               last_updated INT NOT NULL DEFAULT '0',
+               remote_ip VARCHAR(16) NOT NULL DEFAULT '',
+               name_f VARCHAR(50) NOT NULL DEFAULT '',
+               name_l VARCHAR(50) NOT NULL DEFAULT '',
+               avg_agents_in_view FLOAT NOT NULL DEFAULT '0',
+               min_agents_in_view INT NOT NULL DEFAULT '0',
+               max_agents_in_view INT NOT NULL DEFAULT '0',
+               mode_agents_in_view INT NOT NULL DEFAULT '0',
+               avg_fps FLOAT NOT NULL DEFAULT '0',
+               min_fps FLOAT NOT NULL DEFAULT '0',
+               max_fps FLOAT NOT NULL DEFAULT '0',
+               mode_fps FLOAT NOT NULL DEFAULT '0',
+               a_language VARCHAR(25) NOT NULL DEFAULT '',
+               mem_use FLOAT NOT NULL DEFAULT '0',
+               meters_traveled FLOAT NOT NULL DEFAULT '0',
+               avg_ping FLOAT NOT NULL DEFAULT '0',
+               min_ping FLOAT NOT NULL DEFAULT '0',
+               max_ping FLOAT NOT NULL DEFAULT '0',
+               mode_ping FLOAT NOT NULL DEFAULT '0',
+               regions_visited INT NOT NULL DEFAULT '0',
+               run_time FLOAT NOT NULL DEFAULT '0',
+               avg_sim_fps FLOAT NOT NULL DEFAULT '0',
+               min_sim_fps FLOAT NOT NULL DEFAULT '0',
+               max_sim_fps FLOAT NOT NULL DEFAULT '0',
+               mode_sim_fps FLOAT NOT NULL DEFAULT '0',
+               start_time FLOAT NOT NULL DEFAULT '0',
+               client_version VARCHAR(255) NOT NULL DEFAULT '',
+               s_cpu VARCHAR(255) NOT NULL DEFAULT '',
+               s_gpu VARCHAR(255) NOT NULL DEFAULT '',
+               s_os VARCHAR(2255) NOT NULL DEFAULT '',
+               s_ram INT NOT NULL DEFAULT '0',
+               d_object_kb FLOAT NOT NULL DEFAULT '0',
+               d_texture_kb FLOAT NOT NULL DEFAULT '0',
+               d_world_kb FLOAT NOT NULL DEFAULT '0',
+               n_in_kb FLOAT NOT NULL DEFAULT '0',
+               n_in_pk INT NOT NULL DEFAULT '0',
+               n_out_kb FLOAT NOT NULL DEFAULT '0',
+               n_out_pk INT NOT NULL DEFAULT '0',
+               f_dropped INT NOT NULL DEFAULT '0',
+               f_failed_resends INT NOT NULL DEFAULT '0',
+               f_invalid INT NOT NULL DEFAULT '0',
+               f_off_circuit INT NOT NULL DEFAULT '0',
+               f_resent INT NOT NULL DEFAULT '0',
+               f_send_packet INT NOT NULL DEFAULT '0'
+            );";
+
+        private const string SQL_STATS_TABLE_INSERT = @"INSERT INTO stats_session_data (
+session_id, agent_id, region_id, last_updated, remote_ip, name_f, name_l, avg_agents_in_view, min_agents_in_view, max_agents_in_view, 
+mode_agents_in_view, avg_fps, min_fps, max_fps, mode_fps, a_language, mem_use, meters_traveled, avg_ping, min_ping, max_ping, mode_ping, 
+regions_visited, run_time, avg_sim_fps, min_sim_fps, max_sim_fps, mode_sim_fps, start_time, client_version, s_cpu, s_gpu, s_os, s_ram,
+d_object_kb, d_texture_kb, n_in_kb, n_in_pk, n_out_kb, n_out_pk, f_dropped, f_failed_resends, f_invalid, f_invalid, f_off_circuit,
+f_resent, f_send_packet
+)
+VALUES
+(
+:session_id, :agent_id, :region_id, :last_updated, :remote_ip, :name_f, :name_l, :avg_agents_in_view, :min_agents_in_view, :max_agents_in_view, 
+:mode_agents_in_view, :avg_fps, :min_fps, :max_fps, :mode_fps, :a_language, :mem_use, :meters_traveled, :avg_ping, :min_ping, :max_ping, :mode_ping, 
+:regions_visited, :run_time, :avg_sim_fps, :min_sim_fps, :max_sim_fps, :mode_sim_fps, :start_time, :client_version, :s_cpu, :s_gpu, :s_os, :s_ram,
+:d_object_kb, :d_texture_kb, :n_in_kb, :n_in_pk, :n_out_kb, :n_out_pk, :f_dropped, :f_failed_resends, :f_invalid, :f_invalid, :f_off_circuit,
+:f_resent, :f_send_packet
+)
+";
+
+        private const string SQL_STATS_TABLE_UPDATE = @"
+UPDATE stats_session_data 
+set session_id=:session_id,
+    agent_id=:agent_id,
+    region_id=:region_id,
+    last_updated=:last_updated,
+    remote_ip=:remote_ip,
+    name_f=:name_f,
+    name_l=:name_l,
+    avg_agents_in_view=:avg_agents_in_view,
+    min_agents_in_view=:min_agents_in_view,
+    max_agents_in_view=:max_agents_in_view,
+    mode_agents_in_view=:mode_agents_in_view,
+    avg_fps=:avg_fps,
+    min_fps=:min_fps,
+    max_fps=:max_fps,
+    mode_fps=:mode_fps,
+    a_language=:a_language,
+    mem_use=:mem_use,
+    meters_traveled=:meters_traveled,
+    avg_ping=:avg_ping,
+    min_ping=:min_ping,
+    max_ping=:max_ping,
+    mode_ping=:mode_ping,
+    regions_visited=:regions_visited,
+    run_time=:run_time,
+    avg_sim_fps=:avg_sim_fps,
+    min_sim_fps=:min_sim_fps,
+    max_sim_fps=:max_sim_fps,
+    mode_sim_fps=:mode_sim_fps,
+    start_time=:start_time,
+    client_version=:client_version,
+    s_cpu=:s_cpu,
+    s_gpu=:s_gpu,
+    s_os=:s_os,
+    s_ram=:s_ram,
+    d_object_kb=:d_object_kb,
+    d_texture_kb=:d_texture_kb,
+    d_world_kb=:d_world_kb,
+    n_in_kb=:n_in_kb,
+    n_in_pk=:n_in_pk,
+    n_out_kb=:n_out_kb,
+    n_out_pk=:n_out_pk,
+    f_dropped=:f_dropped,
+    f_failed_resends=:f_failed_resends,
+    f_invalid=:f_invalid,
+    f_off_circuit=:f_off_circuit,
+    f_resent=:f_resent,
+    f_send_packet=:f_send_packet
+WHERE session_id=:session_key AND agent_id=:agent_key AND region_id=:region_key";
+        #endregion
+    }
+    public static class UserSessionUtil
+    {
+        public static UserSessionData newUserSessionData()
+        {
+            UserSessionData obj = ZeroSession(new UserSessionData());
+            return obj;
+        }
+
+        public static void UpdateMultiItems(ref UserSessionData s, int agents_in_view, float ping, float sim_fps, float fps)
+        {
+            // don't insert zero values here or it'll skew the statistics.
+            if (agents_in_view == 0 && fps == 0 && sim_fps == 0 && ping == 0)
+                return;
+            s._agents_in_view.Add(agents_in_view);
+            s._fps.Add(fps);
+            s._sim_fps.Add(sim_fps);
+            s._ping.Add(ping);
+
+            int[] __agents_in_view = s._agents_in_view.ToArray();
+
+            s.avg_agents_in_view = ArrayAvg_i(__agents_in_view);
+            s.min_agents_in_view = ArrayMin_i(__agents_in_view);
+            s.max_agents_in_view = ArrayMax_i(__agents_in_view);
+            s.mode_agents_in_view = ArrayMode_i(__agents_in_view);
+
+            float[] __fps = s._fps.ToArray();
+            s.avg_fps = ArrayAvg_f(__fps);
+            s.min_fps = ArrayMin_f(__fps);
+            s.max_fps = ArrayMax_f(__fps);
+            s.mode_fps = ArrayMode_f(__fps);
+
+            float[] __sim_fps = s._sim_fps.ToArray();
+            s.avg_sim_fps = ArrayAvg_f(__sim_fps);
+            s.min_sim_fps = ArrayMin_f(__sim_fps);
+            s.max_sim_fps = ArrayMax_f(__sim_fps);
+            s.mode_sim_fps = ArrayMode_f(__sim_fps);
+
+            float[] __ping = s._ping.ToArray();
+            s.avg_ping = ArrayAvg_f(__ping);
+            s.min_ping = ArrayMin_f(__ping);
+            s.max_ping = ArrayMax_f(__ping);
+            s.mode_ping = ArrayMode_f(__ping);
+
+        }
+
+        #region Statistics
+
+        public static int ArrayMin_i(int[] arr)
+        {
+            int cnt = arr.Length;
+            if (cnt == 0)
+                return 0;
+
+            Array.Sort(arr);
+            return arr[0];
+        }
+
+        public static int ArrayMax_i(int[] arr)
+        {
+            int cnt = arr.Length;
+            if (cnt == 0)
+                return 0;
+
+            Array.Sort(arr);
+            return arr[cnt-1];
+        }
+
+        public static float ArrayMin_f(float[] arr)
+        {
+            int cnt = arr.Length;
+            if (cnt == 0)
+                return 0;
+
+            Array.Sort(arr);
+            return arr[0];
+        }
+
+        public static float ArrayMax_f(float[] arr)
+        {
+            int cnt = arr.Length;
+            if (cnt == 0)
+                return 0;
+
+            Array.Sort(arr);
+            return arr[cnt - 1];
+        }
+
+        public static float ArrayAvg_i(int[] arr)
+        {
+            int cnt = arr.Length;
+
+            if (cnt == 0)
+                return 0;
+
+            float result = arr[0];
+
+            for (int i = 1; i < cnt; i++)
+                result += arr[i];
+
+            return result / cnt;
+        }
+
+        public static float ArrayAvg_f(float[] arr)
+        {
+            int cnt = arr.Length;
+
+            if (cnt == 0)
+                return 0;
+
+            float result = arr[0];
+
+            for (int i = 1; i < cnt; i++)
+                result += arr[i];
+
+            return result / cnt;
+        }
+
+
+        public static float ArrayMode_f(float[] arr)
+        {
+            List<float> mode = new List<float>();
+
+            float[] srtArr = new float[arr.Length];
+            float[,] freq = new float[arr.Length, 2];
+            Array.Copy(arr, srtArr, arr.Length);
+            Array.Sort(srtArr);
+
+            float tmp = srtArr[0];
+            int index = 0;
+            int i = 0;
+            while (i < srtArr.Length)
+            {
+                freq[index, 0] = tmp;
+
+                while (tmp == srtArr[i])
+                {
+                    freq[index, 1]++;
+                    i++;
+
+                    if (i > srtArr.Length - 1)
+                        break;
+                }
+
+                if (i < srtArr.Length)
+                {
+                    tmp = srtArr[i];
+                    index++;
+                }
+
+            }
+
+            Array.Clear(srtArr, 0, srtArr.Length);
+
+            for (i = 0; i < srtArr.Length; i++)
+                srtArr[i] = freq[i, 1];
+
+            Array.Sort(srtArr);
+
+            if ((srtArr[srtArr.Length - 1]) == 0 || (srtArr[srtArr.Length - 1]) == 1)
+                return 0;
+
+            float freqtest = (float)freq.Length / freq.Rank;
+
+            for (i = 0; i < freqtest; i++)
+            {
+                if (freq[i, 1] == srtArr[index])
+                    mode.Add(freq[i, 0]);
+
+            }
+
+            return mode.ToArray()[0];
+
+        }
+
+
+        public static int ArrayMode_i(int[] arr)
+        {
+            List<int> mode = new List<int>();
+
+            int[] srtArr = new int[arr.Length];
+            int[,] freq = new int[arr.Length, 2];
+            Array.Copy(arr, srtArr, arr.Length);
+            Array.Sort(srtArr);
+
+            int tmp = srtArr[0];
+            int index = 0;
+            int i = 0;
+            while (i < srtArr.Length)
+            {
+                freq[index, 0] = tmp;
+
+                while (tmp == srtArr[i])
+                {
+                    freq[index, 1]++;
+                    i++;
+
+                    if (i > srtArr.Length - 1)
+                        break;
+                }
+
+                if (i < srtArr.Length)
+                {
+                    tmp = srtArr[i];
+                    index++;
+                }
+
+            }
+
+            Array.Clear(srtArr, 0, srtArr.Length);
+
+            for (i = 0; i < srtArr.Length; i++)
+                srtArr[i] = freq[i, 1];
+
+            Array.Sort(srtArr);
+
+            if ((srtArr[srtArr.Length - 1]) == 0 || (srtArr[srtArr.Length - 1]) == 1)
+                return 0;
+           
+            float freqtest = (float)freq.Length / freq.Rank;
+
+            for (i = 0; i < freqtest; i++)
+            {
+                if (freq[i, 1] == srtArr[index])
+                    mode.Add(freq[i, 0]);
+
+            }
+
+            return mode.ToArray()[0];
+
+        }
+
+        #endregion
+
+        private static UserSessionData ZeroSession(UserSessionData s)
+        {
+            s.session_id = UUID.Zero;
+            s.agent_id = UUID.Zero;
+            s.region_id = UUID.Zero;
+            s.last_updated = Util.UnixTimeSinceEpoch();
+            s.remote_ip = "";
+            s.name_f = "";
+            s.name_l = "";
+            s.avg_agents_in_view = 0;
+            s.min_agents_in_view = 0;
+            s.max_agents_in_view = 0;
+            s.mode_agents_in_view = 0;
+            s.avg_fps = 0;
+            s.min_fps = 0;
+            s.max_fps = 0;
+            s.mode_fps = 0;
+            s.a_language = "";
+            s.mem_use = 0;
+            s.meters_traveled = 0;
+            s.avg_ping = 0;
+            s.min_ping = 0;
+            s.max_ping = 0;
+            s.mode_ping = 0;
+            s.regions_visited = 0;
+            s.run_time = 0;
+            s.avg_sim_fps = 0;
+            s.min_sim_fps = 0;
+            s.max_sim_fps = 0;
+            s.mode_sim_fps = 0;
+            s.start_time = 0;
+            s.client_version = "";
+            s.s_cpu = "";
+            s.s_gpu = "";
+            s.s_os = "";
+            s.s_ram = 0;
+            s.d_object_kb = 0;
+            s.d_texture_kb = 0;
+            s.d_world_kb = 0;
+            s.n_in_kb = 0;
+            s.n_in_pk = 0;
+            s.n_out_kb = 0;
+            s.n_out_pk = 0;
+            s.f_dropped = 0;
+            s.f_failed_resends = 0;
+            s.f_invalid = 0;
+            s.f_off_circuit = 0;
+            s.f_resent = 0;
+            s.f_send_packet = 0;
+            s._ping = new List<float>();
+            s._fps = new List<float>();
+            s._sim_fps = new List<float>();
+            s._agents_in_view = new List<int>();
+            return s;
+        }
+    }
+    #region structs
+
+    public struct UserSessionID
+    {
+        public UUID session_id;
+        public UUID region_id;
+        public string name_f;
+        public string name_l;
+        public UserSessionData session_data;
+    }
+
+    public struct UserSessionData
+    {
+        public UUID session_id;
+        public UUID agent_id;
+        public UUID region_id;
+        public float last_updated;
+        public string remote_ip;
+        public string name_f;
+        public string name_l;
+        public float avg_agents_in_view;
+        public float min_agents_in_view;
+        public float max_agents_in_view;
+        public float mode_agents_in_view;
+        public float avg_fps;
+        public float min_fps;
+        public float max_fps;
+        public float mode_fps;
+        public string a_language;
+        public float mem_use;
+        public float meters_traveled;
+        public float avg_ping;
+        public float min_ping;
+        public float max_ping;
+        public float mode_ping;
+        public int regions_visited;
+        public float run_time;
+        public float avg_sim_fps;
+        public float min_sim_fps;
+        public float max_sim_fps;
+        public float mode_sim_fps;
+        public float start_time;
+        public string client_version;
+        public string s_cpu;
+        public string s_gpu;
+        public string s_os;
+        public int s_ram;
+        public float d_object_kb;
+        public float d_texture_kb;
+        public float d_world_kb;
+        public float n_in_kb;
+        public int n_in_pk;
+        public float n_out_kb;
+        public int n_out_pk;
+        public int f_dropped;
+        public int f_failed_resends;
+        public int f_invalid;
+        public int f_off_circuit;
+        public int f_resent;
+        public int f_send_packet;
+        public List<float> _ping;
+        public List<float> _fps;
+        public List<float> _sim_fps;
+        public List<int> _agents_in_view;
+    }
+
+  
+    #endregion
+
+    public class USimStatsData
+    {
+        private UUID m_regionID = UUID.Zero;
+        private volatile int m_statcounter = 0;
+        private volatile float m_timeDilation;
+        private volatile float m_simFps;
+        private volatile float m_physicsFps;
+        private volatile float m_agentUpdates;
+        private volatile float m_rootAgents;
+        private volatile float m_childAgents;
+        private volatile float m_totalPrims;
+        private volatile float m_activePrims;
+        private volatile float m_totalFrameTime;
+        private volatile float m_netFrameTime;
+        private volatile float m_physicsFrameTime;
+        private volatile float m_otherFrameTime;
+        private volatile float m_imageFrameTime;
+        private volatile float m_inPacketsPerSecond;
+        private volatile float m_outPacketsPerSecond;
+        private volatile float m_unackedBytes;
+        private volatile float m_agentFrameTime;
+        private volatile float m_pendingDownloads;
+        private volatile float m_pendingUploads;
+        private volatile float m_activeScripts;
+        private volatile float m_scriptLinesPerSecond;
+
+        public UUID RegionId { get { return m_regionID; } }
+        public int StatsCounter { get { return m_statcounter; } set { m_statcounter = value;}}
+        public float TimeDilation { get { return m_timeDilation; } }
+        public float SimFps { get { return m_simFps; } }
+        public float PhysicsFps { get { return m_physicsFps; } }
+        public float AgentUpdates { get { return m_agentUpdates; } }
+        public float RootAgents { get { return m_rootAgents; } }
+        public float ChildAgents { get { return m_childAgents; } }
+        public float TotalPrims { get { return m_totalPrims; } }
+        public float ActivePrims { get { return m_activePrims; } }
+        public float TotalFrameTime { get { return m_totalFrameTime; } }
+        public float NetFrameTime { get { return m_netFrameTime; } }
+        public float PhysicsFrameTime { get { return m_physicsFrameTime; } }
+        public float OtherFrameTime { get { return m_otherFrameTime; } }
+        public float ImageFrameTime { get { return m_imageFrameTime; } }
+        public float InPacketsPerSecond { get { return m_inPacketsPerSecond; } }
+        public float OutPacketsPerSecond { get { return m_outPacketsPerSecond; } }
+        public float UnackedBytes { get { return m_unackedBytes; } }
+        public float AgentFrameTime { get { return m_agentFrameTime; } }
+        public float PendingDownloads { get { return m_pendingDownloads; } }
+        public float PendingUploads { get { return m_pendingUploads; } }
+        public float ActiveScripts { get { return m_activeScripts; } }
+        public float ScriptLinesPerSecond { get { return m_scriptLinesPerSecond; } }
+
+        public USimStatsData(UUID pRegionID)
+        {
+            m_regionID = pRegionID;
+        }
+
+        public void ConsumeSimStats(SimStats stats)
+        {
+            m_regionID = stats.RegionUUID;
+            m_timeDilation = stats.StatsBlock[0].StatValue;
+            m_simFps = stats.StatsBlock[1].StatValue;
+            m_physicsFps = stats.StatsBlock[2].StatValue;
+            m_agentUpdates = stats.StatsBlock[3].StatValue;
+            m_rootAgents = stats.StatsBlock[4].StatValue;
+            m_childAgents = stats.StatsBlock[5].StatValue;
+            m_totalPrims = stats.StatsBlock[6].StatValue;
+            m_activePrims = stats.StatsBlock[7].StatValue;
+            m_totalFrameTime = stats.StatsBlock[8].StatValue;
+            m_netFrameTime = stats.StatsBlock[9].StatValue;
+            m_physicsFrameTime = stats.StatsBlock[10].StatValue;
+            m_otherFrameTime = stats.StatsBlock[11].StatValue;
+            m_imageFrameTime = stats.StatsBlock[12].StatValue;
+            m_inPacketsPerSecond = stats.StatsBlock[13].StatValue;
+            m_outPacketsPerSecond = stats.StatsBlock[14].StatValue;
+            m_unackedBytes = stats.StatsBlock[15].StatValue;
+            m_agentFrameTime = stats.StatsBlock[16].StatValue;
+            m_pendingDownloads = stats.StatsBlock[17].StatValue;
+            m_pendingUploads = stats.StatsBlock[18].StatValue;
+            m_activeScripts = stats.StatsBlock[19].StatValue;
+            m_scriptLinesPerSecond = stats.StatsBlock[20].StatValue;
+        }
+    }
+
+}

+ 16 - 0
ThirdPartyLicenses/Prototype.txt

@@ -0,0 +1,16 @@
+Copyright (c) 2005-2008 Sam Stephenson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 4222 - 0
bin/data/prototype.js

@@ -0,0 +1,4222 @@
+/*  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();

+ 20 - 0
bin/data/updater.js

@@ -0,0 +1,20 @@
+var updater = Class.create({
+	initialize: function(divToUpdate, interval, file) {
+		this.divToUpdate = divToUpdate;
+		this.interval = interval;
+		this.file = file;
+		new PeriodicalExecuter(this.getUpdate.bind(this), this.interval);
+	},
+	
+	getUpdate: function() {
+		var oOptions = {
+			method: "POST",
+			parameters: "intervalPeriod="+this.interval,
+			asynchronous: true,
+			onComplete: function (oXHR, Json) {
+				$(this.divToUpdate).innerHTML = oXHR.responseText;
+			}
+		};
+		var oRequest = new Ajax.Updater(this.divToUpdate, this.file, oOptions);
+	}
+});

+ 48 - 0
prebuild.xml

@@ -2267,6 +2267,54 @@
         </Match>
       </Files>
     </Project>
+    <Project name="OpenSim.Region.UserStatistics" path="OpenSim/Region/UserStatistics" type="Library">
+      <Configuration name="Debug">
+        <Options>
+          <OutputPath>../../../bin/</OutputPath>
+        </Options>
+      </Configuration>
+      <Configuration name="Release">
+        <Options>
+          <OutputPath>../../../bin/</OutputPath>
+        </Options>
+      </Configuration>
+
+      <ReferencePath>../../../bin/</ReferencePath>
+      <Reference name="System" localCopy="false"/>
+      <Reference name="System.Data"/>
+      <Reference name="System.Xml"/>
+      <Reference name="System.Drawing"/>
+      <Reference name="System.Runtime.Remoting"/>
+      <Reference name="System.Web"/>
+      <Reference name="OpenMetaverseTypes.dll"/>
+      <Reference name="OpenMetaverse.StructuredData.dll"/>
+      <Reference name="OpenMetaverse.dll"/>
+      <Reference name="OpenSim.Framework"/>
+      <Reference name="OpenSim.Data" />
+      <Reference name="OpenSim.Region.Interfaces" />
+      <Reference name="OpenSim.Region.Environment" />
+      <Reference name="OpenSim.Framework.Console"/>
+      <Reference name="OpenSim.Framework.Servers"/>
+      <Reference name="OpenSim.Framework.Statistics"/>
+      <Reference name="OpenSim.Region.Physics.Manager"/>
+      <Reference name="OpenSim.Grid.AssetServer"/>
+      <Reference name="Mono.Data.SqliteClient"/>
+
+      <!-- For scripting in funny languages by default -->
+      <Reference name="Microsoft.JScript"/>
+      <Reference name="XMLRPC.dll"/>
+      <Reference name="OpenSim.Framework.Communications"/>
+      <Reference name="OpenSim.Data.Base"/>
+      <Reference name="Nini.dll" />
+      <Reference name="log4net.dll"/>
+      <Reference name="DotNetOpenMail.dll"/>
+
+      <Files>
+        <Match pattern="*.cs" recurse="true">
+          <Exclude name="Tests" pattern="Tests" />
+        </Match>
+      </Files>
+    </Project>
 
     <!-- Tools -->