├── .gitignore ├── LICENSE ├── README.md ├── TODO.txt ├── chart ├── chart.go └── chart_test.go ├── clients ├── bolt.go ├── bolt_test.go └── http.go ├── cmd └── cmd.go ├── loader ├── csv.go ├── csv_test.go ├── loader.go └── xls.go ├── main.go ├── parser └── parser.go ├── sample_data ├── GlobalTemperatures.csv └── LakeHuron.csv ├── server ├── handler.go ├── handler_test.go └── server.go ├── types ├── functions.go ├── functions_test.go ├── grouping.go ├── grouping_test.go ├── query.go ├── query_test.go ├── types.go └── types_test.go └── www ├── css ├── app.css ├── bootstrap.min.css └── font-awesome.min.css ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff ├── fontawesome-webfont.woff2 ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── html ├── base.html ├── browse.html ├── explore.html └── panel.html ├── images ├── gopher-fit.ico ├── gopher-fit.png ├── gopher-fit.xcf ├── tcx.png └── temperatures.png └── js ├── app.js ├── bootstrap.min.js └── jquery.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.swp 27 | 28 | Takeout/* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kevin Schoon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fit 2 | 3 | ### Fit 4 | 5 | Fit is a toolkit for exploring and manipulating datasets. Go has [many](https://github.com/gonum) [great](https://github.com/montanaflynn/stats) 6 | statistical libraries but is largely unrepresented in the data science world. 7 | Fit aims to be a general purpose tool for handling data [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load), 8 | analytics, and visualization. 9 | 10 | Note that this project is **largely unfinished** and unsuitable for any practical purpose. 11 | 12 | #### Components 13 | 14 | Below is a rough outline of the different commonents that currently exist in Fit. 15 | 16 | ##### Dataset 17 | 18 | All numerical values are internally represented as `float64` and are logically groupped into a 19 | `Dataset` object. Datasets use the excellent [Gonum Matrix](https://github.com/gonum/matrix) 20 | underneath which are simmilar to Numpy's multi-demensional arrays. 21 | 22 | ##### Loaders 23 | 24 | Loaders perform iterative scanning of a file path and emit an `[]string` array for each row of data 25 | until EOF is reached. Currently only `csv` and `xls` loaders exist. 26 | 27 | ##### Parsers 28 | 29 | Parsers perform pre-processing on string data prior to being loaded into a dataset. 30 | 31 | ###### TimeParser 32 | 33 | TimeParser accepts a [formatted](https://golang.org/pkg/time/#Parse) string and stores the result 34 | as Unix epoch time. 35 | 36 | #### Usage 37 | 38 | fit --help 39 | Usage: fit [OPTIONS] COMMAND [arg...] 40 | 41 | Fit is a toolkit for exploring, extracting, and transforming datasets 42 | 43 | Options: 44 | -d, --db="" Path to a BoltDB database, default: /tmp/fit.db 45 | -s, --server="" Fit API server, default: http://127.0.0.1:8000 46 | -h, --human=true output data as human readable text 47 | -j, --json=false output data in JSON format 48 | -v, --version Show the version and exit 49 | 50 | Commands: 51 | server Run the Fit web server 52 | load load a dataset into BoltDB 53 | ls list datasets loaded into the database with their columns 54 | rm Delete a dataset 55 | query Query values from one or more datasets 56 | 57 | 58 | #### Examples 59 | 60 | 61 | # Start the Fit HTTP server 62 | fit server 63 | # Load sample data 64 | fit load --name LakeHuron sample_data/LakeHuron.csv 65 | # List datasets 66 | fit ls 67 | 68 | NAME ROWS COLS COLUMNS 69 | LakeHuron 98 3 [ time LakeHuron] 70 | 71 | # Query 72 | fit query -n "LakeHuron,time,LakeHuron" 73 | 74 | [time LakeHuron] 75 | 76 | Dims(98, 2) 77 | ⎡ 1875 580.38⎤ 78 | ⎢ 1876 581.86⎥ 79 | ⎢ 1877 580.97⎥ 80 | ⎢ 1878 580.8⎥ 81 | ⎢ 1879 579.79⎥ 82 | . 83 | . 84 | . 85 | ⎢ 1968 578.52⎥ 86 | ⎢ 1969 579.74⎥ 87 | ⎢ 1970 579.31⎥ 88 | ⎢ 1971 579.89⎥ 89 | ⎣ 1972 579.96⎦ 90 | 91 | 92 | fit --json query -n "LakeHuron,time,LakeHuron" |jq . 93 | 94 | { 95 | "Name": "QueryResult", 96 | "Columns": [ 97 | "time", 98 | "LakeHuron" 99 | ], 100 | "Stats": { 101 | "Rows": 98, 102 | "Columns": 2 103 | }, 104 | "Mtx": [ 105 | 1875, 106 | 580.38 .... 107 | 108 | # Open your web browser and perform the same query: 109 | # http://localhost:8000/explore?q=LakeHuron,time&q=LakeHuron,LakeHuron 110 | 111 | 112 | #### Web Interface 113 | 114 | Fit provides a web interface for interactively exploring queries. 115 | 116 | fit 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Truncate column names 2 | Remove extension from default name 3 | Handle missing query values (no panic) 4 | Add ability to rename columns 5 | Fix errors with large datasets 6 | Support arbitrary JSON 7 | Catch matrix panic 8 | Add ability to "discover" time fields 9 | Add new chart types 10 | Add SQL backend 11 | -------------------------------------------------------------------------------- /chart/chart.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | mtx "github.com/gonum/matrix/mat64" 5 | "github.com/gonum/plot" 6 | "github.com/gonum/plot/plotter" 7 | "github.com/gonum/plot/plotutil" 8 | "github.com/gonum/plot/vg" 9 | "github.com/gonum/plot/vg/draw" 10 | "image/color" 11 | ) 12 | 13 | type Config struct { 14 | PrimaryColor color.Color 15 | SecondaryColor color.Color 16 | XLabel string 17 | YLabel string 18 | Title string 19 | Type string 20 | Columns []string 21 | Width vg.Length 22 | Height vg.Length 23 | PlotTime bool 24 | } 25 | 26 | type Vector struct { 27 | Name string 28 | Position int 29 | } 30 | 31 | func getPlot(cfg Config) (*plot.Plot, error) { 32 | plt, err := plot.New() 33 | if err != nil { 34 | return nil, err 35 | } 36 | plt.Legend.Color = cfg.PrimaryColor 37 | plt.Legend.Top = true 38 | plt.BackgroundColor = cfg.SecondaryColor 39 | 40 | //plt.Title.Text = cfg.Title 41 | //plt.Title.Color = cfg.PrimaryColor 42 | //plt.Title.Font.Size = 0.5 * vg.Inch 43 | 44 | //plt.Y.Label.Text = cfg.YLabel 45 | plt.Y.Color = cfg.PrimaryColor 46 | plt.Y.Label.Color = cfg.PrimaryColor 47 | plt.Y.Label.Font.Size = 0.3 * vg.Inch 48 | plt.Y.Tick.Color = cfg.PrimaryColor 49 | plt.Y.Tick.Label.Font.Size = 0.2 * vg.Inch 50 | plt.Y.Tick.Label.Color = cfg.PrimaryColor 51 | 52 | //plt.X.Label.Text = cfg.XLabel 53 | plt.X.Color = cfg.PrimaryColor 54 | plt.X.Label.Color = cfg.PrimaryColor 55 | plt.X.Label.Font.Size = 0.3 * vg.Inch 56 | plt.X.Tick.Color = cfg.PrimaryColor 57 | plt.X.Tick.Label.Color = cfg.PrimaryColor 58 | plt.X.Tick.Label.Font.Size = 0.2 * vg.Inch 59 | 60 | if cfg.PlotTime { 61 | plt.X.Tick.Marker = plot.UnixTimeTicks{Format: "2006-01-02"} 62 | } 63 | return plt, nil 64 | } 65 | 66 | // GetLines returns variadic arguments for plotter.AddLines 67 | // The chart will always plot the first column vector as 68 | // the X axis and remaining columns on the Y axis. 69 | // 70 | // x,y,z 71 | // 1,1,2 72 | // 2,3,4 73 | // 3,5,6 74 | // 75 | // Line Y: 1,1 2,3 3,5 76 | // Line Z: 1,2 2,4 3,6 77 | 78 | func GetLines(mx *mtx.Dense, columns []string) []interface{} { 79 | data := make([]interface{}, 0) 80 | r, c := mx.Dims() 81 | for i := 1; i < c; i++ { 82 | xys := make(plotter.XYs, r) 83 | for j := 0; j < r; j++ { 84 | xys[j].X = mx.At(j, 0) 85 | xys[j].Y = mx.At(j, i) 86 | } 87 | data = append(data, columns[i]) 88 | data = append(data, xys) 89 | } 90 | return data 91 | } 92 | 93 | // GetValues returns an array of plotter.Values 94 | // where each entry is a column vector. 95 | func GetValues(mx *mtx.Dense) []plotter.Values { 96 | _, c := mx.Dims() 97 | values := make([]plotter.Values, c) 98 | for i := 0; i < c; i++ { 99 | values[i] = plotter.Values(mtx.Col(nil, i, mx)) 100 | } 101 | return values 102 | } 103 | 104 | func New(cfg Config, mx *mtx.Dense) (vg.CanvasWriterTo, error) { 105 | plt, err := getPlot(cfg) 106 | if err != nil { 107 | return nil, err 108 | } 109 | switch cfg.Type { 110 | case "box": 111 | values := GetValues(mx) 112 | for i, vals := range values { 113 | box, err := plotter.NewBoxPlot(vg.Points(20), float64(i), vals) 114 | if err != nil { 115 | return nil, err 116 | } 117 | box.WhiskerStyle.Color = cfg.PrimaryColor 118 | box.BoxStyle.Color = cfg.PrimaryColor 119 | box.MedianStyle.Color = cfg.PrimaryColor 120 | plt.Add(box) 121 | } 122 | default: // Default to line chart 123 | if err := plotutil.AddLines(plt, GetLines(mx, cfg.Columns)...); err != nil { 124 | return nil, err 125 | } 126 | } 127 | plt.Add(plotter.NewGrid()) 128 | canvas, err := draw.NewFormattedCanvas(cfg.Width, cfg.Height, "png") 129 | if err != nil { 130 | return nil, err 131 | } 132 | plt.Draw(draw.New(canvas)) 133 | return canvas, nil 134 | } 135 | -------------------------------------------------------------------------------- /chart/chart_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | mtx "github.com/gonum/matrix/mat64" 5 | "github.com/gonum/plot/plotter" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestGetLines(t *testing.T) { 11 | mx := mtx.NewDense(3, 3, []float64{ 12 | 1.0, 1.0, 2.0, 13 | 2.0, 3.0, 4.0, 14 | 3.0, 5.0, 6.0, 15 | }) 16 | data := GetLines(mx, []string{"x", "y", "z"}) 17 | assert.Len(t, data, 4) 18 | assert.IsType(t, "", data[0]) 19 | name := data[0].(string) 20 | assert.Equal(t, "y", name) 21 | assert.IsType(t, plotter.XYs{}, data[1]) 22 | xys := data[1].(plotter.XYs) 23 | assert.Equal(t, 3, xys.Len()) 24 | x, y := xys.XY(0) 25 | assert.Equal(t, 1.0, x) 26 | assert.Equal(t, 1.0, y) 27 | x, y = xys.XY(1) 28 | assert.Equal(t, 2.0, x) 29 | assert.Equal(t, 3.0, y) 30 | x, y = xys.XY(2) 31 | assert.Equal(t, 3.0, x) 32 | assert.Equal(t, 5.0, y) 33 | assert.IsType(t, "", data[2]) 34 | name = data[2].(string) 35 | assert.Equal(t, "z", name) 36 | assert.IsType(t, plotter.XYs{}, data[3]) 37 | xys = data[3].(plotter.XYs) 38 | assert.Equal(t, 3, xys.Len()) 39 | x, y = xys.XY(0) 40 | assert.Equal(t, 1.0, x) 41 | assert.Equal(t, 2.0, y) 42 | x, y = xys.XY(1) 43 | assert.Equal(t, 2.0, x) 44 | assert.Equal(t, 4.0, y) 45 | x, y = xys.XY(2) 46 | assert.Equal(t, 3.0, x) 47 | assert.Equal(t, 6.0, y) 48 | } 49 | -------------------------------------------------------------------------------- /clients/bolt.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/boltdb/bolt" 6 | mtx "github.com/gonum/matrix/mat64" 7 | "github.com/kevinschoon/fit/types" 8 | "time" 9 | ) 10 | 11 | // BoltClient implements the types.Client 12 | // interface with a BoltDB backend. 13 | // BoltClient is the primary database 14 | // backend for Fit. It can also be used 15 | // directly from the command line. 16 | type BoltClient struct { 17 | bolt *bolt.DB 18 | } 19 | 20 | var ( 21 | dsBucket = []byte("datasets") 22 | mxBucket = []byte("matricies") 23 | ) 24 | 25 | func (c *BoltClient) Datasets() (datasets []*types.Dataset, err error) { 26 | err = c.bolt.View(func(tx *bolt.Tx) error { 27 | b := tx.Bucket(dsBucket) 28 | cursor := b.Cursor() 29 | for k, v := cursor.First(); k != nil; k, v = cursor.Next() { 30 | ds := &types.Dataset{} 31 | if err := json.Unmarshal(v, ds); err != nil { 32 | return err 33 | } 34 | datasets = append(datasets, ds) 35 | } 36 | return nil 37 | }) 38 | return datasets, err 39 | } 40 | 41 | func (c *BoltClient) Write(ds *types.Dataset) (err error) { 42 | return c.bolt.Update(func(tx *bolt.Tx) error { 43 | b := tx.Bucket(dsBucket) 44 | raw, err := json.Marshal(ds) 45 | if err != nil { 46 | return err 47 | } 48 | if err = b.Put([]byte(ds.Name), raw); err != nil { 49 | return err 50 | } 51 | if ds.Mtx == nil { // No matricies attached to this dataset 52 | return nil 53 | } 54 | b = tx.Bucket(mxBucket) 55 | raw, err = ds.Mtx.MarshalBinary() 56 | if err != nil { 57 | return err 58 | } 59 | return b.Put([]byte(ds.Name), raw) 60 | }) 61 | } 62 | 63 | func (c *BoltClient) read(name string) (ds *types.Dataset, err error) { 64 | if err = c.bolt.View(func(tx *bolt.Tx) error { 65 | b := tx.Bucket(dsBucket) 66 | raw := b.Get([]byte(name)) 67 | if raw == nil { 68 | return types.ErrNotFound 69 | } 70 | ds = &types.Dataset{} 71 | if err = json.Unmarshal(raw, ds); err != nil { 72 | return err 73 | } 74 | b = tx.Bucket(mxBucket) 75 | raw = b.Get([]byte(name)) 76 | if raw == nil { 77 | return nil // No matricies attached to the dataset 78 | } 79 | ds.Mtx = mtx.NewDense(0, 0, nil) 80 | return ds.Mtx.UnmarshalBinary(raw) 81 | }); err != nil { 82 | return nil, err 83 | } 84 | return ds, nil 85 | } 86 | 87 | func (c *BoltClient) Delete(name string) error { 88 | return c.bolt.Update(func(tx *bolt.Tx) error { 89 | b := tx.Bucket(dsBucket) 90 | if err := b.Delete([]byte(name)); err != nil { 91 | return err 92 | } 93 | b = tx.Bucket(mxBucket) 94 | return b.Delete([]byte(name)) 95 | }) 96 | } 97 | 98 | // Query finds all of the datasets contained 99 | // in Queries and returns a combined dataset 100 | // for each column in the search. The values 101 | // from each dataset are stored entirely in 102 | // memory until the query is complete. This 103 | // means that the total size of all datasets 104 | // queried cannot exceed the total system 105 | // memory. The resulting dataset columns 106 | // will be ordered in the same order they 107 | // were queried for. 108 | func (c *BoltClient) Query(query *types.Query) (*types.Dataset, error) { 109 | var ( 110 | rows int // Row count for new dataset 111 | cols int // Col count for new dataset 112 | other *types.Dataset // Dataset currently being processed 113 | ) 114 | // The new resulting dataset 115 | ds := &types.Dataset{ 116 | Name: "QueryResult", 117 | Columns: make([]string, 0), 118 | } 119 | // Empty array of Vectors where each 120 | // is a column from the queries 121 | vectors := make([]*mtx.Vector, 0) 122 | // Map of datasets already processed 123 | processed := make(map[string]*types.Dataset) 124 | // Range each dataset in the query 125 | for _, dataset := range query.Datasets { 126 | columns := dataset.Columns 127 | //columns := query.Columns(name) 128 | // Check to see if a query for this dataset 129 | // has already been executed 130 | if _, ok := processed[dataset.Name]; !ok { 131 | // Query for the other dataset 132 | other, err := c.read(dataset.Name) 133 | if err != nil { 134 | return nil, err 135 | } 136 | // Resulting matrix should have the sum of 137 | // the number of rows from each unique 138 | // dataset matrix that is queried 139 | r, _ := other.Mtx.Dims() 140 | rows += r 141 | // Add this dataset to the map 142 | // so it is not queried again 143 | processed[dataset.Name] = other 144 | } 145 | // The other dataset we are querying 146 | other = processed[dataset.Name] 147 | // If this is a wild card search 148 | // set columns to equal all available 149 | // columns in the dataset 150 | if len(columns) == 1 { 151 | if columns[0] == "*" { 152 | columns = other.Columns 153 | } 154 | } 155 | // Range each column in the query 156 | for _, name := range columns { 157 | // Get the position (index) of the column 158 | pos := other.CPos(name) 159 | // If the returned position is a negative 160 | // number the column does not exist 161 | if pos < 0 { 162 | return nil, types.ErrNotFound 163 | } 164 | // Append the column to vectors array 165 | vectors = append(vectors, other.Mtx.ColView(pos)) 166 | // Add the column name to the resulting dataset 167 | ds.Columns = append(ds.Columns, name) 168 | } 169 | } 170 | // Resulting number of columns is equal to 171 | // the amount that were queried for 172 | cols = len(vectors) 173 | // Create a new matrix zeroed Matrix 174 | ds.Mtx = mtx.NewDense(rows, cols, nil) 175 | // Fill the matrix with values from each column vector 176 | for i := 0; i < rows; i++ { 177 | for j := 0; j < cols; j++ { 178 | if vectors[j].Len() > i { 179 | ds.Mtx.Set(i, j, vectors[j].At(i, 0)) 180 | } // Zeros are left for missing data 181 | } 182 | } 183 | // Apply any other query options to the resulting dataset 184 | ds.Mtx = query.Apply(ds.Mtx) 185 | return ds, nil 186 | } 187 | 188 | func (c *BoltClient) Close() { 189 | c.bolt.Close() 190 | } 191 | 192 | func NewBoltClient(path string) (types.Client, error) { 193 | b, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second}) 194 | if err != nil { 195 | return nil, err 196 | } 197 | c := &BoltClient{ 198 | bolt: b, 199 | } 200 | // Initialize buckets 201 | err = c.bolt.Update(func(tx *bolt.Tx) error { 202 | if _, err = tx.CreateBucketIfNotExists(dsBucket); err != nil { 203 | return err 204 | } 205 | if _, err = tx.CreateBucketIfNotExists(mxBucket); err != nil { 206 | return err 207 | } 208 | return nil 209 | }) 210 | return types.Client(c), err 211 | } 212 | -------------------------------------------------------------------------------- /clients/bolt_test.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | mtx "github.com/gonum/matrix/mat64" 5 | "github.com/kevinschoon/fit/types" 6 | "github.com/stretchr/testify/assert" 7 | "io/ioutil" 8 | "math/rand" 9 | "os" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var cleanup bool = false 15 | 16 | func NewTestMatrix(r, c int) *mtx.Dense { 17 | values := make([]float64, r*c) 18 | for i := range values { 19 | values[i] = rand.NormFloat64() 20 | } 21 | return mtx.NewDense(r, c, values) 22 | } 23 | 24 | func NewTestDB(t *testing.T) (types.Client, func()) { 25 | f, err := ioutil.TempFile("/tmp", "fit-test") 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | db, err := NewBoltClient(f.Name()) 30 | assert.NoError(t, err) 31 | return db, func() { 32 | if cleanup { 33 | os.Remove(f.Name()) 34 | } 35 | } 36 | } 37 | 38 | func TestDatasets(t *testing.T) { 39 | db, cleanup := NewTestDB(t) 40 | defer cleanup() 41 | datasets, err := db.Datasets() 42 | assert.NoError(t, err) 43 | assert.Equal(t, 0, len(datasets)) 44 | } 45 | 46 | func TestReadWrite(t *testing.T) { 47 | d, cleanup := NewTestDB(t) 48 | defer cleanup() 49 | db := d.(*BoltClient) 50 | dsA := &types.Dataset{ 51 | Name: "TestReadWrite", 52 | Columns: []string{ 53 | "V1", "V2", "V3", "V4", 54 | "V5", "V6", "V7", "V8", 55 | }, 56 | Mtx: NewTestMatrix(128, 8), 57 | } 58 | assert.NoError(t, db.Write(dsA)) 59 | dsB, err := db.read(dsA.Name) 60 | assert.NoError(t, err) 61 | assert.True(t, mtx.Equal(dsA.Mtx, dsB.Mtx)) 62 | assert.Equal(t, 8, len(dsB.Columns)) 63 | assert.Equal(t, "TestReadWrite", dsB.Name) 64 | } 65 | 66 | func TestQuery(t *testing.T) { 67 | db, cleanup := NewTestDB(t) 68 | defer cleanup() 69 | mx1 := mtx.NewDense(2, 4, []float64{ 70 | 1.0, 1.0, 1.0, 1.0, 71 | 2.0, 2.0, 2.0, 2.0, 72 | }) 73 | mx2 := mtx.NewDense(3, 4, []float64{ 74 | 3.0, 3.0, 3.0, 3.0, 75 | 2.0, 2.0, 2.0, 2.0, 76 | 1.0, 1.0, 1.0, 1.0, 77 | }) 78 | assert.NoError(t, db.Write(&types.Dataset{ 79 | Name: "mx1", 80 | Mtx: mx1, 81 | Columns: []string{"A", "B", "C", "D"}}), 82 | ) 83 | assert.NoError(t, db.Write(&types.Dataset{ 84 | Name: "mx2", 85 | Mtx: mx2, 86 | Columns: []string{"E", "F", "G", "H"}}), 87 | ) 88 | // Ensure multiple queries for the same dataset do not 89 | // return multiple rows 90 | ds, err := db.Query(types.NewQuery([]string{"mx1,A,B,B", "mx1,B,C,D"}, "", "")) 91 | assert.NoError(t, err) 92 | mx := ds.Mtx 93 | r, c := mx.Dims() 94 | assert.Equal(t, 2, r) 95 | assert.Equal(t, 6, c) 96 | ds, err = db.Query(types.NewQuery([]string{"mx1,A,B,C", "mx2,E,F,G"}, "", "")) 97 | assert.NoError(t, err) 98 | mx = ds.Mtx 99 | r, c = mx.Dims() 100 | assert.Equal(t, 5, r) 101 | assert.Equal(t, 6, c) 102 | assert.Equal(t, 6, len(ds.Columns)) 103 | assert.Equal(t, 1.0, mx.At(0, ds.CPos("A"))) 104 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("A"))) 105 | assert.Equal(t, 1.0, mx.At(0, ds.CPos("B"))) 106 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("B"))) 107 | assert.Equal(t, 1.0, mx.At(0, ds.CPos("C"))) 108 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("C"))) 109 | assert.Equal(t, 3.0, mx.At(0, ds.CPos("E"))) 110 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("E"))) 111 | assert.Equal(t, 1.0, mx.At(2, ds.CPos("E"))) 112 | assert.Equal(t, 3.0, mx.At(0, ds.CPos("F"))) 113 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("F"))) 114 | assert.Equal(t, 1.0, mx.At(2, ds.CPos("F"))) 115 | assert.Equal(t, 3.0, mx.At(0, ds.CPos("G"))) 116 | assert.Equal(t, 2.0, mx.At(1, ds.CPos("G"))) 117 | assert.Equal(t, 1.0, mx.At(2, ds.CPos("G"))) 118 | _, err = db.Query(types.NewQuery([]string{"mx3"}, "", "")) 119 | assert.Error(t, err, "not found") 120 | _, err = db.Query(types.NewQuery([]string{"mx1,H"}, "", "")) 121 | assert.Error(t, err, "not found") 122 | // Wildcard query 123 | ds, err = db.Query(types.NewQuery([]string{"mx1,*"}, "", "")) 124 | assert.NoError(t, err) 125 | r, c = ds.Mtx.Dims() 126 | assert.Equal(t, 2, r) 127 | assert.Equal(t, 4, c) 128 | } 129 | 130 | func init() { 131 | rand.Seed(time.Now().Unix()) 132 | } 133 | -------------------------------------------------------------------------------- /clients/http.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/kevinschoon/fit/types" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | const timeout time.Duration = 10 * time.Second 17 | 18 | // HTTPClient implements the types.Client 19 | // interface over HTTP 20 | type HTTPClient struct { 21 | baseURL *url.URL 22 | client *http.Client 23 | } 24 | 25 | func (c *HTTPClient) url() *url.URL { 26 | return &url.URL{ 27 | Scheme: c.baseURL.Scheme, 28 | Host: c.baseURL.Host, 29 | Path: "/1/dataset", 30 | } 31 | } 32 | 33 | func (c *HTTPClient) do(req *http.Request) ([]byte, error) { 34 | res, err := c.client.Do(req) 35 | if err != nil { 36 | return nil, err 37 | } 38 | data, err := ioutil.ReadAll(res.Body) 39 | defer res.Body.Close() 40 | if err != nil { 41 | return nil, err 42 | } 43 | if res.StatusCode != http.StatusOK { 44 | log.Printf("API Error: %d - %s\n", res.StatusCode, data) 45 | switch res.StatusCode { 46 | case 404: 47 | return nil, types.ErrNotFound 48 | default: 49 | return nil, types.ErrAPI 50 | } 51 | } 52 | return data, nil 53 | } 54 | 55 | func (c *HTTPClient) Datasets() (datasets []*types.Dataset, err error) { 56 | raw, err := c.do(&http.Request{ 57 | URL: c.url(), 58 | Method: "GET", 59 | }) 60 | if err != nil { 61 | return nil, err 62 | } 63 | err = json.Unmarshal(raw, &datasets) 64 | return datasets, err 65 | } 66 | 67 | func (c *HTTPClient) Write(ds *types.Dataset) (err error) { 68 | ds.WithValues = true 69 | raw, err := json.Marshal(ds) 70 | if err != nil { 71 | return err 72 | } 73 | req, err := http.NewRequest("POST", c.url().String(), io.Reader(bytes.NewReader(raw))) 74 | if err != nil { 75 | return err 76 | } 77 | _, err = c.do(req) 78 | return err 79 | } 80 | 81 | func (c *HTTPClient) Delete(name string) (err error) { 82 | u := c.url() 83 | u.RawQuery = fmt.Sprintf("name=%s", name) 84 | _, err = c.do(&http.Request{ 85 | URL: u, 86 | Method: "DELETE", 87 | }) 88 | return err 89 | } 90 | 91 | func (c *HTTPClient) Query(query *types.Query) (ds *types.Dataset, err error) { 92 | u := c.url() 93 | u.RawQuery = query.String() 94 | raw, err := c.do(&http.Request{ 95 | URL: u, 96 | Method: "GET", 97 | }) 98 | if err != nil { 99 | return nil, err 100 | } 101 | ds = &types.Dataset{ 102 | WithValues: true, 103 | } 104 | err = json.Unmarshal(raw, ds) 105 | return ds, err 106 | } 107 | 108 | func NewHTTPClient(endpoint string) (types.Client, error) { 109 | u, err := url.Parse(endpoint) 110 | if err != nil { 111 | return nil, err 112 | } 113 | c := &HTTPClient{ 114 | baseURL: u, 115 | client: &http.Client{ 116 | Timeout: timeout, 117 | }, 118 | } 119 | return types.Client(c), nil 120 | } 121 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | mtx "github.com/gonum/matrix/mat64" 7 | "github.com/gosuri/uitable" 8 | "github.com/jawher/mow.cli" 9 | 10 | "github.com/kevinschoon/fit/clients" 11 | "github.com/kevinschoon/fit/loader" 12 | "github.com/kevinschoon/fit/parser" 13 | "github.com/kevinschoon/fit/server" 14 | "github.com/kevinschoon/fit/types" 15 | "os" 16 | ) 17 | 18 | const FitVersion string = "0.0.1" 19 | 20 | var ( 21 | app = cli.App("fit", "Fit is a toolkit for exploring, extracting, and transforming datasets") 22 | 23 | dbPath = app.StringOpt("d db", "", "Path to a BoltDB database, default: /tmp/fit.db") 24 | apiURL = app.StringOpt("s server", "", "Fit API server, default: http://127.0.0.1:8000") 25 | asHuman = app.BoolOpt("h human", true, "output data as human readable text") 26 | asJSON = app.BoolOpt("j json", false, "output data in JSON format") 27 | ) 28 | 29 | func FailOnErr(err error) { 30 | if err != nil { 31 | fmt.Println("ERROR:", err.Error()) 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func GetClient(pref string) types.Client { 37 | switch { 38 | case *dbPath != "": 39 | client, err := clients.NewBoltClient(*dbPath) 40 | FailOnErr(err) 41 | return client 42 | case *apiURL != "": 43 | client, err := clients.NewHTTPClient(*apiURL) 44 | FailOnErr(err) 45 | return client 46 | } 47 | if pref == "db" { 48 | client, err := clients.NewBoltClient("/tmp/fit.db") 49 | FailOnErr(err) 50 | return client 51 | } 52 | client, err := clients.NewHTTPClient("http://127.0.0.1:8000") 53 | FailOnErr(err) 54 | return client 55 | } 56 | 57 | func Run() { 58 | app.Version("v version", FitVersion) 59 | 60 | app.Command("server", "Run the Fit web server", func(cmd *cli.Cmd) { 61 | var ( 62 | pattern = cmd.StringOpt("pattern", "127.0.0.1:8000", "IP and port pattern to listen on") 63 | static = cmd.StringOpt("static", "./www", "Path to static assets") 64 | demo = cmd.BoolOpt("demo", false, "Run in Demo Mode") 65 | ) 66 | cmd.Action = func() { 67 | server.RunServer(GetClient("db"), *pattern, *static, FitVersion, *demo) 68 | } 69 | }) 70 | 71 | app.Command("load", "load a dataset into BoltDB", func(cmd *cli.Cmd) { 72 | cmd.Spec = "[[-n] [-s]][-p...][-c...] PATH" 73 | var ( 74 | name = cmd.StringOpt("n name", "", "name of this dataset") 75 | path = cmd.StringArg("PATH", "", "File path") 76 | parserArgs = cmd.StringsOpt("p parser", []string{}, "parsers to apply") 77 | sheet = cmd.StringOpt("s sheet", "", "name of the sheet to load with XLS file") 78 | columns = cmd.StringsOpt("c column", []string{}, "column names") 79 | ) 80 | cmd.Action = func() { 81 | parsers, err := parser.ParsersFromArgs(*parserArgs) 82 | FailOnErr(err) 83 | opts := loader.Options{ 84 | Name: *name, 85 | Path: *path, 86 | Parsers: parsers, 87 | Columns: *columns, 88 | Sheet: *sheet, 89 | } 90 | ds, err := loader.ReadPath(opts) 91 | FailOnErr(err) 92 | FailOnErr(GetClient("").Write(ds)) 93 | } 94 | }) 95 | 96 | app.Command("ls", "list datasets loaded into the database with their columns", func(cmd *cli.Cmd) { 97 | cmd.Action = func() { 98 | datasets, err := GetClient("").Datasets() 99 | FailOnErr(err) 100 | switch { 101 | case *asJSON: 102 | raw, err := json.Marshal(datasets) 103 | FailOnErr(err) 104 | fmt.Println(string(raw)) 105 | default: 106 | tbl := uitable.New() 107 | tbl.AddRow("NAME", "ROWS", "COLS", "COLUMNS") 108 | for _, dataset := range datasets { 109 | tbl.AddRow(dataset.Name, fmt.Sprintf("%d", dataset.Stats.Rows), fmt.Sprintf("%d", dataset.Stats.Columns), dataset.Columns) 110 | } 111 | fmt.Println(tbl) 112 | } 113 | } 114 | }) 115 | 116 | app.Command("rm", "Delete a dataset", func(cmd *cli.Cmd) { 117 | var name = cmd.StringArg("NAME", "", "Name of the dataset to delete") 118 | cmd.Action = func() { 119 | if *name == "" { 120 | cmd.PrintLongHelp() 121 | os.Exit(1) 122 | } 123 | FailOnErr(GetClient("").Delete(*name)) 124 | } 125 | }) 126 | 127 | app.Command("query", "Query values from one or more datasets", func(cmd *cli.Cmd) { 128 | var ( 129 | queryArgs = cmd.StringsArg("QUERY", []string{}, "Query parameters") 130 | lines = cmd.IntOpt("n lines", 10, "number of rows to output") 131 | grouping = cmd.StringOpt("g grouping", "", "grouping to apply to the resulting matrix") 132 | function = cmd.StringOpt("f function", "avg", "function to apply when grouping") 133 | ) 134 | cmd.LongDesc = `Query values from one or more stored datasets. Values from different 135 | datasets can be joined together by specifying multiple query parameters. 136 | 137 | Example: 138 | 139 | fit query -n 10 -g Duration,0,1m -f avg "Dataset1,fuu" "Dataset2,bar,baz" 140 | ` 141 | cmd.Spec = "[OPTIONS] QUERY..." 142 | cmd.Action = func() { 143 | if len(*queryArgs) == 0 { 144 | cmd.PrintLongHelp() 145 | os.Exit(1) 146 | } 147 | ds, err := GetClient("").Query(types.NewQuery(*queryArgs, *function, *grouping)) 148 | FailOnErr(err) 149 | if ds.Len() > 0 { 150 | switch { 151 | case *asJSON: 152 | ds.WithValues = true 153 | raw, err := json.Marshal(ds) 154 | FailOnErr(err) 155 | fmt.Println(string(raw)) 156 | default: 157 | fmt.Printf("\n%s\n", ds.Columns) 158 | if *lines > 0 { 159 | fmt.Printf("\n%v\n\n", mtx.Formatted(ds.Mtx, mtx.Prefix(" "), mtx.Excerpt(*lines))) 160 | } else { 161 | fmt.Printf("\n%v\n\n", mtx.Formatted(ds.Mtx, mtx.Prefix(" "))) 162 | } 163 | } 164 | } 165 | } 166 | }) 167 | FailOnErr(app.Run(os.Args)) 168 | } 169 | -------------------------------------------------------------------------------- /loader/csv.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | ) 7 | 8 | type CSV struct { 9 | Columns []string 10 | reader *csv.Reader 11 | rows [][]string 12 | index int 13 | } 14 | 15 | func (c *CSV) Row() ([]string, error) { 16 | if c.index == len(c.rows) { 17 | return nil, io.EOF 18 | } 19 | row := c.rows[c.index] 20 | c.index++ 21 | return row, nil 22 | } 23 | 24 | func (c CSV) Dims() (int, int) { 25 | return len(c.rows), len(c.Columns) 26 | } 27 | 28 | func NewCSV(reader io.Reader) (*CSV, error) { 29 | c := &CSV{ 30 | reader: csv.NewReader(reader), 31 | } 32 | // Read the first record in the CSV to load column names 33 | row, err := c.reader.Read() 34 | if err != nil { 35 | return c, err 36 | } 37 | c.Columns = make([]string, len(row)) 38 | for i, name := range row { 39 | c.Columns[i] = name 40 | } 41 | // Load the entire CSV into memory so 42 | // we can get it's demensions 43 | for { 44 | row, err := c.reader.Read() 45 | if err == io.EOF { 46 | break 47 | } 48 | if err != nil { 49 | return nil, err 50 | } 51 | c.rows = append(c.rows, row) 52 | } 53 | return c, nil 54 | } 55 | -------------------------------------------------------------------------------- /loader/csv_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "io" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var Simple string = ` 11 | "","time","LakeHuron" 12 | "1",1875,580.38 13 | "2",1876,581.86 14 | "3",1877,580.97 15 | "4",1878,580.8 16 | "5",1879,579.79 17 | ` 18 | 19 | func TestCSVLoader(t *testing.T) { 20 | c, err := NewCSV(strings.NewReader(Simple)) 21 | assert.NoError(t, err) 22 | assert.Len(t, c.Columns, 3) 23 | rows, cols := c.Dims() 24 | assert.Equal(t, 5, rows) 25 | assert.Equal(t, 3, cols) 26 | values, err := c.Row() 27 | assert.NoError(t, err) 28 | assert.Len(t, values, 3) 29 | assert.Equal(t, "1", values[0]) 30 | assert.Equal(t, "1875", values[1]) 31 | assert.Equal(t, "580.38", values[2]) 32 | values, err = c.Row() 33 | assert.NoError(t, err) 34 | assert.Len(t, values, 3) 35 | assert.Equal(t, "2", values[0]) 36 | assert.Equal(t, "1876", values[1]) 37 | assert.Equal(t, "581.86", values[2]) 38 | values, err = c.Row() 39 | assert.NoError(t, err) 40 | assert.Equal(t, "3", values[0]) 41 | assert.Equal(t, "1877", values[1]) 42 | assert.Equal(t, "580.97", values[2]) 43 | values, err = c.Row() 44 | assert.NoError(t, err) 45 | assert.Equal(t, "4", values[0]) 46 | assert.Equal(t, "1878", values[1]) 47 | assert.Equal(t, "580.8", values[2]) 48 | values, err = c.Row() 49 | assert.NoError(t, err) 50 | assert.Equal(t, "5", values[0]) 51 | assert.Equal(t, "1879", values[1]) 52 | assert.Equal(t, "579.79", values[2]) 53 | _, err = c.Row() 54 | assert.Error(t, err) 55 | assert.Equal(t, err, io.EOF) 56 | } 57 | -------------------------------------------------------------------------------- /loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | mtx "github.com/gonum/matrix/mat64" 7 | "github.com/kevinschoon/fit/parser" 8 | "github.com/kevinschoon/fit/types" 9 | "io" 10 | "math" 11 | "os" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | var ErrUnequalValues = errors.New("unequal value size") 17 | 18 | type Rower interface { 19 | Row() ([]string, error) 20 | Dims() (int, int) 21 | } 22 | 23 | type Options struct { 24 | Name string 25 | Path string 26 | Enc string 27 | Columns []string 28 | Sheet string // Sheet name (XLS) 29 | Size int64 // File Size (XLS) 30 | Parsers map[int]parser.Parser 31 | } 32 | 33 | // Rower returns a Rower based on the configured options 34 | func (opts *Options) Rower(fp *os.File) (Rower, error) { 35 | var split []string 36 | if opts.Name == "" { 37 | split = strings.Split(opts.Path, "/") 38 | opts.Name = split[len(split)-1] 39 | if strings.Contains(opts.Name, ".") { 40 | opts.Name = strings.Split(opts.Name, ".")[0] 41 | } 42 | } 43 | if opts.Enc == "" { 44 | split = strings.Split(opts.Path, "/") 45 | split = strings.Split(split[len(split)-1], ".") 46 | opts.Enc = split[len(split)-1] 47 | } 48 | switch { 49 | case opts.Enc == "csv": 50 | csv, err := NewCSV(fp) 51 | if err != nil { 52 | return nil, err 53 | } 54 | if len(opts.Columns) == 0 { 55 | opts.Columns = csv.Columns 56 | } 57 | return csv, nil 58 | case opts.Enc == "xls" || opts.Enc == "xlsx": 59 | xls, err := NewXLS(fp, *opts) 60 | if err != nil { 61 | return nil, err 62 | } 63 | if len(opts.Columns) == 0 { 64 | return nil, fmt.Errorf("Specify at least one column") 65 | } 66 | return xls, nil 67 | } 68 | panic(fmt.Sprintf("unknown encoding: %d", opts.Enc)) 69 | } 70 | 71 | func Matrix(rower Rower, parsers map[int]parser.Parser) (*mtx.Dense, error) { 72 | r, c := rower.Dims() 73 | mx := mtx.NewDense(r, c, nil) 74 | for j := 0; j < r; j++ { 75 | strs, err := rower.Row() 76 | if err == io.EOF { 77 | break 78 | } 79 | if err != nil { 80 | return nil, err 81 | } 82 | row := make([]float64, c) 83 | for i, str := range strs { 84 | if parser, ok := parsers[i]; ok { 85 | if value, err := parser.Parse(str); err == nil { 86 | row[i] = value 87 | continue 88 | } 89 | } 90 | if value, err := strconv.ParseFloat(str, 64); err == nil { 91 | row[i] = value 92 | continue 93 | } 94 | row[i] = math.NaN() 95 | } 96 | mx.SetRow(j, row) 97 | } 98 | return mx, nil 99 | } 100 | 101 | func ReadPath(opts Options) (*types.Dataset, error) { 102 | var ( 103 | rower Rower 104 | mx *mtx.Dense 105 | ) 106 | fp, err := os.Open(opts.Path) 107 | if err != nil { 108 | return nil, err 109 | } 110 | stats, err := fp.Stat() 111 | if err != nil { 112 | return nil, err 113 | } 114 | defer fp.Close() 115 | opts.Size = stats.Size() 116 | rower, err = opts.Rower(fp) 117 | if err != nil { 118 | return nil, err 119 | } 120 | mx, err = Matrix(rower, opts.Parsers) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return &types.Dataset{ 125 | Name: opts.Name, 126 | Columns: opts.Columns, 127 | Mtx: mx, 128 | }, nil 129 | } 130 | -------------------------------------------------------------------------------- /loader/xls.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "errors" 5 | "github.com/tealeg/xlsx" 6 | "io" 7 | ) 8 | 9 | var InvalidXLS = errors.New("Invalid XLS") 10 | 11 | type XLS struct { 12 | file *xlsx.File 13 | sheet *xlsx.Sheet 14 | index int 15 | columns []string 16 | } 17 | 18 | func (x *XLS) Row() ([]string, error) { 19 | if x.index > x.sheet.MaxRow { 20 | return nil, io.EOF 21 | } 22 | values := make([]string, len(x.columns)) 23 | for i := 0; i < len(values); i++ { 24 | if str, err := x.sheet.Cell(x.index, i).String(); err == nil { 25 | values[i] = str 26 | } 27 | } 28 | x.index++ 29 | return values, nil 30 | } 31 | 32 | func (x XLS) Dims() (int, int) { 33 | return x.sheet.MaxRow, len(x.columns) 34 | } 35 | 36 | func NewXLS(reader io.ReaderAt, opts Options) (*XLS, error) { 37 | f, err := xlsx.OpenReaderAt(reader, opts.Size) 38 | if err != nil { 39 | return nil, err 40 | } 41 | xls := &XLS{ 42 | columns: opts.Columns, 43 | } 44 | if opts.Sheet != "" { 45 | if sheet, ok := f.Sheet[opts.Sheet]; ok { 46 | xls.sheet = sheet 47 | } else { 48 | return nil, InvalidXLS 49 | } 50 | } else { 51 | if len(f.Sheets) == 0 { 52 | return nil, InvalidXLS 53 | } 54 | xls.sheet = f.Sheets[0] 55 | } 56 | return xls, nil 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kevinschoon/fit/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Run() 9 | } 10 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Parser interface { 11 | Parse(string) (float64, error) 12 | } 13 | 14 | // Parsers loads Parser types from the array of strings 15 | // Example: 16 | // 1,Time,2006-01-02 17 | // ^-^----^----index(int),name(string),format(string) 18 | func ParsersFromArgs(args []string) (map[int]Parser, error) { 19 | parsers := make(map[int]Parser) 20 | for _, arg := range args { 21 | split := strings.Split(arg, ",") 22 | if len(split) < 3 { 23 | return nil, fmt.Errorf("Bad parser opts: %s", arg) 24 | } 25 | index, err := strconv.ParseInt(split[0], 0, 64) 26 | if err != nil { 27 | return nil, fmt.Errorf("Bad parser opts: %s", arg) 28 | } 29 | switch split[1] { 30 | case "Time": 31 | if len(split) != 3 { 32 | return nil, fmt.Errorf("Bad parser opts: %s", arg) 33 | } 34 | parsers[int(index)] = TimeParser{Format: split[2]} 35 | default: 36 | return nil, fmt.Errorf("Unknown parser: %s", split[1]) 37 | } 38 | } 39 | return parsers, nil 40 | } 41 | 42 | type TimeParser struct { 43 | Format string 44 | } 45 | 46 | func (t TimeParser) Parse(v string) (float64, error) { 47 | parsed, err := time.Parse(t.Format, v) 48 | if err != nil { 49 | return 0.0, err 50 | } 51 | return float64(parsed.Unix()), nil 52 | } 53 | -------------------------------------------------------------------------------- /sample_data/LakeHuron.csv: -------------------------------------------------------------------------------- 1 | "","time","LakeHuron" 2 | "1",1875,580.38 3 | "2",1876,581.86 4 | "3",1877,580.97 5 | "4",1878,580.8 6 | "5",1879,579.79 7 | "6",1880,580.39 8 | "7",1881,580.42 9 | "8",1882,580.82 10 | "9",1883,581.4 11 | "10",1884,581.32 12 | "11",1885,581.44 13 | "12",1886,581.68 14 | "13",1887,581.17 15 | "14",1888,580.53 16 | "15",1889,580.01 17 | "16",1890,579.91 18 | "17",1891,579.14 19 | "18",1892,579.16 20 | "19",1893,579.55 21 | "20",1894,579.67 22 | "21",1895,578.44 23 | "22",1896,578.24 24 | "23",1897,579.1 25 | "24",1898,579.09 26 | "25",1899,579.35 27 | "26",1900,578.82 28 | "27",1901,579.32 29 | "28",1902,579.01 30 | "29",1903,579 31 | "30",1904,579.8 32 | "31",1905,579.83 33 | "32",1906,579.72 34 | "33",1907,579.89 35 | "34",1908,580.01 36 | "35",1909,579.37 37 | "36",1910,578.69 38 | "37",1911,578.19 39 | "38",1912,578.67 40 | "39",1913,579.55 41 | "40",1914,578.92 42 | "41",1915,578.09 43 | "42",1916,579.37 44 | "43",1917,580.13 45 | "44",1918,580.14 46 | "45",1919,579.51 47 | "46",1920,579.24 48 | "47",1921,578.66 49 | "48",1922,578.86 50 | "49",1923,578.05 51 | "50",1924,577.79 52 | "51",1925,576.75 53 | "52",1926,576.75 54 | "53",1927,577.82 55 | "54",1928,578.64 56 | "55",1929,580.58 57 | "56",1930,579.48 58 | "57",1931,577.38 59 | "58",1932,576.9 60 | "59",1933,576.94 61 | "60",1934,576.24 62 | "61",1935,576.84 63 | "62",1936,576.85 64 | "63",1937,576.9 65 | "64",1938,577.79 66 | "65",1939,578.18 67 | "66",1940,577.51 68 | "67",1941,577.23 69 | "68",1942,578.42 70 | "69",1943,579.61 71 | "70",1944,579.05 72 | "71",1945,579.26 73 | "72",1946,579.22 74 | "73",1947,579.38 75 | "74",1948,579.1 76 | "75",1949,577.95 77 | "76",1950,578.12 78 | "77",1951,579.75 79 | "78",1952,580.85 80 | "79",1953,580.41 81 | "80",1954,579.96 82 | "81",1955,579.61 83 | "82",1956,578.76 84 | "83",1957,578.18 85 | "84",1958,577.21 86 | "85",1959,577.13 87 | "86",1960,579.1 88 | "87",1961,578.25 89 | "88",1962,577.91 90 | "89",1963,576.89 91 | "90",1964,575.96 92 | "91",1965,576.8 93 | "92",1966,577.68 94 | "93",1967,578.38 95 | "94",1968,578.52 96 | "95",1969,579.74 97 | "96",1970,579.31 98 | "97",1971,579.89 99 | "98",1972,579.96 100 | -------------------------------------------------------------------------------- /server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gonum/plot/vg" 7 | "github.com/kevinschoon/fit/chart" 8 | "github.com/kevinschoon/fit/types" 9 | "image/color" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "text/template" 14 | ) 15 | 16 | type Response struct { 17 | Title string 18 | Explore bool // Display Data Explorer 19 | Browse bool // Display Datasets Listing 20 | Keys []string // Series Keys to Display 21 | ChartURL string // URL for rendering the chart 22 | Datasets []*types.Dataset 23 | Dataset *types.Dataset 24 | Query url.Values 25 | DemoMode bool 26 | Version string 27 | } 28 | 29 | type Handler struct { 30 | db types.Client 31 | version string 32 | templates []string 33 | defaults Response 34 | } 35 | 36 | func (handler Handler) response() *Response { 37 | return &Response{ 38 | DemoMode: handler.defaults.DemoMode, 39 | Version: handler.defaults.Version, 40 | } 41 | } 42 | 43 | func (handler Handler) Chart(w http.ResponseWriter, r *http.Request) error { 44 | ds, err := handler.db.Query(types.NewQueryQS(r.URL)) 45 | if err != nil { 46 | return err 47 | } 48 | cfg := chart.Config{ 49 | Title: ds.Name, 50 | PrimaryColor: color.White, 51 | SecondaryColor: color.Black, 52 | Width: 18 * vg.Inch, 53 | Height: 5 * vg.Inch, 54 | Type: r.URL.Query().Get("type"), 55 | Columns: types.NewQueryQS(r.URL).Columns(), 56 | } 57 | if w, err := strconv.ParseInt(r.URL.Query().Get("width"), 0, 64); err == nil { 58 | if w < 20 { // Prevent potentially horrible DOS 59 | cfg.Width = vg.Length(w) * vg.Inch 60 | } 61 | } 62 | if h, err := strconv.ParseInt(r.URL.Query().Get("height"), 0, 64); err == nil { 63 | if h < 20 { 64 | cfg.Height = vg.Length(h) * vg.Inch 65 | } 66 | } 67 | canvas, err := chart.New(cfg, ds.Mtx) 68 | if err != nil { 69 | return err 70 | } 71 | _, err = canvas.WriteTo(w) 72 | return err 73 | } 74 | 75 | func (handler Handler) DatasetAPI(w http.ResponseWriter, r *http.Request) error { 76 | switch r.Method { 77 | case "GET": 78 | query := types.NewQueryQS(r.URL) 79 | if query.Len() > 0 { // If URL contains a query return the query result 80 | ds, err := handler.db.Query(query) 81 | if err != nil { 82 | return err 83 | } 84 | ds.WithValues = true // Encode values in the dataset response 85 | if err := json.NewEncoder(w).Encode(ds); err != nil { 86 | return err 87 | } 88 | } else { // Otherwise return an array of all datasets 89 | datasets, err := handler.db.Datasets() 90 | if err != nil { 91 | return err 92 | } 93 | if err = json.NewEncoder(w).Encode(datasets); err != nil { 94 | return err 95 | } 96 | } 97 | case "POST": 98 | ds := &types.Dataset{WithValues: true} 99 | if err := json.NewDecoder(r.Body).Decode(ds); err != nil { 100 | return err 101 | } 102 | if err := handler.db.Write(ds); err != nil { 103 | return err 104 | } 105 | case "DELETE": 106 | if name := r.URL.Query().Get("name"); name != "" { 107 | if err := handler.db.Delete(name); err != nil { 108 | return err 109 | } 110 | } else { 111 | return fmt.Errorf("specify name") 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func (handler Handler) Home(w http.ResponseWriter, r *http.Request) error { 118 | tmpl, err := template.ParseFiles(handler.templates...) 119 | if err != nil { 120 | return err 121 | } 122 | response := handler.response() 123 | datasets, err := handler.db.Datasets() 124 | if err != nil { 125 | return err 126 | } 127 | response.Datasets = datasets 128 | response.Query = r.URL.Query() 129 | response.Title = "Browse" 130 | response.Browse = true 131 | return tmpl.Execute(w, response) 132 | } 133 | 134 | func (handler Handler) Explore(w http.ResponseWriter, r *http.Request) error { 135 | tmpl, err := template.ParseFiles(handler.templates...) 136 | if err != nil { 137 | return err 138 | } 139 | response := handler.response() 140 | datasets, err := handler.db.Datasets() 141 | if err != nil { 142 | return err 143 | } 144 | response.Datasets = datasets 145 | response.Query = r.URL.Query() 146 | response.Explore = true 147 | chartURL := &url.URL{ 148 | Scheme: r.URL.Scheme, 149 | Host: r.URL.Host, 150 | Path: "/chart", 151 | RawQuery: r.URL.Query().Encode(), 152 | } 153 | response.ChartURL = chartURL.String() 154 | ds, err := handler.db.Query(types.NewQueryQS(r.URL)) 155 | if err != nil { 156 | return err 157 | } 158 | response.Dataset = ds 159 | return tmpl.Execute(w, response) 160 | } 161 | -------------------------------------------------------------------------------- /server/handler_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/kevinschoon/fit/clients" 7 | "github.com/kevinschoon/fit/types" 8 | "github.com/stretchr/testify/assert" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "testing" 15 | ) 16 | 17 | var cleanup bool = false 18 | 19 | var ( 20 | datasetRaw []byte = []byte(`{"Name": "TestDataset", "Columns": ["V1", "V2"]}`) 21 | valuesRaw []byte = []byte(`[1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0]`) 22 | ) 23 | 24 | type MockWriter struct { 25 | fp *os.File 26 | } 27 | 28 | func (writer MockWriter) Header() http.Header { return http.Header{} } 29 | 30 | func (writer MockWriter) Write(data []byte) (int, error) { 31 | return writer.fp.Write(data) 32 | } 33 | func (writer MockWriter) WriteHeader(int) {} 34 | 35 | type MockReader struct { 36 | *bytes.Reader 37 | } 38 | 39 | func (reader MockReader) Close() error { return nil } 40 | 41 | func NewTestDB(t *testing.T) (types.Client, func()) { 42 | f, err := ioutil.TempFile("/tmp", "fit-test-") 43 | assert.NoError(t, err) 44 | db, err := clients.NewBoltClient(f.Name()) 45 | assert.NoError(t, err) 46 | return db, func() { 47 | if cleanup { 48 | os.Remove(f.Name()) 49 | } 50 | } 51 | } 52 | 53 | func NewMockWriter(t *testing.T) (MockWriter, func()) { 54 | fp, err := ioutil.TempFile("/tmp", "fit-test-writer-") 55 | assert.NoError(t, err) 56 | return MockWriter{fp: fp}, func() { 57 | fp.Close() 58 | if cleanup { 59 | os.Remove(fp.Name()) 60 | } 61 | } 62 | } 63 | 64 | func TestDatasetAPI(t *testing.T) { 65 | db, cleanup := NewTestDB(t) 66 | defer cleanup() 67 | handler := Handler{db: db} 68 | reader := MockReader{bytes.NewReader(datasetRaw)} 69 | assert.NoError(t, handler.DatasetAPI(MockWriter{fp: nil}, &http.Request{ 70 | Method: "POST", 71 | Body: io.ReadCloser(reader), 72 | })) 73 | writer, cleanup := NewMockWriter(t) 74 | defer cleanup() 75 | assert.NoError(t, handler.DatasetAPI(writer, &http.Request{ 76 | URL: &url.URL{}, 77 | Method: "GET", 78 | })) 79 | raw, err := ioutil.ReadFile(writer.fp.Name()) 80 | assert.NoError(t, err) 81 | ds := []*types.Dataset{} 82 | assert.NoError(t, json.Unmarshal(raw, &ds)) 83 | assert.Len(t, ds, 1) 84 | assert.Equal(t, "V1", ds[0].Columns[0]) 85 | assert.Equal(t, "V2", ds[0].Columns[1]) 86 | assert.Equal(t, 0, ds[0].Stats.Rows) 87 | assert.Equal(t, 0, ds[0].Stats.Columns) 88 | assert.NoError(t, handler.DatasetAPI(MockWriter{fp: nil}, &http.Request{ 89 | Method: "DELETE", 90 | URL: &url.URL{ 91 | RawQuery: "name=TestDataset", 92 | }, 93 | })) 94 | } 95 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "text/template" 10 | 11 | "github.com/gorilla/handlers" 12 | "github.com/gorilla/mux" 13 | "github.com/kevinschoon/fit/types" 14 | ) 15 | 16 | type ErrorHandler func(http.ResponseWriter, *http.Request) error 17 | 18 | func (fn ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | HandleError(fn(w, r), w, r) 20 | } 21 | 22 | func HandleError(err error, w http.ResponseWriter, r *http.Request) { 23 | if err != nil { 24 | fmt.Println("ERROR: ", err.Error()) 25 | switch err.(type) { 26 | case template.ExecError: 27 | default: 28 | switch err { 29 | case types.ErrNotFound: 30 | http.NotFound(w, r) 31 | default: 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | } 34 | } 35 | } 36 | } 37 | 38 | type StaticHandler struct { 39 | Path string // Path to static assets directory 40 | Allowed []string // Array of allowed directories 41 | } 42 | 43 | func (handler StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 44 | for _, directory := range handler.Allowed { 45 | if mux.Vars(r)["directory"] == directory { 46 | if files, err := ioutil.ReadDir(fmt.Sprintf("%s/%s", handler.Path, directory)); err == nil { 47 | for _, file := range files { 48 | if mux.Vars(r)["file"] == file.Name() && file.Mode().IsRegular() { 49 | http.ServeFile(w, r, fmt.Sprintf("%s/%s/%s", handler.Path, directory, file.Name())) 50 | return 51 | } 52 | } 53 | } 54 | } 55 | } 56 | http.NotFound(w, r) 57 | } 58 | 59 | func RunServer(db types.Client, pattern, path, version string, demo bool) { 60 | templates := []string{ 61 | fmt.Sprintf("%s/html/base.html", path), 62 | fmt.Sprintf("%s/html/panel.html", path), 63 | fmt.Sprintf("%s/html/explore.html", path), 64 | fmt.Sprintf("%s/html/browse.html", path), 65 | } 66 | router := mux.NewRouter().StrictSlash(true) 67 | handler := Handler{db: db, templates: templates, defaults: Response{DemoMode: demo, Version: version}} 68 | router.Handle("/", ErrorHandler(handler.Home)) 69 | router.Handle("/explore", ErrorHandler(handler.Explore)) 70 | router.Handle("/chart", ErrorHandler(handler.Chart)).Methods("GET") 71 | if demo { 72 | router.Handle("/1/dataset", ErrorHandler(handler.DatasetAPI)).Methods("GET") 73 | } else { 74 | router.Handle("/1/dataset", ErrorHandler(handler.DatasetAPI)).Methods("GET", "POST", "PUT", "DELETE") 75 | } 76 | router.Handle("/static/{directory}/{file}", StaticHandler{ 77 | Path: path, 78 | Allowed: []string{ 79 | "images", 80 | "css", 81 | "js", 82 | "fonts", 83 | }, 84 | }) 85 | log.Printf("Fit server listening @ %s", pattern) 86 | log.Fatal(http.ListenAndServe(pattern, handlers.CombinedLoggingHandler(os.Stdout, router))) 87 | } 88 | -------------------------------------------------------------------------------- /types/functions.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | mtx "github.com/gonum/matrix/mat64" 5 | "strings" 6 | ) 7 | 8 | type Function struct { 9 | Name string 10 | } 11 | 12 | func (fn Function) apply(mx []mtx.Matrix, f func(mtx.Matrix) float64) *mtx.Dense { 13 | var ( 14 | cols int 15 | result *mtx.Dense 16 | ) 17 | for i, view := range mx { 18 | if result == nil { 19 | _, cols = view.Dims() 20 | result = mtx.NewDense(len(mx), cols, nil) 21 | } 22 | // TODO: Determine if there is a better way to accomplish 23 | // this without type assertion 24 | other := view.(*mtx.Dense) 25 | for j := 0; j < cols; j++ { 26 | result.Set(i, j, f(other.ColView(j))) 27 | } 28 | } 29 | return result 30 | } 31 | 32 | func (fn Function) Apply(mx []mtx.Matrix) *mtx.Dense { 33 | switch strings.ToLower(fn.Name) { 34 | case "min": 35 | return fn.apply(mx, mtx.Min) 36 | case "max": 37 | return fn.apply(mx, mtx.Max) 38 | case "sum": 39 | return fn.apply(mx, mtx.Sum) 40 | default: // Use the average 41 | if len(mx) > 0 { 42 | return fn.apply(mx, func(o mtx.Matrix) float64 { 43 | r, _ := o.Dims() 44 | return mtx.Sum(o) / float64(r) 45 | }) 46 | } 47 | } 48 | return mtx.NewDense(0, 0, nil) 49 | } 50 | -------------------------------------------------------------------------------- /types/functions_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | -------------------------------------------------------------------------------- /types/grouping.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | mtx "github.com/gonum/matrix/mat64" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type grouping struct { 13 | Name string 14 | Index int 15 | Max string 16 | } 17 | 18 | // Grouping represents a "group by" configuration 19 | type Grouping struct { 20 | Name string 21 | Index int 22 | Max time.Duration 23 | } 24 | 25 | func (grp Grouping) UnmarshalJSON(data []byte) error { 26 | in := &grouping{} 27 | if err := json.Unmarshal(data, in); err != nil { 28 | return err 29 | } 30 | grp.Name = in.Name 31 | grp.Index = in.Index 32 | max, _ := time.ParseDuration(in.Max) 33 | grp.Max = max 34 | return nil 35 | } 36 | 37 | func (grp Grouping) MarshalJSON() ([]byte, error) { 38 | return json.Marshal(&grouping{ 39 | Name: grp.Name, 40 | Index: grp.Index, 41 | Max: grp.Max.String(), 42 | }) 43 | } 44 | 45 | func (grp Grouping) String() string { 46 | return fmt.Sprintf("%s,%d,%s", grp.Name, grp.Index, grp.Max.String()) 47 | } 48 | 49 | func (grp Grouping) Group(other *mtx.Dense) []mtx.Matrix { 50 | r, c := other.Dims() 51 | views := make([]mtx.Matrix, 0) 52 | var ( 53 | view mtx.Matrix 54 | previous time.Time 55 | current time.Time 56 | duration time.Duration 57 | ) 58 | for i, j := 0, 1; i+j <= r; j++ { 59 | view = other.View(i, 0, j, c) 60 | current = time.Unix(int64(view.At(j-1, grp.Index)), 0).UTC() 61 | duration += current.Sub(previous) 62 | if duration >= grp.Max { 63 | views = append(views, view) 64 | i, j = i+j, 0 65 | duration = time.Duration(0) 66 | } 67 | previous = current 68 | } 69 | return views 70 | } 71 | 72 | // NewGrouping returns a grouping based on a string 73 | // parameter. See documentation for Duration str format 74 | // 75 | // Duration,0,1min 76 | // ^--------^-^----Name,Index,DurationStr 77 | func NewGrouping(arg string) *Grouping { 78 | split := strings.Split(arg, ",") 79 | var grouping *Grouping 80 | if len(split) > 0 { 81 | grouping = &Grouping{ 82 | Name: split[0], 83 | } 84 | } 85 | if len(split) >= 2 { 86 | index, _ := strconv.ParseInt(split[1], 0, 64) 87 | grouping.Index = int(index) 88 | } 89 | if len(split) >= 3 { 90 | duration, _ := time.ParseDuration(split[2]) 91 | grouping.Max = duration 92 | } 93 | return grouping 94 | } 95 | -------------------------------------------------------------------------------- /types/grouping_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | mtx "github.com/gonum/matrix/mat64" 6 | "github.com/stretchr/testify/assert" 7 | "math" 8 | "math/rand" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func round(num float64) int { 14 | return int(num + math.Copysign(0.5, num)) 15 | } 16 | 17 | func toFixed(num float64, precision int) float64 { 18 | output := math.Pow(10, float64(precision)) 19 | return float64(round(num*output)) / output 20 | } 21 | 22 | func TestGrouping(t *testing.T) { 23 | grouping := NewGrouping("Duration,0,2s") 24 | assert.Equal(t, 2*time.Second, grouping.Max) 25 | assert.Equal(t, 0, grouping.Index) 26 | assert.Equal(t, "Duration,0,2s", grouping.String()) 27 | mx := mtx.NewDense(10, 10, nil) 28 | r, c := mx.Dims() 29 | start := time.Date(2001, time.January, 0, 0, 0, 1, 1, time.UTC) 30 | for i := 0; i < r; i++ { 31 | mx.Set(i, 0, float64(start.Unix())) 32 | mx.Set(i, 1, 1.0) 33 | start = start.Add(1 * time.Second) 34 | for j := 2; j < c; j++ { 35 | mx.Set(i, j, toFixed(rand.Float64(), 2)) 36 | } 37 | } 38 | assert.Equal(t, mtx.Sum(mx.ColView(1)), 10.0) 39 | assert.True(t, mtx.Sum(mx.ColView(2)) < 10) 40 | views := grouping.Group(mx) 41 | assert.Len(t, views, 5) 42 | rows := 0 43 | for _, v := range views { 44 | fmt.Println(mtx.Formatted(v)) 45 | r, _ := v.Dims() 46 | rows += r 47 | } 48 | assert.Equal(t, 10, r) 49 | //mx = apply(result, Avg) 50 | //r, c = mx.Dims() 51 | //assert.Equal(t, 5, r) 52 | //assert.Equal(t, 10, c) 53 | //fmt.Println(mtx.Formatted(mx)) 54 | } 55 | 56 | func init() { 57 | rand.Seed(time.Now().Unix()) 58 | } 59 | -------------------------------------------------------------------------------- /types/query.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | mtx "github.com/gonum/matrix/mat64" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // Query can be used to combine the results 10 | // of multiple datasets into a single 11 | // matrix of values. Queries can originate 12 | // from the command line as arguments, 13 | // a URL query string, or a JSON encoded 14 | // payload. 15 | // 16 | // Command line arguments take the same 17 | // form as URL encoding 18 | // 19 | // Text Specification: 20 | // d=DS1,x,y&d=DS2,z,fuu&grouping=Duration,0,1m&fn=avg 21 | // 22 | type Query struct { 23 | Datasets []struct { 24 | Name string // Name of the dataset 25 | Columns []string // Columns within the dataset to query 26 | } 27 | Function *Function 28 | Grouping *Grouping 29 | } 30 | 31 | // Len returns the length of the Query 32 | func (query Query) Len() int { 33 | return len(query.Datasets) 34 | } 35 | 36 | // Columns returns a flattened ordered 37 | // array of Column names 38 | func (query Query) Columns() []string { 39 | columns := make([]string, 0) 40 | for _, dataset := range query.Datasets { 41 | for _, column := range dataset.Columns { 42 | columns = append(columns, column) 43 | } 44 | } 45 | return columns 46 | } 47 | 48 | // String returns a valid URL query string 49 | func (query Query) String() string { 50 | values := url.Values{} 51 | values.Add("fn", query.Function.Name) 52 | if query.Grouping != nil { 53 | values.Add("grouping", query.Grouping.String()) 54 | } 55 | for _, dataset := range query.Datasets { 56 | args := make([]string, len(dataset.Columns)+1) 57 | args[0] = dataset.Name 58 | for i, column := range dataset.Columns { 59 | args[i+1] = column 60 | } 61 | values.Add("q", strings.TrimRight(strings.Join(args, ","), ",")) 62 | } 63 | return values.Encode() 64 | } 65 | 66 | // Apply returns a new modified matrix based on the query 67 | func (query Query) Apply(mx *mtx.Dense) *mtx.Dense { 68 | if query.Grouping != nil { 69 | return query.Function.Apply(query.Grouping.Group(mx)) 70 | } 71 | return mx 72 | } 73 | 74 | // NewQuery constructs a Query from the provided 75 | // args and optionally specified function. 76 | // If function is specified the query returns 77 | // aggregated 78 | func NewQuery(args []string, function, grouping string) *Query { 79 | query := &Query{ 80 | Datasets: make([]struct { 81 | Name string 82 | Columns []string 83 | }, len(args)), 84 | Function: &Function{ 85 | Name: function, 86 | }, 87 | } 88 | if grouping != "" { 89 | query.Grouping = NewGrouping(grouping) 90 | } 91 | for i, arg := range args { 92 | split := strings.Split(arg, ",") 93 | if len(split) >= 1 { 94 | query.Datasets[i].Name = split[0] 95 | } 96 | if len(split) > 1 { 97 | query.Datasets[i].Columns = split[1:] 98 | } 99 | } 100 | return query 101 | } 102 | 103 | // NewQueryQS constructs a query from a url.URL 104 | func NewQueryQS(u *url.URL) *Query { 105 | var args []string 106 | query := u.Query() 107 | if q, ok := query["q"]; ok { 108 | args = q 109 | } 110 | return NewQuery(args, query.Get("fn"), query.Get("grouping")) 111 | } 112 | -------------------------------------------------------------------------------- /types/query_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/url" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestQuery(t *testing.T) { 11 | u, err := url.Parse("http://localhost/?q=D0,x,y,z&q=D1,z&grouping=Duration,0,1m&fn=avg") 12 | assert.NoError(t, err) 13 | query := NewQueryQS(u) 14 | assert.Equal(t, 2, query.Len()) 15 | assert.Equal(t, "D0", query.Datasets[0].Name) 16 | assert.Equal(t, 3, len(query.Datasets[0].Columns)) 17 | assert.Equal(t, "x", query.Datasets[0].Columns[0]) 18 | assert.Equal(t, "y", query.Datasets[0].Columns[1]) 19 | assert.Equal(t, "z", query.Datasets[0].Columns[2]) 20 | assert.Equal(t, 1, len(query.Datasets[1].Columns)) 21 | assert.Equal(t, "z", query.Datasets[1].Columns[0]) 22 | assert.Equal(t, "D1", query.Datasets[1].Name) 23 | assert.Equal(t, "avg", query.Function.Name) 24 | assert.Equal(t, 0, query.Grouping.Index) 25 | assert.Equal(t, time.Minute, query.Grouping.Max) 26 | assert.Equal(t, "fn=avg&grouping=Duration%2C0%2C1m0s&q=D0%2Cx%2Cy%2Cz&q=D1%2Cz", query.String()) 27 | } 28 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/gonum/matrix" 7 | mtx "github.com/gonum/matrix/mat64" 8 | "io" 9 | "math" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | ErrAPI = errors.New("api error") 15 | ErrNoData = errors.New("no data") 16 | ErrNotFound = errors.New("not found") 17 | ErrBadQuery = errors.New("bad query") 18 | ) 19 | 20 | // Work around for handling NaN values in JSON 21 | // https://github.com/golang/go/issues/3480 22 | type value float64 23 | 24 | func (v value) MarshalJSON() ([]byte, error) { 25 | if math.IsNaN(float64(v)) { 26 | return json.Marshal(nil) 27 | } 28 | return json.Marshal(float64(v)) 29 | } 30 | 31 | func (v *value) UnmarshalJSON(data []byte) error { 32 | if err := json.Unmarshal(data, nil); err != nil { 33 | val := float64(0.0) 34 | if err := json.Unmarshal(data, &val); err == nil { 35 | *v = value(val) 36 | } 37 | } else { 38 | *v = value(math.NaN()) 39 | } 40 | return nil 41 | } 42 | 43 | type values []value 44 | 45 | type Client interface { 46 | Datasets() ([]*Dataset, error) 47 | Write(*Dataset) error 48 | Delete(string) error 49 | Query(*Query) (*Dataset, error) 50 | } 51 | 52 | // Stats contain statistics about the 53 | // underlying data in a dataset 54 | type Stats struct { 55 | Rows int 56 | Columns int 57 | } 58 | 59 | type dataset struct { 60 | Name string 61 | Columns []string 62 | Stats *Stats 63 | Mtx []value 64 | } 65 | 66 | // Dataset consists of a name and 67 | // an ordered array of column names 68 | type Dataset struct { 69 | Name string // Name of this dataset 70 | Columns []string // Ordered array of cols 71 | Mtx *mtx.Dense `json:"-"` // Dense Matrix contains all values in the dataset 72 | Stats *Stats 73 | lock sync.RWMutex 74 | index int 75 | WithValues bool 76 | } 77 | 78 | func (ds *Dataset) MarshalJSON() ([]byte, error) { 79 | ds.stats() 80 | out := &dataset{ 81 | Name: ds.Name, 82 | Columns: ds.Columns, 83 | Stats: ds.Stats, 84 | } 85 | if ds.WithValues && ds.Mtx != nil { 86 | r, c := ds.Mtx.Dims() 87 | out.Mtx = make([]value, r*c) 88 | for i, val := range ds.Mtx.RawMatrix().Data { 89 | out.Mtx[i] = value(val) 90 | } 91 | } 92 | return json.Marshal(out) 93 | } 94 | 95 | func (ds *Dataset) UnmarshalJSON(data []byte) error { 96 | in := &dataset{} 97 | if err := json.Unmarshal(data, in); err != nil { 98 | return err 99 | } 100 | ds.Name = in.Name 101 | ds.Columns = in.Columns 102 | ds.Stats = in.Stats 103 | return matrix.Maybe(func() { 104 | if ds.WithValues && in.Mtx != nil { 105 | values := make([]float64, ds.Stats.Rows*ds.Stats.Columns) 106 | for i := 0; i < len(in.Mtx); i++ { 107 | values[i] = float64(in.Mtx[i]) 108 | } 109 | ds.Mtx = mtx.NewDense(ds.Stats.Rows, ds.Stats.Columns, values) 110 | } 111 | }) 112 | } 113 | 114 | // stats updates the Stats struct 115 | func (ds *Dataset) stats() { 116 | if ds.Stats == nil { 117 | ds.Stats = &Stats{} 118 | } 119 | if ds.Mtx != nil { 120 | ds.Stats.Rows, ds.Stats.Columns = ds.Mtx.Dims() 121 | } 122 | } 123 | 124 | // Len returns the length (number of rows) of the dataset 125 | func (ds Dataset) Len() int { 126 | len := 0 127 | if ds.Mtx != nil { 128 | len, _ = ds.Mtx.Dims() 129 | } 130 | return len 131 | } 132 | 133 | // CPos returns the position of a column 134 | // name in a dataset. If the column 135 | // does not exist it returns -1 136 | func (ds Dataset) CPos(name string) int { 137 | for i, col := range ds.Columns { 138 | if name == col { 139 | return i 140 | } 141 | } 142 | return -1 143 | } 144 | 145 | // Next returns the next row of values 146 | // If all values have been traversed 147 | // it returns io.EOF. Implements the 148 | // loader.Reader interface 149 | func (ds *Dataset) Next() ([]float64, error) { 150 | ds.lock.Lock() 151 | defer ds.lock.Unlock() 152 | if ds.Mtx == nil { 153 | return nil, ErrNoData 154 | } 155 | r, _ := ds.Mtx.Dims() 156 | if ds.index >= r { 157 | ds.index = 0 158 | return nil, io.EOF 159 | } 160 | rows := ds.Mtx.RawRowView(ds.index) 161 | ds.index++ 162 | return rows, nil 163 | } 164 | -------------------------------------------------------------------------------- /types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | mtx "github.com/gonum/matrix/mat64" 6 | "github.com/stretchr/testify/assert" 7 | "math" 8 | "testing" 9 | ) 10 | 11 | func TestDatasetJSON(t *testing.T) { 12 | ds := &Dataset{ 13 | Name: "TestDataset", 14 | Columns: []string{"V1", "V2"}, 15 | Mtx: mtx.NewDense(3, 2, []float64{1.0, 1.0, 2.0, 2.0, 3.0, math.NaN()}), 16 | WithValues: true, 17 | } 18 | raw, err := json.Marshal(ds) 19 | assert.NoError(t, err) 20 | out := &Dataset{WithValues: true} 21 | assert.NoError(t, json.Unmarshal(raw, out)) 22 | assert.Equal(t, ds.Name, out.Name) 23 | assert.Equal(t, ds.Columns[0], out.Columns[0]) 24 | assert.Equal(t, ds.Columns[1], out.Columns[1]) 25 | assert.Equal(t, 1.0, ds.Mtx.At(0, 0)) 26 | assert.Equal(t, 1.0, ds.Mtx.At(0, 1)) 27 | 28 | assert.Equal(t, 2.0, ds.Mtx.At(1, 0)) 29 | assert.Equal(t, 2.0, ds.Mtx.At(1, 1)) 30 | 31 | assert.Equal(t, 3.0, ds.Mtx.At(2, 0)) 32 | assert.True(t, math.IsNaN(ds.Mtx.At(2, 1))) 33 | } 34 | -------------------------------------------------------------------------------- /www/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | .options-right { 6 | border-left: 1px solid #ccc; 7 | } 8 | 9 | .panel-body.chart { 10 | background-color: #000; 11 | } 12 | 13 | .browse { 14 | margin-top: 25px; 15 | text-align: center; 16 | } 17 | 18 | .navbar{ 19 | background-color: #222222; 20 | min-height: 0px !important; 21 | padding-top: 2px; 22 | } 23 | 24 | /* Always float the navbar header */ 25 | .navbar-header { 26 | float: left; 27 | } 28 | 29 | /* Undo the collapsing navbar */ 30 | .navbar-collapse { 31 | display: block !important; 32 | height: auto !important; 33 | padding-bottom: 0; 34 | overflow: visible !important; 35 | visibility: visible !important; 36 | } 37 | 38 | .navbar-toggle { 39 | display: none; 40 | } 41 | .navbar-collapse { 42 | border-top: 0; 43 | } 44 | 45 | .navbar-brand { 46 | margin-left: -15px; 47 | } 48 | 49 | /* Always apply the floated nav */ 50 | .navbar-nav { 51 | float: left; 52 | margin: 0; 53 | } 54 | .navbar-nav > li { 55 | float: left; 56 | } 57 | .navbar-nav > li > a { 58 | padding: 15px; 59 | } 60 | 61 | /* Redeclare since we override the float above */ 62 | .navbar-nav.navbar-right { 63 | float: right; 64 | } 65 | 66 | /* Undo custom dropdowns */ 67 | .navbar .navbar-nav .open .dropdown-menu { 68 | position: absolute; 69 | float: left; 70 | border: 1px solid #ccc; 71 | border: 1px solid rgba(0, 0, 0, .15); 72 | border-width: 0 1px 1px; 73 | border-radius: 0 0 4px 4px; 74 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); 75 | box-shadow: 0 6px 12px rgba(0, 0, 0, .175); 76 | } 77 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 78 | color: #333; 79 | } 80 | .navbar .navbar-nav .open .dropdown-menu > li > a:hover, 81 | .navbar .navbar-nav .open .dropdown-menu > li > a:focus, 82 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 83 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 84 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 85 | color: #fff !important; 86 | background-color: #428bca !important; 87 | } 88 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a, 89 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a:hover, 90 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus { 91 | color: #999 !important; 92 | background-color: transparent !important; 93 | } 94 | 95 | /* Undo form expansion */ 96 | .navbar-form { 97 | float: left; 98 | width: auto; 99 | padding-top: 0; 100 | padding-bottom: 0; 101 | margin-right: 0; 102 | margin-left: 0; 103 | border: 0; 104 | -webkit-box-shadow: none; 105 | box-shadow: none; 106 | } 107 | 108 | /* Copy-pasted from forms.less since we mixin the .form-inline styles. */ 109 | .navbar-form .form-group { 110 | display: inline-block; 111 | margin-bottom: 0; 112 | vertical-align: middle; 113 | } 114 | 115 | .navbar-form .form-control { 116 | display: inline-block; 117 | width: auto; 118 | vertical-align: middle; 119 | } 120 | 121 | .navbar-form .form-control-static { 122 | display: inline-block; 123 | } 124 | 125 | .navbar-form .input-group { 126 | display: inline-table; 127 | vertical-align: middle; 128 | } 129 | 130 | .navbar-form .input-group .input-group-addon, 131 | .navbar-form .input-group .input-group-btn, 132 | .navbar-form .input-group .form-control { 133 | width: auto; 134 | } 135 | 136 | .navbar-form .input-group > .form-control { 137 | width: 100%; 138 | } 139 | 140 | .navbar-form .control-label { 141 | margin-bottom: 0; 142 | vertical-align: middle; 143 | } 144 | 145 | .navbar-form .radio, 146 | .navbar-form .checkbox { 147 | display: inline-block; 148 | margin-top: 0; 149 | margin-bottom: 0; 150 | vertical-align: middle; 151 | } 152 | 153 | .navbar-form .radio label, 154 | .navbar-form .checkbox label { 155 | padding-left: 0; 156 | } 157 | 158 | .navbar-form .radio input[type="radio"], 159 | .navbar-form .checkbox input[type="checkbox"] { 160 | position: relative; 161 | margin-left: 0; 162 | } 163 | 164 | .navbar-form .has-feedback .form-control-feedback { 165 | top: 0; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /www/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | -------------------------------------------------------------------------------- /www/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /www/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /www/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /www/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /www/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /www/html/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Fit 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ block "panel" .}}{{end}} 17 | {{if .Explore}} 18 | {{ block "explore" .}}{{end}} 19 | {{else}} 20 | {{ block "browse" .}}{{end}} 21 | {{end}} 22 | 23 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /www/html/browse.html: -------------------------------------------------------------------------------- 1 | {{define "browse"}} 2 |
3 | {{if .DemoMode}} 4 |
5 |
6 |
7 |
8 |

Fit is a tool for reading, manipulating, and exploring time series data.

9 | 10 |
11 |

Explore Example Series

12 |
13 |

TCX Data

14 | 15 | ... 16 | 17 |
18 |
19 |

TCX Data

20 | 21 | ... 22 | 23 |
24 |
25 |

TCX Data

26 | 27 | ... 28 | 29 |
30 |
31 |

TCX Data

32 | 33 | ... 34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | {{else}} 42 |
43 |
44 |
45 |
46 |
47 |

Series

48 |
49 |
    50 | {{range .Datasets}} 51 |
  • {{.Name}}
  • 52 | {{end}} 53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 | {{end}} 61 | {{end}} 62 | -------------------------------------------------------------------------------- /www/html/explore.html: -------------------------------------------------------------------------------- 1 | {{define "explore"}} 2 |
3 |
4 | ... 5 |
6 |
7 |
8 |
9 | 12 | Query Options 13 |
14 |
15 |
16 |
17 |

Query

18 |
19 | X-Axis 20 | 21 |
22 |
23 |
24 | Y-Axis 25 | 26 |
27 |
28 |
29 |
30 |
31 |

Time Range

32 |
33 | Start 34 | 35 |
36 |
37 |
38 | End   39 | 40 |
41 |
42 |
43 |
44 |

Options

45 |
46 | Aggregation 47 | 48 |
49 |
Function
50 | 56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 |
TODO
84 | TODO 85 |
89 |
90 |
91 | 92 | 93 | 132 | 133 | {{end}} 134 | 135 | -------------------------------------------------------------------------------- /www/html/panel.html: -------------------------------------------------------------------------------- 1 | {{define "panel"}} 2 | 3 | 35 | 36 | {{end}} 37 | -------------------------------------------------------------------------------- /www/images/gopher-fit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/images/gopher-fit.ico -------------------------------------------------------------------------------- /www/images/gopher-fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/images/gopher-fit.png -------------------------------------------------------------------------------- /www/images/gopher-fit.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/images/gopher-fit.xcf -------------------------------------------------------------------------------- /www/images/tcx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/images/tcx.png -------------------------------------------------------------------------------- /www/images/temperatures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschoon/fit/fc27e31445f0d6fbcfacc8dbeb0f920657fedcb2/www/images/temperatures.png -------------------------------------------------------------------------------- /www/js/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | function update(uri, key, value) { 3 | /*Thanks http://stackoverflow.com/questions/5999118/add-or-update-query-string-parameter#6021027*/ 4 | if (typeof value != 'undefined') { 5 | var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i"); 6 | var separator = uri.indexOf('?') !== -1 ? "&" : "?"; 7 | if (uri.match(re)) { 8 | return uri.replace(re, '$1' + key + "=" + value + '$2'); 9 | } else { 10 | return uri + separator + key + "=" + value; 11 | } 12 | } 13 | return uri 14 | } 15 | $("#submit").on('click', function() { 16 | var target = window.location.pathname 17 | var Q = window.location.search 18 | Q = update(Q, "X", $("#x").val()) 19 | Q = update(Q, "Y", $("#y").val()) 20 | Q = update(Q, "aggr", $("#aggr").val()) 21 | Q = update(Q, "fn", $("#fn").val()) 22 | Q = update(Q, "start", $("#start").val()) 23 | Q = update(Q, "end", $("#end").val()) 24 | window.location = window.location.pathname + Q 25 | }); 26 | }) 27 | -------------------------------------------------------------------------------- /www/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /www/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.1.0 | (c) jQuery Foundation | jquery.org/license */ 2 | !function(a,b){"use strict";"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){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.0",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null!=a?a<0?this[a+this.length]:this[a]:f.call(this)},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"label"in b&&b.disabled===a||"form"in b&&b.disabled===a||"form"in b&&b.disabled===!1&&(b.isDisabled===a||b.isDisabled!==!a&&("label"in b||!ea(b))!==a)}}function pa(a){return ia(function(b){return b=+b,ia(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 qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(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){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.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===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!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 ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.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},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.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=ga.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=ga.selectors={cacheLength:50,createPseudo:ia,match:V,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(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===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]||ga.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]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.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(_,aa).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("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.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.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},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,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(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),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).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:oa(!1),disabled:oa(!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 X.test(a.nodeName)},input:function(a){return W.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:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.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(_,aa),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=V.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(_,aa),$.test(j[0].type)&&qa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&sa(j),!a)return G.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||$.test(a)&&qa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){if(r.isFunction(b))return r.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return r.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(C.test(b))return r.filter(b,a,c);b=r.filter(b,a)}return r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType})}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/\S+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0, 3 | r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.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 V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ja=/^$|\/(?:java|ecma)script/i,ka={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ka.optgroup=ka.option,ka.tbody=ka.tfoot=ka.colgroup=ka.caption=ka.thead,ka.th=ka.td;function la(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function ma(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=la(l.appendChild(f),"script"),j&&ma(g),c){k=0;while(f=g[k++])ja.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),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),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==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,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;d0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.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 Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(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&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=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):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K); 4 | if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.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){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=e<0?h:f?e:0;i-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.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 Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.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 Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(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}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(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}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(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 r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("