summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog7
-rw-r--r--LICENSE2
-rw-r--r--dat/.gitignore1
-rw-r--r--doc/protocol.md16
-rw-r--r--src/hub/.gitignore2
-rw-r--r--src/hub/Makefile12
-rwxr-xr-xsrc/hub/dump-test-db3
-rw-r--r--src/hub/src/spreadspace.org/sfive-hub/s5hub.go69
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5cvt.go129
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5cvt_test.go150
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5log.go36
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srv.go209
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvForward.go60
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvForwardEs.go62
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvForwardGraphite.go47
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvForwardPiwik.go51
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvPipe.go76
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5srvWeb.go224
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5store.go917
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5store_test.go840
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5typesApi.go96
-rw-r--r--src/hub/src/spreadspace.org/sfive/s5typesStore.go211
-rwxr-xr-xsrc/hub/test-bolt21
-rwxr-xr-xsrc/hub/test-bolter21
-rwxr-xr-xsrc/hub/test-client21
-rwxr-xr-xsrc/hub/test-collector4
-rwxr-xr-xsrc/hub/test-fwd11
-rwxr-xr-xsrc/hub/test-fwd-es10
-rwxr-xr-xsrc/hub/test-fwd-piwik11
-rwxr-xr-xsrc/hub/test-import6
-rwxr-xr-xsrc/hub/test-srv13
-rwxr-xr-xsrc/hub/test-srv-ro13
-rw-r--r--src/viz/README4
-rw-r--r--src/viz/dygraph-combined.js2
-rw-r--r--src/viz/index.css180
-rw-r--r--src/viz/index.html19
-rw-r--r--src/viz/index.js173
-rw-r--r--src/viz/jquery-2.1.1.min.js4
-rw-r--r--src/viz/stats.html82
-rw-r--r--src/viz/updates36
40 files changed, 2176 insertions, 1675 deletions
diff --git a/ChangeLog b/ChangeLog
index 9d751cc..bc1a6c9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+2017.??.?? -- Version 0.2.0
+
+* major rewrite of hub
+ - replaced sqlite with bolt-db which is much faster
+ - drop vizualisation (viz)
+ - added forwarding to piwik
+
2016.10.17 -- Version 0.1.4
* s5proxy support for sfive stats
diff --git a/LICENSE b/LICENSE
index a36cba5..271ed24 100644
--- a/LICENSE
+++ b/LICENSE
@@ -12,7 +12,7 @@
# live and recorded data.
#
#
-# Copyright (C) 2014-2015 Christian Pointner <equinox@spreadspace.org>
+# Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
# Markus Grüneis <gimpf@gimpf.org>
#
# This file is part of sfive.
diff --git a/dat/.gitignore b/dat/.gitignore
new file mode 100644
index 0000000..39fa2b7
--- /dev/null
+++ b/dat/.gitignore
@@ -0,0 +1 @@
+/sample-accesslog.json
diff --git a/doc/protocol.md b/doc/protocol.md
index 74bdbd2..29391c6 100644
--- a/doc/protocol.md
+++ b/doc/protocol.md
@@ -4,12 +4,16 @@ Messages
init
----
-All fields except version are optional. The values in this message are treated as
+All fields except "version" are optional. The values in this message are treated as
defaults which will be used if the corresponding value is missing in subsequent
update messages.
{
"version": 1,
+ "SourceHubUuid": "f7df89b4-171e-4b2f-a8a4-e58ac99e5dc5",
+ "SourceHubDataUpdateId": 23,
+ "ForwardHubUuid": "b041315e-5039-4c75-81e8-9fd42250b011",
+ "ForwardHubDataUpdateId": 42,
"hostname": "myhostname",
"streamer-id": { "content-id": "av-orig", "format": "flash", "quality": "medium" },
"tags": [ "elevate", "2014", "discourse" ]
@@ -22,11 +26,17 @@ data-update
All values which have been defined by the init message are optional. In any case the
values from data updates override values from init. Stateless interfaces will not use
init messages and therefore all values must be defined here.
-If an interface (i.e. REST) has other means to detect protocol versions the version
-field may be omitted entirely.
+"SourceHubUuid", "SourceHubDataUpdateId", "ForwardHubUuid", "ForwardHubDataUpdateId",
+"user-agent", "bytes-received", "tags" and "clients" might be omitted and are treated
+as an empty string, 0 or empty array respectively.
+The start-time will be processesd and stored with millisecond precision.
{
"version": 1,
+ "SourceHubUuid": "f7df89b4-171e-4b2f-a8a4-e58ac99e5dc5",
+ "SourceHubDataUpdateId": 23,
+ "ForwardHubUuid": "b041315e-5039-4c75-81e8-9fd42250b011",
+ "ForwardHubDataUpdateId": 42,
"hostname": "myhostname",
"streamer-id": { "content-id": "av-orig", "format": "flash", "quality": "medium" },
"tags": [ "elevate", "2014", "discourse" ]
diff --git a/src/hub/.gitignore b/src/hub/.gitignore
index 16dc168..dd65bbd 100644
--- a/src/hub/.gitignore
+++ b/src/hub/.gitignore
@@ -5,3 +5,5 @@
/pkg
*.a
*.o
+/test
+/coverage.out
diff --git a/src/hub/Makefile b/src/hub/Makefile
index 0c34f14..87890a4 100644
--- a/src/hub/Makefile
+++ b/src/hub/Makefile
@@ -12,7 +12,7 @@
# live and recorded data.
#
#
-# Copyright (C) 2014-2015 Christian Pointner <equinox@spreadspace.org>
+# Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
# Markus Grüneis <gimpf@gimpf.org>
#
# This file is part of sfive.
@@ -38,13 +38,10 @@ endif
EXECUTEABLE := sfive-hub
-LIBS := "gopkg.in/gorp.v2" \
- "github.com/mattn/go-sqlite3" \
+LIBS := "github.com/boltdb/bolt" \
"github.com/zenazn/goji" \
"github.com/pborman/uuid" \
"github.com/equinox0815/graphite-golang"
-# "github.com/go-sql-driver/mysql"
-# "github.com/ziutek/mymysql/godrv"
all: build test
@@ -83,6 +80,11 @@ bench: getlibs
@echo "testing and benchmarking: sfive"
@$(GOCMD) test -bench=. spreadspace.org/sfive
+cover: getlibs
+ @echo "testing with coverage output"
+ @$(GOCMD) test -coverprofile=coverage.out spreadspace.org/sfive
+ @$(GOCMD) tool cover -html=coverage.out
+
clean:
rm -rf pkg/*/spreadspace.org
rm -rf bin
diff --git a/src/hub/dump-test-db b/src/hub/dump-test-db
deleted file mode 100755
index 9a7aae7..0000000
--- a/src/hub/dump-test-db
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-echo .dump | sqlite3 /var/lib/sfive/db.sqlite
-
diff --git a/src/hub/src/spreadspace.org/sfive-hub/s5hub.go b/src/hub/src/spreadspace.org/sfive-hub/s5hub.go
index 8897e42..4028f3f 100644
--- a/src/hub/src/spreadspace.org/sfive-hub/s5hub.go
+++ b/src/hub/src/spreadspace.org/sfive-hub/s5hub.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package main
import (
@@ -12,8 +44,8 @@ import (
var s5hl = log.New(os.Stderr, "[s5hub]\t", log.LstdFlags)
func main() {
- db := flag.String("db", "/var/lib/sfive/db.sqlite", "path to the sqlite3 database file")
- dbMysql := flag.Bool("db-mysql", false, "use MySQL connector")
+ db := flag.String("db", "/var/lib/sfive/db.bolt", "path to the database file")
+ readOnly := flag.Bool("read-only", false, "open database in read-only mode")
pipe := flag.String("pipe", "/var/run/sfive/pipe", "path to the unix pipe for the pipeserver")
ppipe := flag.String("pipegram", "/var/run/sfive/pipegram", "path to the unix datagram pipe for the pipeserver")
startPipe := flag.Bool("start-pipe-server", true, "start a connection oriented pipe server; see option pipe")
@@ -27,7 +59,6 @@ func main() {
piwikSiteURL := flag.String("piwik-site-url", "", "use this base url for the site")
piwikSiteID := flag.Uint("piwik-site-id", 1, "use this site-id for piwik")
piwikToken := flag.String("piwik-token", "", "the auth token for piwik")
- vizAppDir := flag.String("viz-dir", "/usr/share/sfive/viz", "base-path to the viz application")
help := flag.Bool("help", false, "show usage")
flag.Parse()
@@ -38,11 +69,11 @@ func main() {
return
}
- server, err := sfive.NewServer(*dbMysql, *db)
+ srv, err := sfive.NewServer(*db, *readOnly)
if err != nil {
- s5hl.Fatalf("failed to initialize: %v", err)
+ s5hl.Fatalf(err.Error())
}
- defer server.Close()
+ defer srv.Close()
var wg sync.WaitGroup
@@ -51,7 +82,7 @@ func main() {
go func() {
defer wg.Done()
s5hl.Printf("start pipe at %v\n", *pipe)
- server.ServePipe(*pipe)
+ srv.ServePipe(*pipe)
s5hl.Println("pipe finished")
}()
}
@@ -60,8 +91,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Printf("start pipegram at %v\n", *ppipe)
- server.ServeGramPipe(*ppipe)
+ s5hl.Printf("starting pipegram at %v\n", *ppipe)
+ srv.ServePipegram(*ppipe)
s5hl.Println("pipegram finished")
}()
}
@@ -70,8 +101,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Println("start web-srv")
- server.ServeWeb(*vizAppDir)
+ s5hl.Println("starting web")
+ srv.ServeWeb()
s5hl.Println("web finished")
}()
}
@@ -80,8 +111,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Println("start forward")
- server.RunForwarding(*forward)
+ s5hl.Println("starting forward")
+ srv.RunForwarding(*forward)
s5hl.Println("forward finished")
}()
}
@@ -90,8 +121,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Println("start elastic-search forward")
- server.RunForwardingToElasticSearch(*forwardES)
+ s5hl.Println("starting elastic-search forward")
+ srv.RunForwardingEs(*forwardES)
s5hl.Println("elastic-search forward finished")
}()
}
@@ -100,8 +131,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Println("start graphite forward")
- server.RunForwardingToGraphite(*forwardGraphite, *graphiteBasePath)
+ s5hl.Println("starting graphite forward")
+ srv.RunForwardingGraphite(*forwardGraphite, *graphiteBasePath)
s5hl.Println("graphite forward finished")
}()
}
@@ -110,8 +141,8 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
- s5hl.Println("start piwik forward")
- server.RunForwardingToPiwik(*forwardPiwik, *piwikSiteURL, *piwikSiteID, *piwikToken)
+ s5hl.Println("starting piwik forward")
+ srv.RunForwardingPiwik(*forwardPiwik, *piwikSiteURL, *piwikSiteID, *piwikToken)
s5hl.Println("piwik forward finished")
}()
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5cvt.go b/src/hub/src/spreadspace.org/sfive/s5cvt.go
index cd65cf7..26a7c83 100644
--- a/src/hub/src/spreadspace.org/sfive/s5cvt.go
+++ b/src/hub/src/spreadspace.org/sfive/s5cvt.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -5,71 +37,80 @@ import (
"fmt"
)
-type StatsDecoder interface {
- Decode(jsonString []byte) (StatisticsData, error)
-}
-
-type StatsEncoder interface {
- Encode(data StatisticsData) []byte
-}
+const (
+ ProtocolVersion = 1
+)
-type FilterDecoder interface {
- Decode(jsonString []byte) (StatsFilter, error)
-}
+//
+// Decoder
+//
-type StatefulDecoder struct {
- sourceId SourceId
+type Decoder interface {
+ Decode(jsonString []byte) (DataUpdateFull, error)
}
-type PlainDecoder struct{}
-
-type PlainEncoder struct{}
+// stateless protocol interfaces
+type StatelessDecoder struct{}
-type filterDecoder struct{}
+func NewStatelessDecoder() Decoder {
+ return &StatelessDecoder{}
+}
-func NewStatefulDecoder(jsonString []byte) (decoder StatsDecoder, err error) {
- res := new(StatefulDecoder)
- err = json.Unmarshal(jsonString, &res.sourceId)
- if err != nil {
+func (pd *StatelessDecoder) Decode(jsonString []byte) (res DataUpdateFull, err error) {
+ if err = json.Unmarshal(jsonString, &res); err != nil {
return
}
- if res.sourceId.Version != 1 {
- err = fmt.Errorf("unsupported version, expected 1, actual %v", res.sourceId.Version)
+ if res.Version != ProtocolVersion {
+ err = fmt.Errorf("unsupported version: %d, expected: %d", res.Version, ProtocolVersion)
}
- decoder = res
return
}
-func NewPlainDecoder() StatsDecoder {
- return new(PlainDecoder)
+// stateful protocol interfaces
+type StatefulDecoder struct {
+ Version uint
+ SourceId
}
-func NewFilterDecoder() FilterDecoder {
- return new(filterDecoder)
+func NewStatefulDecoder(jsonString []byte) (Decoder, error) {
+ res := &StatefulDecoder{}
+ if err := json.Unmarshal(jsonString, &res); err != nil {
+ return nil, err
+ }
+ if res.Version != ProtocolVersion {
+ return nil, fmt.Errorf("unsupported version: %d, expected: %d", res.Version, ProtocolVersion)
+ }
+ return res, nil
}
-func (self *StatefulDecoder) Decode(jsonString []byte) (dat StatisticsData, err error) {
- dat.CopyFromSourceId(&self.sourceId)
- err = json.Unmarshal(jsonString, &dat)
- // like in PlainDecoder, let the client decide how to use partial results
- // (Unmarshal returns partial results in case of errors)
+func (sd *StatefulDecoder) Decode(jsonString []byte) (res DataUpdateFull, err error) {
+ res.Version = sd.Version
+ res.CopyFromSourceId(&sd.SourceId)
+ if err = json.Unmarshal(jsonString, &res); err != nil {
+ return
+ }
+ if res.Version != sd.Version {
+ err = fmt.Errorf("unsupported version: %d, expected: %d", res.Version, sd.Version)
+ }
return
}
-func (self *PlainDecoder) Decode(jsonString []byte) (dat StatisticsData, err error) {
- err = json.Unmarshal(jsonString, &dat)
- return
+//
+// Encoder
+//
+
+type Encoder interface {
+ Encode(data DataUpdateFull) ([]byte, error)
}
-func (self *PlainEncoder) Encode(data *StatisticsData) []byte {
- res, err := json.Marshal(data)
- if err != nil {
- s5l.Panicln("failed to encode StatisticsData")
- }
- return res
+type StatelessEncoder struct{}
+
+func NewStatelessEncoder() Encoder {
+ return &StatelessEncoder{}
}
-func (self *filterDecoder) Decode(jsonString []byte) (dat StatsFilter, err error) {
- err = json.Unmarshal(jsonString, &dat)
- return
+func (pe *StatelessEncoder) Encode(data DataUpdateFull) (res []byte, err error) {
+ data.Version = ProtocolVersion
+ data.StartTime = data.StartTime.UTC()
+ return json.Marshal(data)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5cvt_test.go b/src/hub/src/spreadspace.org/sfive/s5cvt_test.go
index 32f35dd..5a79d85 100644
--- a/src/hub/src/spreadspace.org/sfive/s5cvt_test.go
+++ b/src/hub/src/spreadspace.org/sfive/s5cvt_test.go
@@ -1,62 +1,150 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
+ "fmt"
"reflect"
"testing"
"time"
)
var (
- sourceIdFields = `"hostname": "localhost", "streamer-id": {"quality": "low", "content-id": "av", "format": "webm"}, "tags": ["elevate", "2014"], "version": 1`
- sourceIdData = `{` + sourceIdFields + `}`
- sourceIdDataStruct = SourceId{Hostname: "localhost", StreamId: StreamId{Quality: "low", ContentId: "av", Format: "webm"}, Tags: []string{"elevate", "2014"}, Version: 1}
- updateFields = `"data": {"bytes-sent": 1, "client-count": 3, "bytes-received": 1}, "start-time": "2014-08-24T14:35:33.847282Z", "duration-ms": 5000`
- updateData = "{" + updateFields + "}"
- updateDataStruct = DataUpdate{Data: SourceData{BytesSent: 1, ClientCount: 3, BytesReceived: 1}, StartTime: time.Date(2014, time.August, 24, 14, 35, 33, 847282000, time.UTC), Duration: 5000}
- testData = "{" + sourceIdFields + "," + updateFields + "}"
+ initDataEncoded = `"hostname": "localhost", "streamer-id": {"quality": "low", "content-id": "av", "format": "webm"}, "tags": ["elevate", "2014"]`
+ initDataStruct = SourceId{Hostname: "localhost", StreamId: StreamId{Quality: "low", ContentId: "av", Format: "webm"}, Tags: []string{"elevate", "2014"}}
+ updateDataEncoded = `"data": {"bytes-sent": 1, "client-count": 3, "bytes-received": 1}, "start-time": "2014-08-24T14:35:33.847282Z", "duration-ms": 5000`
+ updateDataStruct = DataUpdate{Data: SourceData{BytesSent: 1, ClientCount: 3, BytesReceived: 1}, StartTime: time.Date(2014, time.August, 24, 14, 35, 33, 847282000, time.UTC), Duration: 5000}
)
-func GetExpected() *StatisticsData {
- expected := new(StatisticsData)
- expected.CopyFromSourceId(&sourceIdDataStruct)
+func GetExpected() (expected DataUpdateFull) {
+ expected.Version = ProtocolVersion
+ expected.CopyFromSourceId(&initDataStruct)
expected.CopyFromUpdate(&updateDataStruct)
- return expected
+ return
}
func TestDecodeStateful(t *testing.T) {
- dc, err := NewStatefulDecoder([]byte(sourceIdData))
+ // invalid init message
+ if _, err := NewStatefulDecoder([]byte("this is not json")); err == nil {
+ t.Fatalf("creating decoder with invalid json should throw an error")
+ }
+
+ // wrong protocol version
+ wrongProtoVerMsg := fmt.Sprintf(`{ "version": %d, %s }`, ProtocolVersion+1, initDataEncoded)
+ if _, err := NewStatefulDecoder([]byte(wrongProtoVerMsg)); err == nil {
+ t.Fatalf("creating decoder with wrong protocol version should throw an error")
+ }
+
+ // valid init message
+ statefulInitMsg := fmt.Sprintf(`{ "version": %d, %s }`, ProtocolVersion, initDataEncoded)
+ dec, err := NewStatefulDecoder([]byte(statefulInitMsg))
if err != nil {
- t.Errorf("Creating decoder failed with %v", err)
- return
+ t.Fatalf("creating decoder failed: %v", err)
+ }
+
+ // invalid data-update message
+ if _, err := dec.Decode([]byte("this isn't valid json either")); err == nil {
+ t.Fatalf("decoding message with invalid json should throw an error")
}
- dat, err := dc.Decode([]byte(testData))
+
+ // wrong protocol version
+ wrongProtoVerMsg = fmt.Sprintf(`{ "version": %d, %s }`, ProtocolVersion+1, updateDataEncoded)
+ if _, err := dec.Decode([]byte(wrongProtoVerMsg)); err == nil {
+ t.Fatalf("decoding message with wrong protocol version should throw an error")
+ }
+
+ // valid data-update message
+ statefulUpdateMsg := fmt.Sprintf(`{ "version": %d, %s }`, ProtocolVersion, updateDataEncoded)
+ decoded, err := dec.Decode([]byte(statefulUpdateMsg))
if err != nil {
- t.Errorf("Decode failed with %v", err)
- return
+ t.Fatalf("decoding message failed: %v", err)
}
+
+ // compare with expected result
expected := GetExpected()
- if !reflect.DeepEqual(dat, *expected) {
- t.Errorf("should have been equal\nactual: %v\nexpected: %v\n", &dat, expected)
+ if !reflect.DeepEqual(decoded, expected) {
+ t.Fatalf("decoding failed:\n actual: %v\n expected: %v\n", decoded, expected)
}
}
-func TestDecodePlain(t *testing.T) {
- ec := new(PlainDecoder)
- dat, err := ec.Decode([]byte(testData))
+func TestDecodeStateless(t *testing.T) {
+ dec := NewStatelessDecoder()
+
+ // invalid message
+ if _, err := dec.Decode([]byte("this is still not json")); err == nil {
+ t.Fatalf("decoding invalid json should throw an error")
+ }
+
+ // wrong protocol version
+ wrongProtoVerMsg := fmt.Sprintf(`{ "version": %d, %s, %s }`, ProtocolVersion+1, initDataEncoded, updateDataEncoded)
+ if _, err := dec.Decode([]byte(wrongProtoVerMsg)); err == nil {
+ t.Fatalf("decoding message with wrong protocol version should throw an error")
+ }
+
+ // valid message
+ statelessDataMsg := fmt.Sprintf(`{ "version": %d, %s, %s }`, ProtocolVersion, initDataEncoded, updateDataEncoded)
+ decoded, err := dec.Decode([]byte(statelessDataMsg))
if err != nil {
- t.Errorf("Decode failed with %v", err)
- return
+ t.Fatalf("decoding message failed: %v", err)
}
+
+ // compare with expected result
expected := GetExpected()
- if !reflect.DeepEqual(dat, *expected) {
- t.Errorf("should have been equal\nactual: %v\nexpected: %v\n", &dat, expected)
+ if !reflect.DeepEqual(decoded, expected) {
+ t.Fatalf("decoding failed:\n actual: %v\n expected: %v\n", decoded, expected)
}
}
-func TestEncode(t *testing.T) {
- ec := new(PlainEncoder)
- td := new(StatisticsData)
- td.CopyFromSourceId(&sourceIdDataStruct)
+func TestEncodeStateless(t *testing.T) {
+ enc := NewStatelessEncoder()
+
+ var td DataUpdateFull
+ td.CopyFromSourceId(&initDataStruct)
td.CopyFromUpdate(&updateDataStruct)
- t.Logf("dada: %v", ec.Encode(td))
+
+ encoded, err := enc.Encode(td)
+ if err != nil {
+ t.Fatalf("encoding message failed: %v", err)
+ }
+
+ decoded, err := NewStatelessDecoder().Decode(encoded)
+ if err != nil {
+ t.Fatalf("decoding message failed: %v", err)
+ }
+
+ expected := td
+ expected.Version = ProtocolVersion
+ if !reflect.DeepEqual(decoded, expected) {
+ t.Fatalf("encoding failed:\n actual: %v\n expected: %v\n", decoded, expected)
+ }
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5log.go b/src/hub/src/spreadspace.org/sfive/s5log.go
index 9b80f6f..4801902 100644
--- a/src/hub/src/spreadspace.org/sfive/s5log.go
+++ b/src/hub/src/spreadspace.org/sfive/s5log.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -9,6 +41,6 @@ import (
var (
s5l = log.New(os.Stderr, "[s5]\t", log.LstdFlags)
// use ioutil.Discard to switch that thing off
- // s5tl = log.New(os.Stderr, "[s5dbg]\t", log.LstdFlags)
- s5tl = log.New(ioutil.Discard, "[s5dbg]\t", log.LstdFlags)
+ // s5dl = log.New(os.Stderr, "[s5dbg]\t", log.LstdFlags)
+ s5dl = log.New(ioutil.Discard, "[s5dbg]\t", log.LstdFlags)
)
diff --git a/src/hub/src/spreadspace.org/sfive/s5srv.go b/src/hub/src/spreadspace.org/sfive/s5srv.go
index 9cf4c64..76805c3 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srv.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srv.go
@@ -1,40 +1,60 @@
-package sfive
-
-import "time"
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
-type appendManyToken struct {
- data []StatisticsData
- response chan bool
-}
+package sfive
-type queryStatsResult struct {
- stats StatsResult
- err error
+type appendToken struct {
+ data DataUpdateFull
+ response chan error
}
-type queryStatsToken struct {
- filter *StatsFilter
- response chan queryStatsResult
+type appendManyToken struct {
+ data []DataUpdateFull
+ response chan error
}
type getUpdatesResult struct {
- values []StatisticsData
+ values []DataUpdateFull
err error
}
type getUpdatesAfterToken struct {
id int
- response chan getUpdatesResult
-}
-
-type getUpdatesToken struct {
- filter *StatsFilter
+ limit int
response chan getUpdatesResult
}
type getHubIdResult struct {
- id string
- err error
+ id string
}
type getHubIdToken struct {
@@ -50,141 +70,108 @@ type getLastUpdateIdToken struct {
response chan getLastUpdateIdResult
}
-type StatsSinkServer struct {
- store sqliteStore
+type Server struct {
+ store Store
quit chan bool
done chan bool
- appendData chan StatisticsData
- appendManyData chan appendManyToken // chan []StatisticsData
- getStatsChan chan queryStatsToken
+ appendChan chan appendToken
+ appendManyChan chan appendManyToken
getUpdatesAfterChan chan getUpdatesAfterToken
- getUpdatesChan chan getUpdatesToken
getHubIdChan chan getHubIdToken
getLastUpdateIdChan chan getLastUpdateIdToken
}
-func (self StatsSinkServer) appendActor() {
- defer func() { self.done <- true }()
+func (srv Server) appendActor() {
+ defer func() { srv.done <- true }()
for {
select {
- case <-self.quit:
+ case <-srv.quit:
return
- case value := <-self.appendData:
- var err error
- for tryNum := 0; tryNum < 5; tryNum++ {
- err = self.store.Append(value)
- if err != nil {
- time.Sleep(1 * time.Second)
- } else {
- break
- }
- }
- if err != nil {
- s5l.Printf("failed to store data: %v\n", err)
- }
- case token := <-self.appendManyData:
- err := self.store.AppendMany(token.data)
- if err != nil {
- s5l.Printf("failed to store many data: %v\n", err)
- token.response <- false
- } else {
- token.response <- true
- }
- case token := <-self.getStatsChan:
- stats, err := self.store.GetStats(token.filter)
- token.response <- queryStatsResult{stats, err}
- case token := <-self.getUpdatesAfterChan:
- values, err := self.store.GetUpdatesAfter(token.id)
- token.response <- getUpdatesResult{values, err}
- case token := <-self.getUpdatesChan:
- values, err := self.store.GetUpdates(token.filter)
+ case token := <-srv.appendChan:
+ token.response <- srv.store.Append(token.data)
+ case token := <-srv.appendManyChan:
+ token.response <- srv.store.AppendMany(token.data)
+ case token := <-srv.getUpdatesAfterChan:
+ values, err := srv.store.GetUpdatesAfter(token.id, token.limit)
token.response <- getUpdatesResult{values, err}
- case token := <-self.getHubIdChan:
- storeId, err := self.store.GetStoreId()
- token.response <- getHubIdResult{storeId, err}
- case token := <-self.getLastUpdateIdChan:
- lastUpdateId, err := self.store.GetLastUpdateId()
- if lastUpdateId != nil {
- token.response <- getLastUpdateIdResult{*lastUpdateId, err}
- } else {
- token.response <- getLastUpdateIdResult{0, err}
- }
+ case token := <-srv.getHubIdChan:
+ token.response <- getHubIdResult{srv.store.GetStoreId()}
+ case token := <-srv.getLastUpdateIdChan:
+ lastUpdateId, err := srv.store.GetLastUpdateId()
+ token.response <- getLastUpdateIdResult{lastUpdateId, err}
}
}
}
-func (self StatsSinkServer) getUpdatesAfterInvoke(id int) ([]StatisticsData, error) {
- token := getUpdatesAfterToken{id: id, response: make(chan getUpdatesResult, 1)}
+func (srv Server) Append(data DataUpdateFull) error {
+ token := appendToken{data: data, response: make(chan error, 1)}
defer close(token.response)
- self.getUpdatesAfterChan <- token
- res := <-token.response
- return res.values, res.err
+ srv.appendChan <- token
+ return <-token.response
}
-func (self StatsSinkServer) getUpdatesInvoke(filter *StatsFilter) ([]StatisticsData, error) {
- token := getUpdatesToken{filter: filter, response: make(chan getUpdatesResult, 1)}
+func (srv Server) AppendMany(data []DataUpdateFull) error {
+ token := appendManyToken{data: data, response: make(chan error, 1)}
defer close(token.response)
- self.getUpdatesChan <- token
- res := <-token.response
- return res.values, res.err
+ srv.appendManyChan <- token
+ return <-token.response
}
-func (self StatsSinkServer) getStatsInvoke(filter *StatsFilter) (StatsResult, error) {
- token := queryStatsToken{filter: filter, response: make(chan queryStatsResult, 1)}
+func (srv Server) GetUpdatesAfter(id, limit int) ([]DataUpdateFull, error) {
+ token := getUpdatesAfterToken{id: id, limit: limit, response: make(chan getUpdatesResult, 1)}
defer close(token.response)
- self.getStatsChan <- token
+ srv.getUpdatesAfterChan <- token
res := <-token.response
- return res.stats, res.err
+ return res.values, res.err
}
-func (self StatsSinkServer) getHubIdInvoke() (string, error) {
+func (srv Server) GetHubId() string {
token := getHubIdToken{response: make(chan getHubIdResult, 1)}
defer close(token.response)
- self.getHubIdChan <- token
+ srv.getHubIdChan <- token
res := <-token.response
- return res.id, res.err
+ return res.id
}
-func (self StatsSinkServer) getLastUpdateIdInvoke() (int, error) {
+func (srv Server) GetLastUpdateId() (int, error) {
token := getLastUpdateIdToken{response: make(chan getLastUpdateIdResult, 1)}
defer close(token.response)
- self.getLastUpdateIdChan <- token
+ srv.getLastUpdateIdChan <- token
res := <-token.response
return res.id, res.err
}
-func (self StatsSinkServer) Close() {
- self.quit <- true
- <-self.done
- close(self.quit)
- close(self.done)
- close(self.appendData)
- close(self.appendManyData)
- close(self.getStatsChan)
- close(self.getUpdatesAfterChan)
- close(self.getUpdatesChan)
- close(self.getHubIdChan)
- close(self.getLastUpdateIdChan)
- self.store.Close()
+func (srv Server) Close() {
+ s5l.Printf("server: shutting down\n")
+ srv.quit <- true
+ <-srv.done
+ close(srv.quit)
+ close(srv.done)
+ close(srv.appendChan)
+ close(srv.appendManyChan)
+ close(srv.getUpdatesAfterChan)
+ close(srv.getHubIdChan)
+ close(srv.getLastUpdateIdChan)
+ srv.store.Close()
+ s5l.Printf("server: finished\n")
}
-func NewServer(mysql bool, dbPath string) (server *StatsSinkServer, err error) {
+func NewServer(dbPath string, readOnly bool) (server *Server, err error) {
// TODO read configuration and create instance with correct settings
- server = new(StatsSinkServer)
- server.store, err = NewStore(mysql, dbPath)
+ server = &Server{}
+ server.store, err = NewStore(dbPath, readOnly)
if err != nil {
return
}
server.quit = make(chan bool)
server.done = make(chan bool)
- server.appendData = make(chan StatisticsData, 5)
- server.appendManyData = make(chan appendManyToken, 5)
- server.getStatsChan = make(chan queryStatsToken, 5)
- server.getUpdatesAfterChan = make(chan getUpdatesAfterToken, 1)
- server.getUpdatesChan = make(chan getUpdatesToken, 3)
- server.getHubIdChan = make(chan getHubIdToken, 1)
- server.getLastUpdateIdChan = make(chan getLastUpdateIdToken, 1)
+ server.appendChan = make(chan appendToken, 32)
+ server.appendManyChan = make(chan appendManyToken, 32)
+ server.getUpdatesAfterChan = make(chan getUpdatesAfterToken, 32)
+ server.getHubIdChan = make(chan getHubIdToken, 32)
+ server.getLastUpdateIdChan = make(chan getLastUpdateIdToken, 32)
go server.appendActor()
+ s5l.Printf("server: started\n")
return
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvForward.go b/src/hub/src/spreadspace.org/sfive/s5srvForward.go
index a072b2a..d6874de 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvForward.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvForward.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -9,24 +41,19 @@ import (
"time"
)
-func findMaxId(values []StatisticsData) int {
+func findMaxId(values []DataUpdateFull) int {
maxId := -1
for i := range values {
id := values[i].SourceHubDataUpdateId
- if id != nil && *id > maxId {
- maxId = *id
+ if id > maxId {
+ maxId = id
}
}
return maxId
}
-func (self StatsSinkServer) getLastUpdate(baseurl string, client *http.Client) (latestId int, storeId string, err error) {
- storeId, err = self.getHubIdInvoke()
-
- if err != nil {
- s5l.Printf("fwd: failed to get own hubid: %v\n", err)
- return
- }
+func (srv Server) forwardGetLastUpdate(baseurl string, client *http.Client) (latestId int, storeId string, err error) {
+ storeId = srv.GetHubId()
var resp *http.Response
resp, err = client.Get(baseurl + "/lastupdate/" + storeId)
@@ -63,11 +90,11 @@ func (self StatsSinkServer) getLastUpdate(baseurl string, client *http.Client) (
return
}
-func (self StatsSinkServer) handleForwarding(baseurl string, client *http.Client) {
+func (srv Server) forwardRun(baseurl string, client *http.Client) {
url := baseurl + "/updates"
tryResync:
for {
- lastId, _, err := self.getLastUpdate(baseurl, client)
+ lastId, _, err := srv.forwardGetLastUpdate(baseurl, client)
if err != nil {
s5l.Printf("fwd: lastupdate returned err: %v", err)
@@ -79,7 +106,7 @@ tryResync:
nextBatch:
for {
- updates, err := self.getUpdatesAfterInvoke(lastId)
+ updates, err := srv.GetUpdatesAfter(lastId, 5000)
if err != nil {
s5l.Printf("fwd: failed reading updates: %v\n", err)
time.Sleep(500 * time.Millisecond)
@@ -93,7 +120,7 @@ tryResync:
continue nextBatch
}
- data, err := json.Marshal(StatisticsDataContainer{updates})
+ data, err := json.Marshal(DataUpdateFullContainer{updates})
if err != nil {
s5l.Panicf("fwd: encode failed: %v\n", err)
@@ -116,11 +143,10 @@ tryResync:
s5l.Printf("fwd: post OK")
lastId = findMaxId(updates)
s5l.Printf("fwd: new lastid: %d", lastId)
- time.Sleep(1 * time.Second)
}
}
}
-func (self StatsSinkServer) RunForwarding(forwardBaseUrl string) {
- self.handleForwarding(forwardBaseUrl, http.DefaultClient)
+func (srv Server) RunForwarding(forwardBaseUrl string) {
+ srv.forwardRun(forwardBaseUrl, http.DefaultClient)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvForwardEs.go b/src/hub/src/spreadspace.org/sfive/s5srvForwardEs.go
index 19abb1e..8880b8a 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvForwardEs.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvForwardEs.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -10,23 +42,18 @@ import (
"time"
)
-const lastUpdateJson = `{
+const forwardEsLastUpdateJson = `{
"query": {"match": { "SourceHubUuid": "%s" } },
"aggregations": { "last-id" : { "max" : { "field": "SourceHubDataUpdateId" } } }
}`
-func (self StatsSinkServer) getLastUpdateEs(baseurl string, client *http.Client) (latestId int, storeId string, err error) {
+func (srv Server) forwardEsGetLastUpdate(baseurl string, client *http.Client) (latestId int, storeId string, err error) {
url := baseurl + "/dataupdate/_search?search_type=count"
- storeId, err = self.getHubIdInvoke()
-
- if err != nil {
- s5l.Printf("fwd-es: failed to get own hubid: %v\n", err)
- return
- }
+ storeId = srv.GetHubId()
- queryJson := fmt.Sprintf(lastUpdateJson, storeId)
- s5tl.Printf("fwd-es: query: %s", queryJson)
+ queryJson := fmt.Sprintf(forwardEsLastUpdateJson, storeId)
+ s5dl.Printf("fwd-es: query: %s", queryJson)
var resp *http.Response
resp, err = client.Post(url, "application/json", strings.NewReader(queryJson))
@@ -48,7 +75,7 @@ func (self StatsSinkServer) getLastUpdateEs(baseurl string, client *http.Client)
return
}
- s5tl.Printf("fwd-es: lastupdate response: %s\n", body)
+ s5dl.Printf("fwd-es: lastupdate response: %s\n", body)
if len(body) == 0 {
latestId = -1
@@ -73,11 +100,11 @@ func (self StatsSinkServer) getLastUpdateEs(baseurl string, client *http.Client)
return
}
-func (self StatsSinkServer) handleForwardingToElasticSearch(baseurl string, client *http.Client) {
+func (srv Server) forwardEsRun(baseurl string, client *http.Client) {
url := baseurl + "/_bulk"
tryResync:
for {
- lastId, _, err := self.getLastUpdateEs(baseurl, client)
+ lastId, _, err := srv.forwardEsGetLastUpdate(baseurl, client)
if err != nil {
s5l.Printf("fwd-es: lastupdate returned err: %v", err)
time.Sleep(5 * time.Second)
@@ -87,7 +114,7 @@ tryResync:
nextBatch:
for {
- updates, err := self.getUpdatesAfterInvoke(lastId)
+ updates, err := srv.GetUpdatesAfter(lastId, 5000)
if err != nil {
s5l.Printf("fwd-es: failed reading updates: %v\n", err)
time.Sleep(500 * time.Millisecond)
@@ -114,8 +141,6 @@ tryResync:
postData.WriteRune('\n')
}
- //s5tl.Printf("fwd-es: marshalled:\n%v\n", (string)(postData.Bytes()))
-
s5l.Printf("fwd-es: marshal OK")
resp, err := client.Post(url, "application/json", bytes.NewReader(postData.Bytes()))
@@ -136,11 +161,10 @@ tryResync:
s5l.Printf("fwd-es: all posts OK")
lastId = findMaxId(updates)
s5l.Printf("fwd-es: new lastid: %d", lastId)
- //time.Sleep(1 * time.Second)
}
}
}
-func (self StatsSinkServer) RunForwardingToElasticSearch(forwardBaseUrl string) {
- self.handleForwardingToElasticSearch(forwardBaseUrl, http.DefaultClient)
+func (srv Server) RunForwardingEs(forwardBaseUrl string) {
+ srv.forwardEsRun(forwardBaseUrl, http.DefaultClient)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvForwardGraphite.go b/src/hub/src/spreadspace.org/sfive/s5srvForwardGraphite.go
index 9779960..2c0043b 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvForwardGraphite.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvForwardGraphite.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -7,8 +39,8 @@ import (
"github.com/equinox0815/graphite-golang"
)
-func (self StatsSinkServer) getLastUpdateGraphite(conn *graphite.Graphite) (latestId int, storeId string, err error) {
- latestId, err = self.getLastUpdateIdInvoke()
+func (srv Server) forwardGraphiteGetLastUpdate(conn *graphite.Graphite) (latestId int, storeId string, err error) {
+ latestId, err = srv.GetLastUpdateId()
if err != nil {
s5l.Printf("fwd-graphite: failed to get own hubid: %v\n", err)
return
@@ -17,7 +49,7 @@ func (self StatsSinkServer) getLastUpdateGraphite(conn *graphite.Graphite) (late
return
}
-func (self StatsSinkServer) handleForwardingToGraphite(forwardHost string, basePath string) {
+func (srv Server) forwardGraphiteRun(forwardHost string, basePath string) {
tryResync:
for {
client, err := graphite.NewGraphiteFromAddress(forwardHost)
@@ -27,7 +59,7 @@ tryResync:
continue tryResync
}
- lastId, _, err := self.getLastUpdateGraphite(client)
+ lastId, _, err := srv.forwardGraphiteGetLastUpdate(client)
if err != nil {
s5l.Printf("fwd-graphite: lastupdate returned err: %v", err)
client.Disconnect()
@@ -38,7 +70,7 @@ tryResync:
nextBatch:
for {
- updates, err := self.getUpdatesAfterInvoke(lastId)
+ updates, err := srv.GetUpdatesAfter(lastId, 5000)
if err != nil {
s5l.Printf("fwd-graphite: failed reading updates: %v\n", err)
time.Sleep(500 * time.Millisecond)
@@ -75,11 +107,10 @@ tryResync:
s5l.Printf("fwd-graphite: all metrics sent")
lastId = findMaxId(updates)
s5l.Printf("fwd-graphite: new lastid: %d", lastId)
- //time.Sleep(1 * time.Second)
}
}
}
-func (self StatsSinkServer) RunForwardingToGraphite(forwardHost string, basePath string) {
- self.handleForwardingToGraphite(forwardHost, basePath)
+func (srv Server) RunForwardingGraphite(forwardHost string, basePath string) {
+ srv.forwardGraphiteRun(forwardHost, basePath)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvForwardPiwik.go b/src/hub/src/spreadspace.org/sfive/s5srvForwardPiwik.go
index 5a25622..3d44500 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvForwardPiwik.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvForwardPiwik.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -12,15 +44,15 @@ import (
"time"
)
-type PiwikBulkRequest struct {
+type forwardPiwikBulkRequest struct {
Requests []string `json:"requests"`
TokenAuth string `json:"token_auth"`
}
-func (self StatsSinkServer) getLastUpdatePiwik(piwikURL, siteURL string, siteID uint, token string, client *http.Client) (latestId int, storeId string, err error) {
+func (srv Server) forwardPiwikGetLastUpdate(piwikURL, siteURL string, siteID uint, token string, client *http.Client) (latestId int, storeId string, err error) {
// TODO: ask piwik what the last update was...
- latestId, err = 0, nil //self.getLastUpdateIdInvoke()
+ latestId, err = 0, nil //srv.getLastUpdateIdInvoke()
if err != nil {
s5l.Printf("fwd-piwik: failed to get own hubid: %v\n", err)
return
@@ -29,10 +61,10 @@ func (self StatsSinkServer) getLastUpdatePiwik(piwikURL, siteURL string, siteID
return
}
-func (self StatsSinkServer) handleForwardingToPiwik(piwikURL, siteURL string, siteID uint, token string, client *http.Client) {
+func (srv Server) forwardPiwikRun(piwikURL, siteURL string, siteID uint, token string, client *http.Client) {
tryResync:
for {
- lastId, _, err := self.getLastUpdatePiwik(piwikURL, siteURL, siteID, token, client)
+ lastId, _, err := srv.forwardPiwikGetLastUpdate(piwikURL, siteURL, siteID, token, client)
if err != nil {
s5l.Printf("fwd-piwik: lastupdate returned err: %v", err)
time.Sleep(5 * time.Second)
@@ -42,7 +74,7 @@ tryResync:
nextBatch:
for {
- updates, err := self.getUpdatesAfterInvoke(lastId)
+ updates, err := srv.GetUpdatesAfter(lastId, 5000)
if err != nil {
s5l.Printf("fwd-piwik: failed reading updates: %v\n", err)
time.Sleep(500 * time.Millisecond)
@@ -56,7 +88,7 @@ tryResync:
continue nextBatch
}
- req := PiwikBulkRequest{TokenAuth: token}
+ req := forwardPiwikBulkRequest{TokenAuth: token}
for _, update := range updates {
if len(update.Data.Clients) == 0 {
continue
@@ -101,11 +133,10 @@ tryResync:
s5l.Printf("fwd-piwik: all posts OK")
lastId = findMaxId(updates)
s5l.Printf("fwd-piwik: new lastid: %d", lastId)
- //time.Sleep(1 * time.Second)
}
}
}
-func (self StatsSinkServer) RunForwardingToPiwik(piwikURL, siteURL string, siteID uint, piwikToken string) {
- self.handleForwardingToPiwik(piwikURL, siteURL, siteID, piwikToken, http.DefaultClient)
+func (srv Server) RunForwardingPiwik(piwikURL, siteURL string, siteID uint, piwikToken string) {
+ srv.forwardPiwikRun(piwikURL, siteURL, siteID, piwikToken, http.DefaultClient)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvPipe.go b/src/hub/src/spreadspace.org/sfive/s5srvPipe.go
index 8084461..4b48d99 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvPipe.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvPipe.go
@@ -1,15 +1,50 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
"bufio"
+ "io"
"net"
)
-func (self StatsSinkServer) handleConnection(conn net.Conn) {
+func (srv Server) pipeHandle(conn net.Conn) {
reader := bufio.NewReader(conn)
buffer, err := reader.ReadBytes('\n')
if err != nil {
- s5l.Printf("pipe: failed to read from connection: %v\n", err)
+ if err != io.EOF {
+ s5l.Printf("pipe: failed to read from connection: %v\n", err)
+ }
return
}
marshaller, err := NewStatefulDecoder(buffer)
@@ -21,42 +56,47 @@ func (self StatsSinkServer) handleConnection(conn net.Conn) {
for {
buffer, err := reader.ReadBytes('\n')
if err != nil {
- s5l.Printf("pipe: failed to read from connection: %v\n", err)
+ if err != io.EOF {
+ s5l.Printf("pipe: failed to read from connection: %v\n", err)
+ }
return
}
- // s5l.Printf("msg: %v", string(buffer))
-
value, err := marshaller.Decode(buffer)
if err != nil {
s5l.Printf("pipe: failed to decode message: %v\n", err)
continue
}
- self.appendData <- value
+ if err = srv.Append(value); err != nil {
+ s5l.Printf("pipe: failed to store data: %v\n", err)
+ }
}
}
-func (self StatsSinkServer) handlePacketConn(pconn net.PacketConn) {
- decoder := NewPlainDecoder()
+func (srv Server) pipegramHandle(pconn net.PacketConn) {
+ decoder := NewStatelessDecoder()
buffer := make([]byte, 64*1024)
for {
n, _, err := pconn.ReadFrom(buffer)
if err != nil {
- s5l.Printf("p-pipe: failed read: %v", err)
+ s5l.Printf("pipegram: failed read: %v", err)
continue
}
data := buffer[0:n]
value, err := decoder.Decode(data)
- if err == nil {
- self.appendData <- value
- } else {
- s5l.Printf("p-pipe: failed to decode message: %v\n", err)
+ if err != nil {
+ s5l.Printf("pipegram: failed to decode message: %v\n", err)
+ continue
+ }
+
+ if err = srv.Append(value); err != nil {
+ s5l.Printf("pipegram: failed to store data: %v\n", err)
}
}
}
-func (self StatsSinkServer) ServePipe(pipePath string) {
+func (srv Server) ServePipe(pipePath string) {
ln, err := net.Listen("unix", pipePath)
if err != nil {
s5l.Printf("pipe: failed to connect: %v", err)
@@ -71,17 +111,17 @@ func (self StatsSinkServer) ServePipe(pipePath string) {
// ignore
continue
}
- go self.handleConnection(conn)
+ go srv.pipeHandle(conn)
}
}
-func (self StatsSinkServer) ServeGramPipe(pipePath string) {
+func (srv Server) ServePipegram(pipePath string) {
pconn, err := net.ListenPacket("unixgram", pipePath)
if err != nil {
- s5l.Printf("p-pipe: failed to listen: %v", err)
+ s5l.Printf("pipegram: failed to listen: %v", err)
return
}
defer pconn.Close()
- self.handlePacketConn(pconn)
+ srv.pipegramHandle(pconn)
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5srvWeb.go b/src/hub/src/spreadspace.org/sfive/s5srvWeb.go
index af9e986..80af8c5 100644
--- a/src/hub/src/spreadspace.org/sfive/s5srvWeb.go
+++ b/src/hub/src/spreadspace.org/sfive/s5srvWeb.go
@@ -1,3 +1,35 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
@@ -5,26 +37,25 @@ import (
"fmt"
"io/ioutil"
"net/http"
- "os"
"strconv"
- "time"
"github.com/zenazn/goji"
"github.com/zenazn/goji/web"
)
-func hello(c web.C, w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Hello, %s!", c.URLParams["name"])
+func (srv Server) webHealthz(c web.C, w http.ResponseWriter, r *http.Request) {
+ // TODO: do a more sophisticated check
+ fmt.Fprintf(w, "%s\n", srv.GetHubId())
}
-func (self StatsSinkServer) getTagList(c web.C, w http.ResponseWriter, r *http.Request) {
- const resourceName = "tags"
- values, err := self.store.GetTags()
+func (srv Server) webGetHubsList(c web.C, w http.ResponseWriter, r *http.Request) {
+ const resourceName = "hubs"
+ values, err := srv.store.GetHubs()
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
return
}
- jsonString, err := json.Marshal(DataContainer{values})
+ jsonString, err := json.Marshal(GenericDataContainer{values})
if err != nil {
http.Error(w, fmt.Sprintf("failed to marshal %s: %v", resourceName, err), http.StatusInternalServerError)
return
@@ -32,14 +63,14 @@ func (self StatsSinkServer) getTagList(c web.C, w http.ResponseWriter, r *http.R
fmt.Fprintf(w, "%s", jsonString)
}
-func (self StatsSinkServer) getSourcesList(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webGetSourcesList(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "sources"
- values, err := self.store.GetSources()
+ values, err := srv.store.GetSources()
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
return
}
- jsonString, err := json.Marshal(DataContainer{values})
+ jsonString, err := json.Marshal(GenericDataContainer{values})
if err != nil {
http.Error(w, fmt.Sprintf("failed to marshal %s: %v", resourceName, err), http.StatusInternalServerError)
return
@@ -47,16 +78,20 @@ func (self StatsSinkServer) getSourcesList(c web.C, w http.ResponseWriter, r *ht
fmt.Fprintf(w, "%s", jsonString)
}
-func (self StatsSinkServer) getSource(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webGetSource(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "source"
id, err := strconv.ParseInt(c.URLParams["id"], 10, 64)
if err != nil {
http.Error(w, fmt.Sprintf("invalid id: %s: %v", resourceName, err), http.StatusBadRequest)
return
}
- value, err := self.store.GetSource(int(id))
+ value, err := srv.store.GetSource(int(id))
if err != nil {
- http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
+ if err == ErrNotFound {
+ http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusNotFound)
+ } else {
+ http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
+ }
return
}
jsonString, err := json.Marshal(value)
@@ -67,78 +102,14 @@ func (self StatsSinkServer) getSource(c web.C, w http.ResponseWriter, r *http.Re
fmt.Fprintf(w, "%s", jsonString)
}
-func getFilter(r *http.Request) (filter StatsFilter) {
- from := r.FormValue("from")
- if from != "" {
- fromT, err := time.Parse(time.RFC3339, from)
- if err == nil {
- filter.start = &fromT
- }
- }
-
- to := r.FormValue("to")
- if to != "" {
- toT, err := time.Parse(time.RFC3339, to)
- if err == nil {
- filter.end = &toT
- }
- }
-
- hostname := r.FormValue("hostname")
- if hostname != "" {
- filter.hostname = &hostname
- }
-
- contentId := r.FormValue("contentId")
- if contentId != "" {
- filter.contentId = &contentId
- }
-
- format := r.FormValue("format")
- if format != "" {
- filter.format = &format
- }
-
- quality := r.FormValue("quality")
- if quality != "" {
- filter.quality = &quality
- }
-
- afterUpdateId := r.FormValue("afterUpdateId")
- if afterUpdateId != "" {
- id, err := strconv.ParseInt(afterUpdateId, 10, 32)
- if err == nil {
- idInt := int(id)
- filter.afterUpdateId = &idInt
- }
- }
-
- limit := r.FormValue("limit")
- if limit != "" {
- limitInt, err := strconv.ParseInt(limit, 10, 32)
- if err == nil {
- limitIntInt := int(limitInt)
- filter.limit = &limitIntInt
- }
- }
-
- sortOrder := r.FormValue("sortOrder")
- if sortOrder != "" {
- filter.sortOrder = &sortOrder
- }
-
- return
-}
-
-func (self StatsSinkServer) getUpdateList(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webGetUpdateList(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "updates"
- filter := getFilter(r)
- values, err := self.getUpdatesInvoke(&filter)
+ values, err := srv.GetUpdatesAfter(-1, 3) // TODO: get start and limit from param
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
return
}
- jsonString, err := json.Marshal(DataContainer{values})
+ jsonString, err := json.Marshal(GenericDataContainer{values})
if err != nil {
http.Error(w, fmt.Sprintf("failed to marshal %s: %v", resourceName, err), http.StatusInternalServerError)
return
@@ -146,16 +117,20 @@ func (self StatsSinkServer) getUpdateList(c web.C, w http.ResponseWriter, r *htt
fmt.Fprintf(w, "%s", jsonString)
}
-func (self StatsSinkServer) getUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webGetUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "update"
id, err := strconv.ParseInt(c.URLParams["id"], 10, 64)
if err != nil {
http.Error(w, fmt.Sprintf("invalid id: %s: %v", resourceName, err), http.StatusBadRequest)
return
}
- value, err := self.store.GetUpdate(int(id))
+ value, err := srv.store.GetUpdate(int(id))
if err != nil {
- http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
+ if err == ErrNotFound {
+ http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusNotFound)
+ } else {
+ http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
+ }
return
}
jsonString, err := json.Marshal(value)
@@ -166,9 +141,9 @@ func (self StatsSinkServer) getUpdate(c web.C, w http.ResponseWriter, r *http.Re
fmt.Fprintf(w, "%s", jsonString)
}
-func (self StatsSinkServer) postUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webPostUpdate(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "update"
- decoder := NewPlainDecoder()
+ decoder := NewStatelessDecoder()
buffer, err := ioutil.ReadAll(r.Body)
if err != nil {
@@ -177,17 +152,14 @@ func (self StatsSinkServer) postUpdate(c web.C, w http.ResponseWriter, r *http.R
return
}
- container := StatisticsDataContainer{}
+ // TODO: add different API endpoint called bulk
+ container := DataUpdateFullContainer{}
err = json.Unmarshal(buffer, &container)
if err == nil {
- token := appendManyToken{
- data: container.Data,
- response: make(chan bool, 2)}
- defer close(token.response)
- self.appendManyData <- token
- success := <-token.response
- if !success {
- http.Error(w, "failed to store data", http.StatusInternalServerError)
+ if err = srv.AppendMany(container.Data); err != nil {
+ http.Error(w, fmt.Sprintf("failed to store data: %s", err), http.StatusInternalServerError)
+ } else {
+ fmt.Fprintf(w, "%d update(s) successfully stored.", len(container.Data))
}
return
}
@@ -200,59 +172,43 @@ func (self StatsSinkServer) postUpdate(c web.C, w http.ResponseWriter, r *http.R
return
}
- self.appendData <- data
- // TODO send response channel, wait for OK
+ if err = srv.Append(data); err != nil {
+ http.Error(w, fmt.Sprintf("failed to store data: %s", err), http.StatusInternalServerError)
+ } else {
+ fmt.Fprintf(w, "1 update successfully stored.")
+ }
}
-func (self StatsSinkServer) getLastUpdateIdForUuid(c web.C, w http.ResponseWriter, r *http.Request) {
+func (srv Server) webGetLastUpdateId(c web.C, w http.ResponseWriter, r *http.Request) {
const resourceName = "lastupdateid"
- id := c.URLParams["id"]
- value, err := self.store.GetLastUpdateForUuid(id)
+ value, err := srv.store.GetLastUpdateId()
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
return
}
- if value != nil {
- fmt.Fprintf(w, "%d", *value)
- }
+ fmt.Fprintf(w, "%d", value)
}
-func (self StatsSinkServer) getStats(c web.C, w http.ResponseWriter, r *http.Request) {
- const resourceName = "stats"
- filter := getFilter(r)
- values, err := self.getStatsInvoke(&filter)
-
+func (srv Server) webGetLastUpdateIdForUuid(c web.C, w http.ResponseWriter, r *http.Request) {
+ const resourceName = "lastupdateid"
+ id := c.URLParams["id"]
+ value, err := srv.store.GetLastUpdateForUuid(id)
if err != nil {
http.Error(w, fmt.Sprintf("failed to retrieve %s: %v", resourceName, err), http.StatusInternalServerError)
return
}
- jsonString, err := json.Marshal(values)
- if err != nil {
- http.Error(w, fmt.Sprintf("failed to marshal %s: %v", resourceName, err), http.StatusInternalServerError)
- return
- }
- fmt.Fprintf(w, "%s", jsonString)
+ fmt.Fprintf(w, "%d", value)
}
-func (self StatsSinkServer) ServeWeb(vizAppLocation string) {
- if _, err := os.Stat(vizAppLocation); err != nil {
- if os.IsNotExist(err) {
- s5l.Panicf("web: viz-app at %s does not exist.", vizAppLocation)
- } else {
- s5l.Printf("web: failed to stat %s: %v", vizAppLocation, err)
- }
- }
-
- goji.Get("/hello/:name", hello)
- goji.Get("/tags", self.getTagList)
- goji.Get("/sources", self.getSourcesList)
- goji.Get("/sources/:id", self.getSource)
- goji.Get("/updates", self.getUpdateList)
- goji.Get("/updates/:id", self.getUpdate)
- goji.Post("/updates", self.postUpdate)
- goji.Get("/lastupdate/:id", self.getLastUpdateIdForUuid)
- goji.Get("/stats", self.getStats)
- goji.Handle("/viz/*", http.StripPrefix("/viz/", http.FileServer(http.Dir(vizAppLocation))))
-
+func (srv Server) ServeWeb() {
+ goji.Get("/healthz", srv.webHealthz)
+ goji.Get("/hubs", srv.webGetHubsList)
+ goji.Get("/sources", srv.webGetSourcesList)
+ goji.Get("/sources/:id", srv.webGetSource)
+ goji.Get("/updates", srv.webGetUpdateList)
+ goji.Get("/updates/:id", srv.webGetUpdate)
+ goji.Post("/updates", srv.webPostUpdate)
+ goji.Get("/lastupdate", srv.webGetLastUpdateId)
+ goji.Get("/lastupdate/:id", srv.webGetLastUpdateIdForUuid)
goji.Serve()
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5store.go b/src/hub/src/spreadspace.org/sfive/s5store.go
index 6fb5f8a..a59166f 100644
--- a/src/hub/src/spreadspace.org/sfive/s5store.go
+++ b/src/hub/src/spreadspace.org/sfive/s5store.go
@@ -1,643 +1,562 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
- "database/sql"
+ "encoding/json"
+ "errors"
"fmt"
+ "os"
"time"
- _ "github.com/mattn/go-sqlite3"
+ "github.com/boltdb/bolt"
"github.com/pborman/uuid"
- "gopkg.in/gorp.v2"
)
-type sqliteStore struct {
- db *gorp.DbMap
- hubId string
-}
-
-func tagsFromStatisticsData(value StatisticsData) []tagDb {
- tags := make([]tagDb, len(value.SourceId.Tags))
- for i := range value.SourceId.Tags {
- tags[i] = tagDb{Id: -1, Name: value.SourceId.Tags[i]}
- }
- return tags
-}
-
-func sourceFromStatisticsData(value StatisticsData) sourceDb {
- return sourceDb{
- -1,
- StreamId{
- ContentId: value.SourceId.StreamId.ContentId,
- Format: value.SourceId.StreamId.Format,
- Quality: value.SourceId.StreamId.Quality,
- },
- SourceId{
- Hostname: value.SourceId.Hostname},
- }
-}
-
-func clientsFromStatisticsData(value StatisticsData) []clientDataDb {
- res := make([]clientDataDb, len(value.Data.Clients))
- for i := range value.Data.Clients {
- res[i] = clientDataDb{-1, -1, value.Data.Clients[i]}
- }
- return res
-}
-
-func dataUpdateFromStatisticsData(value StatisticsData) dataUpdateDb {
- return dataUpdateDb{
- -1,
- -1,
- value.SourceHubUuid,
- value.SourceHubDataUpdateId,
- value.StartTime.Unix(),
- value.Duration,
- value.Data.ClientCount,
- value.Data.BytesReceived,
- value.Data.BytesSent}
-}
+const (
+ StoreGetUpdatesLimit = 42000
+ StoreVersion = 1
+)
-func updateFromStatisticsData(value StatisticsData) (dataUpdateDb, []clientDataDb, sourceDb, []tagDb) {
- du := dataUpdateFromStatisticsData(value)
- cd := clientsFromStatisticsData(value)
- src := sourceFromStatisticsData(value)
- tags := tagsFromStatisticsData(value)
+var (
+ storeBuckets = []string{latestUpdatesBn, hubUuidsFwdBn, hubUuidsRevBn, dataUpdatesBn,
+ sourcesFwdBn, sourcesRevBn, clientDataBn, userAgentsFwdBn, userAgentsRevBn}
+)
- return du, cd, src, tags
+type Store struct {
+ version int
+ hubUuid string
+ db *bolt.DB
+ readOnly bool
}
-func initDb(mysql bool, path string) (res *gorp.DbMap, hubId string, err error) {
- // connect to db using standard Go database/sql API
- var db *sql.DB
- var dialect gorp.Dialect
+//
+// Initialization and Destruction
+//
- if mysql {
- db, err = sql.Open("mysql", path)
- if err != nil {
- return
+func openDb(dbPath string, readOnly bool) (db *bolt.DB, version int, hubUuid string, err error) {
+ if _, err = os.Stat(dbPath); err != nil {
+ if os.IsNotExist(err) {
+ err = nil
}
- dialect = gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"}
- } else {
- db, err = sql.Open("sqlite3", path)
- if err != nil {
- return
- }
- dialect = gorp.SqliteDialect{}
+ return
}
-
- dbmap := &gorp.DbMap{Db: db, Dialect: dialect}
- // dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myapp:", log.Lmicroseconds))
-
- dbmap.AddTableWithName(tagDb{}, tagsTn).SetKeys(true, "Id").ColMap("Name").SetUnique(true)
- dbmap.AddTableWithName(sourceTagsDb{}, sourceTagsTn).SetKeys(false, "TagId", "SourceId")
- dbmap.AddTableWithName(sourceDb{}, sourcesTn).SetKeys(true, "Id").SetUniqueTogether("ContentId", "Format", "Quality", "Hostname")
- dbmap.AddTableWithName(clientDataDb{}, clientdataUpdatesTn).SetKeys(true, "Id")
- dbmap.AddTableWithName(dataUpdateDb{}, dataUpdatesTn).SetKeys(true, "Id")
- dbmap.AddTableWithName(hubInfoDb{}, hubInfoTn).SetKeys(false, "Name")
-
- // TODO use some real migration, yadda yadda
- err = dbmap.CreateTablesIfNotExists()
+ opts := &bolt.Options{Timeout: 100 * time.Millisecond, ReadOnly: readOnly}
+ db, err = bolt.Open(dbPath, 0600, opts)
if err != nil {
return
}
- hubId, err = dbmap.SelectStr("select Value from " + hubInfoTn + " where Name = 'HubUuid'")
- // TODO handle only not-found this way
- if err != nil || hubId == "" {
- hubId = uuid.New()
- _, err = db.Exec("insert into "+hubInfoTn+" values ('HubUuid', ?)", hubId)
- if err != nil {
- hubId = ""
- return
+ err = db.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(hubInfoBn))
+ if b == nil {
+ return errors.New("store: failed to open, bucket '" + hubInfoBn + "'does not exist.")
+ }
+ bVersion := b.Get([]byte(storeVersionKey))
+ if bVersion == nil {
+ return errors.New("store: failed to open, no hub version found.")
+ }
+ version = btoi(bVersion)
+ if version != StoreVersion {
+ return fmt.Errorf("store: failed to open, wrong hub version: %d (expected: %d)", version, StoreVersion)
}
- }
-
- res = dbmap
- return
-}
-func isEmptyFilter(filter *StatsFilter) bool {
- if filter == nil {
- return true
- }
- if filter.start == nil &&
- filter.end == nil &&
- filter.hostname == nil &&
- filter.contentId == nil &&
- filter.format == nil &&
- filter.quality == nil &&
- (filter.tagsAny == nil || len(filter.tagsAny) == 0) &&
- filter.afterUpdateId == nil &&
- filter.limit == nil {
- return true
- }
- return false
-}
+ bHubId := b.Get([]byte(hubUuidKey))
+ if bHubId != nil {
+ hubUuid = string(bHubId)
+ }
+ if hubUuid == "" {
+ return errors.New("store: failed to open, UUID does not exist or is empty.")
+ }
-func insertAnd(needsAnd *bool) (res string) {
- if *needsAnd {
- res = " and"
- *needsAnd = false
+ for _, bn := range storeBuckets {
+ if b := tx.Bucket([]byte(bn)); b == nil {
+ return errors.New("store: failed to open, bucket '" + bn + "' does not exist.")
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ db.Close()
}
return
}
-type dataUpdateQueryResult struct {
- dataUpdateDb
- StreamId
- SourceId
-}
-
-func getFilteredDataUpdateSelect(filter *StatsFilter) (string, map[string]interface{}) {
- const baseQuery = "(select * from " + dataUpdatesTn + "," + sourcesTn + " on " + dataUpdatesTn + ".SourceId = " + sourcesTn + ".Id"
- if isEmptyFilter(filter) {
- return baseQuery + ")", nil
+func createDb(dbPath string) (db *bolt.DB, version int, hubUuid string, err error) {
+ db, err = bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond})
+ if err != nil {
+ return
}
- query := baseQuery
- parameters := make(map[string]interface{})
- needsAnd := false
-
- if filter.start != nil || filter.end != nil || filter.afterUpdateId != nil {
- query += " WHERE"
-
- if filter.start != nil {
- query += insertAnd(&needsAnd)
- query += " StartTime >= :filterstart"
- parameters["filterstart"] = filter.start.Unix()
- needsAnd = true
+ err = db.Update(func(tx *bolt.Tx) error {
+ for _, bn := range storeBuckets {
+ if _, err := tx.CreateBucket([]byte(bn)); err != nil {
+ return err
+ }
}
- if filter.end != nil {
- query += insertAnd(&needsAnd)
- query += " StartTime < :filterend"
- parameters["filterend"] = filter.end.Unix()
- needsAnd = true
+
+ b, err := tx.CreateBucket([]byte(hubInfoBn))
+ if err != nil {
+ return err
}
- if filter.afterUpdateId != nil {
- query += insertAnd(&needsAnd)
- query += " " + dataUpdatesTn + ".Id > :afterUpdateId"
- parameters["afterUpdateId"] = *filter.afterUpdateId
- needsAnd = true
+ version = StoreVersion
+ if err := b.Put([]byte(storeVersionKey), itob(version)); err != nil {
+ return err
}
- }
-
- if filter.sortOrder != nil {
- if *filter.sortOrder == "desc" {
- query += " ORDER BY " + dataUpdatesTn + ".Id DESC"
+ hubUuid = uuid.New()
+ if err := b.Put([]byte(hubUuidKey), []byte(hubUuid)); err != nil {
+ return err
}
+ return nil
+ })
+ if err != nil {
+ db.Close()
}
- if filter.limit != nil {
- query += " LIMIT :limit"
- parameters["limit"] = *filter.limit
- }
-
- // TODO other fields
- query += ")"
- return query, parameters
-}
-func (s sqliteStore) findTag(name string) (tag *tagDb, err error) {
- t := tagDb{}
- err = s.db.SelectOne(&t, "select * from "+tagsTn+" where Name = ?", name)
- if err == nil {
- tag = &t
- }
return
}
-func (s sqliteStore) insertNewTags(tags []tagDb) (err error) {
- for i := range tags {
- var t *tagDb
- t, err = s.findTag(tags[i].Name)
- if err != nil {
- _, err = s.db.Exec("insert into "+tagsTn+" VALUES (NULL, ?)", tags[i].Name)
- }
- t, err = s.findTag(tags[i].Name)
+func NewStore(dbPath string, readOnly bool) (Store, error) {
+ db, version, hubid, err := openDb(dbPath, readOnly)
+ if err != nil {
+ return Store{}, err
+ }
- if err == nil {
- tags[i] = *t
+ if db != nil {
+ if readOnly {
+ s5l.Printf("store: opened read-only (UUID: %s)", hubid)
} else {
- break
+ s5l.Printf("store: opened (UUID: %s)", hubid)
}
+ return Store{version, hubid, db, readOnly}, nil
}
- return
-}
-
-func (s sqliteStore) findSource(src sourceDb) (res *sourceDb, err error) {
- t := sourceDb{}
- err = s.db.SelectOne(
- &t,
- "select Id from "+sourcesTn+" where ContentId = ? and Format = ? and Quality = ? and Hostname = ?",
- src.ContentId,
- src.Format,
- src.Quality,
- src.Hostname)
+ if readOnly {
+ return Store{}, errors.New("store: failed to open, requested read-only mode but store file does not exist.")
+ }
- if err == nil {
- res = &t
+ db, version, hubid, err = createDb(dbPath)
+ if err != nil {
+ return Store{}, err
}
+ s5l.Printf("store: initialized (UUID: %s)", hubid)
+ return Store{version, hubid, db, readOnly}, nil
+}
- return
+func (st Store) Close() {
+ s5l.Printf("store: closing")
+ st.db.Close()
}
-func (s sqliteStore) insertNewSource(src *sourceDb) (err error) {
- var t *sourceDb
- t, err = s.findSource(*src)
- if err == nil {
- *src = *t
- } else {
- err = s.db.Insert(src)
+//
+// Append data
+//
+
+// append key-value pairs to buckets
+
+func (st Store) insertNewHub(tx *bolt.Tx, hubUuid string) (hubId int, err error) {
+ if hubUuid == "" {
+ return
}
- return
-}
+ bf := tx.Bucket([]byte(hubUuidsFwdBn))
+ bf.FillPercent = 1.0 // we only do appends
+ br := tx.Bucket([]byte(hubUuidsRevBn))
+ br.FillPercent = 1.0 // we only do appends
-func (s sqliteStore) insertSourceTagLinks(src sourceDb, tags []tagDb) (err error) {
- st := make([]sourceTagsDb, len(tags))
- for i := range tags {
- st[i].TagId = tags[i].Id
- st[i].SourceId = src.Id
- }
- for i := range st {
- _, err = s.db.Exec(
- "insert or ignore into "+sourceTagsTn+" values (?,?)",
- st[i].TagId,
- st[i].SourceId)
- // err = s.db.Insert(&st[i])
- if err != nil {
- // TODO
- //fmt.Printf("st\n")
- return
- }
+ bHubId := bf.Get([]byte(hubUuid))
+ if bHubId != nil {
+ return btoi(bHubId), nil
}
- return
-}
-func (s sqliteStore) insertDataUpdateEntry(src sourceDb, du *dataUpdateDb) (err error) {
- du.SourceId = src.Id
- err = s.db.Insert(du)
- if err != nil {
- //fmt.Printf("du\n")
+ next, _ := bf.NextSequence()
+ hubId = int(next)
+ if err = bf.Put([]byte(hubUuid), itob(hubId)); err != nil {
return
}
- return
-}
-
-func (s sqliteStore) insertDataUpdateClientEntries(cd []clientDataDb, du dataUpdateDb) (err error) {
- for i := range cd {
- cd[i].DataUpdatesId = du.Id
- err = s.db.Insert(&cd[i])
- if err != nil {
- // TODO
- return
- }
+ if err = br.Put(itob(hubId), []byte(hubUuid)); err != nil {
+ return
}
- return
+
+ return hubId, err
}
-func (s sqliteStore) appendItem(du dataUpdateDb, cd []clientDataDb, src sourceDb, tags []tagDb) (err error) {
- err = s.insertNewTags(tags)
- if err != nil {
- return
+func (st Store) insertNewSource(tx *bolt.Tx, src sourceDb) (srcId int, err error) {
+ bf := tx.Bucket([]byte(sourcesFwdBn))
+ // br.FillPercent = 1.0 // these appends are not ordered (the key is the slug and not an integer id)
+ br := tx.Bucket([]byte(sourcesRevBn))
+ br.FillPercent = 1.0 // we only do appends (with ever incrementing interger ids)
+
+ slug := src.Slug()
+ bSrcId := bf.Get([]byte(slug))
+ if bSrcId != nil {
+ return btoi(bSrcId), nil
}
- err = s.insertNewSource(&src)
- if err != nil {
- //fmt.Printf("src\n")
+ var jsonData []byte
+ if jsonData, err = json.Marshal(src); err != nil {
return
}
- err = s.insertSourceTagLinks(src, tags)
- if err != nil {
+ next, _ := bf.NextSequence()
+ srcId = int(next)
+ if err = bf.Put([]byte(slug), itob(srcId)); err != nil {
return
}
-
- err = s.insertDataUpdateEntry(src, &du)
- if err != nil {
+ if err = br.Put(itob(srcId), jsonData); err != nil {
return
}
- err = s.insertDataUpdateClientEntries(cd, du)
- if err != nil {
+ return srcId, err
+}
+
+func (st Store) insertDataUpdate(tx *bolt.Tx, du dataUpdateDb) (duId int, err error) {
+ b := tx.Bucket([]byte(dataUpdatesBn))
+ b.FillPercent = 1.0 // we only do appends
+
+ next, _ := b.NextSequence()
+ duId = int(next)
+
+ var jsonData []byte
+ if jsonData, err = json.Marshal(du); err != nil {
return
}
-
+ err = b.Put(itob(duId), jsonData)
return
}
-// this function is the biggest pile of copy/pasted crap while sick that is still compilable.
-func (s sqliteStore) Append(update StatisticsData) (err error) {
- var tx *gorp.Transaction
- tx, err = s.db.Begin()
- if err != nil {
- return
+func (st Store) insertNewUserAgent(tx *bolt.Tx, ua string) (uaId int, err error) {
+ bf := tx.Bucket([]byte(userAgentsFwdBn))
+ bf.FillPercent = 1.0 // we only do appends
+ br := tx.Bucket([]byte(userAgentsRevBn))
+ br.FillPercent = 1.0 // we only do appends
+
+ bUaId := bf.Get([]byte(ua))
+ if bUaId != nil {
+ return btoi(bUaId), nil
}
- du, cd, src, tags := updateFromStatisticsData(update)
- //s5l.Printf("blah: %v", du)
- err = s.appendItem(du, cd, src, tags)
- if err != nil {
- tx.Rollback()
+ next, _ := bf.NextSequence()
+ uaId = int(next)
+ if err = bf.Put([]byte(ua), itob(uaId)); err != nil {
+ return
+ }
+ if err = br.Put(itob(uaId), []byte(ua)); err != nil {
return
}
- return tx.Commit()
+ return uaId, err
}
-func (s sqliteStore) AppendMany(updates []StatisticsData) (err error) {
- var tx *gorp.Transaction
- tx, err = s.db.Begin()
- if err != nil {
- return
+func (st Store) insertClientData(tx *bolt.Tx, duId int, cd []ClientData) error {
+ if len(cd) == 0 {
+ return nil
}
- for _, update := range updates {
- du, cd, src, tags := updateFromStatisticsData(update)
- err = s.appendItem(du, cd, src, tags)
+ b := tx.Bucket([]byte(clientDataBn))
+ b.FillPercent = 1.0 // we only do appends
+
+ data := []clientDataDb{}
+ for _, c := range cd {
+ uaId, err := st.insertNewUserAgent(tx, c.UserAgent)
if err != nil {
- tx.Rollback()
- return
+ return err
}
+ data = append(data, clientDataDb{c.Ip, uaId, c.BytesSent})
}
- return tx.Commit()
+ jsonData, err := json.Marshal(data)
+ if err != nil {
+ return err
+ }
+ return b.Put(itob(duId), jsonData)
}
-func castArrayToString(value []interface{}) []string {
- res := make([]string, len(value))
- for i := range value {
- res[i] = value[i].(string)
+func (st Store) setLastUpdateForUuid(tx *bolt.Tx, uuid string, duId int) error {
+ b := tx.Bucket([]byte(latestUpdatesBn))
+ b.FillPercent = 1.0 // we only do appends
+
+ last := b.Get([]byte(uuid))
+ if last != nil && btoi(last) > duId {
+ return nil
}
- return res
+ return b.Put([]byte(uuid), itob(duId))
}
-func (s sqliteStore) GetTags() ([]string, error) {
- res, dbErr := s.db.Select("", "select Name from "+tagsTn)
- if dbErr == nil {
- sRes := castArrayToString(res)
- return sRes, nil
+// Split up the multidimensional dataupdate and append all the key-value pairs
+
+func (st Store) appendItem(tx *bolt.Tx, update DataUpdateFull) (duId int, err error) {
+ du := NewDataUpdateDb(update)
+
+ srcUuid := update.SourceHubUuid
+ if du.SourceHubId, err = st.insertNewHub(tx, srcUuid); err != nil {
+ return
+ }
+ if du.SourceId, err = st.insertNewSource(tx, NewSourceDb(update)); err != nil {
+ return
+ }
+ if duId, err = st.insertDataUpdate(tx, du); err != nil {
+ return
+ }
+ if err = st.insertClientData(tx, duId, update.Data.Clients); err != nil {
+ return
}
- return nil, dbErr
-}
-func (s sqliteStore) GetTagsByDataUpdateId(id int) (res []string, err error) {
- var qres []interface{}
- qres, err = s.db.Select(
- tagDb{},
- "select * from "+tagsTn+" where Id in (select TagId from "+sourceTagsTn+" where SourceId = (select SourceId from "+dataUpdatesTn+" where Id = ?))", id)
- if err == nil {
- res = make([]string, len(qres))
- for i := range qres {
- res[i] = qres[i].(*tagDb).Name
- }
+ if srcUuid != "" {
+ err = st.setLastUpdateForUuid(tx, srcUuid, du.SourceHubDataUpdateId)
+ }
+ if fwdUuid := update.ForwardHubUuid; fwdUuid != "" {
+ err = st.setLastUpdateForUuid(tx, fwdUuid, update.ForwardHubDataUpdateId)
}
return
}
-func (s sqliteStore) GetSources() (res []sourceDb, err error) {
- var qres []interface{}
- qres, err = s.db.Select(sourceDb{}, "select * from "+sourcesTn)
- if err == nil {
- res = make([]sourceDb, len(qres))
- for i := range qres {
- res[i] = *qres[i].(*sourceDb)
- }
+// Public Append Interface
+
+func (st Store) AppendMany(updates []DataUpdateFull) (err error) {
+ if st.readOnly {
+ return ErrReadOnly
}
- return
+ return st.db.Update(func(tx *bolt.Tx) error {
+ for _, update := range updates {
+ if _, err := st.appendItem(tx, update); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
}
-func (s sqliteStore) GetSource(id int) (res sourceDb, err error) {
- err = s.db.SelectOne(&res, "select * from "+sourcesTn+" where Id = ?", id)
- return
+func (st Store) Append(update DataUpdateFull) (err error) {
+ return st.AppendMany([]DataUpdateFull{update})
}
-func (s sqliteStore) GetUpdate(id int) (res dataUpdateDb, err error) {
- err = s.db.SelectOne(&res, "select * from "+dataUpdatesTn+" where Id = ?", id)
- return
-}
+//
+// Fetch data
+//
-func (s sqliteStore) GetClientsByUpdateId(id int) (res []clientDataDb, err error) {
- var qres []interface{}
- qres, err = s.db.Select(clientDataDb{}, "select * from "+clientdataUpdatesTn+" where DataUpdatesId = ?", id)
- if err == nil {
- res = make([]clientDataDb, len(qres))
- for i := range qres {
- res[i] = *qres[i].(*clientDataDb)
- }
+// fetch key-value pairs from buckets
+
+func (st Store) getHub(tx *bolt.Tx, id int) string {
+ b := tx.Bucket([]byte(hubUuidsRevBn))
+ uuid := b.Get(itob(id))
+ if uuid == nil {
+ return ""
}
- return
+ return string(uuid)
}
-var (
- updateColumnSelect = `
- Id,
- SourceHubUuid,
- SourceHubDataUpdateId,
- StartTime,
- Duration,
- ClientCount,
- BytesReceived,
- BytesSent,
- Hostname,
- ContentId,
- Format,
- Quality
-`
-)
+func (st Store) getSource(tx *bolt.Tx, id int) (res sourceDb, err error) {
+ b := tx.Bucket([]byte(sourcesRevBn))
-func (s sqliteStore) CreateStatisticsDataFrom(dat dataUpdateQueryResult) (res StatisticsData, err error) {
- var clientsDb []clientDataDb
- clientsDb, err = s.GetClientsByUpdateId(dat.Id)
- if err != nil {
- s5l.Printf("store GetClients failed: %v", err)
+ jsonData := b.Get(itob(id))
+ if jsonData == nil {
+ err = ErrNotFound
return
}
- tagsDb, err := s.GetTagsByDataUpdateId(dat.Id)
- if err != nil {
- s5l.Printf("store GetClients failed: %v", err)
+ if err = json.Unmarshal(jsonData, &res); err != nil {
return
}
- res.CopyFromDataUpdateDb(dat.dataUpdateDb, s.hubId)
- res.Hostname = dat.Hostname
- res.StreamId.ContentId = dat.ContentId
- res.StreamId.Format = dat.Format
- res.StreamId.Quality = dat.Quality
- res.CopyFromClientDataDb(clientsDb)
- res.Tags = tagsDb
return
}
-func (s sqliteStore) CreateStatisticsDatasFrom(dat []interface{}) (res []StatisticsData, err error) {
- res = make([]StatisticsData, len(dat))
- for i := range dat {
- t := *dat[i].(*dataUpdateQueryResult)
- res[i], _ = s.CreateStatisticsDataFrom(t)
- }
- return
-}
+func (st Store) getClients(tx *bolt.Tx, id int) (res []ClientData, err error) {
+ bc := tx.Bucket([]byte(clientDataBn))
+ bu := tx.Bucket([]byte(userAgentsRevBn))
-func (s sqliteStore) GetUpdatesAfter(id int) (res []StatisticsData, err error) {
- limit := 5000
- sourceSql, parameters := getFilteredDataUpdateSelect(&StatsFilter{afterUpdateId: &id, limit: &limit})
- sql := "SELECT " + updateColumnSelect + " FROM " + sourceSql
- var updates []interface{}
- updates, err = s.db.Select(dataUpdateQueryResult{}, sql, parameters)
- s5tl.Printf("sql: %s", sql)
- if err == nil {
- res, _ = s.CreateStatisticsDatasFrom(updates)
+ jsonData := bc.Get(itob(id))
+ if jsonData == nil {
+ return
+ }
+ data := []clientDataDb{}
+ if err = json.Unmarshal(jsonData, &data); err != nil {
+ return
+ }
+ for _, c := range data {
+ cd := ClientData{Ip: c.Ip, BytesSent: c.BytesSent}
+ ua := bu.Get(itob(c.UserAgentId))
+ if ua != nil {
+ cd.UserAgent = string(ua)
+ }
+ res = append(res, cd)
}
return
}
-func (s sqliteStore) GetUpdates(filter *StatsFilter) (res []StatisticsData, err error) {
- sourceSql, parameters := getFilteredDataUpdateSelect(filter)
- sql := "SELECT " + updateColumnSelect + " FROM " + sourceSql
- s5tl.Printf("store: sql: %s", sql)
- var updates []interface{}
- updates, err = s.db.Select(dataUpdateQueryResult{}, sql, parameters)
- if err == nil {
- res, _ = s.CreateStatisticsDatasFrom(updates)
+// fetch all the key-value pairs and merge them into the multidimensional dataupdate
+
+func (st Store) fetchItem(tx *bolt.Tx, duId int, du dataUpdateDb) (res DataUpdateFull, err error) {
+ res.CopyFromDataUpdateDb(du, st.getHub(tx, du.SourceHubId), st.hubUuid, duId)
+ var src sourceDb
+ if src, err = st.getSource(tx, du.SourceId); err != nil {
+ return
+ }
+ res.CopyFromSourceDb(src)
+ if res.Data.Clients, err = st.getClients(tx, duId); err != nil {
+ return
}
return
}
-type lastUpdateQueryResult struct {
- MaxDataUpdateId *int
-}
+// Public Fetch Interface
-func (s sqliteStore) GetLastUpdateForUuid(uuid string) (updateId *int, err error) {
- result := lastUpdateQueryResult{}
- err = s.db.SelectOne(
- &result,
- "select max(SourceHubDataUpdateId) as MaxDataUpdateId from "+dataUpdatesTn+" where SourceHubUuid = ?",
- uuid)
- if err == nil {
- updateId = result.MaxDataUpdateId
- } else {
- s5l.Printf("db: failed to find max SourceHubDataUpdateId for %s: %v", uuid, err)
+func (st Store) GetUpdatesAfter(id, limit int) (res []DataUpdateFull, err error) {
+ res = []DataUpdateFull{}
+ if id < 0 { // TODO: interpret negative values as last x values
+ id = 0
+ }
+ if limit < 0 || limit > StoreGetUpdatesLimit {
+ s5l.Printf("store: truncating get-update limit to %d (from %d)", StoreGetUpdatesLimit, limit)
+ limit = StoreGetUpdatesLimit
}
+ err = st.db.View(func(tx *bolt.Tx) error {
+ c := tx.Bucket([]byte(dataUpdatesBn)).Cursor()
+ k, v := c.Seek(itob(id))
+ if k == nil {
+ return nil
+ }
+ if btoi(k) == id {
+ k, v = c.Next()
+ }
+ for ; k != nil; k, v = c.Next() {
+ var d dataUpdateDb
+ if err := json.Unmarshal(v, &d); err != nil {
+ return err
+ }
+
+ duf, err := st.fetchItem(tx, btoi(k), d)
+ if err != nil {
+ return err
+ }
+ res = append(res, duf)
+ if len(res) >= limit {
+ return nil
+ }
+ }
+ return nil
+ })
return
}
-func (s sqliteStore) GetLastUpdateId() (updateId *int, err error) {
- result := lastUpdateQueryResult{}
- err = s.db.SelectOne(&result, "select max(Id) as MaxDataUpdateId from "+dataUpdatesTn)
- if err == nil {
- updateId = result.MaxDataUpdateId
- } else {
- s5l.Printf("db: failed to find max DataUpdateId: %v", err)
- }
+func (st Store) GetUpdate(id int) (res DataUpdateFull, err error) {
+ err = st.db.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(dataUpdatesBn))
+
+ jsonData := b.Get(itob(id))
+ if jsonData == nil {
+ return ErrNotFound
+ }
+
+ var d dataUpdateDb
+ if err := json.Unmarshal(jsonData, &d); err != nil {
+ return err
+ }
+
+ var err error
+ if res, err = st.fetchItem(tx, id, d); err != nil {
+ return err
+ }
+ return nil
+ })
return
}
-type statsResult struct {
- UpdateCount *int
- HubCount *int
- SourcesCount *int
- ClientCount *float32
- BytesSent *uint
- BytesReceived *uint
- StartTime *int64
- LastStartTime *int64
-}
+//
+// Auxilliary Data
+//
-type StatsResult struct {
- UpdateCount int
- HubCount int
- SourcesCount int
- ClientCount float32
- BytesSent uint
- BytesReceived uint
- StartTime time.Time
- LastStartTime time.Time
+func (st Store) GetStoreId() string {
+ return st.hubUuid
}
-func toApiStatsResult(value statsResult) (res StatsResult) {
- if value.UpdateCount != nil {
- res.UpdateCount = *value.UpdateCount
- }
- if value.HubCount != nil {
- res.HubCount = *value.HubCount
- }
- if value.SourcesCount != nil {
- res.SourcesCount = *value.SourcesCount
- }
- if value.ClientCount != nil {
- res.ClientCount = *value.ClientCount
- }
- if value.BytesSent != nil {
- res.BytesSent = *value.BytesSent
- }
- if value.BytesReceived != nil {
- res.BytesReceived = *value.BytesReceived
- }
- if value.StartTime != nil {
- res.StartTime = time.Unix(*value.StartTime, 0)
- }
- if value.LastStartTime != nil {
- res.LastStartTime = time.Unix(*value.LastStartTime, 0)
- }
- return res
+func (st Store) GetLastUpdateId() (updateId int, err error) {
+ err = st.db.View(func(tx *bolt.Tx) error {
+ updateId = int(tx.Bucket([]byte(dataUpdatesBn)).Sequence())
+ return nil
+ })
+ return
}
-var (
- statsGroupSelect = `
-SELECT
- count(*) as UpdateCount,
- SourceHubUuid as SourceHubUuid,
- count(distinct SourceId) as SourcesCount,
- avg(ClientCount) as ClientCount,
- sum(BytesSent) as BytesSent,
- sum(BytesReceived) as BytesReceived,
- min(StartTime) as StartTime,
- max(StartTime) as LastStartTime
-FROM
-`
- statsGroupClause = `
-GROUP BY
- SourceHubUuid
-`
- statsAggregateSelect = `
-SELECT
- sum(UpdateCount) as UpdateCount,
- count(distinct SourceHubUuid) as HubCount,
- sum(SourcesCount) as SourcesCount,
- sum(ClientCount) as ClientCount,
- sum(BytesSent) as BytesSent,
- sum(BytesReceived) as BytesReceived,
- min(StartTime) as StartTime,
- max(LastStartTime) as LastStartTime
-FROM
-`
-)
-
-func (s sqliteStore) GetStats(filter *StatsFilter) (StatsResult, error) { // (map[string]interface{}, error) {
- sourceSql, parameters := getFilteredDataUpdateSelect(filter)
- _ = sourceSql
- sql := fmt.Sprintf("%s (%s %s %s)", statsAggregateSelect, statsGroupSelect, sourceSql, statsGroupClause)
- s5tl.Printf("store: stats sql: %s", sql)
- res := statsResult{}
- err := s.db.SelectOne(&res, sql, parameters)
- if err == nil {
- return toApiStatsResult(res), nil
- }
- return StatsResult{}, err
+func (st Store) GetLastUpdateForUuid(uuid string) (updateId int, err error) {
+ err = st.db.View(func(tx *bolt.Tx) error {
+ bUpdateId := tx.Bucket([]byte(latestUpdatesBn)).Get([]byte(uuid))
+ if bUpdateId == nil {
+ return nil
+ }
+ updateId = btoi(bUpdateId)
+ return nil
+ })
+ return
}
-func (s sqliteStore) GetStoreId() (uuid string, err error) {
- uuid, err = s.db.SelectStr("select Value from HubInfo where Name = ?", "HubUuid")
+func (st Store) GetHubs() (res []string, err error) {
+ res = []string{st.hubUuid}
+ err = st.db.View(func(tx *bolt.Tx) error {
+ c := tx.Bucket([]byte(hubUuidsRevBn)).Cursor()
+ for k, v := c.First(); k != nil; k, v = c.Next() {
+ res = append(res, string(v))
+ }
+ return nil
+ })
return
}
-func NewStore(mysql bool, path string) (sqliteStore, error) {
- db, hubid, err := initDb(mysql, path)
- if err != nil {
- return sqliteStore{}, err
- }
- return sqliteStore{db, hubid}, nil
+func (st Store) GetSource(id int) (res SourceId, err error) {
+ err = st.db.View(func(tx *bolt.Tx) error {
+ src, err := st.getSource(tx, id)
+ if err != nil {
+ return err
+ }
+ res.CopyFromSourceDb(src)
+ return nil
+ })
+ return
}
-func (s sqliteStore) Close() {
- s.db.Db.Close()
+func (st Store) GetSources() (res []SourceId, err error) {
+ res = []SourceId{}
+ err = st.db.View(func(tx *bolt.Tx) error {
+ c := tx.Bucket([]byte(sourcesRevBn)).Cursor()
+ for k, v := c.First(); k != nil; k, v = c.Next() {
+ var s sourceDb
+ if err := json.Unmarshal(v, &s); err != nil {
+ return err
+ }
+ var src SourceId
+ src.CopyFromSourceDb(s)
+ res = append(res, src)
+ }
+ return nil
+ })
+ return
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5store_test.go b/src/hub/src/spreadspace.org/sfive/s5store_test.go
index 409bd08..5fcae5e 100644
--- a/src/hub/src/spreadspace.org/sfive/s5store_test.go
+++ b/src/hub/src/spreadspace.org/sfive/s5store_test.go
@@ -1,179 +1,815 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
+ "fmt"
+ "io"
+ "os"
+ "os/user"
+ "reflect"
"testing"
"time"
+
+ "github.com/boltdb/bolt"
)
-func TestAppend(t *testing.T) {
- store, err := NewStore(false, "file:memdb1?mode=memory&cache=shared")
- if err != nil {
- t.Errorf("Failed to initialize: %v", err)
- return
+var (
+ testBoltPath = "/run/s5hub_testing_db.bolt"
+ testBoltPath2 = "/run/s5hub_testing_db2.bolt"
+ testBoltPathFwd = "/run/s5hub_testing_db_fwd.bolt"
+ testBoltPathFinal = "/run/s5hub_testing_db_final.bolt"
+
+ streamIdData = StreamId{ContentId: "talkingheads", Format: "7bitascii", Quality: "high"}
+ sourceData = SourceId{Hostname: "streamer", Tags: []string{"tag1", "master"}, StreamId: streamIdData}
+ updateData = DataUpdate{Data: SourceData{ClientCount: 3, BytesReceived: 42, BytesSent: 136}, Duration: 5000}
+ clientsData = []ClientData{
+ ClientData{"127.0.0.1", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0", 6400},
+ ClientData{"10.12.0.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400},
+ ClientData{"127.0.0.1", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0", 6400},
+ ClientData{"192.168.0.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400},
+ ClientData{"172.16.0.2", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400}}
+)
+
+func generateTestData(n int) ([]DataUpdateFull, int) {
+ hostnames := []string{"streamer1", "streamer2"}
+ contents := []string{"av", "audio"}
+ formats := []string{"webm", "flash", "hls"}
+ qualities := []string{"high", "medium", "low"}
+ tags := []string{"2017", "elevate", "discourse"}
+ duration := int64(15000)
+
+ starttime := time.Now()
+ numSrcs := len(hostnames) * len(contents) * len(formats) * len(qualities)
+ if n < 0 {
+ n = numSrcs
}
- defer store.Close()
+ var data []DataUpdateFull
+ for i := 0; i < n; i += numSrcs {
+ for _, hostname := range hostnames {
+ for _, content := range contents {
+ for _, format := range formats {
+ for _, quality := range qualities {
+ d := DataUpdateFull{}
+ d.Version = ProtocolVersion
+
+ d.SourceId.Hostname = hostname
+ d.SourceId.StreamId.ContentId = content
+ d.SourceId.StreamId.Format = format
+ d.SourceId.StreamId.Quality = quality
+ d.SourceId.Tags = tags
- startTime := time.Date(2014, time.August, 24, 14, 35, 33, 847282000, time.UTC)
- update := DataUpdate{Data: SourceData{BytesSent: 1, ClientCount: 3, BytesReceived: 1}, StartTime: startTime, Duration: 5000}
- streamId := StreamId{ContentId: "content", Format: "7bitascii", Quality: QualityHigh}
- source := SourceId{Hostname: "localhost", Tags: []string{"tag1", "master"}, StreamId: streamId, Version: 1}
- dat := StatisticsData{nil, nil, source, update}
+ d.DataUpdate.StartTime = starttime
+ d.DataUpdate.Duration = duration
+ d.DataUpdate.Data.ClientCount = uint(len(clientsData))
+ d.DataUpdate.Data.BytesSent = 6400 * uint(len(clientsData))
- err = store.Append(dat)
+ d.DataUpdate.Data.Clients = clientsData
+ data = append(data, d)
+ }
+ }
+ }
+ }
+ starttime = starttime.Add(time.Duration(duration) * time.Millisecond)
+ }
+ return data[:n], numSrcs
+}
+
+func TestMain(m *testing.M) {
+ u, err := user.Current()
if err != nil {
- t.Errorf("Failed to append: %v", err)
- return
+ os.Exit(-1)
+ }
+ testBoltPath = fmt.Sprintf("/run/user/%s/s5hub_testing_db.bolt", u.Uid)
+ testBoltPath2 = fmt.Sprintf("/run/user/%s/s5hub_testing_db2.bolt", u.Uid)
+ testBoltPathFwd = fmt.Sprintf("/run/user/%s/s5hub_testing_db_fwd.bolt", u.Uid)
+ testBoltPathFinal = fmt.Sprintf("/run/user/%s/s5hub_testing_db_final.bolt", u.Uid)
+ os.Exit(m.Run())
+}
+
+//
+// Testing
+//
+
+func TestOpen(t *testing.T) {
+ // non-existing directory
+ if _, err := NewStore("/nonexistend/db.bolt", false); err == nil {
+ t.Fatalf("opening store in nonexisting directory should throw an error")
+ }
+
+ // store path is a directory
+ os.Remove(testBoltPath)
+ if err := os.MkdirAll(testBoltPath, 0700); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store using a directory should throw an error")
}
- stats, err := store.GetStats(nil)
+ // exisiting but non-database file
+ os.Remove(testBoltPath)
+ if f, err := os.Create(testBoltPath); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ } else {
+ io.WriteString(f, "this is not a bolt db.")
+ f.Close()
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store using a invalid database should throw an error")
+ }
+
+ // bolt db without HubInfo Bucket
+ os.Remove(testBoltPath)
+ db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond})
if err != nil {
- t.Errorf("Failed to get stats: %v", err)
+ t.Fatalf("unexpected error: %v", err)
+ } else {
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store without HubInfo Bucket should throw an error")
+ }
+
+ // bolt db no version key
+ if db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
} else {
- clientCount := int(stats.ClientCount)
- updateCount := stats.UpdateCount
- if 3 != clientCount {
- t.Errorf("Failed fo append, invalid number of clients, 3 != %v", clientCount)
+ err = db.Update(func(tx *bolt.Tx) error {
+ if _, err := tx.CreateBucket([]byte(hubInfoBn)); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
}
- if 1 != updateCount {
- t.Errorf("Failed to append, invalid number of updates, 1 != %v", updateCount)
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
}
}
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store without a database version should throw an error")
+ }
- queryStartTime := time.Date(2015, time.December, 24, 1, 1, 1, 0, time.UTC)
- filterStruct := StatsFilter{start: &queryStartTime}
- stats, err = store.GetStats(&filterStruct)
- if err != nil {
- t.Errorf("Failed to get stats: %v", err)
+ // bolt db wrong version
+ if db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ } else {
+ err = db.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(hubInfoBn))
+ if err := b.Put([]byte(storeVersionKey), itob(0)); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store with wrong database version should throw an error")
+ }
+
+ // bolt db no UUID key
+ if db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
} else {
- updateCount := stats.UpdateCount
- if 0 != updateCount {
- t.Errorf("Failed to filter entries by start time, 0 != %v", updateCount)
+ err = db.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(hubInfoBn))
+ if err := b.Put([]byte(storeVersionKey), itob(StoreVersion)); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store without a database UUID should throw an error")
+ }
+
+ // bolt db empty UUID
+ if db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ } else {
+ err = db.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(hubInfoBn))
+ if err := b.Put([]byte(hubUuidKey), []byte("")); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store with empty UUID should throw an error")
+ }
+
+ // bolt db with missing buckets
+ if db, err := bolt.Open(testBoltPath, 0600, &bolt.Options{Timeout: 100 * time.Millisecond}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ } else {
+ err = db.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(hubInfoBn))
+ if err := b.Put([]byte(hubUuidKey), []byte("6d1a2192-7cb6-404d-80fb-add9b40a8f33")); err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err = db.Close(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening store with (some) buckets missing should throw an error")
+ }
+
+ // create new bolt-db and reopen it
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("creating new store failed: %v", err)
+ }
+ createdUuid := store.hubUuid
+ store.Close()
+
+ store, err = NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("re-opening existing store failed: %v", err)
+ }
+ if createdUuid != store.hubUuid {
+ t.Fatalf("UUID of opened store differs from the one previously generated: '%s' != '%s'", createdUuid, store.hubUuid)
+ }
+
+ if _, err := NewStore(testBoltPath, false); err == nil {
+ t.Fatalf("opening already opened database should throw an error")
+ }
+ store.Close()
+}
+
+func TestAppendAndFetch(t *testing.T) {
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer store.Close()
+
+ if _, err := store.GetUpdate(17); err != ErrNotFound {
+ t.Fatalf("fetching not exisiting update should return not-found, error=%v", err)
+ }
+
+ upd := updateData
+ upd.StartTime = time.Date(2014, time.August, 24, 14, 35, 33, 847000000, time.UTC)
+ upd.Data.Clients = clientsData
+ in := DataUpdateFull{0, "", -1, "", -1, sourceData, upd}
+
+ if err = store.Append(in); err != nil {
+ t.Fatalf("failed to append update: %v", err)
+ }
+
+ out, err := store.GetUpdate(1)
+ if err != nil {
+ t.Fatalf("failed to fetch update: %v", err)
+ }
+ out.StartTime = out.StartTime.UTC() // this would normally be handled by the protocol encoder
+
+ expected := in
+ expected.SourceHubUuid = store.GetStoreId()
+ expected.SourceHubDataUpdateId = 1
+ expected.ForwardHubUuid = ""
+ expected.ForwardHubDataUpdateId = 0
+
+ if !reflect.DeepEqual(expected, out) {
+ t.Fatalf("failed to fetch update\nactual: %v\nexpected: %v\n", out, expected)
+ }
+
+ // append many
+ var ins []DataUpdateFull
+ upd.StartTime = upd.StartTime.Add(time.Duration(upd.Duration) * time.Millisecond)
+ ins = append(ins, DataUpdateFull{0, "", -1, "", -1, sourceData, upd})
+ upd.StartTime = upd.StartTime.Add(time.Duration(upd.Duration) * time.Millisecond)
+ upd.Data.Clients = nil
+ ins = append(ins, DataUpdateFull{0, "", -1, "", -1, sourceData, upd})
+ if err = store.AppendMany(ins); err != nil {
+ t.Fatalf("failed to append update: %v", err)
+ }
+
+ for i := 0; i < 2; i = i + 1 {
+ out, err = store.GetUpdate(i + 2)
+ if err != nil {
+ t.Fatalf("failed to fetch update: %v", err)
+
+ }
+ out.StartTime = out.StartTime.UTC() // this would normally be handled by the protocol encoder
+ expected = ins[i]
+ expected.SourceHubUuid = store.GetStoreId()
+ expected.SourceHubDataUpdateId = i + 2
+ expected.ForwardHubUuid = ""
+ expected.ForwardHubDataUpdateId = 0
+
+ if !reflect.DeepEqual(expected, out) {
+ t.Fatalf("failed to fetch update\nactual: %v\nexpected: %v\n", out, expected)
}
}
}
-func TestCount(t *testing.T) {
- store, err := NewStore(false, "file:memdb1?mode=memory&cache=shared")
+func TestReadOnly(t *testing.T) {
+ // create read-only db from not-existing file must fail
+ os.Remove(testBoltPath)
+ if _, err := NewStore(testBoltPath, true); err == nil {
+ t.Fatalf("creating a read-only database should throw an error")
+ }
+
+ // prepare a store with one data-update
+ store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ upd := updateData
+ upd.StartTime = time.Date(2014, time.August, 24, 14, 35, 33, 847000000, time.UTC)
+ upd.Data.Clients = clientsData
+ in := DataUpdateFull{0, "", -1, "", -1, sourceData, upd}
+ if err = store.Append(in); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ store.Close()
+
+ // read-only db from existing file must succeed
+ store, err = NewStore(testBoltPath, true)
if err != nil {
- t.Errorf("Failed to initialize: %v", err)
+ t.Fatalf("opening existing store in read-only mode failed: %v", err)
}
defer store.Close()
- stats, err := store.GetStats(nil)
- clientCount := int(stats.ClientCount)
- if 0 != clientCount {
- t.Errorf("Failed to count correctly.")
+ // fetching the date-update from read-only store must succeed
+ if _, err := store.GetUpdate(1); err != nil {
+ t.Fatalf("failed to fetch update: %v", err)
+ }
+
+ // appending to read-only store must fail
+ if err = store.Append(in); err == nil {
+ t.Fatalf("appending to read-only store should throw an error")
}
}
func TestGetUpdatesAfter(t *testing.T) {
- store, err := NewStore(false, "file:memdb1?mode=memory&cache=shared")
+ // prepare a store with 3 data-updates
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
if err != nil {
- t.Errorf("Failed to initialize: %v", err)
- return
+ t.Fatalf("unexpected error: %v", err)
}
defer store.Close()
- startTime := time.Date(2014, time.August, 24, 14, 35, 33, 847282000, time.UTC)
- update := DataUpdate{Data: SourceData{BytesSent: 1, ClientCount: 3, BytesReceived: 1}, StartTime: startTime, Duration: 5000}
- streamId := StreamId{ContentId: "content", Format: "7bitascii", Quality: QualityHigh}
- source := SourceId{Hostname: "localhost", Tags: []string{"tag1", "master"}, StreamId: streamId, Version: 1}
- dat := StatisticsData{nil, nil, source, update}
+ upd := updateData
+ upd.StartTime = time.Date(2014, time.August, 24, 14, 35, 33, 847000000, time.UTC)
+ upd.Data.Clients = clientsData
- err = store.Append(dat)
+ expected := []DataUpdateFull{}
+ for i := 0; i < 3; i = i + 1 {
+ in := DataUpdateFull{0, "", -1, "", -1, sourceData, upd}
+ if err = store.Append(in); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ e := in
+ e.SourceHubUuid = store.hubUuid
+ e.SourceHubDataUpdateId = i + 1
+ e.ForwardHubUuid = ""
+ e.ForwardHubDataUpdateId = 0
+ expected = append(expected, e)
+ upd.StartTime = upd.StartTime.Add(time.Duration(upd.Duration) * time.Millisecond)
+ }
+
+ // check if there are 3 updates in store
+ lastId, err := store.GetLastUpdateId()
if err != nil {
- t.Errorf("Failed to retrieve: %v", err)
- return
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != 3 {
+ t.Fatalf("failed to get last update ID: got %d updates, expected 3", lastId)
}
- res, err := store.GetUpdatesAfter(2)
- t.Logf("got updates (err %v):\n%#v", err, res)
-}
+ // all the updates
+ updList, err := store.GetUpdatesAfter(-1, -1)
+ if err != nil {
+ t.Fatalf("failed to fetch updates: %v", err)
+ }
+ for i, _ := range updList {
+ updList[i].StartTime = updList[i].StartTime.UTC() // this would normally be handled by the protocol encoder
+ }
-func generateStatisticsData(n int) (data []StatisticsData) {
- hostnames := []string{"streamer1", "streamer2"}
- contents := []string{"av", "audio"}
- formats := []string{"webm", "flash", "hls"}
- qualities := []string{"high", "medium", "low"}
+ if !reflect.DeepEqual(expected, updList) {
+ t.Fatalf("failed to fetch updates\nactual: %v\nexpected: %v\n", updList, expected)
+ }
- numcombis := len(hostnames) * len(contents) * len(formats) * len(qualities)
+ // first 2
+ updList, err = store.GetUpdatesAfter(0, 2)
+ if err != nil {
+ t.Fatalf("failed to fetch updates: %v", err)
+ }
+ for i, _ := range updList {
+ updList[i].StartTime = updList[i].StartTime.UTC() // this would normally be handled by the protocol encoder
+ }
+ if len(updList) != 2 {
+ t.Fatalf("failed to fetch updates: got %d updates, expected 1", len(updList))
+ }
+ if !reflect.DeepEqual(expected[:2], updList) {
+ t.Fatalf("failed to fetch updates\nactual: %v\nexpected: %v\n", updList, expected[:2])
+ }
- clients := []ClientData{
- ClientData{"127.0.0.1", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0", 6400},
- ClientData{"10.12.0.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400},
- ClientData{"127.0.0.1", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0", 6400},
- ClientData{"192.168.0.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400},
- ClientData{"172.16.0.2", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/57.0.2987.98 Chrome/57.0.2987.98 Safari/537.36", 6400}}
- starttime := time.Now()
- duration := int64(15000)
- tags := []string{"2017", "elevate", "discourse"}
+ // all after id 2
+ updList, err = store.GetUpdatesAfter(2, -1)
+ if err != nil {
+ t.Fatalf("failed to fetch updates: %v", err)
+ }
+ for i, _ := range updList {
+ updList[i].StartTime = updList[i].StartTime.UTC() // this would normally be handled by the protocol encoder
+ }
- for i := 0; i < n; i += numcombis {
- for _, hostname := range hostnames {
- for _, content := range contents {
- for _, format := range formats {
- for _, quality := range qualities {
- d := StatisticsData{}
- d.SourceId.Version = 1
- d.SourceId.Hostname = hostname
- d.SourceId.Tags = tags
+ if !reflect.DeepEqual(expected[2:], updList) {
+ t.Fatalf("failed to fetch updates\nactual: %v\nexpected: %v\n", updList, expected[2:])
+ }
- d.SourceId.StreamId.ContentId = content
- d.SourceId.StreamId.Format = format
- d.SourceId.StreamId.Quality = quality
+ // all after id 3 -> should return nothing
+ updList, err = store.GetUpdatesAfter(3, -1)
+ if err != nil {
+ t.Fatalf("failed to fetch updates: %v", err)
+ }
+ if len(updList) != 0 {
+ t.Fatalf("failed to fetch updates: got %d updates, expected 0", len(updList))
+ }
+}
- d.DataUpdate.StartTime = starttime
- d.DataUpdate.Duration = duration
- d.DataUpdate.Data.ClientCount = uint(len(clients))
- d.DataUpdate.Data.BytesSent = 6400 * uint(len(clients))
+func TestForwardedDataUpdates(t *testing.T) {
+ // prepare a new store
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer store.Close()
- for _, client := range clients {
- c := ClientData{client.Ip, client.UserAgent, client.BytesSent}
- d.DataUpdate.Data.Clients = append(d.DataUpdate.Data.Clients, c)
- }
- data = append(data, d)
- }
- }
- }
+ // generate/append some local updates
+ data, _ := generateTestData(10)
+ if err := store.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ myLastId := 10
+ forwardedHub := "05defdfa-e7d1-4ca8-8b5c-02abb0088d29"
+
+ // check if there are no updates for this hub in store
+ lastId, err := store.GetLastUpdateForUuid(forwardedHub)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != 0 {
+ t.Fatalf("failed to get last update ID: %d, expected 0", lastId)
+ }
+
+ // get list of all hubs
+ hubs, err := store.GetHubs()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(hubs) != 1 {
+ t.Fatalf("failed to get hub UUIDs: got %d hubs, expected 1", len(hubs))
+ }
+ if hubs[0] != store.hubUuid {
+ t.Fatalf("fist hub should be the own stores UUID but is: %s", hubs[0])
+ }
+
+ // append 3 forwarded data-updates
+ upd := updateData
+ upd.StartTime = time.Date(2014, time.August, 24, 14, 35, 33, 847000000, time.UTC)
+ upd.Data.Clients = clientsData
+
+ expected := []DataUpdateFull{}
+ for i := 0; i < 3; i = i + 1 {
+ in := DataUpdateFull{0, "", -1, "", -1, sourceData, upd}
+ in.SourceHubUuid = forwardedHub
+ in.SourceHubDataUpdateId = 3 - i // out of order
+ if err = store.Append(in); err != nil {
+ t.Fatalf("unexpected error: %v", err)
}
- starttime = starttime.Add(time.Duration(duration) * time.Millisecond)
+ myLastId = myLastId + 1
+ in.ForwardHubUuid = store.GetStoreId()
+ in.ForwardHubDataUpdateId = myLastId
+ expected = append(expected, in)
+ upd.StartTime = upd.StartTime.Add(time.Duration(upd.Duration) * time.Millisecond)
+ }
+
+ out, err := store.GetUpdatesAfter(10, 3)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ for i, _ := range out {
+ out[i].StartTime = out[i].StartTime.UTC() // this would normally be handled by the protocol encoder
+ }
+ if !reflect.DeepEqual(out, expected) {
+ t.Fatalf("failed to fetch source\nactual: %v\nexpected: %v\n", out, expected)
+ }
+
+ // check if the last update for this hub is 3
+ lastId, err = store.GetLastUpdateForUuid(forwardedHub)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != 3 {
+ t.Fatalf("failed to get last update ID: got %d updates, expected 3", lastId)
+ }
+
+ // get list of all hubs
+ hubs, err = store.GetHubs()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(hubs) != 2 {
+ t.Fatalf("failed to get hub UUIDs: got %d hubs, expected 2", len(hubs))
+ }
+ if hubs[0] != store.hubUuid {
+ t.Fatalf("fist hub should be the own stores UUID but is: %s", hubs[0])
+ }
+ if hubs[1] != forwardedHub {
+ t.Fatalf("second hub UUID is wrong: %s, expected: %s", hubs[1], forwardedHub)
+ }
+
+ // check if the last update is now 13
+ lastId, _ = store.GetLastUpdateId()
+ if lastId != 13 {
+ t.Fatalf("failed to get last update ID: got %d updates, expected 3", lastId)
}
- return
}
+func checkForwardedDataUpdates2(t *testing.T, src1Store, src2Store, fwdStore, finalStore Store, fwdSrc1Id, fwdSrc2Id, finalSrc1Id, finalSrc2Id, finalFwdId int) {
+ lastId, err := fwdStore.GetLastUpdateForUuid(src1Store.GetStoreId())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != fwdSrc1Id {
+ t.Fatalf("failed to get last update ID: %d, expected %d", lastId, fwdSrc1Id)
+ }
+ lastId, err = fwdStore.GetLastUpdateForUuid(src2Store.GetStoreId())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != fwdSrc2Id {
+ t.Fatalf("failed to get last update ID: %d, expected %d", lastId, fwdSrc2Id)
+ }
+ lastId, err = finalStore.GetLastUpdateForUuid(src1Store.GetStoreId())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != finalSrc1Id {
+ t.Fatalf("failed to get last update ID: %d, expected %d", lastId, finalSrc1Id)
+ }
+ lastId, err = finalStore.GetLastUpdateForUuid(src2Store.GetStoreId())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != finalSrc2Id {
+ t.Fatalf("failed to get last update ID: %d, expected %d", lastId, finalSrc2Id)
+ }
+ lastId, err = finalStore.GetLastUpdateForUuid(fwdStore.GetStoreId())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if lastId != finalFwdId {
+ t.Fatalf("failed to get last update ID: %d, expected %d", lastId, finalFwdId)
+ }
+}
+
+func TestForwardedDataUpdates2(t *testing.T) {
+ // prepare 4 new stores
+ os.Remove(testBoltPath)
+ src1Store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer src1Store.Close()
+
+ os.Remove(testBoltPath2)
+ src2Store, err := NewStore(testBoltPath2, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer src2Store.Close()
+
+ os.Remove(testBoltPathFwd)
+ fwdStore, err := NewStore(testBoltPathFwd, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer fwdStore.Close()
+
+ os.Remove(testBoltPathFinal)
+ finalStore, err := NewStore(testBoltPathFinal, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer finalStore.Close()
+
+ // generate/append some updates to src
+ data, _ := generateTestData(10)
+ if err := src1Store.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ data, _ = generateTestData(7)
+ if err := src2Store.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 0, 0, 0, 0, 0)
+
+ // forward 5 updates from src1 to fwd
+ if data, err = src1Store.GetUpdatesAfter(0, 5); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := fwdStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 5, 0, 0, 0, 0)
+
+ // forward 3 updates from src2 to fwd
+ if data, err = src2Store.GetUpdatesAfter(0, 3); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := fwdStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 5, 3, 0, 0, 0)
+
+ // forward 7 updates from fwd to final
+ if data, err = fwdStore.GetUpdatesAfter(0, 7); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := finalStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 5, 3, 5, 2, 7)
+
+ // forward remaining updates from src1 and src2 to fwd
+ if data, err = src1Store.GetUpdatesAfter(5, -1); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := fwdStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if data, err = src2Store.GetUpdatesAfter(3, -1); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := fwdStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 10, 7, 5, 2, 7)
+
+ // forward remainging from fwd to final
+ if data, err = fwdStore.GetUpdatesAfter(7, -1); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := finalStore.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // check last updates so far
+ checkForwardedDataUpdates2(t, src1Store, src2Store, fwdStore, finalStore, 10, 7, 10, 7, 17)
+}
+
+func TestGetSources(t *testing.T) {
+ // prepare a new store
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ defer store.Close()
+
+ if _, err := store.GetSource(17); err != ErrNotFound {
+ t.Fatalf("fetching not exisiting source should return not-found, error=%v", err)
+ }
+
+ // generate/append some data
+ data, numSrcs := generateTestData(-1)
+ if err := store.AppendMany(data); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // fetch first source
+ src, err := store.GetSource(1)
+ if err != nil {
+ t.Fatalf("fetching source failed: %v", err)
+ }
+ if !reflect.DeepEqual(data[0].SourceId, src) {
+ t.Fatalf("failed to fetch source\nactual: %v\nexpected: %v\n", src, data[0].SourceId)
+ }
+
+ // fetch all the sources
+ srcList, err := store.GetSources()
+ if err != nil {
+ t.Fatalf("fetching sources failed: %v", err)
+ }
+ if len(srcList) != numSrcs {
+ t.Fatalf("wrong number of sources: %d, expected %d", len(srcList), numSrcs)
+ }
+ // the result will be orderd using the slug so doing a DeepEqual doesn't work here
+}
+
+//
+// Benchmarking
+//
+
func BenchmarkAppendMany(b *testing.B) {
- store, err := NewStore(false, "file:memdb1?mode=memory&cache=shared")
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
if err != nil {
- b.Errorf("Failed to initialize: %v", err)
+ b.Fatalf("unexpected error: %v", err)
}
defer store.Close()
- data := generateStatisticsData(b.N)
+ data, _ := generateTestData(b.N)
b.ResetTimer()
if err := store.AppendMany(data); err != nil {
- b.Errorf("Failed to append: %v", err)
+ b.Fatalf("unexpected error: %v", err)
}
}
func BenchmarkGetUpdatesAfter(b *testing.B) {
- store, err := NewStore(false, "file:memdb1?mode=memory&cache=shared")
+ os.Remove(testBoltPath)
+ store, err := NewStore(testBoltPath, false)
if err != nil {
- b.Errorf("Failed to initialize: %v", err)
+ b.Fatalf("unexpected error: %v", err)
}
defer store.Close()
- data := generateStatisticsData(b.N)
+ data, _ := generateTestData(b.N)
if err := store.AppendMany(data); err != nil {
- b.Errorf("Failed to append: %v", err)
+ b.Fatalf("unexpected error: %v", err)
}
b.ResetTimer()
latestId := -1
for {
- updates, err := store.GetUpdatesAfter(latestId)
+ updates, err := store.GetUpdatesAfter(latestId, -1)
if err != nil {
- b.Errorf("Failed to retrieve: %v", err)
+ b.Fatalf("failed to retrieve: %v", err)
}
if len(updates) == 0 {
break
diff --git a/src/hub/src/spreadspace.org/sfive/s5typesApi.go b/src/hub/src/spreadspace.org/sfive/s5typesApi.go
index 5b2b29f..12e92c8 100644
--- a/src/hub/src/spreadspace.org/sfive/s5typesApi.go
+++ b/src/hub/src/spreadspace.org/sfive/s5typesApi.go
@@ -1,13 +1,39 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import "time"
-const (
- QualityLow = "low"
- QualityMedium = "medium"
- QualityHigh = "high"
-)
-
type StreamId struct {
ContentId string `json:"content-id"`
Format string `json:"format"`
@@ -15,23 +41,22 @@ type StreamId struct {
}
type SourceId struct {
- Version uint `json:"version" db:"-"`
Hostname string `json:"hostname"`
- StreamId StreamId `json:"streamer-id" db:"-"`
- Tags []string `json:"tags" db:"-"`
+ StreamId StreamId `json:"streamer-id"`
+ Tags []string `json:"tags,omitempty"`
}
type ClientData struct {
Ip string `json:"ip"`
- UserAgent string `json:"user-agent"`
+ UserAgent string `json:"user-agent,omitempty"`
BytesSent uint `json:"bytes-sent"`
}
type SourceData struct {
ClientCount uint `json:"client-count"`
- BytesReceived uint `json:"bytes-received"`
+ BytesReceived uint `json:"bytes-received,omitempty"`
BytesSent uint `json:"bytes-sent"`
- Clients []ClientData `json:"clients"`
+ Clients []ClientData `json:"clients,omitempty"`
}
type DataUpdate struct {
@@ -40,43 +65,32 @@ type DataUpdate struct {
Data SourceData `json:"data"`
}
-type StatisticsData struct {
- SourceHubUuid *string
- SourceHubDataUpdateId *int
+type DataUpdateFull struct {
+ Version uint `json:"version"`
+ SourceHubUuid string `json:"SourceHubUuid,omitempty"`
+ SourceHubDataUpdateId int `json:"SourceHubDataUpdateId,omitempty"`
+ ForwardHubUuid string `json:"ForwardHubUuid,omitempty"`
+ ForwardHubDataUpdateId int `json:"ForwardHubDataUpdateId,omitempty"`
SourceId
DataUpdate
}
-type DataContainer struct {
- Data interface{} `json:"data"`
-}
-
-type StatisticsDataContainer struct {
- Data []StatisticsData `json:"data"`
+type DataUpdateFullContainer struct {
+ Data []DataUpdateFull `json:"data"`
}
-type StatsFilter struct {
- start *time.Time
- end *time.Time
- hostname *string
- contentId *string
- format *string
- quality *string
- tagsAny []string
- afterUpdateId *int
- limit *int
- sortOrder *string
+func (duf *DataUpdateFull) CopyFromSourceId(src *SourceId) {
+ duf.Hostname = src.Hostname
+ duf.StreamId = src.StreamId
+ duf.Tags = src.Tags
}
-func (self *StatisticsData) CopyFromSourceId(id *SourceId) {
- self.Hostname = id.Hostname
- self.StreamId = id.StreamId
- self.Tags = id.Tags
- self.Version = id.Version
+func (duf *DataUpdateFull) CopyFromUpdate(du *DataUpdate) {
+ duf.StartTime = du.StartTime
+ duf.Duration = du.Duration
+ duf.Data = du.Data
}
-func (self *StatisticsData) CopyFromUpdate(id *DataUpdate) {
- self.StartTime = id.StartTime
- self.Duration = id.Duration
- self.Data = id.Data
+type GenericDataContainer struct {
+ Data interface{} `json:"data"`
}
diff --git a/src/hub/src/spreadspace.org/sfive/s5typesStore.go b/src/hub/src/spreadspace.org/sfive/s5typesStore.go
index f06e953..bba4433 100644
--- a/src/hub/src/spreadspace.org/sfive/s5typesStore.go
+++ b/src/hub/src/spreadspace.org/sfive/s5typesStore.go
@@ -1,120 +1,161 @@
+//
+// sfive
+//
+// sfive - spreadspace streaming statistics suite is a generic
+// statistic collection tool for streaming server infrastuctures.
+// The system collects and stores meta data like number of views
+// and throughput from a number of streaming servers and stores
+// it in a global data store.
+// The data acquisition is designed to be generic and extensible in
+// order to support different streaming software.
+// sfive also contains tools and applications to filter and visualize
+// live and recorded data.
+//
+//
+// Copyright (C) 2014-2017 Christian Pointner <equinox@spreadspace.org>
+// Markus Grüneis <gimpf@gimpf.org>
+//
+// This file is part of sfive.
+//
+// sfive is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3
+// as published by the Free Software Foundation.
+//
+// sfive is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with sfive. If not, see <http://www.gnu.org/licenses/>.
+//
+
package sfive
import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "strings"
"time"
)
-// compared to JSON DTOs, DB types are flattened, and use key-relations instead of collections
-// this is very much not normalized at all, because I'm too lazy to type
+var (
+ ErrNotFound = errors.New("not found")
+ ErrReadOnly = errors.New("store is in read-only mode")
+)
-// table names
const (
- tagsTn = "Tags"
- sourceTagsTn = "StreamToTagMap"
- sourcesTn = "Sources"
- clientdataUpdatesTn = "ClientDataUpdates"
- dataUpdatesTn = "DataUpdates"
- hubInfoTn = "HubInfo"
+ // bucket names
+ hubInfoBn = "HubInfo"
+ latestUpdatesBn = "LatestUpdates"
+ hubUuidsFwdBn = "HubUUIDsFwd"
+ hubUuidsRevBn = "HubUUIDsRev"
+ dataUpdatesBn = "DataUpdates"
+ sourcesFwdBn = "SourcesFwd"
+ sourcesRevBn = "SourcesRev"
+ clientDataBn = "ClientData"
+ userAgentsFwdBn = "UserAgentsFwd"
+ userAgentsRevBn = "UserAgentsRev"
+
+ // well-known keys
+ hubUuidKey = "HubUUID"
+ storeVersionKey = "Version"
)
-type hubInfoDb struct {
- Name string
- Value string
+// stored in sourcesRevBn
+type streamIdDb struct {
+ ContentId string `json:"c"`
+ Format string `json:"f"`
+ Quality string `json:"q"`
}
-// stored in tagsTn
-type tagDb struct {
- Id int
- Name string
+type sourceDb struct {
+ Hostname string `json:"h"`
+ StreamId streamIdDb `json:"s"`
+ Tags []string `json:"t"`
}
-// stored in sourceTagsTn
-// Stream m:n Tag
-type sourceTagsDb struct {
- TagId int // foreign key to tagsTn
- SourceId int // foreign key to sourcesTn
+func NewSourceDb(value DataUpdateFull) sourceDb {
+ return sourceDb{
+ Hostname: value.SourceId.Hostname,
+ StreamId: streamIdDb{
+ ContentId: value.SourceId.StreamId.ContentId,
+ Format: value.SourceId.StreamId.Format,
+ Quality: value.SourceId.StreamId.Quality,
+ },
+ Tags: value.SourceId.Tags,
+ }
}
-// stored in sourcesTn
-type sourceDb struct {
- Id int
- StreamId
- SourceId
+func (s sourceDb) Slug() string {
+ return fmt.Sprintf("%s/%s/%s/%s/%s", s.Hostname, s.StreamId.ContentId, s.StreamId.Format, s.StreamId.Quality, strings.Join(s.Tags, ","))
}
-// stored in clientdataUpdatesTn
-// ClientData n:1 DataUpdate
-type clientDataDb struct {
- Id int
- DataUpdatesId int // foreign key to dataUpdatesTn
- ClientData
+func (s *SourceId) CopyFromSourceDb(v sourceDb) {
+ s.Hostname = v.Hostname
+ s.StreamId.ContentId = v.StreamId.ContentId
+ s.StreamId.Format = v.StreamId.Format
+ s.StreamId.Quality = v.StreamId.Quality
+ s.Tags = v.Tags
}
-// stored in dataUpdatesTn
-// in DB, StatisticsData/DataUpdate is flattened compared to JSON DTOs
-type dataUpdateDb struct {
- Id int
- SourceId int // foreign key to sourcesTn
- SourceHubUuid *string
- SourceHubDataUpdateId *int
- StartTime int64 // time.Time
- Duration int64 // duration in milliseconds
- ClientCount uint
- BytesReceived uint
- BytesSent uint
+// stored in clientDataBn
+type clientDataDb struct {
+ Ip string `json:"ip"`
+ UserAgentId int `json:"ua"`
+ BytesSent uint `json:"bs"`
}
-func (self *SourceId) CopyFromSourceDb(value sourceDb) {
- self.Version = value.Version
- self.Hostname = value.Hostname
- self.StreamId.ContentId = value.ContentId
- self.StreamId.Format = value.Format
- self.StreamId.Quality = value.Quality
+// stored in dataUpdatesBn
+type dataUpdateDb struct {
+ SourceHubId int `json:"h,omitempty"`
+ SourceHubDataUpdateId int `json:"hi,omitempty"`
+ SourceId int `json:"si"`
+ StartTime int64 `json:"st"` // unix timestamp in milliseconds
+ Duration int64 `json:"du"` // duration in milliseconds
+ ClientCount uint `json:"cc,omitempty"`
+ BytesReceived uint `json:"br,omitempty"`
+ BytesSent uint `json:"bs,omitempty"`
}
-func (self *SourceId) CopyFromTagsDb(values []tagDb) {
- tags := make([]string, len(values))
- for i := range values {
- tags[i] = values[i].Name
+func NewDataUpdateDb(v DataUpdateFull) dataUpdateDb {
+ return dataUpdateDb{
+ -1,
+ v.SourceHubDataUpdateId,
+ -1,
+ int64(v.StartTime.Unix()*1000) + int64(v.StartTime.Nanosecond()/1000000),
+ v.Duration,
+ v.Data.ClientCount,
+ v.Data.BytesReceived,
+ v.Data.BytesSent,
}
- self.Tags = tags
}
-func (self *StatisticsData) CopyFromDataUpdateDb(value dataUpdateDb, hubId string) {
- if value.SourceHubUuid == nil {
- self.SourceHubUuid = &hubId
+func (s *DataUpdateFull) CopyFromDataUpdateDb(v dataUpdateDb, srcHubUuid, hubUuid string, id int) {
+ if srcHubUuid == "" {
+ s.SourceHubUuid = hubUuid
+ s.SourceHubDataUpdateId = id
} else {
- self.SourceHubUuid = value.SourceHubUuid
- }
- if value.SourceHubDataUpdateId == nil {
- self.SourceHubDataUpdateId = &value.Id
- } else {
- self.SourceHubDataUpdateId = value.SourceHubDataUpdateId
+ s.SourceHubUuid = srcHubUuid
+ s.SourceHubDataUpdateId = v.SourceHubDataUpdateId
+ s.ForwardHubUuid = hubUuid
+ s.ForwardHubDataUpdateId = id
}
- self.StartTime = time.Unix(value.StartTime, 0)
- self.Duration = value.Duration
- self.Data.ClientCount = value.ClientCount
- self.Data.BytesReceived = value.BytesReceived
- self.Data.BytesSent = value.BytesSent
+ s.StartTime = time.Unix((v.StartTime / 1000), (v.StartTime%1000)*1000000)
+ s.Duration = v.Duration
+ s.Data.ClientCount = v.ClientCount
+ s.Data.BytesReceived = v.BytesReceived
+ s.Data.BytesSent = v.BytesSent
}
-func (self *StatisticsData) CopyFromClientDataDb(values []clientDataDb) {
- clients := make([]ClientData, len(values))
- for i := range values {
- clients[i].Ip = values[i].Ip
- clients[i].UserAgent = values[i].UserAgent
- clients[i].BytesSent = values[i].BytesSent
- }
- self.Data.Clients = clients
+func itob(v int) []byte {
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, uint64(v))
+ return b
}
-func cvtToApiStatisticsData(
- hubId string, source sourceDb, update dataUpdateDb, clients []clientDataDb, tags []tagDb) StatisticsData {
- res := StatisticsData{}
- res.CopyFromSourceDb(source)
- res.CopyFromDataUpdateDb(update, hubId)
- res.CopyFromClientDataDb(clients)
- res.CopyFromTagsDb(tags)
- return res
+func btoi(b []byte) int {
+ return int(binary.BigEndian.Uint64(b))
}
diff --git a/src/hub/test-bolt b/src/hub/test-bolt
new file mode 100755
index 0000000..1787181
--- /dev/null
+++ b/src/hub/test-bolt
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name> [ ... args-to-bolt ... ]"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+shift
+
+BIN="$(go env GOPATH)/bin/bolt"
+if [ ! -x "$BIN" ]; then
+ echo "bolt not found. Please run:"
+ echo ""
+ echo " go get -u github.com/boltdb/bolt/..."
+ echo ""
+ exit 1
+fi
+
+exec "$BIN" $@ "$TEST_DB"
diff --git a/src/hub/test-bolter b/src/hub/test-bolter
new file mode 100755
index 0000000..24c8901
--- /dev/null
+++ b/src/hub/test-bolter
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name> [ ... args-to-bolter ... ]"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+shift
+
+BIN="$(go env GOPATH)/bin/bolter"
+if [ ! -x "$BIN" ]; then
+ echo "bolter not found. Please run:"
+ echo ""
+ echo " go get -u github.com/hasit/bolter"
+ echo ""
+ exit 1
+fi
+
+exec "$BIN" --file "$TEST_DB" $@
diff --git a/src/hub/test-client b/src/hub/test-client
index 8a24c7d..fcb9a98 100755
--- a/src/hub/test-client
+++ b/src/hub/test-client
@@ -1,23 +1,22 @@
#!/bin/sh
+
+TEST_D="./test"
+
echo pipe: import sample.json
echo ------------------------
-socat file:../../dat/sample.json,rdonly unix-client:/run/sfive/pipe
+socat file:../../dat/sample.json,rdonly "unix-client:$TEST_D/pipe"
echo pipe-gram: import sample-gram.json
echo ----------------------------------
-while read x; do echo "$x" | socat stdio unix-sendto:/run/sfive/pipegram; done < ../../dat/sample-gram.json
+while read x; do echo "$x" | socat stdio "unix-sendto:$TEST_D/pipegram"; done < ../../dat/sample-gram.json
+
+echo post update
+echo -----------
+curl -i --data @../../dat/sample-post.json 'http://localhost:8000/updates'
echo show query result
echo -----------------
-curl -i 'http://localhost:8000/updates?from=2013-10-21T00:00:00Z&to=2013-10-21T12:31:00Z'
+curl -i 'http://localhost:8000/updates'
-echo '\npost update'
-echo ------------
-curl -i --data @../../dat/sample-post.json 'http://localhost:8000/updates'
-
-echo show stats
-echo ----------
-curl -i 'http://localhost:8000/stats'
echo '\n\ndone'
-
diff --git a/src/hub/test-collector b/src/hub/test-collector
deleted file mode 100755
index e6ca367..0000000
--- a/src/hub/test-collector
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-
-./bin/sfive-hub -db "file:memdb1?mode=memory&cache=shared" -start-pipe-server=false -start-pipegram-server=false -start-web-server -viz-dir "$(pwd)/../viz" -bind=":8000"
-
diff --git a/src/hub/test-fwd b/src/hub/test-fwd
new file mode 100755
index 0000000..371aa55
--- /dev/null
+++ b/src/hub/test-fwd
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name>"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+
+exec ./bin/sfive-hub -db "$TEST_DB" -read-only -start-pipe-server=false -start-pipegram-server=false -start-web-server=false -forward-url="http://localhost:8000"
diff --git a/src/hub/test-fwd-es b/src/hub/test-fwd-es
index 3f3eb12..61bbcf7 100755
--- a/src/hub/test-fwd-es
+++ b/src/hub/test-fwd-es
@@ -1,5 +1,11 @@
#!/bin/sh
-rm -f /run/sfive/pipe /run/sfive/pipegram
-./bin/sfive-hub -start-pipe-server=false -start-pipegram-server=false -start-web-server=false -db db.sqlite -forward-es-url="http://stream.elevate.at:9200/e14"
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name>"
+ exit 1
+fi
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+
+exec ./bin/sfive-hub -db "$TEST_DB" -read-only -start-pipe-server=false -start-pipegram-server=false -start-web-server=false -forward-es-url="http://stream.elevate.at:9200/testing"
diff --git a/src/hub/test-fwd-piwik b/src/hub/test-fwd-piwik
index 1d45219..8a2ae33 100755
--- a/src/hub/test-fwd-piwik
+++ b/src/hub/test-fwd-piwik
@@ -1,4 +1,11 @@
#!/bin/sh
-rm -f /run/sfive/pipe /run/sfive/pipegram
-./bin/sfive-hub -start-pipe-server=false -start-pipegram-server=false -start-web-server=false -db db.sqlite -forward-piwik-url="http://localhost/piwik.php" -piwik-token "asdfjlkasjdflk" -piwik-site-id 4 -piwik-site-url "https://stream.elevate.at"
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name>"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+
+exec ./bin/sfive-hub -db "$TEST_DB" -read-only -start-pipe-server=false -start-pipegram-server=false -start-web-server=false -forward-piwik-url="http://localhost/piwik.php" -piwik-token "asdfjlkasjdflk" -piwik-site-id 4 -piwik-site-url "https://stream.elevate.at"
diff --git a/src/hub/test-import b/src/hub/test-import
index d1a5044..a6c2942 100755
--- a/src/hub/test-import
+++ b/src/hub/test-import
@@ -1,10 +1,12 @@
#!/bin/sh
+
+TEST_D="./test"
+
echo pipe: import sample.json
echo ------------------------
[ -f ../../dat/sample-access.json ] || zcat ../../dat/sample-accesslog.json.gz > ../../dat/sample-accesslog.json
-socat file:../../dat/sample-accesslog.json,rdonly unix-client:/run/sfive/pipe
+socat file:../../dat/sample-accesslog.json,rdonly "unix-client:$TEST_D/pipe"
echo '\n\ndone'
-
diff --git a/src/hub/test-srv b/src/hub/test-srv
index 254549a..064fa3a 100755
--- a/src/hub/test-srv
+++ b/src/hub/test-srv
@@ -1,4 +1,13 @@
#!/bin/sh
-rm -f /run/sfive/pipe /run/sfive/pipegram
-./bin/sfive-hub -db /var/lib/sfive/db.sqlite -start-pipe-server -pipe /var/run/sfive/pipe -start-pipegram-server -pipegram /var/run/sfive/pipegram -start-web-server -viz-dir "$(pwd)/../viz" -bind=":8001"
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name>"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+
+mkdir -p "$TEST_D"
+rm -f "$TEST_D/pipe" "$TEST_D/pipegram"
+exec ./bin/sfive-hub -db "$TEST_DB" -start-pipe-server -pipe "$TEST_D/pipe" -start-pipegram-server -pipegram "$TEST_D/pipegram" -start-web-server -bind=":8000"
diff --git a/src/hub/test-srv-ro b/src/hub/test-srv-ro
new file mode 100755
index 0000000..56fe440
--- /dev/null
+++ b/src/hub/test-srv-ro
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <db-name>"
+ exit 1
+fi
+
+TEST_D="./test"
+TEST_DB="$TEST_D/$1.bolt"
+
+mkdir -p "$TEST_D"
+rm -f "$TEST_D/pipe" "$TEST_D/pipegram"
+exec ./bin/sfive-hub -db "$TEST_DB" -read-only -start-pipe-server -pipe "$TEST_D/pipe" -start-pipegram-server -pipegram "$TEST_D/pipegram" -start-web-server -bind=":8000"
diff --git a/src/viz/README b/src/viz/README
deleted file mode 100644
index 6e4d40c..0000000
--- a/src/viz/README
+++ /dev/null
@@ -1,4 +0,0 @@
-Data Vizualisation
-==================
-
-A single page web application using data form HUB and showing it nicly. \ No newline at end of file
diff --git a/src/viz/dygraph-combined.js b/src/viz/dygraph-combined.js
deleted file mode 100644
index ad25e11..0000000
--- a/src/viz/dygraph-combined.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! @license Copyright 2011 Dan Vanderkam (danvdk@gmail.com) MIT-licensed (http://opensource.org/licenses/MIT) */
-Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};"use strict";function RGBColorParser(f){this.ok=false;if(f.charAt(0)=="#"){f=f.substr(1,6)}f=f.replace(/ /g,"");f=f.toLowerCase();var b={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var g in b){if(f==g){f=b[g]}}var e=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(i){return[parseInt(i[1]),parseInt(i[2]),parseInt(i[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(i){return[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(i){return[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]}}];for(var c=0;c<e.length;c++){var j=e[c].re;var a=e[c].process;var h=j.exec(f);if(h){var d=a(h);this.r=d[0];this.g=d[1];this.b=d[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var i=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(i.length==1){i="0"+i}return"#"+l+k+i}}function printStackTrace(b){b=b||{guess:true};var c=b.e||null,e=!!b.guess;var d=new printStackTrace.implementation(),a=d.run(c);return(e)?d.guessAnonymousFunctions(a):a}printStackTrace.implementation=function(){};printStackTrace.implementation.prototype={run:function(a,b){a=a||this.createException();b=b||this.mode(a);if(b==="other"){return this.other(arguments.callee)}else{return this[b](a)}},createException:function(){try{this.undef()}catch(a){return a}},mode:function(a){if(a["arguments"]&&a.stack){return"chrome"}else{if(typeof a.message==="string"&&typeof window!=="undefined"&&window.opera){if(!a.stacktrace){return"opera9"}if(a.message.indexOf("\n")>-1&&a.message.split("\n").length>a.stacktrace.split("\n").length){return"opera9"}if(!a.stack){return"opera10a"}if(a.stacktrace.indexOf("called from line")<0){return"opera10b"}return"opera11"}else{if(a.stack){return"firefox"}}}return"other"},instrumentFunction:function(b,d,e){b=b||window;var a=b[d];b[d]=function c(){e.call(this,printStackTrace().slice(4));return b[d]._instrumented.apply(this,arguments)};b[d]._instrumented=a},deinstrumentFunction:function(a,b){if(a[b].constructor===Function&&a[b]._instrumented&&a[b]._instrumented.constructor===Function){a[b]=a[b]._instrumented}},chrome:function(b){var a=(b.stack+"\n").replace(/^\S[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^([^\(]+?)([\n$])/gm,"{anonymous}()@$1$2").replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm,"{anonymous}()@$1").split("\n");a.pop();return a},firefox:function(a){return a.stack.replace(/(?:\n@:0)?\s+$/m,"").replace(/^\(/gm,"{anonymous}(").split("\n")},opera11:function(g){var a="{anonymous}",h=/^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/;var k=g.stacktrace.split("\n"),l=[];for(var c=0,f=k.length;c<f;c+=2){var d=h.exec(k[c]);if(d){var j=d[4]+":"+d[1]+":"+d[2];var b=d[3]||"global code";b=b.replace(/<anonymous function: (\S+)>/,"$1").replace(/<anonymous function>/,a);l.push(b+"@"+j+" -- "+k[c+1].replace(/^\s+/,""))}}return l},opera10b:function(g){var a="{anonymous}",h=/^(.*)@(.+):(\d+)$/;var j=g.stacktrace.split("\n"),k=[];for(var c=0,f=j.length;c<f;c++){var d=h.exec(j[c]);if(d){var b=d[1]?(d[1]+"()"):"global code";k.push(b+"@"+d[2]+":"+d[3])}}return k},opera10a:function(g){var a="{anonymous}",h=/Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;var j=g.stacktrace.split("\n"),k=[];for(var c=0,f=j.length;c<f;c+=2){var d=h.exec(j[c]);if(d){var b=d[3]||a;k.push(b+"()@"+d[2]+":"+d[1]+" -- "+j[c+1].replace(/^\s+/,""))}}return k},opera9:function(j){var d="{anonymous}",h=/Line (\d+).*script (?:in )?(\S+)/i;var c=j.message.split("\n"),b=[];for(var g=2,a=c.length;g<a;g+=2){var f=h.exec(c[g]);if(f){b.push(d+"()@"+f[2]+":"+f[1]+" -- "+c[g+1].replace(/^\s+/,""))}}return b},other:function(g){var b="{anonymous}",f=/function\s*([\w\-$]+)?\s*\(/i,a=[],d,c,e=10;while(g&&a.length<e){d=f.test(g.toString())?RegExp.$1||b:b;c=Array.prototype.slice.call(g["arguments"]||[]);a[a.length]=d+"("+this.stringifyArguments(c)+")";g=g.caller}return a},stringifyArguments:function(c){var b=[];var e=Array.prototype.slice;for(var d=0;d<c.length;++d){var a=c[d];if(a===undefined){b[d]="undefined"}else{if(a===null){b[d]="null"}else{if(a.constructor){if(a.constructor===Array){if(a.length<3){b[d]="["+this.stringifyArguments(a)+"]"}else{b[d]="["+this.stringifyArguments(e.call(a,0,1))+"..."+this.stringifyArguments(e.call(a,-1))+"]"}}else{if(a.constructor===Object){b[d]="#object"}else{if(a.constructor===Function){b[d]="#function"}else{if(a.constructor===String){b[d]='"'+a+'"'}else{if(a.constructor===Number){b[d]=a}}}}}}}}}return b.join(",")},sourceCache:{},ajax:function(a){var b=this.createXMLHTTPObject();if(b){try{b.open("GET",a,false);b.send(null);return b.responseText}catch(c){}}return""},createXMLHTTPObject:function(){var c,a=[function(){return new XMLHttpRequest()},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var b=0;b<a.length;b++){try{c=a[b]();this.createXMLHTTPObject=a[b];return c}catch(d){}}},isSameDomain:function(a){return a.indexOf(location.hostname)!==-1},getSource:function(a){if(!(a in this.sourceCache)){this.sourceCache[a]=this.ajax(a).split("\n")}return this.sourceCache[a]},guessAnonymousFunctions:function(k){for(var g=0;g<k.length;++g){var f=/\{anonymous\}\(.*\)@(.*)/,l=/^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,b=k[g],c=f.exec(b);if(c){var e=l.exec(c[1]),d=e[1],a=e[2],j=e[3]||0;if(d&&this.isSameDomain(d)&&a){var h=this.guessAnonymousFunction(d,a,j);k[g]=b.replace("{anonymous}",h)}}}return k},guessAnonymousFunction:function(c,f,a){var b;try{b=this.findFunctionName(this.getSource(c),f)}catch(d){b="getSource failed with url: "+c+", exception: "+d.toString()}return b},findFunctionName:function(a,e){var g=/function\s+([^(]*?)\s*\(([^)]*)\)/;var k=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/;var h=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/;var b="",l,j=Math.min(e,20),d,c;for(var f=0;f<j;++f){l=a[e-f-1];c=l.indexOf("//");if(c>=0){l=l.substr(0,c)}if(l){b=l+b;d=k.exec(b);if(d&&d[1]){return d[1]}d=g.exec(b);if(d&&d[1]){return d[1]}d=h.exec(b);if(d&&d[1]){return d[1]}}}return"(?)"}};CanvasRenderingContext2D.prototype.installPattern=function(e){if(typeof(this.isPatternInstalled)!=="undefined"){throw"Must un-install old line pattern before installing a new one."}this.isPatternInstalled=true;var g=[0,0];var b=[];var f=this.beginPath;var d=this.lineTo;var c=this.moveTo;var a=this.stroke;this.uninstallPattern=function(){this.beginPath=f;this.lineTo=d;this.moveTo=c;this.stroke=a;this.uninstallPattern=undefined;this.isPatternInstalled=undefined};this.beginPath=function(){b=[];f.call(this)};this.moveTo=function(h,i){b.push([[h,i]]);c.call(this,h,i)};this.lineTo=function(h,j){var i=b[b.length-1];i.push([h,j])};this.stroke=function(){if(b.length===0){a.call(this);return}for(var p=0;p<b.length;p++){var o=b[p];var l=o[0][0],u=o[0][1];for(var n=1;n<o.length;n++){var h=o[n][0],s=o[n][1];this.save();var w=(h-l);var v=(s-u);var r=Math.sqrt(w*w+v*v);var k=Math.atan2(v,w);this.translate(l,u);c.call(this,0,0);this.rotate(k);var m=g[0];var t=0;while(r>t){var q=e[m];if(g[1]){t+=g[1]}else{t+=q}if(t>r){g=[m,t-r];t=r}else{g=[(m+1)%e.length,0]}if(m%2===0){d.call(this,t,0)}else{c.call(this,t,0)}m=(m+1)%e.length}this.restore();l=h,u=s}}a.call(this);b=[]}};CanvasRenderingContext2D.prototype.uninstallPattern=function(){throw"Must install a line pattern before uninstalling it."};var DygraphOptions=(function(){var a=function(b){this.dygraph_=b;this.yAxes_=[];this.xAxis_={};this.series_={};this.global_=this.dygraph_.attrs_;this.user_=this.dygraph_.user_attrs_||{};this.labels_=[];this.highlightSeries_=this.get("highlightSeriesOpts")||{};this.reparseSeries()};a.AXIS_STRING_MAPPINGS_={y:0,Y:0,y1:0,Y1:0,y2:1,Y2:1};a.axisToIndex_=function(b){if(typeof(b)=="string"){if(a.AXIS_STRING_MAPPINGS_.hasOwnProperty(b)){return a.AXIS_STRING_MAPPINGS_[b]}throw"Unknown axis : "+b}if(typeof(b)=="number"){if(b===0||b===1){return b}throw"Dygraphs only supports two y-axes, indexed from 0-1."}if(b){throw"Unknown axis : "+b}return 0};a.prototype.reparseSeries=function(){var g=this.get("labels");if(!g){return}this.labels_=g.slice(1);this.yAxes_=[{series:[],options:{}}];this.xAxis_={options:{}};this.series_={};var h=!this.user_.series;if(h){var c=0;for(var j=0;j<this.labels_.length;j++){var i=this.labels_[j];var e=this.user_[i]||{};var b=0;var d=e.axis;if(typeof(d)=="object"){b=++c;this.yAxes_[b]={series:[i],options:d}}if(!d){this.yAxes_[0].series.push(i)}this.series_[i]={idx:j,yAxis:b,options:e}}for(var j=0;j<this.labels_.length;j++){var i=this.labels_[j];var e=this.series_[i]["options"];var d=e.axis;if(typeof(d)=="string"){if(!this.series_.hasOwnProperty(d)){Dygraph.error("Series "+i+" wants to share a y-axis with series "+d+", which does not define its own axis.");return}var b=this.series_[d].yAxis;this.series_[i].yAxis=b;this.yAxes_[b].series.push(i)}}}else{for(var j=0;j<this.labels_.length;j++){var i=this.labels_[j];var e=this.user_.series[i]||{};var b=a.axisToIndex_(e.axis);this.series_[i]={idx:j,yAxis:b,options:e};if(!this.yAxes_[b]){this.yAxes_[b]={series:[i],options:{}}}else{this.yAxes_[b].series.push(i)}}}var f=this.user_.axes||{};Dygraph.update(this.yAxes_[0].options,f.y||{});if(this.yAxes_.length>1){Dygraph.update(this.yAxes_[1].options,f.y2||{})}Dygraph.update(this.xAxis_.options,f.x||{})};a.prototype.get=function(c){var b=this.getGlobalUser_(c);if(b!==null){return b}return this.getGlobalDefault_(c)};a.prototype.getGlobalUser_=function(b){if(this.user_.hasOwnProperty(b)){return this.user_[b]}return null};a.prototype.getGlobalDefault_=function(b){if(this.global_.hasOwnProperty(b)){return this.global_[b]}if(Dygraph.DEFAULT_ATTRS.hasOwnProperty(b)){return Dygraph.DEFAULT_ATTRS[b]}return null};a.prototype.getForAxis=function(d,e){var f;var i;if(typeof(e)=="number"){f=e;i=f===0?"y":"y2"}else{if(e=="y1"){e="y"}if(e=="y"){f=0}else{if(e=="y2"){f=1}else{if(e=="x"){f=-1}else{throw"Unknown axis "+e}}}i=e}var g=(f==-1)?this.xAxis_:this.yAxes_[f];if(g){var h=g.options;if(h.hasOwnProperty(d)){return h[d]}}var c=this.getGlobalUser_(d);if(c!==null){return c}var b=Dygraph.DEFAULT_ATTRS.axes[i];if(b.hasOwnProperty(d)){return b[d]}return this.getGlobalDefault_(d)};a.prototype.getForSeries=function(c,e){if(e===this.dygraph_.getHighlightSeries()){if(this.highlightSeries_.hasOwnProperty(c)){return this.highlightSeries_[c]}}if(!this.series_.hasOwnProperty(e)){throw"Unknown series: "+e}var d=this.series_[e];var b=d.options;if(b.hasOwnProperty(c)){return b[c]}return this.getForAxis(c,d.yAxis)};a.prototype.numAxes=function(){return this.yAxes_.length};a.prototype.axisForSeries=function(b){return this.series_[b].yAxis};a.prototype.axisOptions=function(b){return this.yAxes_[b].options};a.prototype.seriesForAxis=function(b){return this.yAxes_[b].series};a.prototype.seriesNames=function(){return this.labels_};return a})();"use strict";var DygraphLayout=function(a){this.dygraph_=a;this.points=[];this.setNames=[];this.annotations=[];this.yAxes_=null;this.xTicks_=null;this.yTicks_=null};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.points.push(b);this.setNames.push(a)};DygraphLayout.prototype.getPlotArea=function(){return this.area_};DygraphLayout.prototype.computePlotArea=function(){var a={x:0,y:0};a.w=this.dygraph_.width_-a.x-this.attr_("rightGap");a.h=this.dygraph_.height_;var b={chart_div:this.dygraph_.graphDiv,reserveSpaceLeft:function(c){var d={x:a.x,y:a.y,w:c,h:a.h};a.x+=c;a.w-=c;return d},reserveSpaceRight:function(c){var d={x:a.x+a.w-c,y:a.y,w:c,h:a.h};a.w-=c;return d},reserveSpaceTop:function(c){var d={x:a.x,y:a.y,w:a.w,h:c};a.y+=c;a.h-=c;return d},reserveSpaceBottom:function(c){var d={x:a.x,y:a.y+a.h-c,w:a.w,h:c};a.h-=c;return d},chartRect:function(){return{x:a.x,y:a.y,w:a.w,h:a.h}}};this.dygraph_.cascadeEvents_("layout",b);this.area_=a};DygraphLayout.prototype.setAnnotations=function(d){this.annotations=[];var e=this.attr_("xValueParser")||function(a){return a};for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&d[c].x===undefined){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.setXTicks=function(a){this.xTicks_=a};DygraphLayout.prototype.setYAxes=function(a){this.yAxes_=a};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){var a=this.dygraph_.xAxisRange();this.minxval=a[0];this.maxxval=a[1];var d=a[1]-a[0];this.xscale=(d!==0?1/d:1);for(var b=0;b<this.yAxes_.length;b++){var c=this.yAxes_[b];c.minyval=c.computedValueRange[0];c.maxyval=c.computedValueRange[1];c.yrange=c.maxyval-c.minyval;c.yscale=(c.yrange!==0?1/c.yrange:1);if(c.g.attr_("logscale")){c.ylogrange=Dygraph.log10(c.maxyval)-Dygraph.log10(c.minyval);c.ylogscale=(c.ylogrange!==0?1/c.ylogrange:1);if(!isFinite(c.ylogrange)||isNaN(c.ylogrange)){c.g.error("axis "+b+" of graph at "+c.g+" can't be displayed in log scale for range ["+c.minyval+" - "+c.maxyval+"]")}}}};DygraphLayout._calcYNormal=function(b,c,a){if(a){return 1-((Dygraph.log10(c)-Dygraph.log10(b.minyval))*b.ylogscale)}else{return 1-((c-b.minyval)*b.yscale)}};DygraphLayout.prototype._evaluateLineCharts=function(){var c=this.attr_("connectSeparatedPoints");var k=this.attr_("stackedGraph");var e=this.attr_("errorBars")||this.attr_("customBars");for(var a=0;a<this.points.length;a++){var l=this.points[a];var f=this.setNames[a];var b=this.dygraph_.axisPropertiesForSeries(f);var g=this.dygraph_.attributes_.getForSeries("logscale",f);for(var d=0;d<l.length;d++){var i=l[d];i.x=(i.xval-this.minxval)*this.xscale;var h=i.yval;if(k){i.y_stacked=DygraphLayout._calcYNormal(b,i.yval_stacked,g);if(h!==null&&!isNaN(h)){h=i.yval_stacked}}if(h===null){h=NaN;if(!c){i.yval=NaN}}i.y=DygraphLayout._calcYNormal(b,h,g);if(e){i.y_top=DygraphLayout._calcYNormal(b,h-i.yval_minus,g);i.y_bottom=DygraphLayout._calcYNormal(b,h+i.yval_plus,g)}}}};DygraphLayout.parseFloat_=function(a){if(a===null){return NaN}return a};DygraphLayout.prototype._evaluateLineTicks=function(){var d,c,b,f;this.xticks=[];for(d=0;d<this.xTicks_.length;d++){c=this.xTicks_[d];b=c.label;f=this.xscale*(c.v-this.minxval);if((f>=0)&&(f<=1)){this.xticks.push([f,b])}}this.yticks=[];for(d=0;d<this.yAxes_.length;d++){var e=this.yAxes_[d];for(var a=0;a<e.ticks.length;a++){c=e.ticks[a];b=c.label;f=this.dygraph_.toPercentYCoord(c.v,d);if((f>=0)&&(f<=1)){this.yticks.push([d,f,b])}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var d;var g={};for(d=0;d<this.annotations.length;d++){var b=this.annotations[d];g[b.xval+","+b.series]=b}this.annotated_points=[];if(!this.annotations||!this.annotations.length){return}for(var h=0;h<this.points.length;h++){var e=this.points[h];for(d=0;d<e.length;d++){var f=e[d];var c=f.xval+","+f.name;if(c in g){f.annotation=g[c];this.annotated_points.push(f)}}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.points;delete this.setNames;delete this.setPointsLengths;delete this.setPointsOffsets;this.points=[];this.setNames=[];this.setPointsLengths=[];this.setPointsOffsets=[]};"use strict";var DygraphCanvasRenderer=function(d,c,b,e){this.dygraph_=d;this.layout=e;this.element=c;this.elementContext=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.area=e.getPlotArea();this.container.style.position="relative";this.container.style.width=this.width+"px";if(this.dygraph_.isUsingExcanvas_){this._createIEClipArea()}else{if(!Dygraph.isAndroid()){var a=this.dygraph_.canvas_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip();a=this.dygraph_.hidden_ctx_;a.beginPath();a.rect(this.area.x,this.area.y,this.area.w,this.area.h);a.clip()}}};DygraphCanvasRenderer.prototype.attr_=function(a,b){return this.dygraph_.attr_(a,b)};DygraphCanvasRenderer.prototype.clear=function(){var a;if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}a=this.elementContext}catch(b){return}}a=this.elementContext;a.clearRect(0,0,this.width,this.height)};DygraphCanvasRenderer.isSupported=function(f){var b=null;try{if(typeof(f)=="undefined"||f===null){b=document.createElement("canvas")}else{b=f}b.getContext("2d")}catch(c){var d=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!d)||(d[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.render=function(){this._updatePoints();this._renderLineChart()};DygraphCanvasRenderer.prototype._createIEClipArea=function(){var g="dygraph-clip-div";var f=this.dygraph_.graphDiv;for(var e=f.childNodes.length-1;e>=0;e--){if(f.childNodes[e].className==g){f.removeChild(f.childNodes[e])}}var c=document.bgColor;var d=this.dygraph_.graphDiv;while(d!=document){var a=d.currentStyle.backgroundColor;if(a&&a!="transparent"){c=a;break}d=d.parentNode}function b(j){if(j.w===0||j.h===0){return}var i=document.createElement("div");i.className=g;i.style.backgroundColor=c;i.style.position="absolute";i.style.left=j.x+"px";i.style.top=j.y+"px";i.style.width=j.w+"px";i.style.height=j.h+"px";f.appendChild(i)}var h=this.area;b({x:0,y:0,w:h.x,h:this.height});b({x:h.x,y:0,w:this.width-h.x,h:h.y});b({x:h.x+h.w,y:0,w:this.width-h.x-h.w,h:this.height});b({x:h.x,y:h.y+h.h,w:this.width-h.x,h:this.height-h.h-h.y})};DygraphCanvasRenderer._getIteratorPredicate=function(a){return a?DygraphCanvasRenderer._predicateThatSkipsEmptyPoints:null};DygraphCanvasRenderer._predicateThatSkipsEmptyPoints=function(b,a){return b[a].yval!==null};DygraphCanvasRenderer._drawStyledLine=function(i,a,m,q,f,n,d){var h=i.dygraph;var c=h.getOption("stepPlot",i.setName);if(!Dygraph.isArrayLike(q)){q=null}var l=h.getOption("drawGapEdgePoints",i.setName);var o=i.points;var k=Dygraph.createIterator(o,0,o.length,DygraphCanvasRenderer._getIteratorPredicate(h.getOption("connectSeparatedPoints")));var j=q&&(q.length>=2);var p=i.drawingContext;p.save();if(j){p.installPattern(q)}var b=DygraphCanvasRenderer._drawSeries(i,k,m,d,f,l,c,a);DygraphCanvasRenderer._drawPointsOnLine(i,b,n,a,d);if(j){p.uninstallPattern()}p.restore()};DygraphCanvasRenderer._drawSeries=function(v,t,m,h,p,s,g,q){var b=null;var w=null;var k=null;var j;var o;var l=[];var f=true;var n=v.drawingContext;n.beginPath();n.strokeStyle=q;n.lineWidth=m;var c=t.array_;var u=t.end_;var a=t.predicate_;for(var r=t.start_;r<u;r++){o=c[r];if(a){while(r<u&&!a(c,r)){r++}if(r==u){break}o=c[r]}if(o.canvasy===null||o.canvasy!=o.canvasy){if(g&&b!==null){n.moveTo(b,w);n.lineTo(o.canvasx,w)}b=w=null}else{j=false;if(s||!b){t.nextIdx_=r;t.next();k=t.hasNext?t.peek.canvasy:null;var d=k===null||k!=k;j=(!b&&d);if(s){if((!f&&!b)||(t.hasNext&&d)){j=true}}}if(b!==null){if(m){if(g){n.moveTo(b,w);n.lineTo(o.canvasx,w)}n.lineTo(o.canvasx,o.canvasy)}}else{n.moveTo(o.canvasx,o.canvasy)}if(p||j){l.push([o.canvasx,o.canvasy,o.idx])}b=o.canvasx;w=o.canvasy}f=false}n.stroke();return l};DygraphCanvasRenderer._drawPointsOnLine=function(h,i,f,d,g){var c=h.drawingContext;for(var b=0;b<i.length;b++){var a=i[b];c.save();f(h.dygraph,h.setName,c,a[0],a[1],d,g,a[2]);c.restore()}};DygraphCanvasRenderer.prototype._updatePoints=function(){var e=this.layout.points;for(var c=e.length;c--;){var d=e[c];for(var b=d.length;b--;){var a=d[b];a.canvasx=this.area.w*a.x+this.area.x;a.canvasy=this.area.h*a.y+this.area.y}}};DygraphCanvasRenderer.prototype._renderLineChart=function(g,u){var h=u||this.elementContext;var n;var a=this.layout.points;var s=this.layout.setNames;var b;this.colors=this.dygraph_.colorsMap_;var o=this.attr_("plotter");var f=o;if(!Dygraph.isArrayLike(f)){f=[f]}var c={};for(n=0;n<s.length;n++){b=s[n];var t=this.attr_("plotter",b);if(t==o){continue}c[b]=t}for(n=0;n<f.length;n++){var r=f[n];var q=(n==f.length-1);for(var l=0;l<a.length;l++){b=s[l];if(g&&b!=g){continue}var m=a[l];var e=r;if(b in c){if(q){e=c[b]}else{continue}}var k=this.colors[b];var d=this.dygraph_.getOption("strokeWidth",b);h.save();h.strokeStyle=k;h.lineWidth=d;e({points:m,setName:b,drawingContext:h,color:k,strokeWidth:d,dygraph:this.dygraph_,axis:this.dygraph_.axisPropertiesForSeries(b),plotArea:this.area,seriesIndex:l,seriesCount:a.length,singleSeriesName:g,allSeriesPoints:a});h.restore()}}};DygraphCanvasRenderer._Plotters={linePlotter:function(a){DygraphCanvasRenderer._linePlotter(a)},fillPlotter:function(a){DygraphCanvasRenderer._fillPlotter(a)},errorPlotter:function(a){DygraphCanvasRenderer._errorPlotter(a)}};DygraphCanvasRenderer._linePlotter=function(f){var d=f.dygraph;var h=f.setName;var i=f.strokeWidth;var a=d.getOption("strokeBorderWidth",h);var j=d.getOption("drawPointCallback",h)||Dygraph.Circles.DEFAULT;var k=d.getOption("strokePattern",h);var c=d.getOption("drawPoints",h);var b=d.getOption("pointSize",h);if(a&&i){DygraphCanvasRenderer._drawStyledLine(f,d.getOption("strokeBorderColor",h),i+2*a,k,c,j,b)}DygraphCanvasRenderer._drawStyledLine(f,f.color,i,k,c,j,b)};DygraphCanvasRenderer._errorPlotter=function(s){var r=s.dygraph;var f=s.setName;var t=r.getOption("errorBars")||r.getOption("customBars");if(!t){return}var k=r.getOption("fillGraph",f);if(k){r.warn("Can't use fillGraph option with error bars")}var m=s.drawingContext;var n=s.color;var o=r.getOption("fillAlpha",f);var i=r.getOption("stepPlot",f);var p=s.points;var q=Dygraph.createIterator(p,0,p.length,DygraphCanvasRenderer._getIteratorPredicate(r.getOption("connectSeparatedPoints")));var j;var h=NaN;var c=NaN;var d=[-1,-1];var a=new RGBColorParser(n);var u="rgba("+a.r+","+a.g+","+a.b+","+o+")";m.fillStyle=u;m.beginPath();var b=function(e){return(e===null||e===undefined||isNaN(e))};while(q.hasNext){var l=q.next();if((!i&&b(l.y))||(i&&!isNaN(c)&&b(c))){h=NaN;continue}if(i){j=[l.y_bottom,l.y_top];c=l.y}else{j=[l.y_bottom,l.y_top]}j[0]=s.plotArea.h*j[0]+s.plotArea.y;j[1]=s.plotArea.h*j[1]+s.plotArea.y;if(!isNaN(h)){if(i){m.moveTo(h,d[0]);m.lineTo(l.canvasx,d[0]);m.lineTo(l.canvasx,d[1])}else{m.moveTo(h,d[0]);m.lineTo(l.canvasx,j[0]);m.lineTo(l.canvasx,j[1])}m.lineTo(h,d[1]);m.closePath()}d=j;h=l.canvasx}m.fill()};DygraphCanvasRenderer._fillPlotter=function(F){if(F.singleSeriesName){return}if(F.seriesIndex!==0){return}var D=F.dygraph;var I=D.getLabels().slice(1);for(var C=I.length;C>=0;C--){if(!D.visibility()[C]){I.splice(C,1)}}var h=(function(){for(var e=0;e<I.length;e++){if(D.getOption("fillGraph",I[e])){return true}}return false})();if(!h){return}var w=F.drawingContext;var E=F.plotArea;var c=F.allSeriesPoints;var z=c.length;var y=D.getOption("fillAlpha");var k=D.getOption("stackedGraph");var q=D.getColors();var s={};var a;var r;for(var u=z-1;u>=0;u--){var n=I[u];if(!D.getOption("fillGraph",n)){continue}var p=D.getOption("stepPlot",n);var x=q[u];var f=D.axisPropertiesForSeries(n);var d=1+f.minyval*f.yscale;if(d<0){d=0}else{if(d>1){d=1}}d=E.h*d+E.y;var B=c[u];var A=Dygraph.createIterator(B,0,B.length,DygraphCanvasRenderer._getIteratorPredicate(D.getOption("connectSeparatedPoints")));var m=NaN;var l=[-1,-1];var t;var b=new RGBColorParser(x);var H="rgba("+b.r+","+b.g+","+b.b+","+y+")";w.fillStyle=H;w.beginPath();var j,G=true;while(A.hasNext){var v=A.next();if(!Dygraph.isOK(v.y)){m=NaN;if(v.y_stacked!==null&&!isNaN(v.y_stacked)){s[v.canvasx]=E.h*v.y_stacked+E.y}continue}if(k){if(!G&&j==v.xval){continue}else{G=false;j=v.xval}a=s[v.canvasx];var o;if(a===undefined){o=d}else{if(r){o=a[0]}else{o=a}}t=[v.canvasy,o];if(p){if(l[0]===-1){s[v.canvasx]=[v.canvasy,d]}else{s[v.canvasx]=[v.canvasy,l[0]]}}else{s[v.canvasx]=v.canvasy}}else{t=[v.canvasy,d]}if(!isNaN(m)){w.moveTo(m,l[0]);if(p){w.lineTo(v.canvasx,l[0])}else{w.lineTo(v.canvasx,t[0])}if(r&&a){w.lineTo(v.canvasx,a[1])}else{w.lineTo(v.canvasx,t[1])}w.lineTo(m,l[1]);w.closePath()}l=t;m=v.canvasx}r=p;w.fill()}};"use strict";var Dygraph=function(d,c,b,a){this.is_initial_draw_=true;this.readyFns_=[];if(a!==undefined){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(d,c,b,a)}else{this.__init__(d,c,b)}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.0.1";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.ANIMATION_STEPS=12;Dygraph.ANIMATION_DURATION=200;Dygraph.KMB_LABELS=["K","M","B","T","Q"];Dygraph.KMG2_BIG_LABELS=["k","M","G","T","P","E","Z","Y"];Dygraph.KMG2_SMALL_LABELS=["m","u","n","p","f","a","z","y"];Dygraph.numberValueFormatter=function(s,a,u,l){var e=a("sigFigs");if(e!==null){return Dygraph.floatFormat(s,e)}var c=a("digitsAfterDecimal");var o=a("maxNumberWidth");var b=a("labelsKMB");var r=a("labelsKMG2");var q;if(s!==0&&(Math.abs(s)>=Math.pow(10,o)||Math.abs(s)<Math.pow(10,-c))){q=s.toExponential(c)}else{q=""+Dygraph.round_(s,c)}if(b||r){var f;var t=[];var m=[];if(b){f=1000;t=Dygraph.KMB_LABELS}if(r){if(b){Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}f=1024;t=Dygraph.KMG2_BIG_LABELS;m=Dygraph.KMG2_SMALL_LABELS}var p=Math.abs(s);var d=Dygraph.pow(f,t.length);for(var h=t.length-1;h>=0;h--,d/=f){if(p>=d){q=Dygraph.round_(s/d,c)+t[h];break}}if(r){var i=String(s.toExponential()).split("e-");if(i.length===2&&i[1]>=3&&i[1]<=24){if(i[1]%3>0){q=Dygraph.round_(i[0]/Dygraph.pow(10,(i[1]%3)),c)}else{q=Number(i[0]).toFixed(2)}q+=m[Math.floor(i[1]/3)-1]}}}return q};Dygraph.numberAxisLabelFormatter=function(a,d,c,b){return Dygraph.numberValueFormatter(a,c,b)};Dygraph.dateString_=function(e){var i=Dygraph.zeropad;var h=new Date(e);var f=""+h.getFullYear();var g=i(h.getMonth()+1);var a=i(h.getDate());var c="";var b=h.getHours()*3600+h.getMinutes()*60+h.getSeconds();if(b){c=" "+Dygraph.hmsString_(e)}return f+"/"+g+"/"+a+c};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.DECADAL){return b.strftime("%Y")}else{if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a===0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}}};Dygraph.Plotters=DygraphCanvasRenderer._Plotters;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,highlightSeriesOpts:null,highlightSeriesBackgroundAlpha:0.5,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,digitsAfterDecimal:2,maxNumberWidth:6,sigFigs:null,strokeWidth:1,strokeBorderWidth:0,strokeBorderColor:"white",axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueParser:Dygraph.dateParser,delimiter:",",sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,stackedGraphNaNFill:"all",hideOverlayOnMouseOut:true,legend:"onmouseover",stepPlot:false,avoidMinZero:false,xRangePad:0,yRangePad:null,drawAxesAtZero:false,titleHeight:28,xLabelHeight:18,yLabelWidth:18,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.3,gridLineWidth:0.3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",interactionModel:null,animatedZooms:false,showRangeSelector:false,rangeSelectorHeight:40,rangeSelectorPlotStrokeColor:"#808FAB",rangeSelectorPlotFillColor:"#A7B1C4",plotter:[Dygraph.Plotters.fillPlotter,Dygraph.Plotters.errorPlotter,Dygraph.Plotters.linePlotter],plugins:[],axes:{x:{pixelsPerLabel:60,axisLabelFormatter:Dygraph.dateAxisFormatter,valueFormatter:Dygraph.dateString_,drawGrid:true,independentTicks:true,ticker:null},y:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,drawGrid:true,independentTicks:true,ticker:null},y2:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,drawGrid:false,independentTicks:false,ticker:null}}};Dygraph.HORIZONTAL=1;Dygraph.VERTICAL=2;Dygraph.PLUGINS=[];Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!==null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(a,c,l){if(/MSIE/.test(navigator.userAgent)&&!window.opera&&typeof(G_vmlCanvasManager)!="undefined"&&document.readyState!="complete"){var o=this;setTimeout(function(){o.__init__(a,c,l)},100);return}if(l===null||l===undefined){l={}}l=Dygraph.mapLegacyOptions_(l);if(typeof(a)=="string"){a=document.getElementById(a)}if(!a){Dygraph.error("Constructing dygraph with a non-existent div!");return}this.isUsingExcanvas_=typeof(G_vmlCanvasManager)!="undefined";this.maindiv_=a;this.file_=c;this.rollPeriod_=l.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=l.fractions||false;this.dateWindow_=l.dateWindow||null;this.annotations_=[];this.zoomed_x_=false;this.zoomed_y_=false;a.innerHTML="";if(a.style.width===""&&l.width){a.style.width=l.width+"px"}if(a.style.height===""&&l.height){a.style.height=l.height+"px"}if(a.style.height===""&&a.clientHeight===0){a.style.height=Dygraph.DEFAULT_HEIGHT+"px";if(a.style.width===""){a.style.width=Dygraph.DEFAULT_WIDTH+"px"}}this.width_=a.clientWidth||l.width||0;this.height_=a.clientHeight||l.height||0;if(l.stackedGraph){l.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,l);this.attrs_={};Dygraph.updateDeep(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.setIndexByName_={};this.datasetIndex_=[];this.registeredEvents_=[];this.eventListeners_={};this.attributes_=new DygraphOptions(this);this.createInterface_();this.plugins_=[];var d=Dygraph.PLUGINS.concat(this.getOption("plugins"));for(var g=0;g<d.length;g++){var k=d[g];var f=new k();var j={plugin:f,events:{},options:{},pluginOptions:{}};var b=f.activate(this);for(var h in b){j.events[h]=b[h]}this.plugins_.push(j)}for(var g=0;g<this.plugins_.length;g++){var n=this.plugins_[g];for(var h in n.events){if(!n.events.hasOwnProperty(h)){continue}var m=n.events[h];var e=[n.plugin,m];if(!(h in this.eventListeners_)){this.eventListeners_[h]=[e]}else{this.eventListeners_[h].push(e)}}}this.createDragInterface_();this.start_()};Dygraph.prototype.cascadeEvents_=function(c,b){if(!(c in this.eventListeners_)){return true}var g={dygraph:this,cancelable:false,defaultPrevented:false,preventDefault:function(){if(!g.cancelable){throw"Cannot call preventDefault on non-cancelable event."}g.defaultPrevented=true},propagationStopped:false,stopPropagation:function(){g.propagationStopped=true}};Dygraph.update(g,b);var a=this.eventListeners_[c];if(a){for(var d=a.length-1;d>=0;d--){var f=a[d][0];var h=a[d][1];h.call(f,g);if(g.propagationStopped){break}}}return g.defaultPrevented};Dygraph.prototype.isZoomed=function(a){if(a===null||a===undefined){return this.zoomed_x_||this.zoomed_y_}if(a==="x"){return this.zoomed_x_}if(a==="y"){return this.zoomed_y_}throw"axis parameter is ["+a+"] must be null, 'x' or 'y'."};Dygraph.prototype.toString=function(){var a=this.maindiv_;var b=(a&&a.id)?a.id:a;return"[Dygraph "+b+"]"};Dygraph.prototype.attr_=function(b,a){return a?this.attributes_.getForSeries(b,a):this.attributes_.get(b)};Dygraph.prototype.getOption=function(a,b){return this.attr_(a,b)};Dygraph.prototype.getOptionForAxis=function(a,b){return this.attributes_.getForAxis(a,b)};Dygraph.prototype.optionsViewForAxis_=function(b){var a=this;return function(c){var d=a.user_attrs_.axes;if(d&&d[b]&&d[b].hasOwnProperty(c)){return d[b][c]}if(typeof(a.user_attrs_[c])!="undefined"){return a.user_attrs_[c]}d=a.attrs_.axes;if(d&&d[b]&&d[b].hasOwnProperty(c)){return d[b][c]}if(b=="y"&&a.axes_[0].hasOwnProperty(c)){return a.axes_[0][c]}else{if(b=="y2"&&a.axes_[1].hasOwnProperty(c)){return a.axes_[1][c]}}return a.attr_(c)}};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){return this.dateWindow_?this.dateWindow_:this.xAxisExtremes()};Dygraph.prototype.xAxisExtremes=function(){var d=this.attr_("xRangePad")/this.plotter_.area.w;if(this.numRows()===0){return[0-d,1+d]}var c=this.rawData_[0][0];var b=this.rawData_[this.rawData_.length-1][0];if(d){var a=b-c;c-=a*d;b+=a*d}return[c,b]};Dygraph.prototype.yAxisRange=function(a){if(typeof(a)=="undefined"){a=0}if(a<0||a>=this.axes_.length){return null}var b=this.axes_[a];return[b.computedValueRange[0],b.computedValueRange[1]]};Dygraph.prototype.yAxisRanges=function(){var a=[];for(var b=0;b<this.axes_.length;b++){a.push(this.yAxisRange(b))}return a};Dygraph.prototype.toDomCoords=function(a,c,b){return[this.toDomXCoord(a),this.toDomYCoord(c,b)]};Dygraph.prototype.toDomXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return c.x+(b-a[0])/(a[1]-a[0])*c.w};Dygraph.prototype.toDomYCoord=function(d,a){var c=this.toPercentYCoord(d,a);if(c===null){return null}var b=this.plotter_.area;return b.y+c*b.h};Dygraph.prototype.toDataCoords=function(a,c,b){return[this.toDataXCoord(a),this.toDataYCoord(c,b)]};Dygraph.prototype.toDataXCoord=function(b){if(b===null){return null}var c=this.plotter_.area;var a=this.xAxisRange();return a[0]+(b-c.x)/c.w*(a[1]-a[0])};Dygraph.prototype.toDataYCoord=function(h,b){if(h===null){return null}var c=this.plotter_.area;var g=this.yAxisRange(b);if(typeof(b)=="undefined"){b=0}if(!this.attributes_.getForAxis("logscale",b)){return g[0]+(c.y+c.h-h)/c.h*(g[1]-g[0])}else{var f=(h-c.y)/c.h;var a=Dygraph.log10(g[1]);var e=a-(f*(a-Dygraph.log10(g[0])));var d=Math.pow(Dygraph.LOG_SCALE,e);return d}};Dygraph.prototype.toPercentYCoord=function(f,c){if(f===null){return null}if(typeof(c)=="undefined"){c=0}var e=this.yAxisRange(c);var d;var b=this.attributes_.getForAxis("logscale",c);if(!b){d=(e[1]-f)/(e[1]-e[0])}else{var a=Dygraph.log10(e[1]);d=(a-Dygraph.log10(f))/(a-Dygraph.log10(e[0]))}return d};Dygraph.prototype.toPercentXCoord=function(b){if(b===null){return null}var a=this.xAxisRange();return(b-a[0])/(a[1]-a[0])};Dygraph.prototype.numColumns=function(){if(!this.rawData_){return 0}return this.rawData_[0]?this.rawData_[0].length:this.attr_("labels").length};Dygraph.prototype.numRows=function(){if(!this.rawData_){return 0}return this.rawData_.length};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.textAlign="left";a.appendChild(this.graphDiv);this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.resizeElements_();this.canvas_ctx_=Dygraph.getContext(this.canvas_);this.hidden_ctx_=Dygraph.getContext(this.hidden_);this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.createMouseEventElement_();this.layout_=new DygraphLayout(this);var b=this;this.mouseMoveHandler_=function(c){b.mouseMove_(c)};this.mouseOutHandler_=function(f){var d=f.target||f.fromElement;var c=f.relatedTarget||f.toElement;if(Dygraph.isNodeContainedBy(d,b.graphDiv)&&!Dygraph.isNodeContainedBy(c,b.graphDiv)){b.mouseOut_(f)}};this.addAndTrackEvent(window,"mouseout",this.mouseOutHandler_);this.addAndTrackEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler_);if(!this.resizeHandler_){this.resizeHandler_=function(c){b.resize()};this.addAndTrackEvent(window,"resize",this.resizeHandler_)}};Dygraph.prototype.resizeElements_=function(){this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.hidden_.width=this.width_;this.hidden_.height=this.height_;this.hidden_.style.width=this.width_+"px";this.hidden_.style.height=this.height_+"px"};Dygraph.prototype.destroy=function(){this.canvas_ctx_.restore();this.hidden_ctx_.restore();var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};this.removeTrackedEvents_();Dygraph.removeEvent(window,"mouseout",this.mouseOutHandler_);Dygraph.removeEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler_);Dygraph.removeEvent(window,"resize",this.resizeHandler_);this.resizeHandler_=null;a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.prototype.createMouseEventElement_=function(){if(this.isUsingExcanvas_){var a=document.createElement("div");a.style.position="absolute";a.style.backgroundColor="white";a.style.filter="alpha(opacity=0)";a.style.width=this.width_+"px";a.style.height=this.height_+"px";this.graphDiv.appendChild(a);return a}else{return this.canvas_}};Dygraph.prototype.setColors_=function(){var g=this.getLabels();var e=g.length-1;this.colors_=[];this.colorsMap_={};var a=this.attr_("colors");var d;if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var k=Math.ceil(e/2);for(d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var h=d%2?Math.ceil(d/2):(k+d/2);var f=(1*h/(1+e));var j=Dygraph.hsvToRGB(f,c,b);this.colors_.push(j);this.colorsMap_[g[d]]=j}}else{for(d=0;d<e;d++){if(!this.visibility()[d]){continue}var j=a[d%a.length];this.colors_.push(j);this.colorsMap_[g[1+d]]=j}}};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.prototype.getPropertiesForSeries=function(c){var a=-1;var d=this.getLabels();for(var b=1;b<d.length;b++){if(d[b]==c){a=b;break}}if(a==-1){return null}return{name:c,column:a,visible:this.visibility()[a-1],color:this.colorsMap_[c],axis:1+this.attributes_.axisForSeries(c)}};Dygraph.prototype.createRollInterface_=function(){if(!this.roller_){this.roller_=document.createElement("input");this.roller_.type="text";this.roller_.style.display="none";this.graphDiv.appendChild(this.roller_)}var e=this.attr_("showRoller")?"block":"none";var d=this.plotter_.area;var b={position:"absolute",zIndex:10,top:(d.y+d.h-25)+"px",left:(d.x+1)+"px",display:e};this.roller_.size="2";this.roller_.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){this.roller_.style[a]=b[a]}}var c=this;this.roller_.onchange=function(){c.adjustRoll(c.roller_.value)}};Dygraph.prototype.dragGetX_=function(b,a){return Dygraph.pageX(b)-a.px};Dygraph.prototype.dragGetY_=function(b,a){return Dygraph.pageY(b)-a.py};Dygraph.prototype.createDragInterface_=function(){var c={isZooming:false,isPanning:false,is2DPan:false,dragStartX:null,dragStartY:null,dragEndX:null,dragEndY:null,dragDirection:null,prevEndX:null,prevEndY:null,prevDragDirection:null,cancelNextDblclick:false,initialLeftmostDate:null,xUnitsPerPixel:null,dateRange:null,px:0,py:0,boundedDates:null,boundedValues:null,tarp:new Dygraph.IFrameTarp(),initializeMouseDown:function(j,i,h){if(j.preventDefault){j.preventDefault()}else{j.returnValue=false;j.cancelBubble=true}h.px=Dygraph.findPosX(i.canvas_);h.py=Dygraph.findPosY(i.canvas_);h.dragStartX=i.dragGetX_(j,h);h.dragStartY=i.dragGetY_(j,h);h.cancelNextDblclick=false;h.tarp.cover()}};var f=this.attr_("interactionModel");var b=this;var e=function(g){return function(h){g(h,b,c)}};for(var a in f){if(!f.hasOwnProperty(a)){continue}this.addAndTrackEvent(this.mouseEventElement_,a,e(f[a]))}var d=function(h){if(c.isZooming||c.isPanning){c.isZooming=false;c.dragStartX=null;c.dragStartY=null}if(c.isPanning){c.isPanning=false;c.draggingDate=null;c.dateRange=null;for(var g=0;g<b.axes_.length;g++){delete b.axes_[g].draggingValue;delete b.axes_[g].dragValueRange}}c.tarp.uncover()};this.addAndTrackEvent(document,"mouseup",d)};Dygraph.prototype.drawZoomRect_=function(e,c,i,b,g,a,f,d){var h=this.canvas_ctx_;if(a==Dygraph.HORIZONTAL){h.clearRect(Math.min(c,f),this.layout_.getPlotArea().y,Math.abs(c-f),this.layout_.getPlotArea().h)}else{if(a==Dygraph.VERTICAL){h.clearRect(this.layout_.getPlotArea().x,Math.min(b,d),this.layout_.getPlotArea().w,Math.abs(b-d))}}if(e==Dygraph.HORIZONTAL){if(i&&c){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(Math.min(c,i),this.layout_.getPlotArea().y,Math.abs(i-c),this.layout_.getPlotArea().h)}}else{if(e==Dygraph.VERTICAL){if(g&&b){h.fillStyle="rgba(128,128,128,0.33)";h.fillRect(this.layout_.getPlotArea().x,Math.min(b,g),this.layout_.getPlotArea().w,Math.abs(g-b))}}}if(this.isUsingExcanvas_){this.currentZoomRectArgs_=[e,c,i,b,g,0,0,0]}};Dygraph.prototype.clearZoomRect_=function(){this.currentZoomRectArgs_=null;this.canvas_ctx_.clearRect(0,0,this.canvas_.width,this.canvas_.height)};Dygraph.prototype.doZoomX_=function(c,a){this.currentZoomRectArgs_=null;var b=this.toDataXCoord(c);var d=this.toDataXCoord(a);this.doZoomXDates_(b,d)};Dygraph.zoomAnimationFunction=function(c,b){var a=1.5;return(1-Math.pow(a,-c))/(1-Math.pow(a,-b))};Dygraph.prototype.doZoomXDates_=function(c,e){var a=this.xAxisRange();var d=[c,e];this.zoomed_x_=true;var b=this;this.doAnimatedZoom(a,d,null,null,function(){if(b.attr_("zoomCallback")){b.attr_("zoomCallback")(c,e,b.yAxisRanges())}})};Dygraph.prototype.doZoomY_=function(h,f){this.currentZoomRectArgs_=null;var c=this.yAxisRanges();var b=[];for(var e=0;e<this.axes_.length;e++){var d=this.toDataYCoord(h,e);var a=this.toDataYCoord(f,e);b.push([a,d])}this.zoomed_y_=true;var g=this;this.doAnimatedZoom(null,null,c,b,function(){if(g.attr_("zoomCallback")){var i=g.xAxisRange();g.attr_("zoomCallback")(i[0],i[1],g.yAxisRanges())}})};Dygraph.prototype.resetZoom=function(){var c=false,d=false,a=false;if(this.dateWindow_!==null){c=true;d=true}for(var g=0;g<this.axes_.length;g++){if(typeof(this.axes_[g].valueWindow)!=="undefined"&&this.axes_[g].valueWindow!==null){c=true;a=true}}this.clearSelection();if(c){this.zoomed_x_=false;this.zoomed_y_=false;var f=this.rawData_[0][0];var b=this.rawData_[this.rawData_.length-1][0];if(!this.attr_("animatedZooms")){this.dateWindow_=null;for(g=0;g<this.axes_.length;g++){if(this.axes_[g].valueWindow!==null){delete this.axes_[g].valueWindow}}this.drawGraph_();if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(f,b,this.yAxisRanges())}return}var l=null,m=null,k=null,h=null;if(d){l=this.xAxisRange();m=[f,b]}if(a){k=this.yAxisRanges();var n=this.gatherDatasets_(this.rolledSeries_,null);var o=n.extremes;this.computeYAxisRanges_(o);h=[];for(g=0;g<this.axes_.length;g++){var e=this.axes_[g];h.push((e.valueRange!==null&&e.valueRange!==undefined)?e.valueRange:e.extremeRange)}}var j=this;this.doAnimatedZoom(l,m,k,h,function(){j.dateWindow_=null;for(var p=0;p<j.axes_.length;p++){if(j.axes_[p].valueWindow!==null){delete j.axes_[p].valueWindow}}if(j.attr_("zoomCallback")){j.attr_("zoomCallback")(f,b,j.yAxisRanges())}})}};Dygraph.prototype.doAnimatedZoom=function(a,e,b,c,m){var i=this.attr_("animatedZooms")?Dygraph.ANIMATION_STEPS:1;var l=[];var k=[];var f,d;if(a!==null&&e!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);l[f-1]=[a[0]*(1-d)+d*e[0],a[1]*(1-d)+d*e[1]]}}if(b!==null&&c!==null){for(f=1;f<=i;f++){d=Dygraph.zoomAnimationFunction(f,i);var n=[];for(var g=0;g<this.axes_.length;g++){n.push([b[g][0]*(1-d)+d*c[g][0],b[g][1]*(1-d)+d*c[g][1]])}k[f-1]=n}}var h=this;Dygraph.repeatAndCleanup(function(p){if(k.length){for(var o=0;o<h.axes_.length;o++){var j=k[p][o];h.axes_[o].valueWindow=[j[0],j[1]]}}if(l.length){h.dateWindow_=l[p]}h.drawGraph_()},i,Dygraph.ANIMATION_DURATION/i,m)};Dygraph.prototype.getArea=function(){return this.plotter_.area};Dygraph.prototype.eventToDomCoords=function(c){if(c.offsetX&&c.offsetY){return[c.offsetX,c.offsetY]}else{var b=Dygraph.pageX(c)-Dygraph.findPosX(this.mouseEventElement_);var a=Dygraph.pageY(c)-Dygraph.findPosY(this.mouseEventElement_);return[b,a]}};Dygraph.prototype.findClosestRow=function(a){var g=Infinity;var h=-1;var e=this.layout_.points;for(var c=0;c<e.length;c++){var l=e[c];var d=l.length;for(var b=0;b<d;b++){var k=l[b];if(!Dygraph.isValidPoint(k,true)){continue}var f=Math.abs(k.canvasx-a);if(f<g){g=f;h=k.idx}}}return h};Dygraph.prototype.findClosestPoint=function(f,e){var k=Infinity;var h,o,n,l,d,c,j;for(var b=this.layout_.points.length-1;b>=0;--b){var m=this.layout_.points[b];for(var g=0;g<m.length;++g){l=m[g];if(!Dygraph.isValidPoint(l)){continue}o=l.canvasx-f;n=l.canvasy-e;h=o*o+n*n;if(h<k){k=h;d=l;c=b;j=l.idx}}}var a=this.layout_.setNames[c];return{row:j,seriesName:a,point:d}};Dygraph.prototype.findStackedPoint=function(i,h){var p=this.findClosestRow(i);var f,c;for(var d=0;d<this.layout_.points.length;++d){var g=this.getLeftBoundary_(d);var e=p-g;var l=this.layout_.points[d];if(e>=l.length){continue}var m=l[e];if(!Dygraph.isValidPoint(m)){continue}var j=m.canvasy;if(i>m.canvasx&&e+1<l.length){var k=l[e+1];if(Dygraph.isValidPoint(k)){var o=k.canvasx-m.canvasx;if(o>0){var a=(i-m.canvasx)/o;j+=a*(k.canvasy-m.canvasy)}}}else{if(i<m.canvasx&&e>0){var n=l[e-1];if(Dygraph.isValidPoint(n)){var o=m.canvasx-n.canvasx;if(o>0){var a=(m.canvasx-i)/o;j+=a*(n.canvasy-m.canvasy)}}}}if(d===0||j<h){f=m;c=d}}var b=this.layout_.setNames[c];return{row:p,seriesName:b,point:f}};Dygraph.prototype.mouseMove_=function(b){var h=this.layout_.points;if(h===undefined||h===null){return}var e=this.eventToDomCoords(b);var a=e[0];var j=e[1];var f=this.attr_("highlightSeriesOpts");var d=false;if(f&&!this.isSeriesLocked()){var c;if(this.attr_("stackedGraph")){c=this.findStackedPoint(a,j)}else{c=this.findClosestPoint(a,j)}d=this.setSelection(c.row,c.seriesName)}else{var g=this.findClosestRow(a);d=this.setSelection(g)}var i=this.attr_("highlightCallback");if(i&&d){i(b,this.lastx_,this.selPoints_,this.lastRow_,this.highlightSet_)}};Dygraph.prototype.getLeftBoundary_=function(b){if(this.boundaryIds_[b]){return this.boundaryIds_[b][0]}else{for(var a=0;a<this.boundaryIds_.length;a++){if(this.boundaryIds_[a]!==undefined){return this.boundaryIds_[a][0]}}return 0}};Dygraph.prototype.animateSelection_=function(f){var e=10;var c=30;if(this.fadeLevel===undefined){this.fadeLevel=0}if(this.animateId===undefined){this.animateId=0}var g=this.fadeLevel;var b=f<0?g:e-g;if(b<=0){if(this.fadeLevel){this.updateSelection_(1)}return}var a=++this.animateId;var d=this;Dygraph.repeatAndCleanup(function(h){if(d.animateId!=a){return}d.fadeLevel+=f;if(d.fadeLevel===0){d.clearSelection()}else{d.updateSelection_(d.fadeLevel/e)}},b,c,function(){})};Dygraph.prototype.updateSelection_=function(d){this.cascadeEvents_("select",{selectedX:this.lastx_,selectedPoints:this.selPoints_});var h;var n=this.canvas_ctx_;if(this.attr_("highlightSeriesOpts")){n.clearRect(0,0,this.width_,this.height_);var f=1-this.attr_("highlightSeriesBackgroundAlpha");if(f){var g=true;if(g){if(d===undefined){this.animateSelection_(1);return}f*=d}n.fillStyle="rgba(255,255,255,"+f+")";n.fillRect(0,0,this.width_,this.height_)}this.plotter_._renderLineChart(this.highlightSet_,n)}else{if(this.previousVerticalX_>=0){var j=0;var k=this.attr_("labels");for(h=1;h<k.length;h++){var c=this.attr_("highlightCircleSize",k[h]);if(c>j){j=c}}var l=this.previousVerticalX_;n.clearRect(l-j-1,0,2*j+2,this.height_)}}if(this.isUsingExcanvas_&&this.currentZoomRectArgs_){Dygraph.prototype.drawZoomRect_.apply(this,this.currentZoomRectArgs_)}if(this.selPoints_.length>0){var b=this.selPoints_[0].canvasx;n.save();for(h=0;h<this.selPoints_.length;h++){var o=this.selPoints_[h];if(!Dygraph.isOK(o.canvasy)){continue}var a=this.attr_("highlightCircleSize",o.name);var m=this.attr_("drawHighlightPointCallback",o.name);var e=this.plotter_.colors[o.name];if(!m){m=Dygraph.Circles.DEFAULT}n.lineWidth=this.attr_("strokeWidth",o.name);n.strokeStyle=e;n.fillStyle=e;m(this.g,o.name,n,b,o.canvasy,e,a,o.idx)}n.restore();this.previousVerticalX_=b}};Dygraph.prototype.setSelection=function(f,h,g){this.selPoints_=[];var e=false;if(f!==false&&f>=0){if(f!=this.lastRow_){e=true}this.lastRow_=f;for(var d=0;d<this.layout_.points.length;++d){var b=this.layout_.points[d];var c=f-this.getLeftBoundary_(d);if(c<b.length){var a=b[c];if(a.yval!==null){this.selPoints_.push(a)}}}}else{if(this.lastRow_>=0){e=true}this.lastRow_=-1}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval}else{this.lastx_=-1}if(h!==undefined){if(this.highlightSet_!==h){e=true}this.highlightSet_=h}if(g!==undefined){this.lockedSet_=g}if(e){this.updateSelection_(undefined)}return e};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")&&!this.lockedSet_){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){this.cascadeEvents_("deselect",{});this.lockedSet_=false;if(this.fadeLevel){this.animateSelection_(-1);return}this.canvas_ctx_.clearRect(0,0,this.width_,this.height_);this.fadeLevel=0;this.selPoints_=[];this.lastx_=-1;this.lastRow_=-1;this.highlightSet_=null};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var c=0;c<this.layout_.points.length;c++){var a=this.layout_.points[c];for(var b=0;b<a.length;b++){if(a[b].x==this.selPoints_[0].x){return a[b].idx}}}return -1};Dygraph.prototype.getHighlightSeries=function(){return this.highlightSet_};Dygraph.prototype.isSeriesLocked=function(){return this.lockedSet_};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.predraw_()};Dygraph.prototype.addXTicks_=function(){var a;if(this.dateWindow_){a=[this.dateWindow_[0],this.dateWindow_[1]]}else{a=this.xAxisExtremes()}var c=this.optionsViewForAxis_("x");var b=c("ticker")(a[0],a[1],this.width_,c,this);this.layout_.setXTicks(b)};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null,c,g;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(c=0;c<d.length;c++){g=d[c][1][0];if(g===null||isNaN(g)){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f===null||e>f){f=e}if(h===null||a<h){h=a}}}else{for(c=0;c<d.length;c++){g=d[c][1];if(g===null||isNaN(g)){continue}if(f===null||g>f){f=g}if(h===null||g<h){h=g}}}return[h,f]};Dygraph.prototype.predraw_=function(){var e=new Date();this.layout_.computePlotArea();this.computeYAxes_();if(this.plotter_){this.cascadeEvents_("clearChart");this.plotter_.clear()}if(!this.is_initial_draw_){this.canvas_ctx_.restore();this.hidden_ctx_.restore()}this.canvas_ctx_.save();this.hidden_ctx_.save();this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.hidden_ctx_,this.layout_);this.createRollInterface_();this.cascadeEvents_("predraw");this.rolledSeries_=[null];for(var c=1;c<this.numColumns();c++){var d=this.attr_("logscale");var b=this.extractSeries_(this.rawData_,c,d);b=this.rollingAverage(b,this.rollPeriod_);this.rolledSeries_.push(b)}this.drawGraph_();var a=new Date();this.drawingTimeMs_=(a-e)};Dygraph.PointType=undefined;Dygraph.seriesToPoints_=function(b,j,d,f){var h=[];for(var c=0;c<b.length;++c){var k=b[c];var a=j?k[1][0]:k[1];var e=a===null?null:DygraphLayout.parseFloat_(a);var g={x:NaN,y:NaN,xval:DygraphLayout.parseFloat_(k[0]),yval:e,name:d,idx:c+f};if(j){g.y_top=NaN;g.y_bottom=NaN;g.yval_minus=DygraphLayout.parseFloat_(k[1][1]);g.yval_plus=DygraphLayout.parseFloat_(k[1][2])}h.push(g)}return h};Dygraph.stackPoints_=function(m,l,o,f){var h=null;var d=null;var g=null;var c=-1;var k=function(i){if(c>=i){return}for(var p=i;p<m.length;++p){g=null;if(!isNaN(m[p].yval)&&m[p].yval!==null){c=p;g=m[p];break}}};for(var b=0;b<m.length;++b){var j=m[b];var n=j.xval;if(l[n]===undefined){l[n]=0}var e=j.yval;if(isNaN(e)||e===null){k(b);if(d&&g&&f!="none"){e=d.yval+(g.yval-d.yval)*((n-d.xval)/(g.xval-d.xval))}else{if(d&&f=="all"){e=d.yval}else{if(g&&f=="all"){e=g.yval}else{e=0}}}}else{d=j}var a=l[n];if(h!=n){a+=e;l[n]=a}h=n;j.yval_stacked=a;if(a>o[1]){o[1]=a}if(a<o[0]){o[0]=a}}};Dygraph.prototype.gatherDatasets_=function(w,f){var s=[];var t=[];var q=[];var a={};var u,r;var x=this.attr_("errorBars");var h=this.attr_("customBars");var p=x||h;var b=function(i){if(!p){return i[1]===null}else{return h?i[1][1]===null:x?i[1][0]===null:false}};var n=w.length-1;var l;for(u=n;u>=1;u--){if(!this.visibility()[u-1]){continue}if(f){l=w[u];var z=f[0];var j=f[1];var g=null,y=null;for(r=0;r<l.length;r++){if(l[r][0]>=z&&g===null){g=r}if(l[r][0]<=j){y=r}}if(g===null){g=0}var e=g;var d=true;while(d&&e>0){e--;d=b(l[e])}if(y===null){y=l.length-1}var c=y;d=true;while(d&&c<l.length-1){c++;d=b(l[c])}if(e!==g){g=e}if(c!==y){y=c}s[u-1]=[g,y];l=l.slice(g,y+1)}else{l=w[u];s[u-1]=[0,l.length-1]}var v=this.attr_("labels")[u];var o=this.extremeValues_(l);var m=Dygraph.seriesToPoints_(l,p,v,s[u-1][0]);if(this.attr_("stackedGraph")){Dygraph.stackPoints_(m,q,o,this.attr_("stackedGraphNaNFill"))}a[v]=o;t[u]=m}return{points:t,extremes:a,boundaryIds:s}};Dygraph.prototype.drawGraph_=function(){var a=new Date();var d=this.is_initial_draw_;this.is_initial_draw_=false;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var j=this.gatherDatasets_(this.rolledSeries_,this.dateWindow_);var h=j.points;var k=j.extremes;this.boundaryIds_=j.boundaryIds;this.setIndexByName_={};var g=this.attr_("labels");if(g.length>0){this.setIndexByName_[g[0]]=0}var e=0;for(var f=1;f<h.length;f++){this.setIndexByName_[g[f]]=f;if(!this.visibility()[f-1]){continue}this.layout_.addDataset(g[f],h[f]);this.datasetIndex_[f]=e++}this.computeYAxisRanges_(k);this.layout_.setYAxes(this.axes_);this.addXTicks_();var b=this.zoomed_x_;this.zoomed_x_=b;this.layout_.evaluate();this.renderGraph_(d);if(this.attr_("timingName")){var c=new Date();Dygraph.info(this.attr_("timingName")+" - drawGraph: "+(c-a)+"ms")}};Dygraph.prototype.renderGraph_=function(b){this.cascadeEvents_("clearChart");this.plotter_.clear();if(this.attr_("underlayCallback")){this.attr_("underlayCallback")(this.hidden_ctx_,this.layout_.getPlotArea(),this,this)}var c={canvas:this.hidden_,drawingContext:this.hidden_ctx_};this.cascadeEvents_("willDrawChart",c);this.plotter_.render();this.cascadeEvents_("didDrawChart",c);this.lastRow_=-1;this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,b)}if(b){this.readyFired_=true;while(this.readyFns_.length>0){var a=this.readyFns_.pop();a(this)}}};Dygraph.prototype.computeYAxes_=function(){var b,d,c,f,a;if(this.axes_!==undefined&&this.user_attrs_.hasOwnProperty("valueRange")===false){b=[];for(c=0;c<this.axes_.length;c++){b.push(this.axes_[c].valueWindow)}}this.axes_=[];for(d=0;d<this.attributes_.numAxes();d++){f={g:this};Dygraph.update(f,this.attributes_.axisOptions(d));this.axes_[d]=f}a=this.attr_("valueRange");if(a){this.axes_[0].valueRange=a}if(b!==undefined){var e=Math.min(b.length,this.axes_.length);for(c=0;c<e;c++){this.axes_[c].valueWindow=b[c]}}for(d=0;d<this.axes_.length;d++){if(d===0){f=this.optionsViewForAxis_("y"+(d?"2":""));a=f("valueRange");if(a){this.axes_[d].valueRange=a}}else{var g=this.user_attrs_.axes;if(g&&g.y2){a=g.y2.valueRange;if(a){this.axes_[d].valueRange=a}}}}};Dygraph.prototype.numAxes=function(){return this.attributes_.numAxes()};Dygraph.prototype.axisPropertiesForSeries=function(a){return this.axes_[this.attributes_.axisForSeries(a)]};Dygraph.prototype.computeYAxisRanges_=function(a){var g=function(i){return isNaN(parseFloat(i))};var q=this.attributes_.numAxes();var b,x,o,B;var p;for(var y=0;y<q;y++){var c=this.axes_[y];var C=this.attributes_.getForAxis("logscale",y);var G=this.attributes_.getForAxis("includeZero",y);var l=this.attributes_.getForAxis("independentTicks",y);o=this.attributes_.seriesForAxis(y);b=true;B=0.1;if(this.attr_("yRangePad")!==null){b=false;B=this.attr_("yRangePad")/this.plotter_.area.h}if(o.length===0){c.extremeRange=[0,1]}else{var D=Infinity;var A=-Infinity;var t,s;for(var w=0;w<o.length;w++){if(!a.hasOwnProperty(o[w])){continue}t=a[o[w]][0];if(t!==null){D=Math.min(t,D)}s=a[o[w]][1];if(s!==null){A=Math.max(s,A)}}if(G&&!C){if(D>0){D=0}if(A<0){A=0}}if(D==Infinity){D=0}if(A==-Infinity){A=1}x=A-D;if(x===0){if(A!==0){x=Math.abs(A)}else{A=1;x=1}}var h,H;if(C){if(b){h=A+B*x;H=D}else{var E=Math.exp(Math.log(x)*B);h=A*E;H=D/E}}else{h=A+B*x;H=D-B*x;if(b&&!this.attr_("avoidMinZero")){if(H<0&&D>=0){H=0}if(h>0&&A<=0){h=0}}}c.extremeRange=[H,h]}if(c.valueWindow){c.computedValueRange=[c.valueWindow[0],c.valueWindow[1]]}else{if(c.valueRange){var e=g(c.valueRange[0])?c.extremeRange[0]:c.valueRange[0];var d=g(c.valueRange[1])?c.extremeRange[1]:c.valueRange[1];if(!b){if(c.logscale){var E=Math.exp(Math.log(x)*B);e*=E;d/=E}else{x=d-e;e-=x*B;d+=x*B}}c.computedValueRange=[e,d]}else{c.computedValueRange=c.extremeRange}}if(l){c.independentTicks=l;var r=this.optionsViewForAxis_("y"+(y?"2":""));var F=r("ticker");c.ticks=F(c.computedValueRange[0],c.computedValueRange[1],this.height_,r,this);if(!p){p=c}}}if(p===undefined){throw ('Configuration Error: At least one axis has to have the "independentTicks" option activated.')}for(var y=0;y<q;y++){var c=this.axes_[y];if(!c.independentTicks){var r=this.optionsViewForAxis_("y"+(y?"2":""));var F=r("ticker");var m=p.ticks;var n=p.computedValueRange[1]-p.computedValueRange[0];var I=c.computedValueRange[1]-c.computedValueRange[0];var f=[];for(var v=0;v<m.length;v++){var u=(m[v].v-p.computedValueRange[0])/n;var z=c.computedValueRange[0]+u*I;f.push(z)}c.ticks=F(c.computedValueRange[0],c.computedValueRange[1],this.height_,r,this,f)}}};Dygraph.prototype.extractSeries_=function(a,f,c){var g=[];var h=this.attr_("errorBars");var e=this.attr_("customBars");for(var d=0;d<a.length;d++){var l=a[d][0];var m=a[d][f];if(c){if(h||e){for(var b=0;b<m.length;b++){if(m[b]<=0){m=null;break}}}else{if(m<=0){m=null}}}if(m!==null){g.push([l,m])}else{g.push([l,h?[null,null]:e?[null,null,null]:m])}}return g};Dygraph.prototype.rollingAverage=function(l,d){d=Math.min(d,l.length);var b=[];var t=this.attr_("sigma");var G,o,z,x,m,c,F,A;if(this.fractions_){var k=0;var h=0;var e=100;for(z=0;z<l.length;z++){k+=l[z][1][0];h+=l[z][1][1];if(z-d>=0){k-=l[z-d][1][0];h-=l[z-d][1][1]}var C=l[z][0];var w=h?k/h:0;if(this.attr_("errorBars")){if(this.attr_("wilsonInterval")){if(h){var s=w<0?0:w,u=h;var B=t*Math.sqrt(s*(1-s)/u+t*t/(4*u*u));var a=1+t*t/h;G=(s+t*t/(2*h)-B)/a;o=(s+t*t/(2*h)+B)/a;b[z]=[C,[s*e,(s-G)*e,(o-s)*e]]}else{b[z]=[C,[0,0,0]]}}else{A=h?t*Math.sqrt(w*(1-w)/h):1;b[z]=[C,[e*w,e*A,e*A]]}}else{b[z]=[C,e*w]}}}else{if(this.attr_("customBars")){G=0;var D=0;o=0;var g=0;for(z=0;z<l.length;z++){var E=l[z][1];m=E[1];b[z]=[l[z][0],[m,m-E[0],E[2]-m]];if(m!==null&&!isNaN(m)){G+=E[0];D+=m;o+=E[2];g+=1}if(z-d>=0){var r=l[z-d];if(r[1][1]!==null&&!isNaN(r[1][1])){G-=r[1][0];D-=r[1][1];o-=r[1][2];g-=1}}if(g){b[z]=[l[z][0],[1*D/g,1*(D-G)/g,1*(o-D)/g]]}else{b[z]=[l[z][0],[null,null,null]]}}}else{if(!this.attr_("errorBars")){if(d==1){return l}for(z=0;z<l.length;z++){c=0;F=0;for(x=Math.max(0,z-d+1);x<z+1;x++){m=l[x][1];if(m===null||isNaN(m)){continue}F++;c+=l[x][1]}if(F){b[z]=[l[z][0],c/F]}else{b[z]=[l[z][0],null]}}}else{for(z=0;z<l.length;z++){c=0;var f=0;F=0;for(x=Math.max(0,z-d+1);x<z+1;x++){m=l[x][1][0];if(m===null||isNaN(m)){continue}F++;c+=l[x][1][0];f+=Math.pow(l[x][1][1],2)}if(F){A=Math.sqrt(f)/F;b[z]=[l[z][0],[c/F,t*A,t*A]]}else{var q=(d==1)?l[z][1][0]:null;b[z]=[l[z][0],[q,q,q]]}}}}}return b};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;var c=b.indexOf("-");if((c>0&&(b[c-1]!="e"&&b[c-1]!="E"))||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}this.setXAxisOptions_(a)};Dygraph.prototype.setXAxisOptions_=function(a){if(a){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueParser=function(b){return parseFloat(b)};this.attrs_.axes.x.valueFormatter=function(b){return b};this.attrs_.axes.x.ticker=Dygraph.numericLinearTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}};Dygraph.prototype.parseFloat_=function(a,c,b){var e=parseFloat(a);if(!isNaN(e)){return e}if(/^ *$/.test(a)){return null}if(/^ *nan *$/i.test(a)){return NaN}var d="Unable to parse '"+a+"' as a number";if(b!==null&&c!==null){d+=" on line "+(1+c)+" ('"+b+"') of CSV."}this.error(d);return null};Dygraph.prototype.parseCSV_=function(t){var r=[];var s=Dygraph.detectLineDelimiter(t);var a=t.split(s||"\n");var g,k;var p=this.attr_("delimiter");if(a[0].indexOf(p)==-1&&a[0].indexOf("\t")>=0){p="\t"}var b=0;if(!("labels" in this.user_attrs_)){b=1;this.attrs_.labels=a[0].split(p);this.attributes_.reparseSeries()}var o=0;var m;var q=false;var c=this.attr_("labels").length;var f=false;for(var l=b;l<a.length;l++){var e=a[l];o=l;if(e.length===0){continue}if(e[0]=="#"){continue}var d=e.split(p);if(d.length<2){continue}var h=[];if(!q){this.detectTypeFromString_(d[0]);m=this.attr_("xValueParser");q=true}h[0]=m(d[0],this);if(this.fractions_){for(k=1;k<d.length;k++){g=d[k].split("/");if(g.length!=2){this.error('Expected fractional "num/den" values in CSV data but found a value \''+d[k]+"' on line "+(1+l)+" ('"+e+"') which is not of this form.");h[k]=[0,0]}else{h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e)]}}}else{if(this.attr_("errorBars")){if(d.length%2!=1){this.error("Expected alternating (value, stdev.) pairs in CSV data but line "+(1+l)+" has an odd number of values ("+(d.length-1)+"): '"+e+"'")}for(k=1;k<d.length;k+=2){h[(k+1)/2]=[this.parseFloat_(d[k],l,e),this.parseFloat_(d[k+1],l,e)]}}else{if(this.attr_("customBars")){for(k=1;k<d.length;k++){var u=d[k];if(/^ *$/.test(u)){h[k]=[null,null,null]}else{g=u.split(";");if(g.length==3){h[k]=[this.parseFloat_(g[0],l,e),this.parseFloat_(g[1],l,e),this.parseFloat_(g[2],l,e)]}else{this.warn('When using customBars, values must be either blank or "low;center;high" tuples (got "'+u+'" on line '+(1+l))}}}}else{for(k=1;k<d.length;k++){h[k]=this.parseFloat_(d[k],l,e)}}}}if(r.length>0&&h[0]<r[r.length-1][0]){f=true}if(h.length!=c){this.error("Number of columns in line "+l+" ("+h.length+") does not agree with number of labels ("+c+") "+e)}if(l===0&&this.attr_("labels")){var n=true;for(k=0;n&&k<h.length;k++){if(h[k]){n=false}}if(n){this.warn("The dygraphs 'labels' option is set, but the first row of CSV data ('"+e+"') appears to also contain labels. Will drop the CSV labels and use the option labels.");continue}}r.push(h)}if(f){this.warn("CSV is out of order; order it correctly to speed loading.");r.sort(function(j,i){return j[0]-i[0]})}return r};Dygraph.prototype.parseArray_=function(c){if(c.length===0){this.error("Can't plot empty data set");return null}if(c[0].length===0){this.error("Data set cannot contain an empty row");return null}var a;if(this.attr_("labels")===null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(a=1;a<c[0].length;a++){this.attrs_.labels.push("Y"+a)}this.attributes_.reparseSeries()}else{var b=this.attr_("labels");if(b.length!=c[0].length){this.error("Mismatch between number of labels ("+b+") and number of columns in array ("+c[0].length+")");return null}}if(Dygraph.isDateLike(c[0][0])){this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter;var d=Dygraph.clone(c);for(a=0;a<c.length;a++){if(d[a].length===0){this.error("Row "+(1+a)+" of data is empty");return null}if(d[a][0]===null||typeof(d[a][0].getTime)!="function"||isNaN(d[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}d[a][0]=d[a][0].getTime()}return d}else{this.attrs_.axes.x.valueFormatter=function(e){return e};this.attrs_.axes.x.ticker=Dygraph.numericLinearTicks;this.attrs_.axes.x.axisLabelFormatter=Dygraph.numberAxisLabelFormatter;return c}};Dygraph.prototype.parseDataTable_=function(w){var d=function(i){var j=String.fromCharCode(65+i%26);i=Math.floor(i/26);while(i>0){j=String.fromCharCode(65+(i-1)%26)+j.toLowerCase();i=Math.floor((i-1)/26)}return j};var h=w.getNumberOfColumns();var g=w.getNumberOfRows();var f=w.getColumnType(0);if(f=="date"||f=="datetime"){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{if(f=="number"){this.attrs_.xValueParser=function(i){return parseFloat(i)};this.attrs_.axes.x.valueFormatter=function(i){return i};this.attrs_.axes.x.ticker=Dygraph.numericLinearTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+f+"')");return null}}var m=[];var t={};var s=false;var q,o;for(q=1;q<h;q++){var b=w.getColumnType(q);if(b=="number"){m.push(q)}else{if(b=="string"&&this.attr_("displayAnnotations")){var r=m[m.length-1];if(!t.hasOwnProperty(r)){t[r]=[q]}else{t[r].push(q)}s=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var u=[w.getColumnLabel(0)];for(q=0;q<m.length;q++){u.push(w.getColumnLabel(m[q]));if(this.attr_("errorBars")){q+=1}}this.attrs_.labels=u;h=u.length;var v=[];var l=false;var a=[];for(q=0;q<g;q++){var e=[];if(typeof(w.getValue(q,0))==="undefined"||w.getValue(q,0)===null){this.warn("Ignoring row "+q+" of DataTable because of undefined or null first column.");continue}if(f=="date"||f=="datetime"){e.push(w.getValue(q,0).getTime())}else{e.push(w.getValue(q,0))}if(!this.attr_("errorBars")){for(o=0;o<m.length;o++){var c=m[o];e.push(w.getValue(q,c));if(s&&t.hasOwnProperty(c)&&w.getValue(q,t[c][0])!==null){var p={};p.series=w.getColumnLabel(c);p.xval=e[0];p.shortText=d(a.length);p.text="";for(var n=0;n<t[c].length;n++){if(n){p.text+="\n"}p.text+=w.getValue(q,t[c][n])}a.push(p)}}for(o=0;o<e.length;o++){if(!isFinite(e[o])){e[o]=null}}}else{for(o=0;o<h-1;o++){e.push([w.getValue(q,1+2*o),w.getValue(q,2+2*o)])}}if(v.length>0&&e[0]<v[v.length-1][0]){l=true}v.push(e)}if(l){this.warn("DataTable is out of order; order it correctly to speed loading.");v.sort(function(j,i){return j[0]-i[0]})}this.rawData_=v;if(a.length>0){this.setAnnotations(a,true)}this.attributes_.reparseSeries()};Dygraph.prototype.start_=function(){var d=this.file_;if(typeof d=="function"){d=d()}if(Dygraph.isArrayLike(d)){this.rawData_=this.parseArray_(d);this.predraw_()}else{if(typeof d=="object"&&typeof d.getColumnRange=="function"){this.parseDataTable_(d);this.predraw_()}else{if(typeof d=="string"){var c=Dygraph.detectLineDelimiter(d);if(c){this.loadedEvent_(d)}else{var b;if(window.XMLHttpRequest){b=new XMLHttpRequest()}else{b=new ActiveXObject("Microsoft.XMLHTTP")}var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status===200||b.status===0){a.loadedEvent_(b.responseText)}}};b.open("GET",d,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof d))}}}};Dygraph.prototype.updateOptions=function(e,b){if(typeof(b)=="undefined"){b=false}var d=e.file;var c=Dygraph.mapLegacyOptions_(e);if("rollPeriod" in c){this.rollPeriod_=c.rollPeriod}if("dateWindow" in c){this.dateWindow_=c.dateWindow;if(!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_x_=(c.dateWindow!==null)}}if("valueRange" in c&&!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_y_=(c.valueRange!==null)}var a=Dygraph.isPixelChangingOptionList(this.attr_("labels"),c);Dygraph.updateDeep(this.user_attrs_,c);this.attributes_.reparseSeries();if(d){this.file_=d;if(!b){this.start_()}}else{if(!b){if(a){this.predraw_()}else{this.renderGraph_(false)}}}};Dygraph.mapLegacyOptions_=function(c){var a={};for(var b in c){if(b=="file"){continue}if(c.hasOwnProperty(b)){a[b]=c[b]}}var e=function(g,f,h){if(!a.axes){a.axes={}}if(!a.axes[g]){a.axes[g]={}}a.axes[g][f]=h};var d=function(f,g,h){if(typeof(c[f])!="undefined"){Dygraph.warn("Option "+f+" is deprecated. Use the "+h+" option for the "+g+" axis instead. (e.g. { axes : { "+g+" : { "+h+" : ... } } } (see http://dygraphs.com/per-axis.html for more information.");e(g,h,c[f]);delete a[f]}};d("xValueFormatter","x","valueFormatter");d("pixelsPerXLabel","x","pixelsPerLabel");d("xAxisLabelFormatter","x","axisLabelFormatter");d("xTicker","x","ticker");d("yValueFormatter","y","valueFormatter");d("pixelsPerYLabel","y","pixelsPerLabel");d("yAxisLabelFormatter","y","axisLabelFormatter");d("yTicker","y","ticker");return a};Dygraph.prototype.resize=function(d,b){if(this.resize_lock){return}this.resize_lock=true;if((d===null)!=(b===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");d=b=null}var a=this.width_;var c=this.height_;if(d){this.maindiv_.style.width=d+"px";this.maindiv_.style.height=b+"px";this.width_=d;this.height_=b}else{this.width_=this.maindiv_.clientWidth;this.height_=this.maindiv_.clientHeight}if(a!=this.width_||c!=this.height_){this.resizeElements_();this.predraw_()}this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.predraw_()};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.numColumns()-1){this.attrs_.visibility.push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0||b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.predraw_()}};Dygraph.prototype.size=function(){return{width:this.width_,height:this.height_}};Dygraph.prototype.setAnnotations=function(b,a){Dygraph.addAnnotationRule();this.annotations_=b;if(!this.layout_){this.warn("Tried to setAnnotations before dygraph was ready. Try setting them in a ready() block. See dygraphs.com/tests/annotation.html");return}this.layout_.setAnnotations(this.annotations_);if(!a){this.predraw_()}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.getLabels=function(){var a=this.attr_("labels");return a?a.slice():null};Dygraph.prototype.indexFromSetName=function(a){return this.setIndexByName_[a]};Dygraph.prototype.ready=function(a){if(this.is_initial_draw_){this.readyFns_.push(a)}else{a(this)}};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var f="border: 1px solid black; background-color: white; text-align: center;";var e=document.createElement("style");e.type="text/css";document.getElementsByTagName("head")[0].appendChild(e);for(var b=0;b<document.styleSheets.length;b++){if(document.styleSheets[b].disabled){continue}var d=document.styleSheets[b];try{if(d.insertRule){var a=d.cssRules?d.cssRules.length:0;d.insertRule(".dygraphDefaultAnnotation { "+f+" }",a)}else{if(d.addRule){d.addRule(".dygraphDefaultAnnotation",f)}}Dygraph.addedAnnotationCSS=true;return}catch(c){}}this.warn("Unable to add default annotation CSS rule; display may be off.")};var DateGraph=Dygraph;"use strict";Dygraph.LOG_SCALE=10;Dygraph.LN_TEN=Math.log(Dygraph.LOG_SCALE);Dygraph.log10=function(a){return Math.log(a)/Dygraph.LN_TEN};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.LOG_STACK_TRACES=false;Dygraph.DOTTED_LINE=[2,2];Dygraph.DASHED_LINE=[7,3];Dygraph.DOT_DASH_LINE=[7,2,2,2];Dygraph.log=function(c,g){var b;if(typeof(printStackTrace)!="undefined"){try{b=printStackTrace({guess:false});while(b[0].indexOf("stacktrace")!=-1){b.splice(0,1)}b.splice(0,2);for(var d=0;d<b.length;d++){b[d]=b[d].replace(/\([^)]*\/(.*)\)/,"@$1").replace(/\@.*\/([^\/]*)/,"@$1").replace("[object Object].","")}var j=b.splice(0,1)[0];g+=" ("+j.replace(/^.*@ ?/,"")+")"}catch(h){}}if(typeof(window.console)!="undefined"){var a=window.console;var f=function(e,k,i){if(k&&typeof(k)=="function"){k.call(e,i)}else{e.log(i)}};switch(c){case Dygraph.DEBUG:f(a,a.debug,"dygraphs: "+g);break;case Dygraph.INFO:f(a,a.info,"dygraphs: "+g);break;case Dygraph.WARNING:f(a,a.warn,"dygraphs: "+g);break;case Dygraph.ERROR:f(a,a.error,"dygraphs: "+g);break}}if(Dygraph.LOG_STACK_TRACES){window.console.log(b.join("\n"))}};Dygraph.info=function(a){Dygraph.log(Dygraph.INFO,a)};Dygraph.prototype.info=Dygraph.info;Dygraph.warn=function(a){Dygraph.log(Dygraph.WARNING,a)};Dygraph.prototype.warn=Dygraph.warn;Dygraph.error=function(a){Dygraph.log(Dygraph.ERROR,a)};Dygraph.prototype.error=Dygraph.error;Dygraph.getContext=function(a){return(a.getContext("2d"))};Dygraph.addEvent=function addEvent(c,b,a){if(c.addEventListener){c.addEventListener(b,a,false)}else{c[b+a]=function(){a(window.event)};c.attachEvent("on"+b,c[b+a])}};Dygraph.prototype.addAndTrackEvent=function(c,b,a){Dygraph.addEvent(c,b,a);this.registeredEvents_.push({elem:c,type:b,fn:a})};Dygraph.removeEvent=function(c,b,a){if(c.removeEventListener){c.removeEventListener(b,a,false)}else{try{c.detachEvent("on"+b,c[b+a])}catch(d){}c[b+a]=null}};Dygraph.prototype.removeTrackedEvents_=function(){if(this.registeredEvents_){for(var a=0;a<this.registeredEvents_.length;a++){var b=this.registeredEvents_[a];Dygraph.removeEvent(b.elem,b.type,b.fn)}}this.registeredEvents_=[]};Dygraph.cancelEvent=function(a){a=a?a:window.event;if(a.stopPropagation){a.stopPropagation()}if(a.preventDefault){a.preventDefault()}a.cancelBubble=true;a.cancel=true;a.returnValue=false;return false};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.findPosX=function(c){var d=0;if(c.offsetParent){var a=c;while(1){var b="0";if(window.getComputedStyle){b=window.getComputedStyle(a,null).borderLeft||"0"}d+=parseInt(b,10);d+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(c.x){d+=c.x}}while(c&&c!=document.body){d-=c.scrollLeft;c=c.parentNode}return d};Dygraph.findPosY=function(c){var b=0;if(c.offsetParent){var a=c;while(1){var d="0";if(window.getComputedStyle){d=window.getComputedStyle(a,null).borderTop||"0"}b+=parseInt(d,10);b+=a.offsetTop;if(!a.offsetParent){break}a=a.offsetParent}}else{if(c.y){b+=c.y}}while(c&&c!=document.body){b-=c.scrollTop;c=c.parentNode}return b};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document.documentElement;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document.documentElement;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.isOK=function(a){return !!a&&!isNaN(a)};Dygraph.isValidPoint=function(b,a){if(!b){return false}if(b.yval===null){return false}if(b.x===null||b.x===undefined){return false}if(b.y===null||b.y===undefined){return false}if(isNaN(b.x)||(!a&&isNaN(b.y))){return false}return true};Dygraph.floatFormat=function(a,b){var c=Math.min(Math.max(1,b||2),21);return(Math.abs(a)<0.001&&a!==0)?a.toExponential(c-1):a.toPrecision(c)};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.binarySearch=function(a,d,i,e,b){if(e===null||e===undefined||b===null||b===undefined){e=0;b=d.length-1}if(e>b){return -1}if(i===null||i===undefined){i=0}var h=function(j){return j>=0&&j<d.length};var g=parseInt((e+b)/2,10);var c=d[g];var f;if(c==a){return g}else{if(c>a){if(i>0){f=g-1;if(h(f)&&d[f]<a){return g}}return Dygraph.binarySearch(a,d,i,e,g-1)}else{if(c<a){if(i<0){f=g+1;if(h(f)&&d[f]>a){return g}}return Dygraph.binarySearch(a,d,i,g+1,b)}}}return -1};Dygraph.dateParser=function(a){var b;var c;if(a.search("-")==-1||a.search("T")!=-1||a.search("Z")!=-1){c=Dygraph.dateStrToMillis(a);if(c&&!isNaN(c)){return c}}if(a.search("-")!=-1){b=a.replace("-","/","g");while(b.search("-")!=-1){b=b.replace("-","/")}c=Dygraph.dateStrToMillis(b)}else{if(a.length==8){b=a.substr(0,4)+"/"+a.substr(4,2)+"/"+a.substr(6,2);c=Dygraph.dateStrToMillis(b)}else{c=Dygraph.dateStrToMillis(a)}}if(!c||isNaN(c)){Dygraph.error("Couldn't parse "+a+" as a date")}return c};Dygraph.dateStrToMillis=function(a){return new Date(a).getTime()};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.updateDeep=function(b,d){function c(e){return(typeof Node==="object"?e instanceof Node:typeof e==="object"&&typeof e.nodeType==="number"&&typeof e.nodeName==="string")}if(typeof(d)!="undefined"&&d!==null){for(var a in d){if(d.hasOwnProperty(a)){if(d[a]===null){b[a]=null}else{if(Dygraph.isArrayLike(d[a])){b[a]=d[a].slice()}else{if(c(d[a])){b[a]=d[a]}else{if(typeof(d[a])=="object"){if(typeof(b[a])!="object"||b[a]===null){b[a]={}}Dygraph.updateDeep(b[a],d[a])}else{b[a]=d[a]}}}}}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.createCanvas=function(){var a=document.createElement("canvas");var b=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(b&&(typeof(G_vmlCanvasManager)!="undefined")){a=G_vmlCanvasManager.initElement((a))}return a};Dygraph.isAndroid=function(){return(/Android/).test(navigator.userAgent)};Dygraph.Iterator=function(d,c,b,a){c=c||0;b=b||d.length;this.hasNext=true;this.peek=null;this.start_=c;this.array_=d;this.predicate_=a;this.end_=Math.min(d.length,c+b);this.nextIdx_=c-1;this.next()};Dygraph.Iterator.prototype.next=function(){if(!this.hasNext){return null}var c=this.peek;var b=this.nextIdx_+1;var a=false;while(b<this.end_){if(!this.predicate_||this.predicate_(this.array_,b)){this.peek=this.array_[b];a=true;break}b++}this.nextIdx_=b;if(!a){this.hasNext=false;this.peek=null}return c};Dygraph.createIterator=function(d,c,b,a){return new Dygraph.Iterator(d,c,b,a)};Dygraph.requestAnimFrame=(function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1000/60)}})();Dygraph.repeatAndCleanup=function(h,g,f,a){var i=0;var d;var b=new Date().getTime();h(i);if(g==1){a();return}var e=g-1;(function c(){if(i>=g){return}Dygraph.requestAnimFrame.call(window,function(){var l=new Date().getTime();var j=l-b;d=i;i=Math.floor(j/f);var k=i-d;var m=(i+k)>e;if(m||(i>=e)){h(e);a()}else{if(k!==0){h(i)}c()}})})()};Dygraph.isPixelChangingOptionList=function(h,e){var d={annotationClickHandler:true,annotationDblClickHandler:true,annotationMouseOutHandler:true,annotationMouseOverHandler:true,axisLabelColor:true,axisLineColor:true,axisLineWidth:true,clickCallback:true,digitsAfterDecimal:true,drawCallback:true,drawHighlightPointCallback:true,drawPoints:true,drawPointCallback:true,drawXGrid:true,drawYGrid:true,fillAlpha:true,gridLineColor:true,gridLineWidth:true,hideOverlayOnMouseOut:true,highlightCallback:true,highlightCircleSize:true,interactionModel:true,isZoomedIgnoreProgrammaticZoom:true,labelsDiv:true,labelsDivStyles:true,labelsDivWidth:true,labelsKMB:true,labelsKMG2:true,labelsSeparateLines:true,labelsShowZeroValues:true,legend:true,maxNumberWidth:true,panEdgeFraction:true,pixelsPerYLabel:true,pointClickCallback:true,pointSize:true,rangeSelectorPlotFillColor:true,rangeSelectorPlotStrokeColor:true,showLabelsOnHighlight:true,showRoller:true,sigFigs:true,strokeWidth:true,underlayCallback:true,unhighlightCallback:true,xAxisLabelFormatter:true,xTicker:true,xValueFormatter:true,yAxisLabelFormatter:true,yValueFormatter:true,zoomCallback:true};var a=false;var b={};if(h){for(var f=1;f<h.length;f++){b[h[f]]=true}}for(var g in e){if(a){break}if(e.hasOwnProperty(g)){if(b[g]){for(var c in e[g]){if(a){break}if(e[g].hasOwnProperty(c)&&!d[c]){a=true}}}else{if(!d[g]){a=true}}}}return a};Dygraph.compareArrays=function(c,b){if(!Dygraph.isArrayLike(c)||!Dygraph.isArrayLike(b)){return false}if(c.length!==b.length){return false}for(var a=0;a<c.length;a++){if(c[a]!==b[a]){return false}}return true};Dygraph.regularShape_=function(o,c,i,f,e,a,n){a=a||0;n=n||Math.PI*2/c;o.beginPath();var g=a;var d=g;var h=function(){var p=f+(Math.sin(d)*i);var q=e+(-Math.cos(d)*i);return[p,q]};var b=h();var l=b[0];var j=b[1];o.moveTo(l,j);for(var m=0;m<c;m++){d=(m==c-1)?g:(d+n);var k=h();o.lineTo(k[0],k[1])}o.fill();o.stroke()};Dygraph.shapeFunction_=function(b,a,c){return function(j,i,f,e,k,h,d){f.strokeStyle=h;f.fillStyle="white";Dygraph.regularShape_(f,b,d,e,k,a,c)}};Dygraph.Circles={DEFAULT:function(h,f,b,e,d,c,a){b.beginPath();b.fillStyle=c;b.arc(e,d,a,0,2*Math.PI,false);b.fill()},TRIANGLE:Dygraph.shapeFunction_(3),SQUARE:Dygraph.shapeFunction_(4,Math.PI/4),DIAMOND:Dygraph.shapeFunction_(4),PENTAGON:Dygraph.shapeFunction_(5),HEXAGON:Dygraph.shapeFunction_(6),CIRCLE:function(f,e,c,b,h,d,a){c.beginPath();c.strokeStyle=d;c.fillStyle="white";c.arc(b,h,a,0,2*Math.PI,false);c.fill();c.stroke()},STAR:Dygraph.shapeFunction_(5,0,4*Math.PI/5),PLUS:function(f,e,c,b,h,d,a){c.strokeStyle=d;c.beginPath();c.moveTo(b+a,h);c.lineTo(b-a,h);c.closePath();c.stroke();c.beginPath();c.moveTo(b,h+a);c.lineTo(b,h-a);c.closePath();c.stroke()},EX:function(f,e,c,b,h,d,a){c.strokeStyle=d;c.beginPath();c.moveTo(b+a,h+a);c.lineTo(b-a,h-a);c.closePath();c.stroke();c.beginPath();c.moveTo(b+a,h-a);c.lineTo(b-a,h+a);c.closePath();c.stroke()}};Dygraph.IFrameTarp=function(){this.tarps=[]};Dygraph.IFrameTarp.prototype.cover=function(){var f=document.getElementsByTagName("iframe");for(var c=0;c<f.length;c++){var e=f[c];var b=Dygraph.findPosX(e),h=Dygraph.findPosY(e),d=e.offsetWidth,a=e.offsetHeight;var g=document.createElement("div");g.style.position="absolute";g.style.left=b+"px";g.style.top=h+"px";g.style.width=d+"px";g.style.height=a+"px";g.style.zIndex=999;document.body.appendChild(g);this.tarps.push(g)}};Dygraph.IFrameTarp.prototype.uncover=function(){for(var a=0;a<this.tarps.length;a++){this.tarps[a].parentNode.removeChild(this.tarps[a])}this.tarps=[]};Dygraph.detectLineDelimiter=function(c){for(var a=0;a<c.length;a++){var b=c.charAt(a);if(b==="\r"){if(((a+1)<c.length)&&(c.charAt(a+1)==="\n")){return"\r\n"}return b}if(b==="\n"){if(((a+1)<c.length)&&(c.charAt(a+1)==="\r")){return"\n\r"}return b}}return null};Dygraph.isNodeContainedBy=function(b,a){if(a===null||b===null){return false}var c=(b);while(c&&c!==a){c=c.parentNode}return(c===a)};Dygraph.pow=function(a,b){if(b<0){return 1/Math.pow(a,-b)}return Math.pow(a,b)};Dygraph.dateSetters={ms:Date.prototype.setMilliseconds,s:Date.prototype.setSeconds,m:Date.prototype.setMinutes,h:Date.prototype.setHours};Dygraph.setDateSameTZ=function(c,b){var f=c.getTimezoneOffset();for(var a in b){if(!b.hasOwnProperty(a)){continue}var e=Dygraph.dateSetters[a];if(!e){throw"Invalid setter: "+a}e.call(c,b[a]);if(c.getTimezoneOffset()!=f){c.setTime(c.getTime()+(f-c.getTimezoneOffset())*60*1000)}}};"use strict";Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";if(typeof(this.date_graph)!="undefined"){this.date_graph.destroy()}this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var d=this.date_graph.getSelection();if(d<0){return b}var a=this.date_graph.layout_.points;for(var c=0;c<a.length;++c){b.push({row:d,column:c+1})}return b};"use strict";Dygraph.Interaction={};Dygraph.Interaction.startPan=function(o,t,c){var r,b;c.isPanning=true;var k=t.xAxisRange();c.dateRange=k[1]-k[0];c.initialLeftmostDate=k[0];c.xUnitsPerPixel=c.dateRange/(t.plotter_.area.w-1);if(t.attr_("panEdgeFraction")){var x=t.width_*t.attr_("panEdgeFraction");var d=t.xAxisExtremes();var j=t.toDomXCoord(d[0])-x;var l=t.toDomXCoord(d[1])+x;var u=t.toDataXCoord(j);var w=t.toDataXCoord(l);c.boundedDates=[u,w];var f=[];var a=t.height_*t.attr_("panEdgeFraction");for(r=0;r<t.axes_.length;r++){b=t.axes_[r];var p=b.extremeRange;var q=t.toDomYCoord(p[0],r)+a;var s=t.toDomYCoord(p[1],r)-a;var n=t.toDataYCoord(q,r);var e=t.toDataYCoord(s,r);f[r]=[n,e]}c.boundedValues=f}c.is2DPan=false;c.axes=[];for(r=0;r<t.axes_.length;r++){b=t.axes_[r];var h={};var m=t.yAxisRange(r);var v=t.attributes_.getForAxis("logscale",r);if(v){h.initialTopValue=Dygraph.log10(m[1]);h.dragValueRange=Dygraph.log10(m[1])-Dygraph.log10(m[0])}else{h.initialTopValue=m[1];h.dragValueRange=m[1]-m[0]}h.unitsPerPixel=h.dragValueRange/(t.plotter_.area.h-1);c.axes.push(h);if(b.valueWindow||b.valueRange){c.is2DPan=true}}};Dygraph.Interaction.movePan=function(b,k,c){c.dragEndX=k.dragGetX_(b,c);c.dragEndY=k.dragGetY_(b,c);var h=c.initialLeftmostDate-(c.dragEndX-c.dragStartX)*c.xUnitsPerPixel;if(c.boundedDates){h=Math.max(h,c.boundedDates[0])}var a=h+c.dateRange;if(c.boundedDates){if(a>c.boundedDates[1]){h=h-(a-c.boundedDates[1]);a=h+c.dateRange}}k.dateWindow_=[h,a];if(c.is2DPan){var d=c.dragEndY-c.dragStartY;for(var j=0;j<k.axes_.length;j++){var e=k.axes_[j];var o=c.axes[j];var p=d*o.unitsPerPixel;var f=c.boundedValues?c.boundedValues[j]:null;var l=o.initialTopValue+p;if(f){l=Math.min(l,f[1])}var n=l-o.dragValueRange;if(f){if(n<f[0]){l=l-(n-f[0]);n=l-o.dragValueRange}}var m=k.attributes_.getForAxis("logscale",j);if(m){e.valueWindow=[Math.pow(Dygraph.LOG_SCALE,n),Math.pow(Dygraph.LOG_SCALE,l)]}else{e.valueWindow=[n,l]}}}k.drawGraph_(false)};Dygraph.Interaction.endPan=function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}a.isPanning=false;a.is2DPan=false;a.initialLeftmostDate=null;a.dateRange=null;a.valueRange=null;a.boundedDates=null;a.boundedValues=null;a.axes=null};Dygraph.Interaction.startZoom=function(c,b,a){a.isZooming=true;a.zoomMoved=false};Dygraph.Interaction.moveZoom=function(c,b,a){a.zoomMoved=true;a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragStartX-a.dragEndX);var d=Math.abs(a.dragStartY-a.dragEndY);a.dragDirection=(e<d/2)?Dygraph.VERTICAL:Dygraph.HORIZONTAL;b.drawZoomRect_(a.dragDirection,a.dragStartX,a.dragEndX,a.dragStartY,a.dragEndY,a.prevDragDirection,a.prevEndX,a.prevEndY);a.prevEndX=a.dragEndX;a.prevEndY=a.dragEndY;a.prevDragDirection=a.dragDirection};Dygraph.Interaction.treatMouseOpAsClick=function(f,b,d){var k=f.attr_("clickCallback");var n=f.attr_("pointClickCallback");var j=null;if(n){var l=-1;var m=Number.MAX_VALUE;for(var e=0;e<f.selPoints_.length;e++){var c=f.selPoints_[e];var a=Math.pow(c.canvasx-d.dragEndX,2)+Math.pow(c.canvasy-d.dragEndY,2);if(!isNaN(a)&&(l==-1||a<m)){m=a;l=e}}var h=f.attr_("highlightCircleSize")+2;if(m<=h*h){j=f.selPoints_[l]}}if(j){n(b,j)}if(k){k(b,f.lastx_,f.selPoints_)}};Dygraph.Interaction.endZoom=function(c,i,e){e.isZooming=false;e.dragEndX=i.dragGetX_(c,e);e.dragEndY=i.dragGetY_(c,e);var h=Math.abs(e.dragEndX-e.dragStartX);var d=Math.abs(e.dragEndY-e.dragStartY);if(h<2&&d<2&&i.lastx_!==undefined&&i.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(i,c,e)}var b=i.getArea();if(h>=10&&e.dragDirection==Dygraph.HORIZONTAL){var f=Math.min(e.dragStartX,e.dragEndX),k=Math.max(e.dragStartX,e.dragEndX);f=Math.max(f,b.x);k=Math.min(k,b.x+b.w);if(f<k){i.doZoomX_(f,k)}e.cancelNextDblclick=true}else{if(d>=10&&e.dragDirection==Dygraph.VERTICAL){var j=Math.min(e.dragStartY,e.dragEndY),a=Math.max(e.dragStartY,e.dragEndY);j=Math.max(j,b.y);a=Math.min(a,b.y+b.h);if(j<a){i.doZoomY_(j,a)}e.cancelNextDblclick=true}else{if(e.zoomMoved){i.clearZoomRect_()}}}e.dragStartX=null;e.dragStartY=null};Dygraph.Interaction.startTouch=function(f,e,d){f.preventDefault();if(f.touches.length>1){d.startTimeForDoubleTapMs=null}var h=[];for(var c=0;c<f.touches.length;c++){var b=f.touches[c];h.push({pageX:b.pageX,pageY:b.pageY,dataX:e.toDataXCoord(b.pageX),dataY:e.toDataYCoord(b.pageY)})}d.initialTouches=h;if(h.length==1){d.initialPinchCenter=h[0];d.touchDirections={x:true,y:true}}else{if(h.length>=2){d.initialPinchCenter={pageX:0.5*(h[0].pageX+h[1].pageX),pageY:0.5*(h[0].pageY+h[1].pageY),dataX:0.5*(h[0].dataX+h[1].dataX),dataY:0.5*(h[0].dataY+h[1].dataY)};var a=180/Math.PI*Math.atan2(d.initialPinchCenter.pageY-h[0].pageY,h[0].pageX-d.initialPinchCenter.pageX);a=Math.abs(a);if(a>90){a=90-a}d.touchDirections={x:(a<(90-45/2)),y:(a>45/2)}}}d.initialRange={x:e.xAxisRange(),y:e.yAxisRange()}};Dygraph.Interaction.moveTouch=function(n,q,d){d.startTimeForDoubleTapMs=null;var p,l=[];for(p=0;p<n.touches.length;p++){var k=n.touches[p];l.push({pageX:k.pageX,pageY:k.pageY})}var a=d.initialTouches;var h;var j=d.initialPinchCenter;if(l.length==1){h=l[0]}else{h={pageX:0.5*(l[0].pageX+l[1].pageX),pageY:0.5*(l[0].pageY+l[1].pageY)}}var m={pageX:h.pageX-j.pageX,pageY:h.pageY-j.pageY};var f=d.initialRange.x[1]-d.initialRange.x[0];var o=d.initialRange.y[0]-d.initialRange.y[1];m.dataX=(m.pageX/q.plotter_.area.w)*f;m.dataY=(m.pageY/q.plotter_.area.h)*o;var w,c;if(l.length==1){w=1;c=1}else{if(l.length>=2){var e=(a[1].pageX-j.pageX);w=(l[1].pageX-h.pageX)/e;var v=(a[1].pageY-j.pageY);c=(l[1].pageY-h.pageY)/v}}w=Math.min(8,Math.max(0.125,w));c=Math.min(8,Math.max(0.125,c));var u=false;if(d.touchDirections.x){q.dateWindow_=[j.dataX-m.dataX+(d.initialRange.x[0]-j.dataX)/w,j.dataX-m.dataX+(d.initialRange.x[1]-j.dataX)/w];u=true}if(d.touchDirections.y){for(p=0;p<1;p++){var b=q.axes_[p];var s=q.attributes_.getForAxis("logscale",p);if(s){}else{b.valueWindow=[j.dataY-m.dataY+(d.initialRange.y[0]-j.dataY)/c,j.dataY-m.dataY+(d.initialRange.y[1]-j.dataY)/c];u=true}}}q.drawGraph_(false);if(u&&l.length>1&&q.attr_("zoomCallback")){var r=q.xAxisRange();q.attr_("zoomCallback")(r[0],r[1],q.yAxisRanges())}};Dygraph.Interaction.endTouch=function(e,d,c){if(e.touches.length!==0){Dygraph.Interaction.startTouch(e,d,c)}else{if(e.changedTouches.length==1){var a=new Date().getTime();var b=e.changedTouches[0];if(c.startTimeForDoubleTapMs&&a-c.startTimeForDoubleTapMs<500&&c.doubleTapX&&Math.abs(c.doubleTapX-b.screenX)<50&&c.doubleTapY&&Math.abs(c.doubleTapY-b.screenY)<50){d.resetZoom()}else{c.startTimeForDoubleTapMs=a;c.doubleTapX=b.screenX;c.doubleTapY=b.screenY}}}};Dygraph.Interaction.defaultModel={mousedown:function(c,b,a){if(c.button&&c.button==2){return}a.initializeMouseDown(c,b,a);if(c.altKey||c.shiftKey){Dygraph.startPan(c,b,a)}else{Dygraph.startZoom(c,b,a)}},mousemove:function(c,b,a){if(a.isZooming){Dygraph.moveZoom(c,b,a)}else{if(a.isPanning){Dygraph.movePan(c,b,a)}}},mouseup:function(c,b,a){if(a.isZooming){Dygraph.endZoom(c,b,a)}else{if(a.isPanning){Dygraph.endPan(c,b,a)}}},touchstart:function(c,b,a){Dygraph.Interaction.startTouch(c,b,a)},touchmove:function(c,b,a){Dygraph.Interaction.moveTouch(c,b,a)},touchend:function(c,b,a){Dygraph.Interaction.endTouch(c,b,a)},mouseout:function(c,b,a){if(a.isZooming){a.dragEndX=null;a.dragEndY=null;b.clearZoomRect_()}},dblclick:function(c,b,a){if(a.cancelNextDblclick){a.cancelNextDblclick=false;return}if(c.altKey||c.shiftKey){return}b.resetZoom()}};Dygraph.DEFAULT_ATTRS.interactionModel=Dygraph.Interaction.defaultModel;Dygraph.defaultInteractionModel=Dygraph.Interaction.defaultModel;Dygraph.endZoom=Dygraph.Interaction.endZoom;Dygraph.moveZoom=Dygraph.Interaction.moveZoom;Dygraph.startZoom=Dygraph.Interaction.startZoom;Dygraph.endPan=Dygraph.Interaction.endPan;Dygraph.movePan=Dygraph.Interaction.movePan;Dygraph.startPan=Dygraph.Interaction.startPan;Dygraph.Interaction.nonInteractiveModel_={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a)},mouseup:function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}}};Dygraph.Interaction.dragIsPanInteractionModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);Dygraph.startPan(c,b,a)},mousemove:function(c,b,a){if(a.isPanning){Dygraph.movePan(c,b,a)}},mouseup:function(c,b,a){if(a.isPanning){Dygraph.endPan(c,b,a)}}};"use strict";Dygraph.TickList=undefined;Dygraph.Ticker=undefined;Dygraph.numericLinearTicks=function(d,c,i,g,f,h){var e=function(a){if(a==="logscale"){return false}return g(a)};return Dygraph.numericTicks(d,c,i,e,f,h)};Dygraph.numericTicks=function(F,E,u,p,d,q){var z=(p("pixelsPerLabel"));var G=[];var C,A,t,y;if(q){for(C=0;C<q.length;C++){G.push({v:q[C]})}}else{if(p("logscale")){y=Math.floor(u/z);var l=Dygraph.binarySearch(F,Dygraph.PREFERRED_LOG_TICK_VALUES,1);var H=Dygraph.binarySearch(E,Dygraph.PREFERRED_LOG_TICK_VALUES,-1);if(l==-1){l=0}if(H==-1){H=Dygraph.PREFERRED_LOG_TICK_VALUES.length-1}var s=null;if(H-l>=y/4){for(var r=H;r>=l;r--){var m=Dygraph.PREFERRED_LOG_TICK_VALUES[r];var k=Math.log(m/F)/Math.log(E/F)*u;var D={v:m};if(s===null){s={tickValue:m,pixel_coord:k}}else{if(Math.abs(k-s.pixel_coord)>=z){s={tickValue:m,pixel_coord:k}}else{D.label=""}}G.push(D)}G.reverse()}}if(G.length===0){var g=p("labelsKMG2");var n,h;if(g){n=[1,2,4,8,16,32,64,128,256];h=16}else{n=[1,2,5,10,20,50,100];h=10}var w=Math.ceil(u/z);var o=Math.abs(E-F)/w;var v=Math.floor(Math.log(o)/Math.log(h));var f=Math.pow(h,v);var I,x,c,e;for(A=0;A<n.length;A++){I=f*n[A];x=Math.floor(F/I)*I;c=Math.ceil(E/I)*I;y=Math.abs(c-x)/I;e=u/y;if(e>z){break}}if(x>c){I*=-1}for(C=0;C<y;C++){t=x+C*I;G.push({v:t})}}}var B=(p("axisLabelFormatter"));for(C=0;C<G.length;C++){if(G[C].label!==undefined){continue}G[C].label=B(G[C].v,0,p,d)}return G};Dygraph.dateTicker=function(e,c,i,g,f,h){var d=Dygraph.pickDateTickGranularity(e,c,i,g);if(d>=0){return Dygraph.getDateAxis(e,c,d,g,f)}else{return[]}};Dygraph.SECONDLY=0;Dygraph.TWO_SECONDLY=1;Dygraph.FIVE_SECONDLY=2;Dygraph.TEN_SECONDLY=3;Dygraph.THIRTY_SECONDLY=4;Dygraph.MINUTELY=5;Dygraph.TWO_MINUTELY=6;Dygraph.FIVE_MINUTELY=7;Dygraph.TEN_MINUTELY=8;Dygraph.THIRTY_MINUTELY=9;Dygraph.HOURLY=10;Dygraph.TWO_HOURLY=11;Dygraph.SIX_HOURLY=12;Dygraph.DAILY=13;Dygraph.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.CENTENNIAL=20;Dygraph.NUM_GRANULARITIES=21;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.LONG_TICK_PLACEMENTS=[];Dygraph.LONG_TICK_PLACEMENTS[Dygraph.MONTHLY]={months:[0,1,2,3,4,5,6,7,8,9,10,11],year_mod:1};Dygraph.LONG_TICK_PLACEMENTS[Dygraph.QUARTERLY]={months:[0,3,6,9],year_mod:1};Dygraph.LONG_TICK_PLACEMENTS[Dygraph.BIANNUAL]={months:[0,6],year_mod:1};Dygraph.LONG_TICK_PLACEMENTS[Dygraph.ANNUAL]={months:[0],year_mod:1};Dygraph.LONG_TICK_PLACEMENTS[Dygraph.DECADAL]={months:[0],year_mod:10};Dygraph.LONG_TICK_PLACEMENTS[Dygraph.CENTENNIAL]={months:[0],year_mod:100};Dygraph.PREFERRED_LOG_TICK_VALUES=function(){var c=[];for(var b=-39;b<=39;b++){var a=Math.pow(10,b);for(var d=1;d<=9;d++){var e=a*d;c.push(e)}}return c}();Dygraph.pickDateTickGranularity=function(d,c,j,h){var g=(h("pixelsPerLabel"));for(var f=0;f<Dygraph.NUM_GRANULARITIES;f++){var e=Dygraph.numDateTicks(d,c,f);if(j/e>=g){return f}}return -1};Dygraph.numDateTicks=function(e,b,f){if(f<Dygraph.MONTHLY){var g=Dygraph.SHORT_SPACINGS[f];return Math.floor(0.5+1*(b-e)/g)}else{var d=Dygraph.LONG_TICK_PLACEMENTS[f];var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d.months.length/d.year_mod)}};Dygraph.getDateAxis=function(p,l,a,n,z){var w=(n("axisLabelFormatter"));var C=[];var m;if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var y=c/1000;var A=new Date(p);Dygraph.setDateSameTZ(A,{ms:0});var h;if(y<=60){h=A.getSeconds();Dygraph.setDateSameTZ(A,{s:h-h%y})}else{Dygraph.setDateSameTZ(A,{s:0});y/=60;if(y<=60){h=A.getMinutes();Dygraph.setDateSameTZ(A,{m:h-h%y})}else{Dygraph.setDateSameTZ(A,{m:0});y/=60;if(y<=24){h=A.getHours();A.setHours(h-h%y)}else{A.setHours(0);y/=24;if(y==7){A.setDate(A.getDate()-A.getDay())}}}}p=A.getTime();var B=new Date(p).getTimezoneOffset();var e=(c>=Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]);for(m=p;m<=l;m+=c){A=new Date(m);if(e&&A.getTimezoneOffset()!=B){var k=A.getTimezoneOffset()-B;m+=k*60*1000;A=new Date(m);B=A.getTimezoneOffset();if(new Date(m+c).getTimezoneOffset()!=B){m+=c;A=new Date(m);B=A.getTimezoneOffset()}}C.push({v:m,label:w(A,a,n,z)})}}else{var f;var r=1;if(a<Dygraph.NUM_GRANULARITIES){f=Dygraph.LONG_TICK_PLACEMENTS[a].months;r=Dygraph.LONG_TICK_PLACEMENTS[a].year_mod}else{Dygraph.warn("Span of dates is too long")}var v=new Date(p).getFullYear();var q=new Date(l).getFullYear();var b=Dygraph.zeropad;for(var u=v;u<=q;u++){if(u%r!==0){continue}for(var s=0;s<f.length;s++){var o=u+"/"+b(1+f[s])+"/01";m=Dygraph.dateStrToMillis(o);if(m<p||m>l){continue}C.push({v:m,label:w(new Date(m),a,n,z)})}}}return C};if(Dygraph&&Dygraph.DEFAULT_ATTRS&&Dygraph.DEFAULT_ATTRS.axes&&Dygraph.DEFAULT_ATTRS.axes["x"]&&Dygraph.DEFAULT_ATTRS.axes["y"]&&Dygraph.DEFAULT_ATTRS.axes["y2"]){Dygraph.DEFAULT_ATTRS.axes["x"]["ticker"]=Dygraph.dateTicker;Dygraph.DEFAULT_ATTRS.axes["y"]["ticker"]=Dygraph.numericTicks;Dygraph.DEFAULT_ATTRS.axes["y2"]["ticker"]=Dygraph.numericTicks}Dygraph.Plugins={};Dygraph.Plugins.Annotations=(function(){var a=function(){this.annotations_=[]};a.prototype.toString=function(){return"Annotations Plugin"};a.prototype.activate=function(b){return{clearChart:this.clearChart,didDrawChart:this.didDrawChart}};a.prototype.detachLabels=function(){for(var c=0;c<this.annotations_.length;c++){var b=this.annotations_[c];if(b.parentNode){b.parentNode.removeChild(b)}this.annotations_[c]=null}this.annotations_=[]};a.prototype.clearChart=function(b){this.detachLabels()};a.prototype.didDrawChart=function(v){var t=v.dygraph;var r=t.layout_.annotated_points;if(!r||r.length===0){return}var h=v.canvas.parentNode;var x={position:"absolute",fontSize:t.getOption("axisLabelFontSize")+"px",zIndex:10,overflow:"hidden"};var b=function(e,g,i){return function(y){var p=i.annotation;if(p.hasOwnProperty(e)){p[e](p,i,t,y)}else{if(t.getOption(g)){t.getOption(g)(p,i,t,y)}}}};var u=v.dygraph.plotter_.area;var q={};for(var s=0;s<r.length;s++){var l=r[s];if(l.canvasx<u.x||l.canvasx>u.x+u.w||l.canvasy<u.y||l.canvasy>u.y+u.h){continue}var w=l.annotation;var n=6;if(w.hasOwnProperty("tickHeight")){n=w.tickHeight}var j=document.createElement("div");for(var A in x){if(x.hasOwnProperty(A)){j.style[A]=x[A]}}if(!w.hasOwnProperty("icon")){j.className="dygraphDefaultAnnotation"}if(w.hasOwnProperty("cssClass")){j.className+=" "+w.cssClass}var m=w.hasOwnProperty("width")?w.width:16;var k=w.hasOwnProperty("height")?w.height:16;if(w.hasOwnProperty("icon")){var z=document.createElement("img");z.src=w.icon;z.width=m;z.height=k;j.appendChild(z)}else{if(l.annotation.hasOwnProperty("shortText")){j.appendChild(document.createTextNode(l.annotation.shortText))}}var c=l.canvasx-m/2;j.style.left=c+"px";var f=0;if(w.attachAtBottom){var d=(u.y+u.h-k-n);if(q[c]){d-=q[c]}else{q[c]=0}q[c]+=(n+k);f=d}else{f=l.canvasy-k-n}j.style.top=f+"px";j.style.width=m+"px";j.style.height=k+"px";j.title=l.annotation.text;j.style.color=t.colorsMap_[l.name];j.style.borderColor=t.colorsMap_[l.name];w.div=j;t.addAndTrackEvent(j,"click",b("clickHandler","annotationClickHandler",l,this));t.addAndTrackEvent(j,"mouseover",b("mouseOverHandler","annotationMouseOverHandler",l,this));t.addAndTrackEvent(j,"mouseout",b("mouseOutHandler","annotationMouseOutHandler",l,this));t.addAndTrackEvent(j,"dblclick",b("dblClickHandler","annotationDblClickHandler",l,this));h.appendChild(j);this.annotations_.push(j);var o=v.drawingContext;o.save();o.strokeStyle=t.colorsMap_[l.name];o.beginPath();if(!w.attachAtBottom){o.moveTo(l.canvasx,l.canvasy);o.lineTo(l.canvasx,l.canvasy-2-n)}else{var d=f+k;o.moveTo(l.canvasx,d);o.lineTo(l.canvasx,d+n)}o.closePath();o.stroke();o.restore()}};a.prototype.destroy=function(){this.detachLabels()};return a})();Dygraph.Plugins.Axes=(function(){var a=function(){this.xlabels_=[];this.ylabels_=[]};a.prototype.toString=function(){return"Axes Plugin"};a.prototype.activate=function(b){return{layout:this.layout,clearChart:this.clearChart,willDrawChart:this.willDrawChart}};a.prototype.layout=function(f){var d=f.dygraph;if(d.getOption("drawYAxis")){var b=d.getOption("yAxisLabelWidth")+2*d.getOption("axisTickSize");f.reserveSpaceLeft(b)}if(d.getOption("drawXAxis")){var c;if(d.getOption("xAxisHeight")){c=d.getOption("xAxisHeight")}else{c=d.getOptionForAxis("axisLabelFontSize","x")+2*d.getOption("axisTickSize")}f.reserveSpaceBottom(c)}if(d.numAxes()==2){if(d.getOption("drawYAxis")){var b=d.getOption("yAxisLabelWidth")+2*d.getOption("axisTickSize");f.reserveSpaceRight(b)}}else{if(d.numAxes()>2){d.error("Only two y-axes are supported at this time. (Trying to use "+d.numAxes()+")")}}};a.prototype.detachLabels=function(){function b(d){for(var c=0;c<d.length;c++){var e=d[c];if(e.parentNode){e.parentNode.removeChild(e)}}}b(this.xlabels_);b(this.ylabels_);this.xlabels_=[];this.ylabels_=[]};a.prototype.clearChart=function(b){this.detachLabels()};a.prototype.willDrawChart=function(H){var F=H.dygraph;if(!F.getOption("drawXAxis")&&!F.getOption("drawYAxis")){return}function B(e){return Math.round(e)+0.5}function A(e){return Math.round(e)-0.5}var j=H.drawingContext;var v=H.canvas.parentNode;var J=H.canvas.width;var d=H.canvas.height;var s,u,t,E,D;var C=function(e){return{position:"absolute",fontSize:F.getOptionForAxis("axisLabelFontSize",e)+"px",zIndex:10,color:F.getOptionForAxis("axisLabelColor",e),width:F.getOption("axisLabelWidth")+"px",lineHeight:"normal",overflow:"hidden"}};var p={x:C("x"),y:C("y"),y2:C("y2")};var m=function(g,x,y){var K=document.createElement("div");var e=p[y=="y2"?"y2":x];for(var r in e){if(e.hasOwnProperty(r)){K.style[r]=e[r]}}var i=document.createElement("div");i.className="dygraph-axis-label dygraph-axis-label-"+x+(y?" dygraph-axis-label-"+y:"");i.innerHTML=g;K.appendChild(i);return K};j.save();var I=F.layout_;var G=H.dygraph.plotter_.area;if(F.getOption("drawYAxis")){if(I.yticks&&I.yticks.length>0){var h=F.numAxes();for(D=0;D<I.yticks.length;D++){E=I.yticks[D];if(typeof(E)=="function"){return}u=G.x;var o=1;var f="y1";if(E[0]==1){u=G.x+G.w;o=-1;f="y2"}var k=F.getOptionForAxis("axisLabelFontSize",f);t=G.y+E[1]*G.h;s=m(E[2],"y",h==2?f:null);var z=(t-k/2);if(z<0){z=0}if(z+k+3>d){s.style.bottom="0px"}else{s.style.top=z+"px"}if(E[0]===0){s.style.left=(G.x-F.getOption("yAxisLabelWidth")-F.getOption("axisTickSize"))+"px";s.style.textAlign="right"}else{if(E[0]==1){s.style.left=(G.x+G.w+F.getOption("axisTickSize"))+"px";s.style.textAlign="left"}}s.style.width=F.getOption("yAxisLabelWidth")+"px";v.appendChild(s);this.ylabels_.push(s)}var n=this.ylabels_[0];var k=F.getOptionForAxis("axisLabelFontSize","y");var q=parseInt(n.style.top,10)+k;if(q>d-k){n.style.top=(parseInt(n.style.top,10)-k/2)+"px"}}var c;if(F.getOption("drawAxesAtZero")){var w=F.toPercentXCoord(0);if(w>1||w<0||isNaN(w)){w=0}c=B(G.x+w*G.w)}else{c=B(G.x)}j.strokeStyle=F.getOptionForAxis("axisLineColor","y");j.lineWidth=F.getOptionForAxis("axisLineWidth","y");j.beginPath();j.moveTo(c,A(G.y));j.lineTo(c,A(G.y+G.h));j.closePath();j.stroke();if(F.numAxes()==2){j.strokeStyle=F.getOptionForAxis("axisLineColor","y2");j.lineWidth=F.getOptionForAxis("axisLineWidth","y2");j.beginPath();j.moveTo(A(G.x+G.w),A(G.y));j.lineTo(A(G.x+G.w),A(G.y+G.h));j.closePath();j.stroke()}}if(F.getOption("drawXAxis")){if(I.xticks){for(D=0;D<I.xticks.length;D++){E=I.xticks[D];u=G.x+E[0]*G.w;t=G.y+G.h;s=m(E[1],"x");s.style.textAlign="center";s.style.top=(t+F.getOption("axisTickSize"))+"px";var l=(u-F.getOption("axisLabelWidth")/2);if(l+F.getOption("axisLabelWidth")>J){l=J-F.getOption("xAxisLabelWidth");s.style.textAlign="right"}if(l<0){l=0;s.style.textAlign="left"}s.style.left=l+"px";s.style.width=F.getOption("xAxisLabelWidth")+"px";v.appendChild(s);this.xlabels_.push(s)}}j.strokeStyle=F.getOptionForAxis("axisLineColor","x");j.lineWidth=F.getOptionForAxis("axisLineWidth","x");j.beginPath();var b;if(F.getOption("drawAxesAtZero")){var w=F.toPercentYCoord(0,0);if(w>1||w<0){w=1}b=A(G.y+w*G.h)}else{b=A(G.y+G.h)}j.moveTo(B(G.x),b);j.lineTo(B(G.x+G.w),b);j.closePath();j.stroke()}j.restore()};return a})();Dygraph.Plugins.ChartLabels=(function(){var c=function(){this.title_div_=null;this.xlabel_div_=null;this.ylabel_div_=null;this.y2label_div_=null};c.prototype.toString=function(){return"ChartLabels Plugin"};c.prototype.activate=function(d){return{layout:this.layout,didDrawChart:this.didDrawChart}};var b=function(d){var e=document.createElement("div");e.style.position="absolute";e.style.left=d.x+"px";e.style.top=d.y+"px";e.style.width=d.w+"px";e.style.height=d.h+"px";return e};c.prototype.detachLabels_=function(){var e=[this.title_div_,this.xlabel_div_,this.ylabel_div_,this.y2label_div_];for(var d=0;d<e.length;d++){var f=e[d];if(!f){continue}if(f.parentNode){f.parentNode.removeChild(f)}}this.title_div_=null;this.xlabel_div_=null;this.ylabel_div_=null;this.y2label_div_=null};var a=function(l,i,f,h,j){var d=document.createElement("div");d.style.position="absolute";if(f==1){d.style.left="0px"}else{d.style.left=i.x+"px"}d.style.top=i.y+"px";d.style.width=i.w+"px";d.style.height=i.h+"px";d.style.fontSize=(l.getOption("yLabelWidth")-2)+"px";var m=document.createElement("div");m.style.position="absolute";m.style.width=i.h+"px";m.style.height=i.w+"px";m.style.top=(i.h/2-i.w/2)+"px";m.style.left=(i.w/2-i.h/2)+"px";m.style.textAlign="center";var e="rotate("+(f==1?"-":"")+"90deg)";m.style.transform=e;m.style.WebkitTransform=e;m.style.MozTransform=e;m.style.OTransform=e;m.style.msTransform=e;if(typeof(document.documentMode)!=="undefined"&&document.documentMode<9){m.style.filter="progid:DXImageTransform.Microsoft.BasicImage(rotation="+(f==1?"3":"1")+")";m.style.left="0px";m.style.top="0px"}var k=document.createElement("div");k.className=h;k.innerHTML=j;m.appendChild(k);d.appendChild(m);return d};c.prototype.layout=function(k){this.detachLabels_();var i=k.dygraph;var m=k.chart_div;if(i.getOption("title")){var d=k.reserveSpaceTop(i.getOption("titleHeight"));this.title_div_=b(d);this.title_div_.style.textAlign="center";this.title_div_.style.fontSize=(i.getOption("titleHeight")-8)+"px";this.title_div_.style.fontWeight="bold";this.title_div_.style.zIndex=10;var f=document.createElement("div");f.className="dygraph-label dygraph-title";f.innerHTML=i.getOption("title");this.title_div_.appendChild(f);m.appendChild(this.title_div_)}if(i.getOption("xlabel")){var j=k.reserveSpaceBottom(i.getOption("xLabelHeight"));this.xlabel_div_=b(j);this.xlabel_div_.style.textAlign="center";this.xlabel_div_.style.fontSize=(i.getOption("xLabelHeight")-2)+"px";var f=document.createElement("div");f.className="dygraph-label dygraph-xlabel";f.innerHTML=i.getOption("xlabel");this.xlabel_div_.appendChild(f);m.appendChild(this.xlabel_div_)}if(i.getOption("ylabel")){var h=k.reserveSpaceLeft(0);this.ylabel_div_=a(i,h,1,"dygraph-label dygraph-ylabel",i.getOption("ylabel"));m.appendChild(this.ylabel_div_)}if(i.getOption("y2label")&&i.numAxes()==2){var l=k.reserveSpaceRight(0);this.y2label_div_=a(i,l,2,"dygraph-label dygraph-y2label",i.getOption("y2label"));m.appendChild(this.y2label_div_)}};c.prototype.didDrawChart=function(f){var d=f.dygraph;if(this.title_div_){this.title_div_.children[0].innerHTML=d.getOption("title")}if(this.xlabel_div_){this.xlabel_div_.children[0].innerHTML=d.getOption("xlabel")}if(this.ylabel_div_){this.ylabel_div_.children[0].children[0].innerHTML=d.getOption("ylabel")}if(this.y2label_div_){this.y2label_div_.children[0].children[0].innerHTML=d.getOption("y2label")}};c.prototype.clearChart=function(){};c.prototype.destroy=function(){this.detachLabels_()};return c})();Dygraph.Plugins.Grid=(function(){var a=function(){};a.prototype.toString=function(){return"Gridline Plugin"};a.prototype.activate=function(b){return{willDrawChart:this.willDrawChart}};a.prototype.willDrawChart=function(s){var q=s.dygraph;var l=s.drawingContext;var t=q.layout_;var r=s.dygraph.plotter_.area;function k(e){return Math.round(e)+0.5}function j(e){return Math.round(e)-0.5}var h,f,p,u;if(q.getOption("drawYGrid")){var o=["y","y2"];var m=[],v=[],b=[],n=[],d=[];for(var p=0;p<o.length;p++){b[p]=q.getOptionForAxis("drawGrid",o[p]);if(b[p]){m[p]=q.getOptionForAxis("gridLineColor",o[p]);v[p]=q.getOptionForAxis("gridLineWidth",o[p]);d[p]=q.getOptionForAxis("gridLinePattern",o[p]);n[p]=d[p]&&(d[p].length>=2)}}u=t.yticks;l.save();for(p=0;p<u.length;p++){var c=u[p][0];if(b[c]){if(n[c]){l.installPattern(d[c])}l.strokeStyle=m[c];l.lineWidth=v[c];h=k(r.x);f=j(r.y+u[p][1]*r.h);l.beginPath();l.moveTo(h,f);l.lineTo(h+r.w,f);l.closePath();l.stroke();if(n[c]){l.uninstallPattern()}}}l.restore()}if(q.getOption("drawXGrid")&&q.getOptionForAxis("drawGrid","x")){u=t.xticks;l.save();var d=q.getOptionForAxis("gridLinePattern","x");var n=d&&(d.length>=2);if(n){l.installPattern(d)}l.strokeStyle=q.getOptionForAxis("gridLineColor","x");l.lineWidth=q.getOptionForAxis("gridLineWidth","x");for(p=0;p<u.length;p++){h=k(r.x+u[p][0]*r.w);f=j(r.y+r.h);l.beginPath();l.moveTo(h,f);l.lineTo(h,r.y);l.closePath();l.stroke()}if(n){l.uninstallPattern()}l.restore()}};a.prototype.destroy=function(){};return a})();Dygraph.Plugins.Legend=(function(){var c=function(){this.legend_div_=null;this.is_generated_div_=false};c.prototype.toString=function(){return"Legend Plugin"};var a,d;c.prototype.activate=function(j){var m;var f=j.getOption("labelsDivWidth");var l=j.getOption("labelsDiv");if(l&&null!==l){if(typeof(l)=="string"||l instanceof String){m=document.getElementById(l)}else{m=l}}else{var i={position:"absolute",fontSize:"14px",zIndex:10,width:f+"px",top:"0px",left:(j.size().width-f-2)+"px",background:"white",lineHeight:"normal",textAlign:"left",overflow:"hidden"};Dygraph.update(i,j.getOption("labelsDivStyles"));m=document.createElement("div");m.className="dygraph-legend";for(var h in i){if(!i.hasOwnProperty(h)){continue}try{m.style[h]=i[h]}catch(k){this.warn("You are using unsupported css properties for your browser in labelsDivStyles")}}j.graphDiv.appendChild(m);this.is_generated_div_=true}this.legend_div_=m;this.one_em_width_=10;return{select:this.select,deselect:this.deselect,predraw:this.predraw,didDrawChart:this.didDrawChart}};var b=function(g){var f=document.createElement("span");f.setAttribute("style","margin: 0; padding: 0 0 0 1em; border: 0;");g.appendChild(f);var e=f.offsetWidth;g.removeChild(f);return e};c.prototype.select=function(i){var h=i.selectedX;var g=i.selectedPoints;var f=a(i.dygraph,h,g,this.one_em_width_);this.legend_div_.innerHTML=f};c.prototype.deselect=function(h){var f=b(this.legend_div_);this.one_em_width_=f;var g=a(h.dygraph,undefined,undefined,f);this.legend_div_.innerHTML=g};c.prototype.didDrawChart=function(f){this.deselect(f)};c.prototype.predraw=function(h){if(!this.is_generated_div_){return}h.dygraph.graphDiv.appendChild(this.legend_div_);var g=h.dygraph.plotter_.area;var f=h.dygraph.getOption("labelsDivWidth");this.legend_div_.style.left=g.x+g.w-f-1+"px";this.legend_div_.style.top=g.y+"px";this.legend_div_.style.width=f+"px"};c.prototype.destroy=function(){this.legend_div_=null};a=function(w,p,l,f){if(w.getOption("showLabelsOnHighlight")!==true){return""}var r,C,u,s,m;var z=w.getLabels();if(typeof(p)==="undefined"){if(w.getOption("legend")!="always"){return""}C=w.getOption("labelsSeparateLines");r="";for(u=1;u<z.length;u++){var q=w.getPropertiesForSeries(z[u]);if(!q.visible){continue}if(r!==""){r+=(C?"<br/>":" ")}m=w.getOption("strokePattern",z[u]);s=d(m,q.color,f);r+="<span style='font-weight: bold; color: "+q.color+";'>"+s+" "+z[u]+"</span>"}return r}var A=w.optionsViewForAxis_("x");var o=A("valueFormatter");r=o(p,A,z[0],w);if(r!==""){r+=":"}var v=[];var j=w.numAxes();for(u=0;u<j;u++){v[u]=w.optionsViewForAxis_("y"+(u?1+u:""))}var k=w.getOption("labelsShowZeroValues");C=w.getOption("labelsSeparateLines");var B=w.getHighlightSeries();for(u=0;u<l.length;u++){var t=l[u];if(t.yval===0&&!k){continue}if(!Dygraph.isOK(t.canvasy)){continue}if(C){r+="<br/>"}var q=w.getPropertiesForSeries(t.name);var n=v[q.axis-1];var y=n("valueFormatter");var e=y(t.yval,n,t.name,w);var h=(t.name==B)?" class='highlight'":"";r+="<span"+h+"> <b><span style='color: "+q.color+";'>"+t.name+"</span></b>:&nbsp;"+e+"</span>"}return r};d=function(s,h,r){var e=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(e){return"&mdash;"}if(!s||s.length<=1){return'<div style="display: inline-block; position: relative; bottom: .5ex; padding-left: 1em; height: 1px; border-bottom: 2px solid '+h+';"></div>'}var l,k,f,o;var g=0,q=0;var p=[];var n;for(l=0;l<=s.length;l++){g+=s[l%s.length]}n=Math.floor(r/(g-s[0]));if(n>1){for(l=0;l<s.length;l++){p[l]=s[l]/r}q=p.length}else{n=1;for(l=0;l<s.length;l++){p[l]=s[l]/g}q=p.length+1}var m="";for(k=0;k<n;k++){for(l=0;l<q;l+=2){f=p[l%p.length];if(l<s.length){o=p[(l+1)%p.length]}else{o=0}m+='<div style="display: inline-block; position: relative; bottom: .5ex; margin-right: '+o+"em; padding-left: "+f+"em; height: 1px; border-bottom: 2px solid "+h+';"></div>'}}return m};return c})();Dygraph.Plugins.RangeSelector=(function(){var a=function(){this.isIE_=/MSIE/.test(navigator.userAgent)&&!window.opera;this.hasTouchInterface_=typeof(TouchEvent)!="undefined";this.isMobileDevice_=/mobile|android/gi.test(navigator.appVersion);this.interfaceCreated_=false};a.prototype.toString=function(){return"RangeSelector Plugin"};a.prototype.activate=function(b){this.dygraph_=b;this.isUsingExcanvas_=b.isUsingExcanvas_;if(this.getOption_("showRangeSelector")){this.createInterface_()}return{layout:this.reserveSpace_,predraw:this.renderStaticLayer_,didDrawChart:this.renderInteractiveLayer_}};a.prototype.destroy=function(){this.bgcanvas_=null;this.fgcanvas_=null;this.leftZoomHandle_=null;this.rightZoomHandle_=null;this.iePanOverlay_=null};a.prototype.getOption_=function(b){return this.dygraph_.getOption(b)};a.prototype.setDefaultOption_=function(b,c){return this.dygraph_.attrs_[b]=c};a.prototype.createInterface_=function(){this.createCanvases_();if(this.isUsingExcanvas_){this.createIEPanOverlay_()}this.createZoomHandles_();this.initInteraction_();if(this.getOption_("animatedZooms")){this.dygraph_.warn("Animated zooms and range selector are not compatible; disabling animatedZooms.");this.dygraph_.updateOptions({animatedZooms:false},true)}this.interfaceCreated_=true;this.addToGraph_()};a.prototype.addToGraph_=function(){var b=this.graphDiv_=this.dygraph_.graphDiv;b.appendChild(this.bgcanvas_);b.appendChild(this.fgcanvas_);b.appendChild(this.leftZoomHandle_);b.appendChild(this.rightZoomHandle_)};a.prototype.removeFromGraph_=function(){var b=this.graphDiv_;b.removeChild(this.bgcanvas_);b.removeChild(this.fgcanvas_);b.removeChild(this.leftZoomHandle_);b.removeChild(this.rightZoomHandle_);this.graphDiv_=null};a.prototype.reserveSpace_=function(b){if(this.getOption_("showRangeSelector")){b.reserveSpaceBottom(this.getOption_("rangeSelectorHeight")+4)}};a.prototype.renderStaticLayer_=function(){if(!this.updateVisibility_()){return}this.resize_();this.drawStaticLayer_()};a.prototype.renderInteractiveLayer_=function(){if(!this.updateVisibility_()||this.isChangingRange_){return}this.placeZoomHandles_();this.drawInteractiveLayer_()};a.prototype.updateVisibility_=function(){var b=this.getOption_("showRangeSelector");if(b){if(!this.interfaceCreated_){this.createInterface_()}else{if(!this.graphDiv_||!this.graphDiv_.parentNode){this.addToGraph_()}}}else{if(this.graphDiv_){this.removeFromGraph_();var c=this.dygraph_;setTimeout(function(){c.width_=0;c.resize()},1)}}return b};a.prototype.resize_=function(){function d(e,f){e.style.top=f.y+"px";e.style.left=f.x+"px";e.width=f.w;e.height=f.h;e.style.width=e.width+"px";e.style.height=e.height+"px"}var c=this.dygraph_.layout_.getPlotArea();var b=0;if(this.getOption_("drawXAxis")){b=this.getOption_("xAxisHeight")||(this.getOption_("axisLabelFontSize")+2*this.getOption_("axisTickSize"))}this.canvasRect_={x:c.x,y:c.y+c.h+b+4,w:c.w,h:this.getOption_("rangeSelectorHeight")};d(this.bgcanvas_,this.canvasRect_);d(this.fgcanvas_,this.canvasRect_)};a.prototype.createCanvases_=function(){this.bgcanvas_=Dygraph.createCanvas();this.bgcanvas_.className="dygraph-rangesel-bgcanvas";this.bgcanvas_.style.position="absolute";this.bgcanvas_.style.zIndex=9;this.bgcanvas_ctx_=Dygraph.getContext(this.bgcanvas_);this.fgcanvas_=Dygraph.createCanvas();this.fgcanvas_.className="dygraph-rangesel-fgcanvas";this.fgcanvas_.style.position="absolute";this.fgcanvas_.style.zIndex=9;this.fgcanvas_.style.cursor="default";this.fgcanvas_ctx_=Dygraph.getContext(this.fgcanvas_)};a.prototype.createIEPanOverlay_=function(){this.iePanOverlay_=document.createElement("div");this.iePanOverlay_.style.position="absolute";this.iePanOverlay_.style.backgroundColor="white";this.iePanOverlay_.style.filter="alpha(opacity=0)";this.iePanOverlay_.style.display="none";this.iePanOverlay_.style.cursor="move";this.fgcanvas_.appendChild(this.iePanOverlay_)};a.prototype.createZoomHandles_=function(){var b=new Image();b.className="dygraph-rangesel-zoomhandle";b.style.position="absolute";b.style.zIndex=10;b.style.visibility="hidden";b.style.cursor="col-resize";if(/MSIE 7/.test(navigator.userAgent)){b.width=7;b.height=14;b.style.backgroundColor="white";b.style.border="1px solid #333333"}else{b.width=9;b.height=16;b.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAAzwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7sqSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII="}if(this.isMobileDevice_){b.width*=2;b.height*=2}this.leftZoomHandle_=b;this.rightZoomHandle_=b.cloneNode(false)};a.prototype.initInteraction_=function(){var o=this;var i=this.isIE_?document:window;var u=0;var v=null;var s=false;var d=false;var g=!this.isMobileDevice_&&!this.isUsingExcanvas_;var k=new Dygraph.IFrameTarp();var p,f,r,j,w,h,x,t,q,c,l;var e,n,m;p=function(C){var B=o.dygraph_.xAxisExtremes();var z=(B[1]-B[0])/o.canvasRect_.w;var A=B[0]+(C.leftHandlePos-o.canvasRect_.x)*z;var y=B[0]+(C.rightHandlePos-o.canvasRect_.x)*z;return[A,y]};f=function(y){Dygraph.cancelEvent(y);s=true;u=y.clientX;v=y.target?y.target:y.srcElement;if(y.type==="mousedown"||y.type==="dragstart"){Dygraph.addEvent(i,"mousemove",r);Dygraph.addEvent(i,"mouseup",j)}o.fgcanvas_.style.cursor="col-resize";k.cover();return true};r=function(C){if(!s){return false}Dygraph.cancelEvent(C);var z=C.clientX-u;if(Math.abs(z)<4){return true}u=C.clientX;var B=o.getZoomHandleStatus_();var y;if(v==o.leftZoomHandle_){y=B.leftHandlePos+z;y=Math.min(y,B.rightHandlePos-v.width-3);y=Math.max(y,o.canvasRect_.x)}else{y=B.rightHandlePos+z;y=Math.min(y,o.canvasRect_.x+o.canvasRect_.w);y=Math.max(y,B.leftHandlePos+v.width+3)}var A=v.width/2;v.style.left=(y-A)+"px";o.drawInteractiveLayer_();if(g){w()}return true};j=function(y){if(!s){return false}s=false;k.uncover();Dygraph.removeEvent(i,"mousemove",r);Dygraph.removeEvent(i,"mouseup",j);o.fgcanvas_.style.cursor="default";if(!g){w()}return true};w=function(){try{var z=o.getZoomHandleStatus_();o.isChangingRange_=true;if(!z.isZoomed){o.dygraph_.resetZoom()}else{var y=p(z);o.dygraph_.doZoomXDates_(y[0],y[1])}}finally{o.isChangingRange_=false}};h=function(A){if(o.isUsingExcanvas_){return A.srcElement==o.iePanOverlay_}else{var z=o.leftZoomHandle_.getBoundingClientRect();var y=z.left+z.width/2;z=o.rightZoomHandle_.getBoundingClientRect();var B=z.left+z.width/2;return(A.clientX>y&&A.clientX<B)}};x=function(y){if(!d&&h(y)&&o.getZoomHandleStatus_().isZoomed){Dygraph.cancelEvent(y);d=true;u=y.clientX;if(y.type==="mousedown"){Dygraph.addEvent(i,"mousemove",t);Dygraph.addEvent(i,"mouseup",q)}return true}return false};t=function(C){if(!d){return false}Dygraph.cancelEvent(C);var z=C.clientX-u;if(Math.abs(z)<4){return true}u=C.clientX;var B=o.getZoomHandleStatus_();var E=B.leftHandlePos;var y=B.rightHandlePos;var D=y-E;if(E+z<=o.canvasRect_.x){E=o.canvasRect_.x;y=E+D}else{if(y+z>=o.canvasRect_.x+o.canvasRect_.w){y=o.canvasRect_.x+o.canvasRect_.w;E=y-D}else{E+=z;y+=z}}var A=o.leftZoomHandle_.width/2;o.leftZoomHandle_.style.left=(E-A)+"px";o.rightZoomHandle_.style.left=(y-A)+"px";o.drawInteractiveLayer_();if(g){c()}return true};q=function(y){if(!d){return false}d=false;Dygraph.removeEvent(i,"mousemove",t);Dygraph.removeEvent(i,"mouseup",q);if(!g){c()}return true};c=function(){try{o.isChangingRange_=true;o.dygraph_.dateWindow_=p(o.getZoomHandleStatus_());o.dygraph_.drawGraph_(false)}finally{o.isChangingRange_=false}};l=function(y){if(s||d){return}var z=h(y)?"move":"default";if(z!=o.fgcanvas_.style.cursor){o.fgcanvas_.style.cursor=z}};e=function(y){if(y.type=="touchstart"&&y.targetTouches.length==1){if(f(y.targetTouches[0])){Dygraph.cancelEvent(y)}}else{if(y.type=="touchmove"&&y.targetTouches.length==1){if(r(y.targetTouches[0])){Dygraph.cancelEvent(y)}}else{j(y)}}};n=function(y){if(y.type=="touchstart"&&y.targetTouches.length==1){if(x(y.targetTouches[0])){Dygraph.cancelEvent(y)}}else{if(y.type=="touchmove"&&y.targetTouches.length==1){if(t(y.targetTouches[0])){Dygraph.cancelEvent(y)}}else{q(y)}}};m=function(B,A){var z=["touchstart","touchend","touchmove","touchcancel"];for(var y=0;y<z.length;y++){o.dygraph_.addAndTrackEvent(B,z[y],A)}};this.setDefaultOption_("interactionModel",Dygraph.Interaction.dragIsPanInteractionModel);this.setDefaultOption_("panEdgeFraction",0.0001);var b=window.opera?"mousedown":"dragstart";this.dygraph_.addAndTrackEvent(this.leftZoomHandle_,b,f);this.dygraph_.addAndTrackEvent(this.rightZoomHandle_,b,f);if(this.isUsingExcanvas_){this.dygraph_.addAndTrackEvent(this.iePanOverlay_,"mousedown",x)}else{this.dygraph_.addAndTrackEvent(this.fgcanvas_,"mousedown",x);this.dygraph_.addAndTrackEvent(this.fgcanvas_,"mousemove",l)}if(this.hasTouchInterface_){m(this.leftZoomHandle_,e);m(this.rightZoomHandle_,e);m(this.fgcanvas_,n)}};a.prototype.drawStaticLayer_=function(){var b=this.bgcanvas_ctx_;b.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);try{this.drawMiniPlot_()}catch(c){Dygraph.warn(c)}var d=0.5;this.bgcanvas_ctx_.lineWidth=1;b.strokeStyle="gray";b.beginPath();b.moveTo(d,d);b.lineTo(d,this.canvasRect_.h-d);b.lineTo(this.canvasRect_.w-d,this.canvasRect_.h-d);b.lineTo(this.canvasRect_.w-d,d);b.stroke()};a.prototype.drawMiniPlot_=function(){var f=this.getOption_("rangeSelectorPlotFillColor");var r=this.getOption_("rangeSelectorPlotStrokeColor");if(!f&&!r){return}var j=this.getOption_("stepPlot");var v=this.computeCombinedSeriesAndLimits_();var q=v.yMax-v.yMin;var p=this.bgcanvas_ctx_;var n=0.5;var e=this.dygraph_.xAxisExtremes();var o=Math.max(e[1]-e[0],1e-30);var g=(this.canvasRect_.w-n)/o;var u=(this.canvasRect_.h-n)/q;var t=this.canvasRect_.w-n;var b=this.canvasRect_.h-n;var d=null,c=null;p.beginPath();p.moveTo(n,b);for(var s=0;s<v.data.length;s++){var h=v.data[s];var l=((h[0]!==null)?((h[0]-e[0])*g):NaN);var k=((h[1]!==null)?(b-(h[1]-v.yMin)*u):NaN);if(isFinite(l)&&isFinite(k)){if(d===null){p.lineTo(l,b)}else{if(j){p.lineTo(l,c)}}p.lineTo(l,k);d=l;c=k}else{if(d!==null){if(j){p.lineTo(l,c);p.lineTo(l,b)}else{p.lineTo(d,b)}}d=c=null}}p.lineTo(t,b);p.closePath();if(f){var m=this.bgcanvas_ctx_.createLinearGradient(0,0,0,b);m.addColorStop(0,"white");m.addColorStop(1,f);this.bgcanvas_ctx_.fillStyle=m;p.fill()}if(r){this.bgcanvas_ctx_.strokeStyle=r;this.bgcanvas_ctx_.lineWidth=1.5;p.stroke()}};a.prototype.computeCombinedSeriesAndLimits_=function(){var v=this.dygraph_.rawData_;var u=this.getOption_("logscale");var q=[];var d;var h;var m;var t,s,r;var e,g;for(t=0;t<v.length;t++){if(v[t].length>1&&v[t][1]!==null){m=typeof v[t][1]!="number";if(m){d=[];h=[];for(r=0;r<v[t][1].length;r++){d.push(0);h.push(0)}}break}}for(t=0;t<v.length;t++){var l=v[t];e=l[0];if(m){for(r=0;r<d.length;r++){d[r]=h[r]=0}}else{d=h=0}for(s=1;s<l.length;s++){if(this.dygraph_.visibility()[s-1]){var n;if(m){for(r=0;r<d.length;r++){n=l[s][r];if(n===null||isNaN(n)){continue}d[r]+=n;h[r]++}}else{n=l[s];if(n===null||isNaN(n)){continue}d+=n;h++}}}if(m){for(r=0;r<d.length;r++){d[r]/=h[r]}g=d.slice(0)}else{g=d/h}q.push([e,g])}q=this.dygraph_.rollingAverage(q,this.dygraph_.rollPeriod_);if(typeof q[0][1]!="number"){for(t=0;t<q.length;t++){g=q[t][1];q[t][1]=g[0]}}var b=Number.MAX_VALUE;var c=-Number.MAX_VALUE;for(t=0;t<q.length;t++){g=q[t][1];if(g!==null&&isFinite(g)&&(!u||g>0)){b=Math.min(b,g);c=Math.max(c,g)}}var o=0.25;if(u){c=Dygraph.log10(c);c+=c*o;b=Dygraph.log10(b);for(t=0;t<q.length;t++){q[t][1]=Dygraph.log10(q[t][1])}}else{var f;var p=c-b;if(p<=Number.MIN_VALUE){f=c*o}else{f=p*o}c+=f;b-=f}return{data:q,yMin:b,yMax:c}};a.prototype.placeZoomHandles_=function(){var h=this.dygraph_.xAxisExtremes();var b=this.dygraph_.xAxisRange();var c=h[1]-h[0];var j=Math.max(0,(b[0]-h[0])/c);var f=Math.max(0,(h[1]-b[1])/c);var i=this.canvasRect_.x+this.canvasRect_.w*j;var e=this.canvasRect_.x+this.canvasRect_.w*(1-f);var d=Math.max(this.canvasRect_.y,this.canvasRect_.y+(this.canvasRect_.h-this.leftZoomHandle_.height)/2);var g=this.leftZoomHandle_.width/2;this.leftZoomHandle_.style.left=(i-g)+"px";this.leftZoomHandle_.style.top=d+"px";this.rightZoomHandle_.style.left=(e-g)+"px";this.rightZoomHandle_.style.top=this.leftZoomHandle_.style.top;this.leftZoomHandle_.style.visibility="visible";this.rightZoomHandle_.style.visibility="visible"};a.prototype.drawInteractiveLayer_=function(){var c=this.fgcanvas_ctx_;c.clearRect(0,0,this.canvasRect_.w,this.canvasRect_.h);var f=1;var e=this.canvasRect_.w-f;var b=this.canvasRect_.h-f;var h=this.getZoomHandleStatus_();c.strokeStyle="black";if(!h.isZoomed){c.beginPath();c.moveTo(f,f);c.lineTo(f,b);c.lineTo(e,b);c.lineTo(e,f);c.stroke();if(this.iePanOverlay_){this.iePanOverlay_.style.display="none"}}else{var g=Math.max(f,h.leftHandlePos-this.canvasRect_.x);var d=Math.min(e,h.rightHandlePos-this.canvasRect_.x);c.fillStyle="rgba(240, 240, 240, 0.6)";c.fillRect(0,0,g,this.canvasRect_.h);c.fillRect(d,0,this.canvasRect_.w-d,this.canvasRect_.h);c.beginPath();c.moveTo(f,f);c.lineTo(g,f);c.lineTo(g,b);c.lineTo(d,b);c.lineTo(d,f);c.lineTo(e,f);c.stroke();if(this.isUsingExcanvas_){this.iePanOverlay_.style.width=(d-g)+"px";this.iePanOverlay_.style.left=g+"px";this.iePanOverlay_.style.height=b+"px";this.iePanOverlay_.style.display="inline"}}};a.prototype.getZoomHandleStatus_=function(){var c=this.leftZoomHandle_.width/2;var d=parseFloat(this.leftZoomHandle_.style.left)+c;var b=parseFloat(this.rightZoomHandle_.style.left)+c;return{leftHandlePos:d,rightHandlePos:b,isZoomed:(d-1>this.canvasRect_.x||b+1<this.canvasRect_.x+this.canvasRect_.w)}};return a})();Dygraph.PLUGINS.push(Dygraph.Plugins.Legend,Dygraph.Plugins.Axes,Dygraph.Plugins.RangeSelector,Dygraph.Plugins.ChartLabels,Dygraph.Plugins.Annotations,Dygraph.Plugins.Grid); \ No newline at end of file
diff --git a/src/viz/index.css b/src/viz/index.css
deleted file mode 100644
index 00c8feb..0000000
--- a/src/viz/index.css
+++ /dev/null
@@ -1,180 +0,0 @@
-li.size1 {
- width: 13em;
- height:7em;
-}
-
-li.quadsize {
- width:270px;
- height:300px;
-}
-
-li.graphsize {
- width:1050px;
- height:350px;
-}
-
-body {
- font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; color:black;
- font-size:22px;
- padding:0;
- margin:0;
- background-color: rgba(170, 180, 185, 1);
- color: #333;
-}
-
-div.status {
- font-size:20px;
- font-weight:bold;
- background-color:gray;
-}
-
-h1 {
- text-align:center;
- font-variant: small-caps;
- color:#eee;
-}
-
-div.content {
- text-align:center;
- text-transform: capitalize;
-}
-
-div.container {
- box-sizing: border-box;
- font-size: 14px;
- line-height: 20px;
- margin-left: auto;
- margin-right: auto;
- margin-bottom:2em;
- max-width: 1170px;
- padding-left: 15px;
- padding-right: 15px;
- border: 2px solid #DDD;
- background: #FFF;
-}
-
-div.container:after {
- clear: both;
-}
-div.container:before, .container:after {
- content: " ";
- display: table;
-}
-
-main {
- display:block;
-}
-
-h2.dispstreamheader {
- font-variant: small-caps;
-}
-
-ul.controls {
- box-sizing: border-box;
- font-size: 14px;
- line-height: 20px;
- margin-bottom: 10px;
- margin-left: -5px;
- margin-right: -15px;
- margin-top: 0px;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
-}
-
-ul.controls:after {
- clear: both;
-}
-ul.controls:before, ul.controls:after {
- content: " ";
- display: table;
-}
-
-li.dispvalue {
- float:left;
- background-color: #FFF;
- border-bottom-color: #DDD;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- border-bottom-style: solid;
- border-bottom-width: 1px;
- border-image-outset: 0 0 0 0;
- border-image-repeat: stretch stretch;
- border-image-slice: 100% 100% 100% 100%;
- border-image-source: none;
- border-image-width: 1 1 1 1;
- border-left-color: #DDD;
- border-left-style: solid;
- border-left-width: 1px;
- border-right-color: #DDD;
- border-right-style: solid;
- border-right-width: 1px;
- border-top-color: #DDD;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- border-top-style: solid;
- border-top-width: 1px;
- box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 1px 0px;
- box-sizing: border-box;
- font-size: 14px;
- line-height: 20px;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
- margin-bottom: 20px;
- margin-right: 11.7px;
- min-height: 1px;
- padding-left: 0px;
- padding-right: 10px;
- padding-bottom:50px;
- position: relative;
- -moz-border-bottom-colors: none;
- -moz-border-left-colors: none;
- -moz-border-right-colors: none;
- -moz-border-top-colors: none;
-
-}
-
-div.dispvalueheader {
- background-color: #F5F5F5;
- border-bottom-style: solid;
- border-bottom-width: 1px;
- border-color: #DDD;
- border-top-left-radius: 3px;
- border-top-right-radius: 3px;
- box-sizing: border-box;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
- margin-right:-10px;
- padding-bottom: 10px;
- padding-left: 15px;
- padding-right: 15px;
- padding-top: 10px;
-}
-
-input.dispvalueheaderafter {
- float:right;
- display:inline;
- margin-top:-35px;
-}
-
-
-div.dispvaluevalue {
- margin-top:10px;
- margin-left:10px;
- width:100%;
- height:100%;
-}
-.anchor:before {
- content: "";
- display: block;
- position: relative;
- width: 0px;
- height: 5em;
- margin-top: -5em;
-}
-*:before, *:after {
- box-sizing: border-box;
-} \ No newline at end of file
diff --git a/src/viz/index.html b/src/viz/index.html
deleted file mode 100644
index 5d2368d..0000000
--- a/src/viz/index.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<html>
- <head>
- <meta charset="utf-8">
- <title>Sfive Stats</title>
- <link rel="stylesheet" href="index.css" type="text/css" />
- <script src="jquery-2.1.1.min.js" ></script>
- <script type="text/javascript" src="dygraph-combined.js"></script>
- <script type="text/javascript" src="index.js"></script>
- </head>
- <body>
- <div class="">
- <h1>Sfive Stat Display</h1>
- <div id="menu" class="container"><a href="#settings">Settings</a>&nbsp;</div>
- <div id="status" class="container status">Loading Data ... </div>
- <div id="content"></div>
- <div id="settings" class="container"></div>
- </div>
- </body>
-</html>
diff --git a/src/viz/index.js b/src/viz/index.js
deleted file mode 100644
index ee51064..0000000
--- a/src/viz/index.js
+++ /dev/null
@@ -1,173 +0,0 @@
-var qintv_ms = 5000;
-var uripart = "/updates?";
-//var lastfrom = "";
-var lastfrom="limit=12000";
-//var lastfrom = "from="+(new Date(Date.now() - 10*60*1000)).toISOString();
-//var lastfrom = "from="+(new Date(Date.now() - 10*60*1000)).toISOString()+"&limit=1000";
-//console.log(lastfrom);
-//var possible_display_functions = {"Off":"none", "Value":showvalue, "Gauge":showgauge, "Graph":showasgraph};
-var possible_display_functions = {"Off":"none", "Value":showvalue, "Graph":showasgraph};
-var stuff_to_display = {"clients":"none","*":showvalue,"client-count":showasgraph, "bytes-sent":showasgraph};
-var sidseen = [];
-
-$(document).ready(function()
-{
- updateS5Stats();
- setInterval("updateS5Stats()", qintv_ms);
-});
-
-function getstreamdiv(sid) {
- var elem = $("#"+sid);
- if (elem.length<1) {
- $("#content").append('<div class="container" id="'+sid+'jump"><main><h2 class="dispstreamheader">Stream: '+sid+'</h2><ul id="'+sid+'" class="controls"></ul></main></div>');
- $("#menu").append('<a href="#'+sid+'jump">'+sid+'</a>&nbsp;');
- elem = $("#"+sid);
- }
- return elem;
-}
-
-function killvalue(name, nosetselect) {
- stuff_to_display[name]="none";
- $.each(sidseen,function(index,sid){ $("#li-"+sid+"_"+name).remove() });
- if (nosetselect != 1) {
- document.getElementById("opts-"+name).selectedIndex=0;
- }
-}
-
-function showvalue(sid, name) {
- var divid = sid+"_"+name;
- var elem = $("#"+divid);
- if (elem.length<1) {
- getstreamdiv(sid).append('<li class="dispvalue size1" id="li-'+divid+'""><div class="dispvalueheader">'+ name +'</div><input type="button" class="dispvalueheaderafter" value="X" onClick="killvalue(\''+name+'\');"></input><div class="dispvaluevalue" id='+divid+'></div></li>');
- elem = $("#"+divid);
- }
- var jsdvalue = graphdata[divid][graphdata[divid].length-1];
- var jsdate = jsdvalue[0];
- var value = jsdvalue[1];
- elem.html("<strong>"+value+"</strong><br/><span style='font-size:smaller;'>@"+jsdate.toLocaleDateString()+" "+jsdate.toLocaleTimeString()+"</span>");
-}
-
-var gauges = {};
-function showgauge(sid, name) {
- var divid = sid+"_"+name;
- var elem = document.getElementById(divid);
- if (!elem) {
- getstreamdiv(sid).append('<li class="dispvalue quadsize" id="li-'+divid+'""><div class="dispvalueheader">'+ name +'</div><input type="button" class="dispvalueheaderafter" value="X" onClick="killvalue(\''+name+'\');"></input><div class="dispvaluevalue" id='+divid+'></div></li>');
- elem = document.getElementById(divid);
- gauges[divid] = new google.visualization.Gauge(elem);
- }
- var value = graphdata[divid][graphdata[divid].length-1][1];
- var data = google.visualization.arrayToDataTable([["Label", "Value"],[name,value]]);
- gauges[divid].draw(data, {});
-}
-
-var graphs = {}
-function showasgraph(sid, name) {
- var divid = sid+"_"+name;
- var elem = document.getElementById(divid);
- if (!elem) {
- getstreamdiv(sid).append('<li class="dispvalue graphsize" id="li-'+divid+'""><div class="dispvalueheader">'+ name +'</div><input type="button" class="dispvalueheaderafter" value="X" onClick="killvalue(\''+name+'\');"></input><div class="dispvaluevalue" id='+divid+'></div></li>');
- elem = document.getElementById(divid);
- graphs[divid] = new Dygraph(elem,graphdata[divid],{legend:'none',title:sid+"-"+name,labels:["Date/Time",name]});
- } else {
- graphs[divid].updateOptions({'file':graphdata[divid]});
- }
-}
-
-var graphdata = {}
-function savedata(sid, name, value, jsdate) {
- var divid = sid+"_"+name;
- if (!graphdata[divid]) {
- graphdata[divid]=[];
- }
- graphdata[divid].push([jsdate,value]);
-}
-
-function showasetting(name) {
- var setid = "opts-"+name;
- if (! document.getElementById(setid)) {
- var sts = $('<select/>').attr('id',setid).attr('onchange',"changeSetting('"+name+"',this);");
- $.each(possible_display_functions, function(key, value) {
- var stsopt = $("<option/>").text(key);
- if (stuff_to_display[name] == value) {
- stsopt.attr("selected","true");
- }
- sts.append(stsopt);
- });
- var stsdiv=$("<div>"+name+":</div>");
- stsdiv.append(sts);
- $("#settings").append(stsdiv);
- }
-}
-
-function changeSetting(name, selobj) {
- var newdispfunc = possible_display_functions[selobj.options[selobj.selectedIndex].text];
- if (newdispfunc) {
- $("#status").text("Chaging Graph...");
- killvalue(name, 1);
- stuff_to_display[name]=newdispfunc;
- if (typeof(newdispfunc) == 'function') {
- $.each(sidseen,function(index,sid){ newdispfunc(sid,name); });
- }
- $("#status").text("Graph Change Done");
- }
-}
-
-function graphS5Status(data) {
- var mysidseen=[];
- var jsdate;
- $("#status").text("Loading data "+lastfrom);
- $.each(data.data, function(index, stream) {
- var sid = stream["streamer-id"].format + '-' + stream["streamer-id"].quality;
- mysidseen.push(sid);
- jsdate = new Date(stream["start-time"]);
- qintv_ms = stream["duration-ms"]; //doesnt really change anything yet
- var jsdateplus = new Date(jsdate.getTime() + qintv_ms);
- var data = stream.data;
- //console.log(data);
- //console.log(graphdata);
- for (var key in data) {
- savedata(sid, key, data[key], jsdateplus);
- if (! stuff_to_display[key]) {
- stuff_to_display[key]=stuff_to_display['*'];
- }
- }
- });
- $("#status").text("Loading done");
- if (mysidseen.length < 1)
- return;
- sidseen = mysidseen;
- var jsdate1s = new Date(jsdate.getTime() + 1000);
- lastfrom = "from="+jsdate1s.toISOString();
- $("#status").text("Graphing Data");
- $.each(mysidseen, function(index, sid){
- $.each(stuff_to_display, function(key, func){
- if (key!="*")
- {
- showasetting(key);
- if (typeof(func) == 'function')
- {
- func(sid, key);
- }
- }
- });
- });
- $("#status").text("Graphing Done");
-}
-
-function updateS5Stats() {
- var uri = uripart + lastfrom;
- console.log(uri);
- //$.getJSON(uri, "", function(data){alert(data);});
- $.ajax({
- dataType: "json",
- url: uri,
- data: undefined,
- success: graphS5Status, //function(data){alert(data);},
- error: function( jqxhr, textStatus, error ) {
- var err = textStatus + ", " + error;
- console.log( "Request Failed: " + err );}
- });
-}
-
-
diff --git a/src/viz/jquery-2.1.1.min.js b/src/viz/jquery-2.1.1.min.js
deleted file mode 100644
index e5ace11..0000000
--- a/src/viz/jquery-2.1.1.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
-!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)
-},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=L.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var Q=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,R=["Top","Right","Bottom","Left"],S=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)},T=/^(?:checkbox|radio)$/i;!function(){var a=l.createDocumentFragment(),b=a.appendChild(l.createElement("div")),c=l.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||l,d=c.documentElement,e=c.body,a.pageX=b.clientX+(d&&d.scrollLeft||e&&e.scrollLeft||0)-(d&&d.clientLeft||e&&e.clientLeft||0),a.pageY=b.clientY+(d&&d.scrollTop||e&&e.scrollTop||0)-(d&&d.clientTop||e&&e.clientTop||0)),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=W.test(e)?this.mouseHooks:V.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new n.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=l),3===a.target.nodeType&&(a.target=a.target.parentNode),g.filter?g.filter(a,f):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==_()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===_()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=n.extend(new n.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?n.event.trigger(e,null,b):n.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?Z:$):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={isDefaultPrevented:$,isPropagationStopped:$,isImmediatePropagationStopped:$,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=Z,a&&a.preventDefault&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=Z,a&&a.stopPropagation&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=Z,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!n.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.focusinBubbles||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a),!0)};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=L.access(d,b);e||d.addEventListener(a,c,!0),L.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=L.access(d,b)-1;e?L.access(d,b,e):(d.removeEventListener(a,c,!0),L.remove(d,b))}}}),n.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(g in a)this.on(g,b,c,a[g],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=$;else if(!d)return this;return 1===e&&(f=d,d=function(a){return n().off(a),f.apply(this,arguments)},d.guid=f.guid||(f.guid=n.guid++)),this.each(function(){n.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=$),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ab=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ib={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1></$2>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=qb[0].contentDocument,b.write(),b.close(),c=sb(a,b),qb.detach()),rb[a]=c),c}var ub=/^margin/,vb=new RegExp("^("+Q+")(?!px)[a-z%]+$","i"),wb=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)};function xb(a,b,c){var d,e,f,g,h=a.style;return c=c||wb(a),c&&(g=c.getPropertyValue(b)||c[b]),c&&(""!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),vb.test(g)&&ub.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function yb(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d=l.documentElement,e=l.createElement("div"),f=l.createElement("div");if(f.style){f.style.backgroundClip="content-box",f.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===f.style.backgroundClip,e.style.cssText="border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute",e.appendChild(f);function g(){f.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",f.innerHTML="",d.appendChild(e);var g=a.getComputedStyle(f,null);b="1%"!==g.top,c="4px"===g.width,d.removeChild(e)}a.getComputedStyle&&n.extend(k,{pixelPosition:function(){return g(),b},boxSizingReliable:function(){return null==c&&g(),c},reliableMarginRight:function(){var b,c=f.appendChild(l.createElement("div"));return c.style.cssText=f.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",f.style.width="1px",d.appendChild(e),b=!parseFloat(a.getComputedStyle(c,null).marginRight),d.removeChild(e),b}})}}(),n.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var zb=/^(none|table(?!-c[ea]).+)/,Ab=new RegExp("^("+Q+")(.*)$","i"),Bb=new RegExp("^([+-])=("+Q+")","i"),Cb={position:"absolute",visibility:"hidden",display:"block"},Db={letterSpacing:"0",fontWeight:"400"},Eb=["Webkit","O","Moz","ms"];function Fb(a,b){if(b in a)return b;var c=b[0].toUpperCase()+b.slice(1),d=b,e=Eb.length;while(e--)if(b=Eb[e]+c,b in a)return b;return d}function Gb(a,b,c){var d=Ab.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Hb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+R[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+R[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+R[f]+"Width",!0,e))):(g+=n.css(a,"padding"+R[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+R[f]+"Width",!0,e)));return g}function Ib(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=wb(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=xb(a,b,f),(0>e||null==e)&&(e=a.style[b]),vb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Hb(a,b,c||(g?"border":"content"),d,f)+"px"}function Jb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=L.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&S(d)&&(f[g]=L.access(d,"olddisplay",tb(d.nodeName)))):(e=S(d),"none"===c&&e||L.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=xb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Fb(i,h)),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=Bb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(n.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||n.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Fb(a.style,h)),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=xb(a,b,d)),"normal"===e&&b in Db&&(e=Db[b]),""===c||c?(f=parseFloat(e),c===!0||n.isNumeric(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?zb.test(n.css(a,"display"))&&0===a.offsetWidth?n.swap(a,Cb,function(){return Ib(a,b,d)}):Ib(a,b,d):void 0},set:function(a,c,d){var e=d&&wb(a);return Gb(a,c,d?Hb(a,b,d,"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),n.cssHooks.marginRight=yb(k.reliableMarginRight,function(a,b){return b?n.swap(a,{display:"inline-block"},xb,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+R[d]+b]=f[d]||f[d-2]||f[0];return e}},ub.test(a)||(n.cssHooks[a+b].set=Gb)}),n.fn.extend({css:function(a,b){return J(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=wb(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Jb(this,!0)},hide:function(){return Jb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){S(this)?n(this).show():n(this).hide()})}});function Kb(a,b,c,d,e){return new Kb.prototype.init(a,b,c,d,e)}n.Tween=Kb,Kb.prototype={constructor:Kb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Kb.propHooks[this.prop];return a&&a.get?a.get(this):Kb.propHooks._default.get(this)},run:function(a){var b,c=Kb.propHooks[this.prop];return this.pos=b=this.options.duration?n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Kb.propHooks._default.set(this),this}},Kb.prototype.init.prototype=Kb.prototype,Kb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[n.cssProps[a.prop]]||n.cssHooks[a.prop])?n.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Kb.propHooks.scrollTop=Kb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},n.fx=Kb.prototype.init,n.fx.step={};var Lb,Mb,Nb=/^(?:toggle|show|hide)$/,Ob=new RegExp("^(?:([+-])=|)("+Q+")([a-z%]*)$","i"),Pb=/queueHooks$/,Qb=[Vb],Rb={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=Ob.exec(b),f=e&&e[3]||(n.cssNumber[a]?"":"px"),g=(n.cssNumber[a]||"px"!==f&&+d)&&Ob.exec(n.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,n.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function Sb(){return setTimeout(function(){Lb=void 0}),Lb=n.now()}function Tb(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=R[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ub(a,b,c){for(var d,e=(Rb[b]||[]).concat(Rb["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Vb(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&S(a),q=L.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?L.get(a,"olddisplay")||tb(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Nb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?tb(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=L.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;L.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ub(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function Wb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function Xb(a,b,c){var d,e,f=0,g=Qb.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Lb||Sb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:Lb||Sb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(Wb(k,j.opts.specialEasing);g>f;f++)if(d=Qb[f].call(j,a,k,j.opts))return d;return n.map(k,Ub,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(Xb,{tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],Rb[c]=Rb[c]||[],Rb[c].unshift(b)},prefilter:function(a,b){b?Qb.unshift(a):Qb.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(S).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=Xb(this,n.extend({},a),f);(e||L.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=L.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Pb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=L.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Tb(b,!0),a,d,e)}}),n.each({slideDown:Tb("show"),slideUp:Tb("hide"),slideToggle:Tb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Lb=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Lb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Mb||(Mb=setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){clearInterval(Mb),Mb=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(a,b){return a=n.fx?n.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a=l.createElement("input"),b=l.createElement("select"),c=b.appendChild(l.createElement("option"));a.type="checkbox",k.checkOn=""!==a.value,k.optSelected=c.selected,b.disabled=!0,k.optDisabled=!c.disabled,a=l.createElement("input"),a.value="t",a.type="radio",k.radioValue="t"===a.value}();var Yb,Zb,$b=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return J(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===U?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),d=n.attrHooks[b]||(n.expr.match.bool.test(b)?Zb:Yb)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=n.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void n.removeAttr(a,b))
-},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),Zb={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=$b[b]||n.find.attr;$b[b]=function(a,b,d){var e,f;return d||(f=$b[b],$b[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,$b[b]=f),e}});var _b=/^(?:input|select|textarea|button)$/i;n.fn.extend({prop:function(a,b){return J(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!n.isXMLDoc(a),f&&(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){return a.hasAttribute("tabindex")||_b.test(a.nodeName)||a.href?a.tabIndex:-1}}}}),k.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var ac=/[\t\r\n\f]/g;n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h="string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=n.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0===arguments.length||"string"==typeof a&&a,i=0,j=this.length;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,this.className))});if(h)for(b=(a||"").match(E)||[];j>i;i++)if(c=this[i],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(ac," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?n.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(n.isFunction(a)?function(c){n(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=n(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===U||"boolean"===c)&&(this.className&&L.set(this,"__className__",this.className),this.className=this.className||a===!1?"":L.get(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(ac," ").indexOf(b)>=0)return!0;return!1}});var bc=/\r/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(bc,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(d.value,f)>=0)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>=0:void 0}},k.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var cc=n.now(),dc=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(a){var b,c;if(!a||"string"!=typeof a)return null;try{c=new DOMParser,b=c.parseFromString(a,"text/xml")}catch(d){b=void 0}return(!b||b.getElementsByTagName("parsererror").length)&&n.error("Invalid XML: "+a),b};var ec,fc,gc=/#.*$/,hc=/([?&])_=[^&]*/,ic=/^(.*?):[ \t]*([^\r\n]*)$/gm,jc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,kc=/^(?:GET|HEAD)$/,lc=/^\/\//,mc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,nc={},oc={},pc="*/".concat("*");try{fc=location.href}catch(qc){fc=l.createElement("a"),fc.href="",fc=fc.href}ec=mc.exec(fc.toLowerCase())||[];function rc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function sc(a,b,c,d){var e={},f=a===oc;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function tc(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function uc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function vc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fc,type:"GET",isLocal:jc.test(ec[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":pc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?tc(tc(a,n.ajaxSettings),b):tc(n.ajaxSettings,a)},ajaxPrefilter:rc(nc),ajaxTransport:rc(oc),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=n.ajaxSetup({},b),l=k.context||k,m=k.context&&(l.nodeType||l.jquery)?n(l):n.event,o=n.Deferred(),p=n.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!f){f={};while(b=ic.exec(e))f[b[1].toLowerCase()]=b[2]}b=f[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?e:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return c&&c.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||fc)+"").replace(gc,"").replace(lc,ec[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=n.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(h=mc.exec(k.url.toLowerCase()),k.crossDomain=!(!h||h[1]===ec[1]&&h[2]===ec[2]&&(h[3]||("http:"===h[1]?"80":"443"))===(ec[3]||("http:"===ec[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=n.param(k.data,k.traditional)),sc(nc,k,b,v),2===t)return v;i=k.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!kc.test(k.type),d=k.url,k.hasContent||(k.data&&(d=k.url+=(dc.test(d)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=hc.test(d)?d.replace(hc,"$1_="+cc++):d+(dc.test(d)?"&":"?")+"_="+cc++)),k.ifModified&&(n.lastModified[d]&&v.setRequestHeader("If-Modified-Since",n.lastModified[d]),n.etag[d]&&v.setRequestHeader("If-None-Match",n.etag[d])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+pc+"; q=0.01":""):k.accepts["*"]);for(j in k.headers)v.setRequestHeader(j,k.headers[j]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(j in{success:1,error:1,complete:1})v[j](k[j]);if(c=sc(oc,k,b,v)){v.readyState=1,i&&m.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,c.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,f,h){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),c=void 0,e=h||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,f&&(u=uc(k,v,f)),u=vc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(n.lastModified[d]=w),w=v.getResponseHeader("etag"),w&&(n.etag[d]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,i&&m.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),i&&(m.trigger("ajaxComplete",[v,k]),--n.active||n.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return this.each(n.isFunction(a)?function(b){n(this).wrapInner(a.call(this,b))}:function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var wc=/%20/g,xc=/\[\]$/,yc=/\r?\n/g,zc=/^(?:submit|button|image|reset|file)$/i,Ac=/^(?:input|select|textarea|keygen)/i;function Bc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||xc.test(a)?d(a,e):Bc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Bc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Bc(c,a[c],b,e);return d.join("&").replace(wc,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Ac.test(this.nodeName)&&!zc.test(a)&&(this.checked||!T.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(yc,"\r\n")}}):{name:b.name,value:c.replace(yc,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(a){}};var Cc=0,Dc={},Ec={0:200,1223:204},Fc=n.ajaxSettings.xhr();a.ActiveXObject&&n(a).on("unload",function(){for(var a in Dc)Dc[a]()}),k.cors=!!Fc&&"withCredentials"in Fc,k.ajax=Fc=!!Fc,n.ajaxTransport(function(a){var b;return k.cors||Fc&&!a.crossDomain?{send:function(c,d){var e,f=a.xhr(),g=++Cc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)f.setRequestHeader(e,c[e]);b=function(a){return function(){b&&(delete Dc[g],b=f.onload=f.onerror=null,"abort"===a?f.abort():"error"===a?d(f.status,f.statusText):d(Ec[f.status]||f.status,f.statusText,"string"==typeof f.responseText?{text:f.responseText}:void 0,f.getAllResponseHeaders()))}},f.onload=b(),f.onerror=b("error"),b=Dc[g]=b("abort");try{f.send(a.hasContent&&a.data||null)}catch(h){if(b)throw h}},abort:function(){b&&b()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(d,e){b=n("<script>").prop({async:!0,charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&e("error"===a.type?404:200,a.type)}),l.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Gc=[],Hc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Gc.pop()||n.expando+"_"+cc++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Hc.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Hc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Hc,"$1"+e):b.jsonp!==!1&&(b.url+=(dc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Gc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||l;var d=v.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=n.buildFragment([a],b,e),e&&e.length&&n(e).remove(),n.merge([],d.childNodes))};var Ic=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Ic)return Ic.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e,dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,f||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var Jc=a.document.documentElement;function Kc(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(typeof d.getBoundingClientRect!==U&&(e=d.getBoundingClientRect()),c=Kc(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||Jc;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Jc})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(b,c){var d="pageYOffset"===c;n.fn[b]=function(e){return J(this,function(b,e,f){var g=Kc(b);return void 0===f?g?g[c]:b[e]:void(g?g.scrollTo(d?a.pageXOffset:f,d?f:a.pageYOffset):b[e]=f)},b,e,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=yb(k.pixelPosition,function(a,c){return c?(c=xb(a,b),vb.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return J(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Lc=a.jQuery,Mc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Mc),b&&a.jQuery===n&&(a.jQuery=Lc),n},typeof b===U&&(a.jQuery=a.$=n),n});
diff --git a/src/viz/stats.html b/src/viz/stats.html
deleted file mode 100644
index e60e954..0000000
--- a/src/viz/stats.html
+++ /dev/null
@@ -1,82 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="utf-8" />
-<script type="text/javascript" src="/viz/jquery-2.1.1.min.js"></script>
-<style>
-.updateError {
- background-color: #e30;
-}
-</style>
-</head>
-<body>
-<table id="statstable">
-<tr><td>LastUpdate</td><td id="LastUpdate"></td></tr>
-<tr><td>ClientCount</td><td id="ClientCount"></td></tr>
-<tr><td>HubCount</td><td id="HubCount"></td></tr>
-<tr><td>SourcesCount</td><td id="SourcesCount"></td></tr>
-<tr><td>UpdateCount</td><td id="UpdateCount"></td></tr>
-<tr><td>BytesSent</td><td id="BytesSent"></td></tr>
-<tr><td>BytesReceived</td><td id="BytesReceived"></td></tr>
-<tr><td>StartTime</td><td id="StartTime"></td></tr>
-<tr><td>LastStartTime</td><td id="LastStartTime"></td></tr>
-<tr><td>Message</td><td id="message"></td></tr>
-</table>
-<script type="text/javascript">
-var s5stats = {
- lastUpdate: ""
-}
-
-s5stats.pad = function (number) {
- if ( number < 10 ) {
- return '0' + number;
- }
- return number;
-}
-
-s5stats.toQueryIsoData = function(dateStr) {
- ts = new Date(dateStr)
- ts.setTime(ts.getTime() + 1 * 1000) // add a second, lowest resolution step for s5
- return ts.toISOString();
-}
-
-s5stats.updateStats = function() {
- var queryStr = "";
- if (s5stats.lastUpdate == "" && $("#LastStartTime").text() != "") {
- s5stats.lastUpdate = $("#LastStartTime").text()
- }
- if (s5stats.lastUpdate != "") {
- queryStr = "?from=" + s5stats.toQueryIsoData(s5stats.lastUpdate);
- }
-
- $.getJSON("/stats" + queryStr, function(data) {
- $("#ClientCount").text(data.ClientCount);
- $("#HubCount").text(data.HubCount);
- $("#SourcesCount").text(data.SourcesCount);
- $("#UpdateCount").text(data.UpdateCount);
- $("#BytesSent").text(data.BytesSent);
- $("#BytesReceived").text(data.BytesReceived);
- $("#StartTime").text(data.StartTime);
- $("#LastStartTime").text(data.LastStartTime);
- if (data.UpdateCount > 0) {
- s5stats.lastUpdate = data.LastStartTime;
- $("#LastUpdate").text(s5stats.lastUpdate)
- }
- $("#statstable").removeClass("updateError");
- $("#message").text("");
- })
- .fail(function() {
- $("#statstable").addClass("updateError");
- $("#message").text("No connection to Hub!");
- });
-
- setTimeout("s5stats.updateStats()", 5000)
-}
-
-$(document).ready(function()
- {
- s5stats.updateStats();
- });
-</script>
-</body>
-</html>
diff --git a/src/viz/updates b/src/viz/updates
deleted file mode 100644
index ffeeab1..0000000
--- a/src/viz/updates
+++ /dev/null
@@ -1,36 +0,0 @@
-[
-{
- "version": 1,
- "hostname": "myhostname",
- "streamer-id": { "content-id": "av-orig", "format": "flash", "quality": "medium" },
- "tags": [ "elevate", "2014", "discourse" ],
- "start-time": "2014-08-03T12:39:56.123Z",
- "duration-ms": 5000,
- "data": {
- "clients": [
- { "ip": "127.0.0.1", "port": 1234, "bytes-sent": 12094, "user-agent": "Mozilla Version 28" }
- ],
- "client-count": 13,
- "bytes-received": 12345,
- "bytes-sent": 921734098,
- "fuckups-per-second": 9
- }
-},
-{
- "version": 1,
- "hostname": "myhostname",
- "streamer-id": { "content-id": "av-orig", "format": "flash", "quality": "highquality" },
- "tags": [ "elevate", "2014", "discourse" ],
- "start-time": "2014-08-03T12:39:56.123Z",
- "duration-ms": 5000,
- "data": {
- "clients": [
- { "ip": "127.0.0.1", "port": 1234, "bytes-sent": 12094, "user-agent": "Mozilla Version 28" }
- ],
- "client-count": 10,
- "bytes-received": 12345,
- "bytes-sent": 202027,
- "fuckups-per-second": 10
- }
-}
-]