diff --git a/README.md b/README.md index 2ca504d..e83313a 100644 --- a/README.md +++ b/README.md @@ -194,4 +194,11 @@ server { proxy_cache_bypass $http_upgrade; } } -``` \ No newline at end of file +``` + +## Projects Sites +As I develope more projects I would like an easy way to add and host them on my website without having to create another subdomain and generate more ssl certs. I simply want the project site to be accessible under https://jrtechs.net/project_name. + +### State Diagrm of Plan + +![diagram](docs/projectsSites.svg) \ No newline at end of file diff --git a/blogContent/projects/steam/Diagram.svg b/blogContent/projects/steam/Diagram.svg new file mode 100644 index 0000000..ad8ce63 --- /dev/null +++ b/blogContent/projects/steam/Diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blogContent/projects/steam/css/bootstrap.css b/blogContent/projects/steam/css/bootstrap.css new file mode 100644 index 0000000..eaed9ab --- /dev/null +++ b/blogContent/projects/steam/css/bootstrap.css @@ -0,0 +1,9647 @@ +/*! + * Bootswatch v4.1.1 + * Homepage: https://bootswatch.com + * Copyright 2012-2018 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*/ +/*! + * Bootstrap v4.1.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700"); +:root { + --blue: #325D88; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #d9534f; + --orange: #F47C3C; + --yellow: #ffc107; + --green: #93C54B; + --teal: #20c997; + --cyan: #29ABE0; + --white: #fff; + --gray: #8E8C84; + --gray-dark: #3E3F3A; + --primary: #325D88; + --secondary: #8E8C84; + --success: #93C54B; + --info: #29ABE0; + --warning: #F47C3C; + --danger: #d9534f; + --light: #F8F5F0; + --dark: #3E3F3A; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +*, +*::before, +*::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; +} + +@-ms-viewport { + width: device-width; +} + +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} + +body { + margin: 0; + font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #3E3F3A; + text-align: left; + background-color: #fff; +} + +[tabindex="-1"]:focus { + outline: 0 !important; +} + +hr { + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 0; + overflow: visible; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: .5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +dfn { + font-style: italic; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -.25em; +} + +sup { + top: -.5em; +} + +a { + color: #93C54B; + text-decoration: none; + background-color: transparent; + -webkit-text-decoration-skip: objects; +} + +a:hover { + color: #6b9430; + text-decoration: underline; +} + +a:not([href]):not([tabindex]) { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; +} + +a:not([href]):not([tabindex]):focus { + outline: 0; +} + +pre, +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; +} + +figure { + margin: 0 0 1rem; +} + +img { + vertical-align: middle; + border-style: none; +} + +svg:not(:root) { + overflow: hidden; +} + +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #8E8C84; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html [type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +textarea { + overflow: auto; + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: .5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; +} + +progress { + vertical-align: baseline; +} + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: none; +} + +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +summary { + display: list-item; + cursor: pointer; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + margin-bottom: 0.5rem; + font-family: inherit; + font-weight: 400; + line-height: 1.2; + color: inherit; +} + +h1, .h1 { + font-size: 2.1875rem; +} + +h2, .h2 { + font-size: 1.75rem; +} + +h3, .h3 { + font-size: 1.53125rem; +} + +h4, .h4 { + font-size: 1.3125rem; +} + +h5, .h5 { + font-size: 1.09375rem; +} + +h6, .h6 { + font-size: 0.875rem; +} + +.lead { + font-size: 1.09375rem; + font-weight: 300; +} + +.display-1 { + font-size: 6rem; + font-weight: 300; + line-height: 1.2; +} + +.display-2 { + font-size: 5.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-3 { + font-size: 4.5rem; + font-weight: 300; + line-height: 1.2; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; + line-height: 1.2; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} + +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.09375rem; +} + +.blockquote-footer { + display: block; + font-size: 80%; + color: #8E8C84; +} + +.blockquote-footer::before { + content: "\2014 \00A0"; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #DFD7CA; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 90%; + color: #8E8C84; +} + +code { + font-size: 87.5%; + color: #e83e8c; + word-break: break-word; +} + +a > code { + color: inherit; +} + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: 700; +} + +pre { + display: block; + font-size: 87.5%; + color: #212529; +} + +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.row { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.no-gutters { + margin-right: 0; + margin-left: 0; +} + +.no-gutters > .col, +.no-gutters > [class*="col-"] { + padding-right: 0; + padding-left: 0; +} + +.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, +.col-xl-auto { + position: relative; + width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.col-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; +} + +.col-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; +} + +.col-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; +} + +.col-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; +} + +.col-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; +} + +.col-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; +} + +.col-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; +} + +.col-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; +} + +.col-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; +} + +.col-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +.order-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; +} + +.order-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; +} + +.order-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; +} + +.order-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; +} + +.order-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; +} + +.order-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; +} + +.order-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; +} + +.order-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; +} + +.order-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; +} + +.order-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; +} + +.order-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; +} + +.order-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; +} + +.order-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; +} + +.order-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; +} + +.order-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; +} + +.offset-1 { + margin-left: 8.3333333333%; +} + +.offset-2 { + margin-left: 16.6666666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.3333333333%; +} + +.offset-5 { + margin-left: 41.6666666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.3333333333%; +} + +.offset-8 { + margin-left: 66.6666666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.3333333333%; +} + +.offset-11 { + margin-left: 91.6666666667%; +} + +@media (min-width: 576px) { + .col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .col-sm-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-sm-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .col-sm-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .col-sm-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-sm-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .col-sm-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .col-sm-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-sm-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .col-sm-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .col-sm-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-sm-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .col-sm-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .col-sm-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .order-sm-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .order-sm-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .order-sm-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .order-sm-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .order-sm-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .order-sm-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .order-sm-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .order-sm-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .order-sm-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .order-sm-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .order-sm-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .order-sm-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .order-sm-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .order-sm-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.3333333333%; + } + .offset-sm-2 { + margin-left: 16.6666666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.3333333333%; + } + .offset-sm-5 { + margin-left: 41.6666666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.3333333333%; + } + .offset-sm-8 { + margin-left: 66.6666666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.3333333333%; + } + .offset-sm-11 { + margin-left: 91.6666666667%; + } +} + +@media (min-width: 768px) { + .col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .col-md-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-md-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .col-md-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .col-md-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-md-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .col-md-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .col-md-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-md-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .col-md-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .col-md-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-md-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .col-md-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .col-md-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-md-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .order-md-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .order-md-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .order-md-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .order-md-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .order-md-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .order-md-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .order-md-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .order-md-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .order-md-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .order-md-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .order-md-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .order-md-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .order-md-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .order-md-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.3333333333%; + } + .offset-md-2 { + margin-left: 16.6666666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.3333333333%; + } + .offset-md-5 { + margin-left: 41.6666666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.3333333333%; + } + .offset-md-8 { + margin-left: 66.6666666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.3333333333%; + } + .offset-md-11 { + margin-left: 91.6666666667%; + } +} + +@media (min-width: 992px) { + .col-lg { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .col-lg-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-lg-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .col-lg-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .col-lg-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-lg-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .col-lg-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .col-lg-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-lg-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .col-lg-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .col-lg-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-lg-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .col-lg-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .col-lg-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-lg-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .order-lg-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .order-lg-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .order-lg-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .order-lg-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .order-lg-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .order-lg-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .order-lg-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .order-lg-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .order-lg-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .order-lg-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .order-lg-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .order-lg-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .order-lg-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .order-lg-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.3333333333%; + } + .offset-lg-2 { + margin-left: 16.6666666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.3333333333%; + } + .offset-lg-5 { + margin-left: 41.6666666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.3333333333%; + } + .offset-lg-8 { + margin-left: 66.6666666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.3333333333%; + } + .offset-lg-11 { + margin-left: 91.6666666667%; + } +} + +@media (min-width: 1200px) { + .col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .col-xl-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .col-xl-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .col-xl-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .col-xl-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .col-xl-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .col-xl-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .col-xl-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .col-xl-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .col-xl-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .col-xl-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .col-xl-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .col-xl-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .col-xl-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .order-xl-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .order-xl-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .order-xl-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .order-xl-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .order-xl-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .order-xl-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .order-xl-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .order-xl-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .order-xl-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .order-xl-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .order-xl-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .order-xl-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .order-xl-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .order-xl-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .order-xl-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.3333333333%; + } + .offset-xl-2 { + margin-left: 16.6666666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.3333333333%; + } + .offset-xl-5 { + margin-left: 41.6666666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.3333333333%; + } + .offset-xl-8 { + margin-left: 66.6666666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.3333333333%; + } + .offset-xl-11 { + margin-left: 91.6666666667%; + } +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 1rem; + background-color: transparent; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #DFD7CA; +} + +.table thead th { + vertical-align: bottom; + border-bottom: 2px solid #DFD7CA; +} + +.table tbody + tbody { + border-top: 2px solid #DFD7CA; +} + +.table .table { + background-color: #fff; +} + +.table-sm th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #DFD7CA; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #DFD7CA; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #c6d2de; +} + +.table-hover .table-primary:hover { + background-color: #b6c5d5; +} + +.table-hover .table-primary:hover > td, +.table-hover .table-primary:hover > th { + background-color: #b6c5d5; +} + +.table-secondary, +.table-secondary > th, +.table-secondary > td { + background-color: #dfdfdd; +} + +.table-hover .table-secondary:hover { + background-color: #d3d3d0; +} + +.table-hover .table-secondary:hover > td, +.table-hover .table-secondary:hover > th { + background-color: #d3d3d0; +} + +.table-success, +.table-success > th, +.table-success > td { + background-color: #e1efcd; +} + +.table-hover .table-success:hover { + background-color: #d5e9ba; +} + +.table-hover .table-success:hover > td, +.table-hover .table-success:hover > th { + background-color: #d5e9ba; +} + +.table-info, +.table-info > th, +.table-info > td { + background-color: #c3e7f6; +} + +.table-hover .table-info:hover { + background-color: #addef3; +} + +.table-hover .table-info:hover > td, +.table-hover .table-info:hover > th { + background-color: #addef3; +} + +.table-warning, +.table-warning > th, +.table-warning > td { + background-color: #fcdac8; +} + +.table-hover .table-warning:hover { + background-color: #fbcab0; +} + +.table-hover .table-warning:hover > td, +.table-hover .table-warning:hover > th { + background-color: #fbcab0; +} + +.table-danger, +.table-danger > th, +.table-danger > td { + background-color: #f4cfce; +} + +.table-hover .table-danger:hover { + background-color: #efbbb9; +} + +.table-hover .table-danger:hover > td, +.table-hover .table-danger:hover > th { + background-color: #efbbb9; +} + +.table-light, +.table-light > th, +.table-light > td { + background-color: #fdfcfb; +} + +.table-hover .table-light:hover { + background-color: #f5efea; +} + +.table-hover .table-light:hover > td, +.table-hover .table-light:hover > th { + background-color: #f5efea; +} + +.table-dark, +.table-dark > th, +.table-dark > td { + background-color: #c9c9c8; +} + +.table-hover .table-dark:hover { + background-color: #bcbcbb; +} + +.table-hover .table-dark:hover > td, +.table-hover .table-dark:hover > th { + background-color: #bcbcbb; +} + +.table-active, +.table-active > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 0, 0, 0.075); +} + +.table .thead-dark th { + color: #fff; + background-color: #212529; + border-color: #32383e; +} + +.table .thead-light th { + color: #495057; + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.table-dark { + color: #fff; + background-color: #212529; +} + +.table-dark th, +.table-dark td, +.table-dark thead th { + border-color: #32383e; +} + +.table-dark.table-bordered { + border: 0; +} + +.table-dark.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + background-color: rgba(255, 255, 255, 0.075); +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-sm > .table-bordered { + border: 0; + } +} + +@media (max-width: 767.98px) { + .table-responsive-md { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-md > .table-bordered { + border: 0; + } +} + +@media (max-width: 991.98px) { + .table-responsive-lg { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-lg > .table-bordered { + border: 0; + } +} + +@media (max-width: 1199.98px) { + .table-responsive-xl { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + } + .table-responsive-xl > .table-bordered { + border: 0; + } +} + +.table-responsive { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; +} + +.table-responsive > .table-bordered { + border: 0; +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; +} + +@media screen and (prefers-reduced-motion: reduce) { + .form-control { + -webkit-transition: none; + transition: none; + } +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control:focus { + color: #495057; + background-color: #fff; + border-color: #6f9dca; + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.form-control::-webkit-input-placeholder { + color: #8E8C84; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #8E8C84; + opacity: 1; +} + +.form-control::-ms-input-placeholder { + color: #8E8C84; + opacity: 1; +} + +.form-control::placeholder { + color: #8E8C84; + opacity: 1; +} + +.form-control:disabled, .form-control[readonly] { + background-color: #F8F5F0; + opacity: 1; +} + +select.form-control:not([size]):not([multiple]) { + height: calc(2.0625rem + 2px); +} + +select.form-control:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + +.col-form-label { + padding-top: calc(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.09375rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.765625rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding-top: 0.375rem; + padding-bottom: 0.375rem; + margin-bottom: 0; + line-height: 1.5; + color: #3E3F3A; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; +} + +.form-control-plaintext.form-control-sm, .input-group-sm > .form-control-plaintext.form-control, +.input-group-sm > .input-group-prepend > .form-control-plaintext.input-group-text, +.input-group-sm > .input-group-append > .form-control-plaintext.input-group-text, +.input-group-sm > .input-group-prepend > .form-control-plaintext.btn, +.input-group-sm > .input-group-append > .form-control-plaintext.btn, .form-control-plaintext.form-control-lg, .input-group-lg > .form-control-plaintext.form-control, +.input-group-lg > .input-group-prepend > .form-control-plaintext.input-group-text, +.input-group-lg > .input-group-append > .form-control-plaintext.input-group-text, +.input-group-lg > .input-group-prepend > .form-control-plaintext.btn, +.input-group-lg > .input-group-append > .form-control-plaintext.btn { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm, .input-group-sm > .form-control, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.765625rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +select.form-control-sm:not([size]):not([multiple]), .input-group-sm > select.form-control:not([size]):not([multiple]), +.input-group-sm > .input-group-prepend > select.input-group-text:not([size]):not([multiple]), +.input-group-sm > .input-group-append > select.input-group-text:not([size]):not([multiple]), +.input-group-sm > .input-group-prepend > select.btn:not([size]):not([multiple]), +.input-group-sm > .input-group-append > select.btn:not([size]):not([multiple]) { + height: calc(1.6484375rem + 2px); +} + +.form-control-lg, .input-group-lg > .form-control, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.09375rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.form-control:not([size]):not([multiple]), +.input-group-lg > .input-group-prepend > select.input-group-text:not([size]):not([multiple]), +.input-group-lg > .input-group-append > select.input-group-text:not([size]):not([multiple]), +.input-group-lg > .input-group-prepend > select.btn:not([size]):not([multiple]), +.input-group-lg > .input-group-append > select.btn:not([size]):not([multiple]) { + height: calc(2.640625rem + 2px); +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.25rem; +} + +.form-row { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -5px; + margin-left: -5px; +} + +.form-row > .col, +.form-row > [class*="col-"] { + padding-right: 5px; + padding-left: 5px; +} + +.form-check { + position: relative; + display: block; + padding-left: 1.25rem; +} + +.form-check-input { + position: absolute; + margin-top: 0.3rem; + margin-left: -1.25rem; +} + +.form-check-input:disabled ~ .form-check-label { + color: #8E8C84; +} + +.form-check-label { + margin-bottom: 0; +} + +.form-check-inline { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-left: 0; + margin-right: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #93C54B; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: .5rem; + margin-top: .1rem; + font-size: .875rem; + line-height: 1; + color: #fff; + background-color: rgba(147, 197, 75, 0.8); + border-radius: .2rem; +} + +.was-validated .form-control:valid, .form-control.is-valid, .was-validated +.custom-select:valid, +.custom-select.is-valid { + border-color: #93C54B; +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus, .was-validated +.custom-select:valid:focus, +.custom-select.is-valid:focus { + border-color: #93C54B; + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.25); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.25); +} + +.was-validated .form-control:valid ~ .valid-feedback, +.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback, +.form-control.is-valid ~ .valid-tooltip, .was-validated +.custom-select:valid ~ .valid-feedback, +.was-validated +.custom-select:valid ~ .valid-tooltip, +.custom-select.is-valid ~ .valid-feedback, +.custom-select.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control-file:valid ~ .valid-feedback, +.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback, +.form-control-file.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #93C54B; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #93C54B; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + background-color: #cde4ab; +} + +.was-validated .custom-control-input:valid ~ .valid-feedback, +.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback, +.custom-control-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + background-color: #aad172; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + -webkit-box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(147, 197, 75, 0.25); + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(147, 197, 75, 0.25); +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #93C54B; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label::before, .custom-file-input.is-valid ~ .custom-file-label::before { + border-color: inherit; +} + +.was-validated .custom-file-input:valid ~ .valid-feedback, +.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback, +.custom-file-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.25); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #d9534f; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: .5rem; + margin-top: .1rem; + font-size: .875rem; + line-height: 1; + color: #fff; + background-color: rgba(217, 83, 79, 0.8); + border-radius: .2rem; +} + +.was-validated .form-control:invalid, .form-control.is-invalid, .was-validated +.custom-select:invalid, +.custom-select.is-invalid { + border-color: #d9534f; +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus, .was-validated +.custom-select:invalid:focus, +.custom-select.is-invalid:focus { + border-color: #d9534f; + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25); +} + +.was-validated .form-control:invalid ~ .invalid-feedback, +.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback, +.form-control.is-invalid ~ .invalid-tooltip, .was-validated +.custom-select:invalid ~ .invalid-feedback, +.was-validated +.custom-select:invalid ~ .invalid-tooltip, +.custom-select.is-invalid ~ .invalid-feedback, +.custom-select.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control-file:invalid ~ .invalid-feedback, +.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback, +.form-control-file.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #d9534f; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #d9534f; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + background-color: #f0b9b8; +} + +.was-validated .custom-control-input:invalid ~ .invalid-feedback, +.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback, +.custom-control-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + background-color: #e27c79; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + -webkit-box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(217, 83, 79, 0.25); + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(217, 83, 79, 0.25); +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #d9534f; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label::before, .custom-file-input.is-invalid ~ .custom-file-label::before { + border-color: inherit; +} + +.was-validated .custom-file-input:invalid ~ .invalid-feedback, +.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback, +.custom-file-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.25); +} + +.form-inline { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.form-inline .form-check { + width: 100%; +} + +@media (min-width: 576px) { + .form-inline label { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-plaintext { + display: inline-block; + } + .form-inline .input-group, + .form-inline .custom-select { + width: auto; + } + .form-inline .form-check { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + padding-left: 0; + } + .form-inline .form-check-input { + position: relative; + margin-top: 0; + margin-right: 0.25rem; + margin-left: 0; + } + .form-inline .custom-control { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + } + .form-inline .custom-control-label { + margin-bottom: 0; + } +} + +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.25rem; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; +} + +@media screen and (prefers-reduced-motion: reduce) { + .btn { + -webkit-transition: none; + transition: none; + } +} + +.btn:hover, .btn:focus { + text-decoration: none; +} + +.btn:focus, .btn.focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.65; +} + +.btn:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active { + background-image: none; +} + +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + +.btn-primary { + color: #fff; + background-color: #325D88; + border-color: #325D88; +} + +.btn-primary:hover { + color: #fff; + background-color: #284a6c; + border-color: #244463; +} + +.btn-primary:focus, .btn-primary.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); +} + +.btn-primary.disabled, .btn-primary:disabled { + color: #fff; + background-color: #325D88; + border-color: #325D88; +} + +.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, +.show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #244463; + border-color: #213d59; +} + +.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-primary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); +} + +.btn-secondary { + color: #fff; + background-color: #8E8C84; + border-color: #8E8C84; +} + +.btn-secondary:hover { + color: #fff; + background-color: #7b7971; + border-color: #74726b; +} + +.btn-secondary:focus, .btn-secondary.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); + box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); +} + +.btn-secondary.disabled, .btn-secondary:disabled { + color: #fff; + background-color: #8E8C84; + border-color: #8E8C84; +} + +.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, +.show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #74726b; + border-color: #6e6c65; +} + +.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-secondary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); + box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); +} + +.btn-success { + color: #fff; + background-color: #93C54B; + border-color: #93C54B; +} + +.btn-success:hover { + color: #fff; + background-color: #80b139; + border-color: #79a736; +} + +.btn-success:focus, .btn-success.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); +} + +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #93C54B; + border-color: #93C54B; +} + +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #79a736; + border-color: #729e33; +} + +.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-success.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); +} + +.btn-info { + color: #fff; + background-color: #29ABE0; + border-color: #29ABE0; +} + +.btn-info:hover { + color: #fff; + background-color: #1d95c6; + border-color: #1b8dbb; +} + +.btn-info:focus, .btn-info.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); + box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); +} + +.btn-info.disabled, .btn-info:disabled { + color: #fff; + background-color: #29ABE0; + border-color: #29ABE0; +} + +.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, +.show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #1b8dbb; + border-color: #1984b0; +} + +.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-info.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); + box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); +} + +.btn-warning { + color: #fff; + background-color: #F47C3C; + border-color: #F47C3C; +} + +.btn-warning:hover { + color: #fff; + background-color: #f26418; + border-color: #ef5c0e; +} + +.btn-warning:focus, .btn-warning.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); + box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); +} + +.btn-warning.disabled, .btn-warning:disabled { + color: #fff; + background-color: #F47C3C; + border-color: #F47C3C; +} + +.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, +.show > .btn-warning.dropdown-toggle { + color: #fff; + background-color: #ef5c0e; + border-color: #e3570d; +} + +.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-warning.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); + box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; +} + +.btn-danger:hover { + color: #fff; + background-color: #d23430; + border-color: #c9302c; +} + +.btn-danger:focus, .btn-danger.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); +} + +.btn-danger.disabled, .btn-danger:disabled { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; +} + +.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, +.show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #c9302c; + border-color: #bf2e29; +} + +.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-danger.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); +} + +.btn-light { + color: #212529; + background-color: #F8F5F0; + border-color: #F8F5F0; +} + +.btn-light:hover { + color: #212529; + background-color: #ece4d6; + border-color: #e8decd; +} + +.btn-light:focus, .btn-light.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); + box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); +} + +.btn-light.disabled, .btn-light:disabled { + color: #212529; + background-color: #F8F5F0; + border-color: #F8F5F0; +} + +.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, +.show > .btn-light.dropdown-toggle { + color: #212529; + background-color: #e8decd; + border-color: #e4d8c5; +} + +.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-light.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); + box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); +} + +.btn-dark { + color: #fff; + background-color: #3E3F3A; + border-color: #3E3F3A; +} + +.btn-dark:hover { + color: #fff; + background-color: #2a2b28; + border-color: #242422; +} + +.btn-dark:focus, .btn-dark.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); + box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); +} + +.btn-dark.disabled, .btn-dark:disabled { + color: #fff; + background-color: #3E3F3A; + border-color: #3E3F3A; +} + +.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, +.show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #242422; + border-color: #1d1e1b; +} + +.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-dark.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); + box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); +} + +.btn-outline-primary { + color: #325D88; + background-color: transparent; + background-image: none; + border-color: #325D88; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #325D88; + border-color: #325D88; +} + +.btn-outline-primary:focus, .btn-outline-primary.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); +} + +.btn-outline-primary.disabled, .btn-outline-primary:disabled { + color: #325D88; + background-color: transparent; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, +.show > .btn-outline-primary.dropdown-toggle { + color: #fff; + background-color: #325D88; + border-color: #325D88; +} + +.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-primary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.5); +} + +.btn-outline-secondary { + color: #8E8C84; + background-color: transparent; + background-image: none; + border-color: #8E8C84; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #8E8C84; + border-color: #8E8C84; +} + +.btn-outline-secondary:focus, .btn-outline-secondary.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); + box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); +} + +.btn-outline-secondary.disabled, .btn-outline-secondary:disabled { + color: #8E8C84; + background-color: transparent; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, +.show > .btn-outline-secondary.dropdown-toggle { + color: #fff; + background-color: #8E8C84; + border-color: #8E8C84; +} + +.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-secondary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); + box-shadow: 0 0 0 0.2rem rgba(142, 140, 132, 0.5); +} + +.btn-outline-success { + color: #93C54B; + background-color: transparent; + background-image: none; + border-color: #93C54B; +} + +.btn-outline-success:hover { + color: #fff; + background-color: #93C54B; + border-color: #93C54B; +} + +.btn-outline-success:focus, .btn-outline-success.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); +} + +.btn-outline-success.disabled, .btn-outline-success:disabled { + color: #93C54B; + background-color: transparent; +} + +.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, +.show > .btn-outline-success.dropdown-toggle { + color: #fff; + background-color: #93C54B; + border-color: #93C54B; +} + +.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-success.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); + box-shadow: 0 0 0 0.2rem rgba(147, 197, 75, 0.5); +} + +.btn-outline-info { + color: #29ABE0; + background-color: transparent; + background-image: none; + border-color: #29ABE0; +} + +.btn-outline-info:hover { + color: #fff; + background-color: #29ABE0; + border-color: #29ABE0; +} + +.btn-outline-info:focus, .btn-outline-info.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); + box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); +} + +.btn-outline-info.disabled, .btn-outline-info:disabled { + color: #29ABE0; + background-color: transparent; +} + +.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, +.show > .btn-outline-info.dropdown-toggle { + color: #fff; + background-color: #29ABE0; + border-color: #29ABE0; +} + +.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-info.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); + box-shadow: 0 0 0 0.2rem rgba(41, 171, 224, 0.5); +} + +.btn-outline-warning { + color: #F47C3C; + background-color: transparent; + background-image: none; + border-color: #F47C3C; +} + +.btn-outline-warning:hover { + color: #fff; + background-color: #F47C3C; + border-color: #F47C3C; +} + +.btn-outline-warning:focus, .btn-outline-warning.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); + box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); +} + +.btn-outline-warning.disabled, .btn-outline-warning:disabled { + color: #F47C3C; + background-color: transparent; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, +.show > .btn-outline-warning.dropdown-toggle { + color: #fff; + background-color: #F47C3C; + border-color: #F47C3C; +} + +.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-warning.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); + box-shadow: 0 0 0 0.2rem rgba(244, 124, 60, 0.5); +} + +.btn-outline-danger { + color: #d9534f; + background-color: transparent; + background-image: none; + border-color: #d9534f; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; +} + +.btn-outline-danger:focus, .btn-outline-danger.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); +} + +.btn-outline-danger.disabled, .btn-outline-danger:disabled { + color: #d9534f; + background-color: transparent; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, +.show > .btn-outline-danger.dropdown-toggle { + color: #fff; + background-color: #d9534f; + border-color: #d9534f; +} + +.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-danger.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); + box-shadow: 0 0 0 0.2rem rgba(217, 83, 79, 0.5); +} + +.btn-outline-light { + color: #F8F5F0; + background-color: transparent; + background-image: none; + border-color: #F8F5F0; +} + +.btn-outline-light:hover { + color: #212529; + background-color: #F8F5F0; + border-color: #F8F5F0; +} + +.btn-outline-light:focus, .btn-outline-light.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); + box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); +} + +.btn-outline-light.disabled, .btn-outline-light:disabled { + color: #F8F5F0; + background-color: transparent; +} + +.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, +.show > .btn-outline-light.dropdown-toggle { + color: #212529; + background-color: #F8F5F0; + border-color: #F8F5F0; +} + +.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-light.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); + box-shadow: 0 0 0 0.2rem rgba(248, 245, 240, 0.5); +} + +.btn-outline-dark { + color: #3E3F3A; + background-color: transparent; + background-image: none; + border-color: #3E3F3A; +} + +.btn-outline-dark:hover { + color: #fff; + background-color: #3E3F3A; + border-color: #3E3F3A; +} + +.btn-outline-dark:focus, .btn-outline-dark.focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); + box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); +} + +.btn-outline-dark.disabled, .btn-outline-dark:disabled { + color: #3E3F3A; + background-color: transparent; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, +.show > .btn-outline-dark.dropdown-toggle { + color: #fff; + background-color: #3E3F3A; + border-color: #3E3F3A; +} + +.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, +.show > .btn-outline-dark.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); + box-shadow: 0 0 0 0.2rem rgba(62, 63, 58, 0.5); +} + +.btn-link { + font-weight: 400; + color: #93C54B; + background-color: transparent; +} + +.btn-link:hover { + color: #6b9430; + text-decoration: underline; + background-color: transparent; + border-color: transparent; +} + +.btn-link:focus, .btn-link.focus { + text-decoration: underline; + border-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link:disabled, .btn-link.disabled { + color: #8E8C84; + pointer-events: none; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.09375rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.765625rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +@media screen and (prefers-reduced-motion: reduce) { + .fade { + -webkit-transition: none; + transition: none; + } +} + +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +@media screen and (prefers-reduced-motion: reduce) { + .collapsing { + -webkit-transition: none; + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 0.875rem; + color: #3E3F3A; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +.dropup .dropdown-menu { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} + +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-menu { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} + +.dropright .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropright .dropdown-toggle::after { + vertical-align: 0; +} + +.dropleft .dropdown-menu { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + width: 0; + height: 0; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + width: 0; + height: 0; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #F8F5F0; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #8E8C84; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + color: #8E8C84; + text-decoration: none; + background-color: #F8F5F0; +} + +.dropdown-item.active, .dropdown-item:active { + color: #8E8C84; + text-decoration: none; + background-color: #F8F5F0; +} + +.dropdown-item.disabled, .dropdown-item:disabled { + color: #8E8C84; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.765625rem; + color: #8E8C84; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #8E8C84; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group, +.btn-group-vertical .btn + .btn, +.btn-group-vertical .btn + .btn-group, +.btn-group-vertical .btn-group + .btn, +.btn-group-vertical .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.btn-toolbar .input-group { + width: auto; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} + +.btn-group-vertical .btn, +.btn-group-vertical .btn-group { + width: 100%; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-toggle > .btn, +.btn-group-toggle > .btn-group > .btn { + margin-bottom: 0; +} + +.btn-group-toggle > .btn input[type="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + width: 100%; +} + +.input-group > .form-control, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 1%; + margin-bottom: 0; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file:focus { + z-index: 3; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group > .custom-file { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.input-group > .custom-file:not(:last-child) .custom-file-label, +.input-group > .custom-file:not(:last-child) .custom-file-label::after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .custom-file:not(:first-child) .custom-file-label { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group-prepend, +.input-group-append { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + margin-left: -1px; +} + +.input-group-prepend { + margin-right: -1px; +} + +.input-group-append { + margin-left: -1px; +} + +.input-group-text { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 0.375rem 0.75rem; + margin-bottom: 0; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #F8F5F0; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .input-group-append > .btn, +.input-group > .input-group-append > .input-group-text, +.input-group > .input-group-prepend:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .input-group-text, +.input-group > .input-group-prepend:first-child > .btn:not(:first-child), +.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.custom-control { + position: relative; + display: block; + min-height: 1.5rem; + padding-left: 1.5rem; +} + +.custom-control-inline { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + margin-right: 1rem; +} + +.custom-control-input { + position: absolute; + z-index: -1; + opacity: 0; +} + +.custom-control-input:checked ~ .custom-control-label::before { + color: #fff; + background-color: #325D88; +} + +.custom-control-input:focus ~ .custom-control-label::before { + -webkit-box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.custom-control-input:active ~ .custom-control-label::before { + color: #fff; + background-color: #95b6d8; +} + +.custom-control-input:disabled ~ .custom-control-label { + color: #8E8C84; +} + +.custom-control-input:disabled ~ .custom-control-label::before { + background-color: #F8F5F0; +} + +.custom-control-label { + position: relative; + margin-bottom: 0; +} + +.custom-control-label::before { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + pointer-events: none; + content: ""; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: #DFD7CA; +} + +.custom-control-label::after { + position: absolute; + top: 0.25rem; + left: -1.5rem; + display: block; + width: 1rem; + height: 1rem; + content: ""; + background-repeat: no-repeat; + background-position: center center; + background-size: 50% 50%; +} + +.custom-checkbox .custom-control-label::before { + border-radius: 0.25rem; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before { + background-color: #325D88; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { + background-color: #325D88; +} + +.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); +} + +.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(50, 93, 136, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(50, 93, 136, 0.5); +} + +.custom-radio .custom-control-label::before { + border-radius: 50%; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::before { + background-color: #325D88; +} + +.custom-radio .custom-control-input:checked ~ .custom-control-label::after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); +} + +.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(50, 93, 136, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(2.0625rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + vertical-align: middle; + background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%233E3F3A' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center; + background-size: 8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #6f9dca; + outline: 0; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(111, 157, 202, 0.5); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(111, 157, 202, 0.5); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.75rem; + background-image: none; +} + +.custom-select:disabled { + color: #8E8C84; + background-color: #F8F5F0; +} + +.custom-select::-ms-expand { + opacity: 0; +} + +.custom-select-sm { + height: calc(1.6484375rem + 2px); + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 75%; +} + +.custom-select-lg { + height: calc(2.640625rem + 2px); + padding-top: 0.375rem; + padding-bottom: 0.375rem; + font-size: 125%; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(2.0625rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(2.0625rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #6f9dca; + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.custom-file-input:focus ~ .custom-file-label::after { + border-color: #6f9dca; +} + +.custom-file-input:lang(en) ~ .custom-file-label::after { + content: "Browse"; +} + +.custom-file-label { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + height: calc(2.0625rem + 2px); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: 2.0625rem; + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #F8F5F0; + border-left: 1px solid #ced4da; + border-radius: 0 0.25rem 0.25rem 0; +} + +.custom-range { + width: 100%; + padding-left: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-range:focus { + outline: none; +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #325D88; + border: 0; + border-radius: 1rem; + -webkit-appearance: none; + appearance: none; +} + +.custom-range::-webkit-slider-thumb:focus { + outline: none; + -webkit-box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.custom-range::-webkit-slider-thumb:active { + background-color: #95b6d8; +} + +.custom-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #DFD7CA; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #325D88; + border: 0; + border-radius: 1rem; + -moz-appearance: none; + appearance: none; +} + +.custom-range::-moz-range-thumb:focus { + outline: none; + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.custom-range::-moz-range-thumb:active { + background-color: #95b6d8; +} + +.custom-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #DFD7CA; + border-color: transparent; + border-radius: 1rem; +} + +.custom-range::-ms-thumb { + width: 1rem; + height: 1rem; + background-color: #325D88; + border: 0; + border-radius: 1rem; + appearance: none; +} + +.custom-range::-ms-thumb:focus { + outline: none; + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.custom-range::-ms-thumb:active { + background-color: #95b6d8; +} + +.custom-range::-ms-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.5rem; +} + +.custom-range::-ms-fill-lower { + background-color: #DFD7CA; + border-radius: 1rem; +} + +.custom-range::-ms-fill-upper { + margin-right: 15px; + background-color: #DFD7CA; + border-radius: 1rem; +} + +.nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 0.9rem; +} + +.nav-link:hover, .nav-link:focus { + text-decoration: none; +} + +.nav-link.disabled { + color: #DFD7CA; +} + +.nav-tabs { + border-bottom: 1px solid #DFD7CA; +} + +.nav-tabs .nav-item { + margin-bottom: -1px; +} + +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #DFD7CA; +} + +.nav-tabs .nav-link.disabled { + color: #DFD7CA; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #DFD7CA #DFD7CA #fff; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills .nav-link { + border-radius: 0.25rem; +} + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #8E8C84; + background-color: #F8F5F0; +} + +.nav-fill .nav-item { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: center; +} + +.nav-justified .nav-item { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + text-align: center; +} + +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.navbar { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0.5rem 1rem; +} + +.navbar > .container, +.navbar > .container-fluid { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.navbar-brand { + display: inline-block; + padding-top: 0.3359375rem; + padding-bottom: 0.3359375rem; + margin-right: 1rem; + font-size: 1.09375rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + text-decoration: none; +} + +.navbar-nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} + +.navbar-nav .dropdown-menu { + position: static; + float: none; +} + +.navbar-text { + display: inline-block; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-collapse { + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.09375rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + text-decoration: none; +} + +.navbar-toggler:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + content: ""; + background: no-repeat center center; + background-size: 100% 100%; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} + +@media (max-width: 767.98px) { + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 768px) { + .navbar-expand-md { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-md .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } +} + +@media (max-width: 991.98px) { + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 992px) { + .navbar-expand-lg { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-lg .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } +} + +@media (max-width: 1199.98px) { + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 1200px) { + .navbar-expand-xl { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + } + .navbar-expand-xl .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } +} + +.navbar-expand { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid { + padding-right: 0; + padding-left: 0; +} + +.navbar-expand .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; +} + +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} + +.navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; +} + +.navbar-expand .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; +} + +.navbar-expand .navbar-toggler { + display: none; +} + +.navbar-light .navbar-brand { + color: #000; +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: #000; +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: #000; +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: #000; +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.1); +} + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-text a { + color: #000; +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: #000; +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.1); +} + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); +} + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + color: #fff; +} + +.card { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(223, 215, 202, 0.75); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.375rem; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link:hover { + text-decoration: none; +} + +.card-link + .card-link { + margin-left: 1.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(248, 245, 240, 0.25); + border-bottom: 1px solid rgba(223, 215, 202, 0.75); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(248, 245, 240, 0.25); + border-top: 1px solid rgba(223, 215, 202, 0.75); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img { + width: 100%; + border-radius: calc(0.25rem - 1px); +} + +.card-img-top { + width: 100%; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img-bottom { + width: 100%; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); +} + +.card-deck { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +.card-deck .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-deck { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + margin-right: -15px; + margin-left: -15px; + } + .card-deck .card { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + margin-right: 15px; + margin-bottom: 0; + margin-left: 15px; + } +} + +.card-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +.card-group > .card { + margin-bottom: 15px; +} + +@media (min-width: 576px) { + .card-group { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + } + .card-group > .card { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:first-child .card-img-top, + .card-group > .card:first-child .card-header { + border-top-right-radius: 0; + } + .card-group > .card:first-child .card-img-bottom, + .card-group > .card:first-child .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:last-child .card-img-top, + .card-group > .card:last-child .card-header { + border-top-left-radius: 0; + } + .card-group > .card:last-child .card-img-bottom, + .card-group > .card:last-child .card-footer { + border-bottom-left-radius: 0; + } + .card-group > .card:only-child { + border-radius: 0.25rem; + } + .card-group > .card:only-child .card-img-top, + .card-group > .card:only-child .card-header { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + } + .card-group > .card:only-child .card-img-bottom, + .card-group > .card:only-child .card-footer { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; + } + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) { + border-radius: 0; + } + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header, + .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer { + border-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.75rem; +} + +@media (min-width: 576px) { + .card-columns { + -webkit-column-count: 3; + column-count: 3; + -webkit-column-gap: 1.25rem; + column-gap: 1.25rem; + orphans: 1; + widows: 1; + } + .card-columns .card { + display: inline-block; + width: 100%; + } +} + +.accordion .card:not(:first-of-type):not(:last-of-type) { + border-bottom: 0; + border-radius: 0; +} + +.accordion .card:not(:first-of-type) .card-header:first-child { + border-radius: 0; +} + +.accordion .card:first-of-type { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.accordion .card:last-of-type { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.breadcrumb { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #F8F5F0; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.5rem; + color: #8E8C84; + content: "/"; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: underline; +} + +.breadcrumb-item + .breadcrumb-item:hover::before { + text-decoration: none; +} + +.breadcrumb-item.active { + color: #8E8C84; +} + +.pagination { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding-left: 0; + list-style: none; + border-radius: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.75rem; + margin-left: -1px; + line-height: 1.25; + color: #8E8C84; + background-color: #F8F5F0; + border: 1px solid #DFD7CA; +} + +.page-link:hover { + z-index: 2; + color: #8E8C84; + text-decoration: none; + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.page-link:focus { + z-index: 2; + outline: 0; + -webkit-box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); + box-shadow: 0 0 0 0.2rem rgba(50, 93, 136, 0.25); +} + +.page-link:not(:disabled):not(.disabled) { + cursor: pointer; +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.page-item.active .page-link { + z-index: 1; + color: #8E8C84; + background-color: #DFD7CA; + border-color: #DFD7CA; +} + +.page-item.disabled .page-link { + color: #DFD7CA; + pointer-events: none; + cursor: auto; + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.09375rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.765625rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #325D88; +} + +.badge-primary[href]:hover, .badge-primary[href]:focus { + color: #fff; + text-decoration: none; + background-color: #244463; +} + +.badge-secondary { + color: #fff; + background-color: #8E8C84; +} + +.badge-secondary[href]:hover, .badge-secondary[href]:focus { + color: #fff; + text-decoration: none; + background-color: #74726b; +} + +.badge-success { + color: #fff; + background-color: #93C54B; +} + +.badge-success[href]:hover, .badge-success[href]:focus { + color: #fff; + text-decoration: none; + background-color: #79a736; +} + +.badge-info { + color: #fff; + background-color: #29ABE0; +} + +.badge-info[href]:hover, .badge-info[href]:focus { + color: #fff; + text-decoration: none; + background-color: #1b8dbb; +} + +.badge-warning { + color: #fff; + background-color: #F47C3C; +} + +.badge-warning[href]:hover, .badge-warning[href]:focus { + color: #fff; + text-decoration: none; + background-color: #ef5c0e; +} + +.badge-danger { + color: #fff; + background-color: #d9534f; +} + +.badge-danger[href]:hover, .badge-danger[href]:focus { + color: #fff; + text-decoration: none; + background-color: #c9302c; +} + +.badge-light { + color: #212529; + background-color: #F8F5F0; +} + +.badge-light[href]:hover, .badge-light[href]:focus { + color: #212529; + text-decoration: none; + background-color: #e8decd; +} + +.badge-dark { + color: #fff; + background-color: #3E3F3A; +} + +.badge-dark[href]:hover, .badge-dark[href]:focus { + color: #fff; + text-decoration: none; + background-color: #242422; +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #F8F5F0; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; +} + +.alert-dismissible { + padding-right: 3.8125rem; +} + +.alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; +} + +.alert-primary { + color: #1a3047; + background-color: #d6dfe7; + border-color: #c6d2de; +} + +.alert-primary hr { + border-top-color: #b6c5d5; +} + +.alert-primary .alert-link { + color: #0c1722; +} + +.alert-secondary { + color: #4a4945; + background-color: #e8e8e6; + border-color: #dfdfdd; +} + +.alert-secondary hr { + border-top-color: #d3d3d0; +} + +.alert-secondary .alert-link { + color: #302f2c; +} + +.alert-success { + color: #4c6627; + background-color: #e9f3db; + border-color: #e1efcd; +} + +.alert-success hr { + border-top-color: #d5e9ba; +} + +.alert-success .alert-link { + color: #314119; +} + +.alert-info { + color: #155974; + background-color: #d4eef9; + border-color: #c3e7f6; +} + +.alert-info hr { + border-top-color: #addef3; +} + +.alert-info .alert-link { + color: #0d3849; +} + +.alert-warning { + color: #7f401f; + background-color: #fde5d8; + border-color: #fcdac8; +} + +.alert-warning hr { + border-top-color: #fbcab0; +} + +.alert-warning .alert-link { + color: #562b15; +} + +.alert-danger { + color: #712b29; + background-color: #f7dddc; + border-color: #f4cfce; +} + +.alert-danger hr { + border-top-color: #efbbb9; +} + +.alert-danger .alert-link { + color: #4c1d1b; +} + +.alert-light { + color: #817f7d; + background-color: #fefdfc; + border-color: #fdfcfb; +} + +.alert-light hr { + border-top-color: #f5efea; +} + +.alert-light .alert-link { + color: #676664; +} + +.alert-dark { + color: #20211e; + background-color: #d8d9d8; + border-color: #c9c9c8; +} + +.alert-dark hr { + border-top-color: #bcbcbb; +} + +.alert-dark .alert-link { + color: #060606; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 1rem 0; + } + to { + background-position: 0 0; + } +} + +.progress { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.65625rem; + background-color: #DFD7CA; + border-radius: 10px; +} + +.progress-bar { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + color: #325D88; + text-align: center; + white-space: nowrap; + background-color: #325D88; + -webkit-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +@media screen and (prefers-reduced-motion: reduce) { + .progress-bar { + -webkit-transition: none; + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; +} + +.progress-bar-animated { + -webkit-animation: progress-bar-stripes 1s linear infinite; + animation: progress-bar-stripes 1s linear infinite; +} + +.media { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} + +.media-body { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.list-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; +} + +.list-group-item-action { + width: 100%; + color: #3E3F3A; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + color: #3E3F3A; + text-decoration: none; + background-color: #F8F5F0; +} + +.list-group-item-action:active { + color: #3E3F3A; + background-color: #DFD7CA; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #DFD7CA; +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.list-group-item:hover, .list-group-item:focus { + z-index: 1; + text-decoration: none; +} + +.list-group-item.disabled, .list-group-item:disabled { + color: #98978B; + background-color: #fff; +} + +.list-group-item.active { + z-index: 2; + color: #3E3F3A; + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.list-group-flush .list-group-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} + +.list-group-flush:first-child .list-group-item:first-child { + border-top: 0; +} + +.list-group-flush:last-child .list-group-item:last-child { + border-bottom: 0; +} + +.list-group-item-primary { + color: #1a3047; + background-color: #c6d2de; +} + +.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #1a3047; + background-color: #b6c5d5; +} + +.list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #1a3047; + border-color: #1a3047; +} + +.list-group-item-secondary { + color: #4a4945; + background-color: #dfdfdd; +} + +.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #4a4945; + background-color: #d3d3d0; +} + +.list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #4a4945; + border-color: #4a4945; +} + +.list-group-item-success { + color: #4c6627; + background-color: #e1efcd; +} + +.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #4c6627; + background-color: #d5e9ba; +} + +.list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #4c6627; + border-color: #4c6627; +} + +.list-group-item-info { + color: #155974; + background-color: #c3e7f6; +} + +.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #155974; + background-color: #addef3; +} + +.list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #155974; + border-color: #155974; +} + +.list-group-item-warning { + color: #7f401f; + background-color: #fcdac8; +} + +.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #7f401f; + background-color: #fbcab0; +} + +.list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #7f401f; + border-color: #7f401f; +} + +.list-group-item-danger { + color: #712b29; + background-color: #f4cfce; +} + +.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #712b29; + background-color: #efbbb9; +} + +.list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #712b29; + border-color: #712b29; +} + +.list-group-item-light { + color: #817f7d; + background-color: #fdfcfb; +} + +.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #817f7d; + background-color: #f5efea; +} + +.list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #817f7d; + border-color: #817f7d; +} + +.list-group-item-dark { + color: #20211e; + background-color: #c9c9c8; +} + +.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #20211e; + background-color: #bcbcbb; +} + +.list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #20211e; + border-color: #20211e; +} + +.close { + float: right; + font-size: 1.3125rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: none; + opacity: .5; +} + +.close:hover, .close:focus { + color: #000; + text-decoration: none; + opacity: .75; +} + +.close:not(:disabled):not(.disabled) { + cursor: pointer; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + outline: 0; +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform 0.3s ease-out; + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; + -webkit-transform: translate(0, -25%); + transform: translate(0, -25%); +} + +@media screen and (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + -webkit-transition: none; + transition: none; + } +} + +.modal.show .modal-dialog { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-dialog-centered { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-height: calc(100% - (0.5rem * 2)); +} + +.modal-content { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #DFD7CA; + border-radius: 0.3rem; + outline: 0; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop.show { + opacity: 0.5; +} + +.modal-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid #DFD7CA; + border-top-left-radius: 0.3rem; + border-top-right-radius: 0.3rem; +} + +.modal-header .close { + padding: 1rem; + margin: -1rem -1rem -1rem auto; +} + +.modal-title { + margin-bottom: 0; + line-height: 1.5; +} + +.modal-body { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem; +} + +.modal-footer { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 1rem; + border-top: 1px solid #DFD7CA; +} + +.modal-footer > :not(:first-child) { + margin-left: .25rem; +} + +.modal-footer > :not(:last-child) { + margin-right: .25rem; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + .modal-dialog-centered { + min-height: calc(100% - (1.75rem * 2)); + } + .modal-sm { + max-width: 300px; + } +} + +@media (min-width: 992px) { + .modal-lg { + max-width: 800px; + } +} + +.tooltip { + position: absolute; + z-index: 1070; + display: block; + margin: 0; + font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.765625rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: block; + max-width: 276px; + font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.765625rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top .arrow, .bs-popover-auto[x-placement^="top"] .arrow { + bottom: calc((0.5rem + 1px) * -1); +} + +.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before, +.bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { + border-width: 0.5rem 0.5rem 0; +} + +.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before { + bottom: 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { + bottom: 1px; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right .arrow, .bs-popover-auto[x-placement^="right"] .arrow { + left: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before, +.bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { + border-width: 0.5rem 0.5rem 0.5rem 0; +} + +.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before { + left: 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { + left: 1px; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^="bottom"] .arrow { + top: calc((0.5rem + 1px) * -1); +} + +.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before, +.bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { + border-width: 0 0.5rem 0.5rem 0.5rem; +} + +.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before { + top: 0; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { + top: 1px; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #F8F5F0; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left .arrow, .bs-popover-auto[x-placement^="left"] .arrow { + right: calc((0.5rem + 1px) * -1); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before, +.bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { + border-width: 0.5rem 0 0.5rem 0.5rem; +} + +.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before { + right: 0; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { + right: 1px; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 0.875rem; + color: inherit; + background-color: #F8F5F0; + border-bottom: 1px solid #f0e9df; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.75rem; + color: #3E3F3A; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-item { + position: relative; + display: none; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: transform 0.6s ease, -webkit-transform 0.6s ease; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; +} + +@media screen and (prefers-reduced-motion: reduce) { + .carousel-item { + -webkit-transition: none; + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next, +.carousel-item-prev { + position: absolute; + top: 0; +} + +.carousel-item-next.carousel-item-left, +.carousel-item-prev.carousel-item-right { + -webkit-transform: translateX(0); + transform: translateX(0); +} + +@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { + .carousel-item-next.carousel-item-left, + .carousel-item-prev.carousel-item-right { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.carousel-item-next, +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { + .carousel-item-next, + .active.carousel-item-right { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.carousel-item-prev, +.active.carousel-item-left { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); +} + +@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { + .carousel-item-prev, + .active.carousel-item-left { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.carousel-fade .carousel-item { + opacity: 0; + -webkit-transition-duration: .6s; + transition-duration: .6s; + -webkit-transition-property: opacity; + transition-property: opacity; +} + +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + opacity: 1; +} + +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-right { + opacity: 0; +} + +.carousel-fade .carousel-item-next, +.carousel-fade .carousel-item-prev, +.carousel-fade .carousel-item.active, +.carousel-fade .active.carousel-item-left, +.carousel-fade .active.carousel-item-prev { + -webkit-transform: translateX(0); + transform: translateX(0); +} + +@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { + .carousel-fade .carousel-item-next, + .carousel-fade .carousel-item-prev, + .carousel-fade .carousel-item.active, + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-prev { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 15%; + color: #fff; + text-align: center; + opacity: 0.5; +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: .9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 20px; + height: 20px; + background: transparent no-repeat center center; + background-size: 100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 10px; + left: 0; + z-index: 15; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + padding-left: 0; + margin-right: 15%; + margin-left: 15%; + list-style: none; +} + +.carousel-indicators li { + position: relative; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 30px; + height: 3px; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: rgba(255, 255, 255, 0.5); +} + +.carousel-indicators li::before { + position: absolute; + top: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; +} + +.carousel-indicators li::after { + position: absolute; + bottom: -10px; + left: 0; + display: inline-block; + width: 100%; + height: 10px; + content: ""; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.bg-primary { + background-color: #325D88 !important; +} + +a.bg-primary:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #244463 !important; +} + +.bg-secondary { + background-color: #8E8C84 !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #74726b !important; +} + +.bg-success { + background-color: #93C54B !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #79a736 !important; +} + +.bg-info { + background-color: #29ABE0 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #1b8dbb !important; +} + +.bg-warning { + background-color: #F47C3C !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #ef5c0e !important; +} + +.bg-danger { + background-color: #d9534f !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #c9302c !important; +} + +.bg-light { + background-color: #F8F5F0 !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #e8decd !important; +} + +.bg-dark { + background-color: #3E3F3A !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + background-color: #242422 !important; +} + +.bg-white { + background-color: #fff !important; +} + +.bg-transparent { + background-color: transparent !important; +} + +.border { + border: 1px solid #DFD7CA !important; +} + +.border-top { + border-top: 1px solid #DFD7CA !important; +} + +.border-right { + border-right: 1px solid #DFD7CA !important; +} + +.border-bottom { + border-bottom: 1px solid #DFD7CA !important; +} + +.border-left { + border-left: 1px solid #DFD7CA !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-right-0 { + border-right: 0 !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-left-0 { + border-left: 0 !important; +} + +.border-primary { + border-color: #325D88 !important; +} + +.border-secondary { + border-color: #8E8C84 !important; +} + +.border-success { + border-color: #93C54B !important; +} + +.border-info { + border-color: #29ABE0 !important; +} + +.border-warning { + border-color: #F47C3C !important; +} + +.border-danger { + border-color: #d9534f !important; +} + +.border-light { + border-color: #F8F5F0 !important; +} + +.border-dark { + border-color: #3E3F3A !important; +} + +.border-white { + border-color: #fff !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.d-none { + display: none !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; +} + +.d-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + } + .d-sm-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 768px) { + .d-md-none { + display: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + } + .d-md-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + } + .d-lg-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media (min-width: 1200px) { + .d-xl-none { + display: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + } + .d-xl-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +@media print { + .d-print-none { + display: none !important; + } + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + } + .d-print-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; + } +} + +.embed-responsive { + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; +} + +.embed-responsive::before { + display: block; + content: ""; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-21by9::before { + padding-top: 42.8571428571%; +} + +.embed-responsive-16by9::before { + padding-top: 56.25%; +} + +.embed-responsive-4by3::before { + padding-top: 75%; +} + +.embed-responsive-1by1::before { + padding-top: 100%; +} + +.flex-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; +} + +.flex-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; +} + +.flex-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} + +.flex-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} + +.flex-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} + +.flex-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} + +.justify-content-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} + +.justify-content-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} + +.justify-content-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.justify-content-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} + +.align-items-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; +} + +.align-items-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; +} + +.align-items-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; +} + +.align-items-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; +} + +.align-items-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; +} + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} + +@media (min-width: 576px) { + .flex-sm-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-sm-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-sm-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-sm-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-sm-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-sm-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-sm-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-sm-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-sm-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-sm-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-sm-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-sm-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-sm-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-sm-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 768px) { + .flex-md-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-md-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-md-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-md-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-md-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-md-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-md-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-md-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-md-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-md-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-md-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-md-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-md-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-md-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-md-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 992px) { + .flex-lg-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-lg-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-lg-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-lg-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-lg-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-lg-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-lg-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-lg-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-lg-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-lg-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-lg-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-lg-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-lg-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-lg-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +@media (min-width: 1200px) { + .flex-xl-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .flex-xl-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .flex-xl-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .flex-xl-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .flex-xl-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .justify-content-xl-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .justify-content-xl-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .justify-content-xl-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .justify-content-xl-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .align-items-xl-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .align-items-xl-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .align-items-xl-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .align-items-xl-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .align-items-xl-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} + +.float-left { + float: left !important; +} + +.float-right { + float: right !important; +} + +.float-none { + float: none !important; +} + +@media (min-width: 576px) { + .float-sm-left { + float: left !important; + } + .float-sm-right { + float: right !important; + } + .float-sm-none { + float: none !important; + } +} + +@media (min-width: 768px) { + .float-md-left { + float: left !important; + } + .float-md-right { + float: right !important; + } + .float-md-none { + float: none !important; + } +} + +@media (min-width: 992px) { + .float-lg-left { + float: left !important; + } + .float-lg-right { + float: right !important; + } + .float-lg-none { + float: none !important; + } +} + +@media (min-width: 1200px) { + .float-xl-left { + float: left !important; + } + .float-xl-right { + float: right !important; + } + .float-xl-none { + float: none !important; + } +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +@supports ((position: -webkit-sticky) or (position: sticky)) { + .sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + overflow: visible; + clip: auto; + white-space: normal; +} + +.shadow-sm { + -webkit-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + -webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + -webkit-box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.m-0 { + margin: 0 !important; +} + +.mt-0, +.my-0 { + margin-top: 0 !important; +} + +.mr-0, +.mx-0 { + margin-right: 0 !important; +} + +.mb-0, +.my-0 { + margin-bottom: 0 !important; +} + +.ml-0, +.mx-0 { + margin-left: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mt-3, +.my-3 { + margin-top: 1rem !important; +} + +.mr-3, +.mx-3 { + margin-right: 1rem !important; +} + +.mb-3, +.my-3 { + margin-bottom: 1rem !important; +} + +.ml-3, +.mx-3 { + margin-left: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.mt-4, +.my-4 { + margin-top: 1.5rem !important; +} + +.mr-4, +.mx-4 { + margin-right: 1.5rem !important; +} + +.mb-4, +.my-4 { + margin-bottom: 1.5rem !important; +} + +.ml-4, +.mx-4 { + margin-left: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.mt-5, +.my-5 { + margin-top: 3rem !important; +} + +.mr-5, +.mx-5 { + margin-right: 3rem !important; +} + +.mb-5, +.my-5 { + margin-bottom: 3rem !important; +} + +.ml-5, +.mx-5 { + margin-left: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0, +.py-0 { + padding-top: 0 !important; +} + +.pr-0, +.px-0 { + padding-right: 0 !important; +} + +.pb-0, +.py-0 { + padding-bottom: 0 !important; +} + +.pl-0, +.px-0 { + padding-left: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.pt-3, +.py-3 { + padding-top: 1rem !important; +} + +.pr-3, +.px-3 { + padding-right: 1rem !important; +} + +.pb-3, +.py-3 { + padding-bottom: 1rem !important; +} + +.pl-3, +.px-3 { + padding-left: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.pt-4, +.py-4 { + padding-top: 1.5rem !important; +} + +.pr-4, +.px-4 { + padding-right: 1.5rem !important; +} + +.pb-4, +.py-4 { + padding-bottom: 1.5rem !important; +} + +.pl-4, +.px-4 { + padding-left: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.pt-5, +.py-5 { + padding-top: 3rem !important; +} + +.pr-5, +.px-5 { + padding-right: 3rem !important; +} + +.pb-5, +.py-5 { + padding-bottom: 3rem !important; +} + +.pl-5, +.px-5 { + padding-left: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mt-auto, +.my-auto { + margin-top: auto !important; +} + +.mr-auto, +.mx-auto { + margin-right: auto !important; +} + +.mb-auto, +.my-auto { + margin-bottom: auto !important; +} + +.ml-auto, +.mx-auto { + margin-left: auto !important; +} + +@media (min-width: 576px) { + .m-sm-0 { + margin: 0 !important; + } + .mt-sm-0, + .my-sm-0 { + margin-top: 0 !important; + } + .mr-sm-0, + .mx-sm-0 { + margin-right: 0 !important; + } + .mb-sm-0, + .my-sm-0 { + margin-bottom: 0 !important; + } + .ml-sm-0, + .mx-sm-0 { + margin-left: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .mt-sm-3, + .my-sm-3 { + margin-top: 1rem !important; + } + .mr-sm-3, + .mx-sm-3 { + margin-right: 1rem !important; + } + .mb-sm-3, + .my-sm-3 { + margin-bottom: 1rem !important; + } + .ml-sm-3, + .mx-sm-3 { + margin-left: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .mt-sm-4, + .my-sm-4 { + margin-top: 1.5rem !important; + } + .mr-sm-4, + .mx-sm-4 { + margin-right: 1.5rem !important; + } + .mb-sm-4, + .my-sm-4 { + margin-bottom: 1.5rem !important; + } + .ml-sm-4, + .mx-sm-4 { + margin-left: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .mt-sm-5, + .my-sm-5 { + margin-top: 3rem !important; + } + .mr-sm-5, + .mx-sm-5 { + margin-right: 3rem !important; + } + .mb-sm-5, + .my-sm-5 { + margin-bottom: 3rem !important; + } + .ml-sm-5, + .mx-sm-5 { + margin-left: 3rem !important; + } + .p-sm-0 { + padding: 0 !important; + } + .pt-sm-0, + .py-sm-0 { + padding-top: 0 !important; + } + .pr-sm-0, + .px-sm-0 { + padding-right: 0 !important; + } + .pb-sm-0, + .py-sm-0 { + padding-bottom: 0 !important; + } + .pl-sm-0, + .px-sm-0 { + padding-left: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .pt-sm-3, + .py-sm-3 { + padding-top: 1rem !important; + } + .pr-sm-3, + .px-sm-3 { + padding-right: 1rem !important; + } + .pb-sm-3, + .py-sm-3 { + padding-bottom: 1rem !important; + } + .pl-sm-3, + .px-sm-3 { + padding-left: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .pt-sm-4, + .py-sm-4 { + padding-top: 1.5rem !important; + } + .pr-sm-4, + .px-sm-4 { + padding-right: 1.5rem !important; + } + .pb-sm-4, + .py-sm-4 { + padding-bottom: 1.5rem !important; + } + .pl-sm-4, + .px-sm-4 { + padding-left: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .pt-sm-5, + .py-sm-5 { + padding-top: 3rem !important; + } + .pr-sm-5, + .px-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-5, + .py-sm-5 { + padding-bottom: 3rem !important; + } + .pl-sm-5, + .px-sm-5 { + padding-left: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mt-sm-auto, + .my-sm-auto { + margin-top: auto !important; + } + .mr-sm-auto, + .mx-sm-auto { + margin-right: auto !important; + } + .mb-sm-auto, + .my-sm-auto { + margin-bottom: auto !important; + } + .ml-sm-auto, + .mx-sm-auto { + margin-left: auto !important; + } +} + +@media (min-width: 768px) { + .m-md-0 { + margin: 0 !important; + } + .mt-md-0, + .my-md-0 { + margin-top: 0 !important; + } + .mr-md-0, + .mx-md-0 { + margin-right: 0 !important; + } + .mb-md-0, + .my-md-0 { + margin-bottom: 0 !important; + } + .ml-md-0, + .mx-md-0 { + margin-left: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .mt-md-3, + .my-md-3 { + margin-top: 1rem !important; + } + .mr-md-3, + .mx-md-3 { + margin-right: 1rem !important; + } + .mb-md-3, + .my-md-3 { + margin-bottom: 1rem !important; + } + .ml-md-3, + .mx-md-3 { + margin-left: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .mt-md-4, + .my-md-4 { + margin-top: 1.5rem !important; + } + .mr-md-4, + .mx-md-4 { + margin-right: 1.5rem !important; + } + .mb-md-4, + .my-md-4 { + margin-bottom: 1.5rem !important; + } + .ml-md-4, + .mx-md-4 { + margin-left: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .mt-md-5, + .my-md-5 { + margin-top: 3rem !important; + } + .mr-md-5, + .mx-md-5 { + margin-right: 3rem !important; + } + .mb-md-5, + .my-md-5 { + margin-bottom: 3rem !important; + } + .ml-md-5, + .mx-md-5 { + margin-left: 3rem !important; + } + .p-md-0 { + padding: 0 !important; + } + .pt-md-0, + .py-md-0 { + padding-top: 0 !important; + } + .pr-md-0, + .px-md-0 { + padding-right: 0 !important; + } + .pb-md-0, + .py-md-0 { + padding-bottom: 0 !important; + } + .pl-md-0, + .px-md-0 { + padding-left: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .pt-md-3, + .py-md-3 { + padding-top: 1rem !important; + } + .pr-md-3, + .px-md-3 { + padding-right: 1rem !important; + } + .pb-md-3, + .py-md-3 { + padding-bottom: 1rem !important; + } + .pl-md-3, + .px-md-3 { + padding-left: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .pt-md-4, + .py-md-4 { + padding-top: 1.5rem !important; + } + .pr-md-4, + .px-md-4 { + padding-right: 1.5rem !important; + } + .pb-md-4, + .py-md-4 { + padding-bottom: 1.5rem !important; + } + .pl-md-4, + .px-md-4 { + padding-left: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .pt-md-5, + .py-md-5 { + padding-top: 3rem !important; + } + .pr-md-5, + .px-md-5 { + padding-right: 3rem !important; + } + .pb-md-5, + .py-md-5 { + padding-bottom: 3rem !important; + } + .pl-md-5, + .px-md-5 { + padding-left: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mt-md-auto, + .my-md-auto { + margin-top: auto !important; + } + .mr-md-auto, + .mx-md-auto { + margin-right: auto !important; + } + .mb-md-auto, + .my-md-auto { + margin-bottom: auto !important; + } + .ml-md-auto, + .mx-md-auto { + margin-left: auto !important; + } +} + +@media (min-width: 992px) { + .m-lg-0 { + margin: 0 !important; + } + .mt-lg-0, + .my-lg-0 { + margin-top: 0 !important; + } + .mr-lg-0, + .mx-lg-0 { + margin-right: 0 !important; + } + .mb-lg-0, + .my-lg-0 { + margin-bottom: 0 !important; + } + .ml-lg-0, + .mx-lg-0 { + margin-left: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .mt-lg-3, + .my-lg-3 { + margin-top: 1rem !important; + } + .mr-lg-3, + .mx-lg-3 { + margin-right: 1rem !important; + } + .mb-lg-3, + .my-lg-3 { + margin-bottom: 1rem !important; + } + .ml-lg-3, + .mx-lg-3 { + margin-left: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .mt-lg-4, + .my-lg-4 { + margin-top: 1.5rem !important; + } + .mr-lg-4, + .mx-lg-4 { + margin-right: 1.5rem !important; + } + .mb-lg-4, + .my-lg-4 { + margin-bottom: 1.5rem !important; + } + .ml-lg-4, + .mx-lg-4 { + margin-left: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .mt-lg-5, + .my-lg-5 { + margin-top: 3rem !important; + } + .mr-lg-5, + .mx-lg-5 { + margin-right: 3rem !important; + } + .mb-lg-5, + .my-lg-5 { + margin-bottom: 3rem !important; + } + .ml-lg-5, + .mx-lg-5 { + margin-left: 3rem !important; + } + .p-lg-0 { + padding: 0 !important; + } + .pt-lg-0, + .py-lg-0 { + padding-top: 0 !important; + } + .pr-lg-0, + .px-lg-0 { + padding-right: 0 !important; + } + .pb-lg-0, + .py-lg-0 { + padding-bottom: 0 !important; + } + .pl-lg-0, + .px-lg-0 { + padding-left: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .pt-lg-3, + .py-lg-3 { + padding-top: 1rem !important; + } + .pr-lg-3, + .px-lg-3 { + padding-right: 1rem !important; + } + .pb-lg-3, + .py-lg-3 { + padding-bottom: 1rem !important; + } + .pl-lg-3, + .px-lg-3 { + padding-left: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .pt-lg-4, + .py-lg-4 { + padding-top: 1.5rem !important; + } + .pr-lg-4, + .px-lg-4 { + padding-right: 1.5rem !important; + } + .pb-lg-4, + .py-lg-4 { + padding-bottom: 1.5rem !important; + } + .pl-lg-4, + .px-lg-4 { + padding-left: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .pt-lg-5, + .py-lg-5 { + padding-top: 3rem !important; + } + .pr-lg-5, + .px-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-5, + .py-lg-5 { + padding-bottom: 3rem !important; + } + .pl-lg-5, + .px-lg-5 { + padding-left: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mt-lg-auto, + .my-lg-auto { + margin-top: auto !important; + } + .mr-lg-auto, + .mx-lg-auto { + margin-right: auto !important; + } + .mb-lg-auto, + .my-lg-auto { + margin-bottom: auto !important; + } + .ml-lg-auto, + .mx-lg-auto { + margin-left: auto !important; + } +} + +@media (min-width: 1200px) { + .m-xl-0 { + margin: 0 !important; + } + .mt-xl-0, + .my-xl-0 { + margin-top: 0 !important; + } + .mr-xl-0, + .mx-xl-0 { + margin-right: 0 !important; + } + .mb-xl-0, + .my-xl-0 { + margin-bottom: 0 !important; + } + .ml-xl-0, + .mx-xl-0 { + margin-left: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .mt-xl-3, + .my-xl-3 { + margin-top: 1rem !important; + } + .mr-xl-3, + .mx-xl-3 { + margin-right: 1rem !important; + } + .mb-xl-3, + .my-xl-3 { + margin-bottom: 1rem !important; + } + .ml-xl-3, + .mx-xl-3 { + margin-left: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .mt-xl-4, + .my-xl-4 { + margin-top: 1.5rem !important; + } + .mr-xl-4, + .mx-xl-4 { + margin-right: 1.5rem !important; + } + .mb-xl-4, + .my-xl-4 { + margin-bottom: 1.5rem !important; + } + .ml-xl-4, + .mx-xl-4 { + margin-left: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .mt-xl-5, + .my-xl-5 { + margin-top: 3rem !important; + } + .mr-xl-5, + .mx-xl-5 { + margin-right: 3rem !important; + } + .mb-xl-5, + .my-xl-5 { + margin-bottom: 3rem !important; + } + .ml-xl-5, + .mx-xl-5 { + margin-left: 3rem !important; + } + .p-xl-0 { + padding: 0 !important; + } + .pt-xl-0, + .py-xl-0 { + padding-top: 0 !important; + } + .pr-xl-0, + .px-xl-0 { + padding-right: 0 !important; + } + .pb-xl-0, + .py-xl-0 { + padding-bottom: 0 !important; + } + .pl-xl-0, + .px-xl-0 { + padding-left: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .pt-xl-3, + .py-xl-3 { + padding-top: 1rem !important; + } + .pr-xl-3, + .px-xl-3 { + padding-right: 1rem !important; + } + .pb-xl-3, + .py-xl-3 { + padding-bottom: 1rem !important; + } + .pl-xl-3, + .px-xl-3 { + padding-left: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .pt-xl-4, + .py-xl-4 { + padding-top: 1.5rem !important; + } + .pr-xl-4, + .px-xl-4 { + padding-right: 1.5rem !important; + } + .pb-xl-4, + .py-xl-4 { + padding-bottom: 1.5rem !important; + } + .pl-xl-4, + .px-xl-4 { + padding-left: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .pt-xl-5, + .py-xl-5 { + padding-top: 3rem !important; + } + .pr-xl-5, + .px-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-5, + .py-xl-5 { + padding-bottom: 3rem !important; + } + .pl-xl-5, + .px-xl-5 { + padding-left: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mt-xl-auto, + .my-xl-auto { + margin-top: auto !important; + } + .mr-xl-auto, + .mx-xl-auto { + margin-right: auto !important; + } + .mb-xl-auto, + .my-xl-auto { + margin-bottom: auto !important; + } + .ml-xl-auto, + .mx-xl-auto { + margin-left: auto !important; + } +} + +.text-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.text-justify { + text-align: justify !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-left { + text-align: left !important; +} + +.text-right { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +@media (min-width: 576px) { + .text-sm-left { + text-align: left !important; + } + .text-sm-right { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} + +@media (min-width: 768px) { + .text-md-left { + text-align: left !important; + } + .text-md-right { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} + +@media (min-width: 992px) { + .text-lg-left { + text-align: left !important; + } + .text-lg-right { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} + +@media (min-width: 1200px) { + .text-xl-left { + text-align: left !important; + } + .text-xl-right { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.font-weight-light { + font-weight: 300 !important; +} + +.font-weight-normal { + font-weight: 400 !important; +} + +.font-weight-bold { + font-weight: 700 !important; +} + +.font-italic { + font-style: italic !important; +} + +.text-white { + color: #fff !important; +} + +.text-primary { + color: #325D88 !important; +} + +a.text-primary:hover, a.text-primary:focus { + color: #244463 !important; +} + +.text-secondary { + color: #8E8C84 !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #74726b !important; +} + +.text-success { + color: #93C54B !important; +} + +a.text-success:hover, a.text-success:focus { + color: #79a736 !important; +} + +.text-info { + color: #29ABE0 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #1b8dbb !important; +} + +.text-warning { + color: #F47C3C !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ef5c0e !important; +} + +.text-danger { + color: #d9534f !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #c9302c !important; +} + +.text-light { + color: #F8F5F0 !important; +} + +a.text-light:hover, a.text-light:focus { + color: #e8decd !important; +} + +.text-dark { + color: #3E3F3A !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #242422 !important; +} + +.text-body { + color: #3E3F3A !important; +} + +.text-muted { + color: #8E8C84 !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media print { + *, + *::before, + *::after { + text-shadow: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a:not(.btn) { + text-decoration: underline; + } + abbr[title]::after { + content: " (" attr(title) ")"; + } + pre { + white-space: pre-wrap !important; + } + pre, + blockquote { + border: 1px solid #98978B; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + @page { + size: a3; + } + body { + min-width: 992px !important; + } + .container { + min-width: 992px !important; + } + .navbar { + display: none; + } + .badge { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #DFD7CA !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #DFD7CA; + } + .table .thead-dark th { + color: inherit; + border-color: #DFD7CA; + } +} + +.bg-primary { + background-color: #3E3F3A !important; +} + +.bg-dark { + background-color: #3E3F3A !important; +} + +.bg-light { + background-color: #F8F5F0 !important; +} + +.sandstone, .navbar .nav-link, .btn, .nav-tabs .nav-link, .nav-pills .nav-link, .breadcrumb, .pagination, .dropdown-menu .dropdown-item, .tooltip { + font-size: 11px; + line-height: 22px; + font-weight: 500; + text-transform: uppercase; +} + +.navbar-form input, +.navbar-form .form-control { + border: none; +} + +.btn:hover { + border-color: transparent; +} + +.btn-success, .btn-warning { + color: #fff; +} + +.table .thead-dark th { + background-color: #3E3F3A; +} + +.nav-tabs .nav-link { + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.nav-tabs .nav-link, +.nav-tabs .nav-link:hover, +.nav-tabs .nav-link:focus { + color: #8E8C84; +} + +.nav-tabs .nav-link.disabled, +.nav-tabs .nav-link.disabled:hover, +.nav-tabs .nav-link.disabled:focus { + background-color: #F8F5F0; + border-color: #DFD7CA; + color: #DFD7CA; +} + +.nav-pills .nav-link { + border: 1px solid transparent; + color: #8E8C84; +} + +.nav-pills .nav-link.active, +.nav-pills .nav-link:hover, +.nav-pills .nav-link:focus { + background-color: #F8F5F0; + border-color: #DFD7CA; +} + +.nav-pills .nav-link.disabled, +.nav-pills .nav-link.disabled:hover { + background-color: transparent; + border-color: transparent; + color: #DFD7CA; +} + +.breadcrumb { + border: 1px solid #DFD7CA; +} + +.pagination a:hover { + text-decoration: none; +} + +.alert { + color: #fff; +} + +.alert a, +.alert .alert-link { + color: #fff; + text-decoration: underline; +} + +.alert-primary, .alert-primary > th, .alert-primary > td { + background-color: #325D88; +} + +.alert-secondary, .alert-secondary > th, .alert-secondary > td { + background-color: #8E8C84; +} + +.alert-success, .alert-success > th, .alert-success > td { + background-color: #93C54B; +} + +.alert-info, .alert-info > th, .alert-info > td { + background-color: #29ABE0; +} + +.alert-danger, .alert-danger > th, .alert-danger > td { + background-color: #d9534f; +} + +.alert-warning, .alert-warning > th, .alert-warning > td { + background-color: #F47C3C; +} + +.alert-dark, .alert-dark > th, .alert-dark > td { + background-color: #3E3F3A; +} + +.alert-light, .alert-light > th, .alert-light > td { + background-color: #F8F5F0; +} + +.alert-light, +.alert-light a:not(.btn), +.alert-light .alert-link { + color: #3E3F3A; +} + +.badge-success, .badge-warning { + color: #fff; +} + +.close { + color: #DFD7CA; + opacity: 1; +} + +.close:hover { + color: #b9a78a; +} + +footer { + color: white; } +footer h3 { + margin-bottom: 30px; } +footer .footer-above { + padding-top: 50px; + background-color: #3E3F3A; } +footer .footer-col { + margin-bottom: 50px; } +footer .footer-below { + padding: 25px 0; + background-color: #3E3F3A; } + + +.btn-social { + font-size: 20px; + line-height: 45px; + display: inline-block; + width: 50px; + height: 50px; + text-align: center; + border: 2px solid white; + border-radius: 100%; } + +.btn-outline { + font-size: 20px; + margin-top: 15px; + transition: all 0.3s ease-in-out; + color: white; + border: solid 2px white; + background: transparent; } +.btn-outline.active, .btn-outline:active, .btn-outline:focus, .btn-outline:hover { + color: #18BC9C; + border: solid 2px white; + background: white; } + + +.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-fw { + width: 1.28571429em; + text-align: center; +} + +.fa-wordpress:before { + content: "\f19a"; +} + +.fa-github:before { + content: "\f09b"; +} + +.fa-github-square:before { + content: "\f092"; +} + +.fa-wordpress:before { + content: "\f19a"; +} + +.fa-youtube:before { + content: "\f167"; +} \ No newline at end of file diff --git a/blogContent/projects/steam/css/bootstrap.min.css b/blogContent/projects/steam/css/bootstrap.min.css new file mode 100644 index 0000000..1a7e64c --- /dev/null +++ b/blogContent/projects/steam/css/bootstrap.min.css @@ -0,0 +1,12 @@ +/*! + * Bootswatch v4.1.1 + * Homepage: https://bootswatch.com + * Copyright 2012-2018 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v4.1.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */@import url("https://fonts.googleapis.com/css?family=Roboto:400,500,700");:root{--blue:#325D88;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#d9534f;--orange:#F47C3C;--yellow:#ffc107;--green:#93C54B;--teal:#20c997;--cyan:#29ABE0;--white:#fff;--gray:#8E8C84;--gray-dark:#3E3F3A;--primary:#325D88;--secondary:#8E8C84;--success:#93C54B;--info:#29ABE0;--warning:#F47C3C;--danger:#d9534f;--light:#F8F5F0;--dark:#3E3F3A;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.875rem;font-weight:400;line-height:1.5;color:#3E3F3A;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#93C54B;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#6b9430;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#8E8C84;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-family:inherit;font-weight:400;line-height:1.2;color:inherit}h1,.h1{font-size:2.1875rem}h2,.h2{font-size:1.75rem}h3,.h3{font-size:1.53125rem}h4,.h4{font-size:1.3125rem}h5,.h5{font-size:1.09375rem}h6,.h6{font-size:0.875rem}.lead{font-size:1.09375rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.09375rem}.blockquote-footer{display:block;font-size:80%;color:#8E8C84}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#fff;border:1px solid #DFD7CA;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#8E8C84}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #DFD7CA}.table thead th{vertical-align:bottom;border-bottom:2px solid #DFD7CA}.table tbody+tbody{border-top:2px solid #DFD7CA}.table .table{background-color:#fff}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #DFD7CA}.table-bordered th,.table-bordered td{border:1px solid #DFD7CA}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,0.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c6d2de}.table-hover .table-primary:hover{background-color:#b6c5d5}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b6c5d5}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#dfdfdd}.table-hover .table-secondary:hover{background-color:#d3d3d0}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#d3d3d0}.table-success,.table-success>th,.table-success>td{background-color:#e1efcd}.table-hover .table-success:hover{background-color:#d5e9ba}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d5e9ba}.table-info,.table-info>th,.table-info>td{background-color:#c3e7f6}.table-hover .table-info:hover{background-color:#addef3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#addef3}.table-warning,.table-warning>th,.table-warning>td{background-color:#fcdac8}.table-hover .table-warning:hover{background-color:#fbcab0}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbcab0}.table-danger,.table-danger>th,.table-danger>td{background-color:#f4cfce}.table-hover .table-danger:hover{background-color:#efbbb9}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#efbbb9}.table-light,.table-light>th,.table-light>td{background-color:#fdfcfb}.table-hover .table-light:hover{background-color:#f5efea}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#f5efea}.table-dark,.table-dark>th,.table-dark>td{background-color:#c9c9c8}.table-hover .table-dark:hover{background-color:#bcbcbb}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#bcbcbb}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#F8F5F0;border-color:#DFD7CA}.table-dark{color:#fff;background-color:#212529}.table-dark th,.table-dark td,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:0.375rem 0.75rem;font-size:0.875rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media screen and (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#6f9dca;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25)}.form-control::-webkit-input-placeholder{color:#8E8C84;opacity:1}.form-control:-ms-input-placeholder{color:#8E8C84;opacity:1}.form-control::-ms-input-placeholder{color:#8E8C84;opacity:1}.form-control::placeholder{color:#8E8C84;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#F8F5F0;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.0625rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.09375rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.765625rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:0.375rem;padding-bottom:0.375rem;margin-bottom:0;line-height:1.5;color:#3E3F3A;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.form-control-plaintext.form-control-lg,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.765625rem;line-height:1.5;border-radius:0.2rem}select.form-control-sm:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]){height:calc(1.6484375rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.09375rem;line-height:1.5;border-radius:0.3rem}select.form-control-lg:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]){height:calc(2.640625rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input:disabled ~ .form-check-label{color:#8E8C84}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#93C54B}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(147,197,75,0.8);border-radius:.2rem}.was-validated .form-control:valid,.form-control.is-valid,.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#93C54B}.was-validated .form-control:valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#93C54B;-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.25);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.25)}.was-validated .form-control:valid ~ .valid-feedback,.was-validated .form-control:valid ~ .valid-tooltip,.form-control.is-valid ~ .valid-feedback,.form-control.is-valid ~ .valid-tooltip,.was-validated .custom-select:valid ~ .valid-feedback,.was-validated .custom-select:valid ~ .valid-tooltip,.custom-select.is-valid ~ .valid-feedback,.custom-select.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control-file:valid ~ .valid-feedback,.was-validated .form-control-file:valid ~ .valid-tooltip,.form-control-file.is-valid ~ .valid-feedback,.form-control-file.is-valid ~ .valid-tooltip{display:block}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#93C54B}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#93C54B}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{background-color:#cde4ab}.was-validated .custom-control-input:valid ~ .valid-feedback,.was-validated .custom-control-input:valid ~ .valid-tooltip,.custom-control-input.is-valid ~ .valid-feedback,.custom-control-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{background-color:#aad172}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(147,197,75,0.25);box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(147,197,75,0.25)}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#93C54B}.was-validated .custom-file-input:valid ~ .custom-file-label::before,.custom-file-input.is-valid ~ .custom-file-label::before{border-color:inherit}.was-validated .custom-file-input:valid ~ .valid-feedback,.was-validated .custom-file-input:valid ~ .valid-tooltip,.custom-file-input.is-valid ~ .valid-feedback,.custom-file-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.25);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#d9534f}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(217,83,79,0.8);border-radius:.2rem}.was-validated .form-control:invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#d9534f}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#d9534f;-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.25);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.25)}.was-validated .form-control:invalid ~ .invalid-feedback,.was-validated .form-control:invalid ~ .invalid-tooltip,.form-control.is-invalid ~ .invalid-feedback,.form-control.is-invalid ~ .invalid-tooltip,.was-validated .custom-select:invalid ~ .invalid-feedback,.was-validated .custom-select:invalid ~ .invalid-tooltip,.custom-select.is-invalid ~ .invalid-feedback,.custom-select.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control-file:invalid ~ .invalid-feedback,.was-validated .form-control-file:invalid ~ .invalid-tooltip,.form-control-file.is-invalid ~ .invalid-feedback,.form-control-file.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#d9534f}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#d9534f}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{background-color:#f0b9b8}.was-validated .custom-control-input:invalid ~ .invalid-feedback,.was-validated .custom-control-input:invalid ~ .invalid-tooltip,.custom-control-input.is-invalid ~ .invalid-feedback,.custom-control-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{background-color:#e27c79}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(217,83,79,0.25);box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(217,83,79,0.25)}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#d9534f}.was-validated .custom-file-input:invalid ~ .custom-file-label::before,.custom-file-input.is-invalid ~ .custom-file-label::before{border-color:inherit}.was-validated .custom-file-input:invalid ~ .invalid-feedback,.was-validated .custom-file-input:invalid ~ .invalid-tooltip,.custom-file-input.is-invalid ~ .invalid-feedback,.custom-file-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.25);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.875rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media screen and (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover,.btn:focus{text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled):active,.btn:not(:disabled):not(.disabled).active{background-image:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#325D88;border-color:#325D88}.btn-primary:hover{color:#fff;background-color:#284a6c;border-color:#244463}.btn-primary:focus,.btn-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#325D88;border-color:#325D88}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#244463;border-color:#213d59}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5)}.btn-secondary{color:#fff;background-color:#8E8C84;border-color:#8E8C84}.btn-secondary:hover{color:#fff;background-color:#7b7971;border-color:#74726b}.btn-secondary:focus,.btn-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5);box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#8E8C84;border-color:#8E8C84}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#74726b;border-color:#6e6c65}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5);box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5)}.btn-success{color:#fff;background-color:#93C54B;border-color:#93C54B}.btn-success:hover{color:#fff;background-color:#80b139;border-color:#79a736}.btn-success:focus,.btn-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#93C54B;border-color:#93C54B}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#79a736;border-color:#729e33}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5)}.btn-info{color:#fff;background-color:#29ABE0;border-color:#29ABE0}.btn-info:hover{color:#fff;background-color:#1d95c6;border-color:#1b8dbb}.btn-info:focus,.btn-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5);box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#29ABE0;border-color:#29ABE0}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#1b8dbb;border-color:#1984b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5);box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5)}.btn-warning{color:#fff;background-color:#F47C3C;border-color:#F47C3C}.btn-warning:hover{color:#fff;background-color:#f26418;border-color:#ef5c0e}.btn-warning:focus,.btn-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5);box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F47C3C;border-color:#F47C3C}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ef5c0e;border-color:#e3570d}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5);box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5)}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#d23430;border-color:#c9302c}.btn-danger:focus,.btn-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#bf2e29}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5)}.btn-light{color:#212529;background-color:#F8F5F0;border-color:#F8F5F0}.btn-light:hover{color:#212529;background-color:#ece4d6;border-color:#e8decd}.btn-light:focus,.btn-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5);box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#F8F5F0;border-color:#F8F5F0}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#e8decd;border-color:#e4d8c5}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5);box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5)}.btn-dark{color:#fff;background-color:#3E3F3A;border-color:#3E3F3A}.btn-dark:hover{color:#fff;background-color:#2a2b28;border-color:#242422}.btn-dark:focus,.btn-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5);box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#3E3F3A;border-color:#3E3F3A}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#242422;border-color:#1d1e1b}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5);box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5)}.btn-outline-primary{color:#325D88;background-color:transparent;background-image:none;border-color:#325D88}.btn-outline-primary:hover{color:#fff;background-color:#325D88;border-color:#325D88}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#325D88;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#325D88;border-color:#325D88}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.5)}.btn-outline-secondary{color:#8E8C84;background-color:transparent;background-image:none;border-color:#8E8C84}.btn-outline-secondary:hover{color:#fff;background-color:#8E8C84;border-color:#8E8C84}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5);box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#8E8C84;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#8E8C84;border-color:#8E8C84}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5);box-shadow:0 0 0 0.2rem rgba(142,140,132,0.5)}.btn-outline-success{color:#93C54B;background-color:transparent;background-image:none;border-color:#93C54B}.btn-outline-success:hover{color:#fff;background-color:#93C54B;border-color:#93C54B}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#93C54B;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#93C54B;border-color:#93C54B}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5);box-shadow:0 0 0 0.2rem rgba(147,197,75,0.5)}.btn-outline-info{color:#29ABE0;background-color:transparent;background-image:none;border-color:#29ABE0}.btn-outline-info:hover{color:#fff;background-color:#29ABE0;border-color:#29ABE0}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5);box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#29ABE0;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#29ABE0;border-color:#29ABE0}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5);box-shadow:0 0 0 0.2rem rgba(41,171,224,0.5)}.btn-outline-warning{color:#F47C3C;background-color:transparent;background-image:none;border-color:#F47C3C}.btn-outline-warning:hover{color:#fff;background-color:#F47C3C;border-color:#F47C3C}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5);box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F47C3C;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F47C3C;border-color:#F47C3C}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5);box-shadow:0 0 0 0.2rem rgba(244,124,60,0.5)}.btn-outline-danger{color:#d9534f;background-color:transparent;background-image:none;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5);box-shadow:0 0 0 0.2rem rgba(217,83,79,0.5)}.btn-outline-light{color:#F8F5F0;background-color:transparent;background-image:none;border-color:#F8F5F0}.btn-outline-light:hover{color:#212529;background-color:#F8F5F0;border-color:#F8F5F0}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5);box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#F8F5F0;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#F8F5F0;border-color:#F8F5F0}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5);box-shadow:0 0 0 0.2rem rgba(248,245,240,0.5)}.btn-outline-dark{color:#3E3F3A;background-color:transparent;background-image:none;border-color:#3E3F3A}.btn-outline-dark:hover{color:#fff;background-color:#3E3F3A;border-color:#3E3F3A}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5);box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#3E3F3A;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#3E3F3A;border-color:#3E3F3A}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5);box-shadow:0 0 0 0.2rem rgba(62,63,58,0.5)}.btn-link{font-weight:400;color:#93C54B;background-color:transparent}.btn-link:hover{color:#6b9430;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link:focus,.btn-link.focus{text-decoration:underline;border-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link:disabled,.btn-link.disabled{color:#8E8C84;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.09375rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.765625rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media screen and (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media screen and (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.875rem;color:#3E3F3A;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:0.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #F8F5F0}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#8E8C84;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#8E8C84;text-decoration:none;background-color:#F8F5F0}.dropdown-item.active,.dropdown-item:active{color:#8E8C84;text-decoration:none;background-color:#F8F5F0}.dropdown-item.disabled,.dropdown-item:disabled{color:#8E8C84;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.765625rem;color:#8E8C84;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#8E8C84}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file:focus{z-index:3}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.875rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#F8F5F0;border:1px solid #ced4da;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;background-color:#325D88}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25)}.custom-control-input:active ~ .custom-control-label::before{color:#fff;background-color:#95b6d8}.custom-control-input:disabled ~ .custom-control-label{color:#8E8C84}.custom-control-input:disabled ~ .custom-control-label::before{background-color:#F8F5F0}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:0.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#DFD7CA}.custom-control-label::after{position:absolute;top:0.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before{background-color:#325D88}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{background-color:#325D88}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(50,93,136,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(50,93,136,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::before{background-color:#325D88}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(50,93,136,0.5)}.custom-select{display:inline-block;width:100%;height:calc(2.0625rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%233E3F3A' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#6f9dca;outline:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.075),0 0 5px rgba(111,157,202,0.5);box-shadow:inset 0 1px 2px rgba(0,0,0,0.075),0 0 5px rgba(111,157,202,0.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#8E8C84;background-color:#F8F5F0}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.6484375rem + 2px);padding-top:0.375rem;padding-bottom:0.375rem;font-size:75%}.custom-select-lg{height:calc(2.640625rem + 2px);padding-top:0.375rem;padding-bottom:0.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.0625rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.0625rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#6f9dca;-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25)}.custom-file-input:focus ~ .custom-file-label::after{border-color:#6f9dca}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.0625rem + 2px);padding:0.375rem 0.75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.0625rem;padding:0.375rem 0.75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#F8F5F0;border-left:1px solid #ced4da;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#325D88;border:0;border-radius:1rem;-webkit-appearance:none;appearance:none}.custom-range::-webkit-slider-thumb:focus{outline:none;-webkit-box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25)}.custom-range::-webkit-slider-thumb:active{background-color:#95b6d8}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#DFD7CA;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#325D88;border:0;border-radius:1rem;-moz-appearance:none;appearance:none}.custom-range::-moz-range-thumb:focus{outline:none;box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25)}.custom-range::-moz-range-thumb:active{background-color:#95b6d8}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#DFD7CA;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;background-color:#325D88;border:0;border-radius:1rem;appearance:none}.custom-range::-ms-thumb:focus{outline:none;box-shadow:0 0 0 1px #fff,0 0 0 0.2rem rgba(50,93,136,0.25)}.custom-range::-ms-thumb:active{background-color:#95b6d8}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#DFD7CA;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#DFD7CA;border-radius:1rem}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 0.9rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#DFD7CA}.nav-tabs{border-bottom:1px solid #DFD7CA}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#DFD7CA}.nav-tabs .nav-link.disabled{color:#DFD7CA;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#495057;background-color:#fff;border-color:#DFD7CA #DFD7CA #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#8E8C84;background-color:#F8F5F0}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:0.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.3359375rem;padding-bottom:0.3359375rem;margin-right:1rem;font-size:1.09375rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.09375rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#000}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#000}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#000}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#000}.navbar-light .navbar-toggler{color:rgba(0,0,0,0.5);border-color:rgba(0,0,0,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,0.5)}.navbar-light .navbar-text a{color:#000}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#000}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(223,215,202,0.75);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:rgba(248,245,240,0.25);border-bottom:1px solid rgba(223,215,202,0.75)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:rgba(248,245,240,0.25);border-top:1px solid rgba(223,215,202,0.75)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(0.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-img-top,.card-group>.card:first-child .card-header{border-top-right-radius:0}.card-group>.card:first-child .card-img-bottom,.card-group>.card:first-child .card-footer{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-img-top,.card-group>.card:last-child .card-header{border-top-left-radius:0}.card-group>.card:last-child .card-img-bottom,.card-group>.card:last-child .card-footer{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:0.25rem}.card-group>.card:only-child .card-img-top,.card-group>.card:only-child .card-header{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.card-group>.card:only-child .card-img-bottom,.card-group>.card:only-child .card-footer{border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer{border-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#F8F5F0;border-radius:0.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#8E8C84;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#8E8C84}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:-1px;line-height:1.25;color:#8E8C84;background-color:#F8F5F0;border:1px solid #DFD7CA}.page-link:hover{z-index:2;color:#8E8C84;text-decoration:none;background-color:#F8F5F0;border-color:#DFD7CA}.page-link:focus{z-index:2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25);box-shadow:0 0 0 0.2rem rgba(50,93,136,0.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:1;color:#8E8C84;background-color:#DFD7CA;border-color:#DFD7CA}.page-item.disabled .page-link{color:#DFD7CA;pointer-events:none;cursor:auto;background-color:#F8F5F0;border-color:#DFD7CA}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.09375rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.765625rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#325D88}.badge-primary[href]:hover,.badge-primary[href]:focus{color:#fff;text-decoration:none;background-color:#244463}.badge-secondary{color:#fff;background-color:#8E8C84}.badge-secondary[href]:hover,.badge-secondary[href]:focus{color:#fff;text-decoration:none;background-color:#74726b}.badge-success{color:#fff;background-color:#93C54B}.badge-success[href]:hover,.badge-success[href]:focus{color:#fff;text-decoration:none;background-color:#79a736}.badge-info{color:#fff;background-color:#29ABE0}.badge-info[href]:hover,.badge-info[href]:focus{color:#fff;text-decoration:none;background-color:#1b8dbb}.badge-warning{color:#fff;background-color:#F47C3C}.badge-warning[href]:hover,.badge-warning[href]:focus{color:#fff;text-decoration:none;background-color:#ef5c0e}.badge-danger{color:#fff;background-color:#d9534f}.badge-danger[href]:hover,.badge-danger[href]:focus{color:#fff;text-decoration:none;background-color:#c9302c}.badge-light{color:#212529;background-color:#F8F5F0}.badge-light[href]:hover,.badge-light[href]:focus{color:#212529;text-decoration:none;background-color:#e8decd}.badge-dark{color:#fff;background-color:#3E3F3A}.badge-dark[href]:hover,.badge-dark[href]:focus{color:#fff;text-decoration:none;background-color:#242422}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#F8F5F0;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.8125rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1a3047;background-color:#d6dfe7;border-color:#c6d2de}.alert-primary hr{border-top-color:#b6c5d5}.alert-primary .alert-link{color:#0c1722}.alert-secondary{color:#4a4945;background-color:#e8e8e6;border-color:#dfdfdd}.alert-secondary hr{border-top-color:#d3d3d0}.alert-secondary .alert-link{color:#302f2c}.alert-success{color:#4c6627;background-color:#e9f3db;border-color:#e1efcd}.alert-success hr{border-top-color:#d5e9ba}.alert-success .alert-link{color:#314119}.alert-info{color:#155974;background-color:#d4eef9;border-color:#c3e7f6}.alert-info hr{border-top-color:#addef3}.alert-info .alert-link{color:#0d3849}.alert-warning{color:#7f401f;background-color:#fde5d8;border-color:#fcdac8}.alert-warning hr{border-top-color:#fbcab0}.alert-warning .alert-link{color:#562b15}.alert-danger{color:#712b29;background-color:#f7dddc;border-color:#f4cfce}.alert-danger hr{border-top-color:#efbbb9}.alert-danger .alert-link{color:#4c1d1b}.alert-light{color:#817f7d;background-color:#fefdfc;border-color:#fdfcfb}.alert-light hr{border-top-color:#f5efea}.alert-light .alert-link{color:#676664}.alert-dark{color:#20211e;background-color:#d8d9d8;border-color:#c9c9c8}.alert-dark hr{border-top-color:#bcbcbb}.alert-dark .alert-link{color:#060606}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:0.65625rem;background-color:#DFD7CA;border-radius:10px}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#325D88;text-align:center;white-space:nowrap;background-color:#325D88;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media screen and (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#3E3F3A;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{color:#3E3F3A;text-decoration:none;background-color:#F8F5F0}.list-group-item-action:active{color:#3E3F3A;background-color:#DFD7CA}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid #DFD7CA}.list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.list-group-item:hover,.list-group-item:focus{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#98978B;background-color:#fff}.list-group-item.active{z-index:2;color:#3E3F3A;background-color:#F8F5F0;border-color:#DFD7CA}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#1a3047;background-color:#c6d2de}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1a3047;background-color:#b6c5d5}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1a3047;border-color:#1a3047}.list-group-item-secondary{color:#4a4945;background-color:#dfdfdd}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#4a4945;background-color:#d3d3d0}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#4a4945;border-color:#4a4945}.list-group-item-success{color:#4c6627;background-color:#e1efcd}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#4c6627;background-color:#d5e9ba}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#4c6627;border-color:#4c6627}.list-group-item-info{color:#155974;background-color:#c3e7f6}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#155974;background-color:#addef3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#155974;border-color:#155974}.list-group-item-warning{color:#7f401f;background-color:#fcdac8}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7f401f;background-color:#fbcab0}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7f401f;border-color:#7f401f}.list-group-item-danger{color:#712b29;background-color:#f4cfce}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#712b29;background-color:#efbbb9}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#712b29;border-color:#712b29}.list-group-item-light{color:#817f7d;background-color:#fdfcfb}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#817f7d;background-color:#f5efea}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#817f7d;border-color:#817f7d}.list-group-item-dark{color:#20211e;background-color:#c9c9c8}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#20211e;background-color:#bcbcbb}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#20211e;border-color:#20211e}.close{float:right;font-size:1.3125rem;font-weight:700;line-height:1;color:#000;text-shadow:none;opacity:.5}.close:hover,.close:focus{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -25%);transform:translate(0, -25%)}@media screen and (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - (0.5rem * 2))}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid #DFD7CA;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #DFD7CA;border-top-left-radius:0.3rem;border-top-right-radius:0.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #DFD7CA}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.765625rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.765625rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top .arrow,.bs-popover-auto[x-placement^="top"] .arrow{bottom:calc((0.5rem + 1px) * -1)}.bs-popover-top .arrow::before,.bs-popover-auto[x-placement^="top"] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-auto[x-placement^="top"] .arrow::after{border-width:0.5rem 0.5rem 0}.bs-popover-top .arrow::before,.bs-popover-auto[x-placement^="top"] .arrow::before{bottom:0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top .arrow::after,.bs-popover-auto[x-placement^="top"] .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right .arrow,.bs-popover-auto[x-placement^="right"] .arrow{left:calc((0.5rem + 1px) * -1);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right .arrow::before,.bs-popover-auto[x-placement^="right"] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-auto[x-placement^="right"] .arrow::after{border-width:0.5rem 0.5rem 0.5rem 0}.bs-popover-right .arrow::before,.bs-popover-auto[x-placement^="right"] .arrow::before{left:0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right .arrow::after,.bs-popover-auto[x-placement^="right"] .arrow::after{left:1px;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom .arrow,.bs-popover-auto[x-placement^="bottom"] .arrow{top:calc((0.5rem + 1px) * -1)}.bs-popover-bottom .arrow::before,.bs-popover-auto[x-placement^="bottom"] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-auto[x-placement^="bottom"] .arrow::after{border-width:0 0.5rem 0.5rem 0.5rem}.bs-popover-bottom .arrow::before,.bs-popover-auto[x-placement^="bottom"] .arrow::before{top:0;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom .arrow::after,.bs-popover-auto[x-placement^="bottom"] .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #F8F5F0}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left .arrow,.bs-popover-auto[x-placement^="left"] .arrow{right:calc((0.5rem + 1px) * -1);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left .arrow::before,.bs-popover-auto[x-placement^="left"] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-auto[x-placement^="left"] .arrow::after{border-width:0.5rem 0 0.5rem 0.5rem}.bs-popover-left .arrow::before,.bs-popover-auto[x-placement^="left"] .arrow::before{right:0;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left .arrow::after,.bs-popover-auto[x-placement^="left"] .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.875rem;color:inherit;background-color:#F8F5F0;border-bottom:1px solid #f0e9df;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#3E3F3A}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;-webkit-transition:-webkit-transform 0.6s ease;transition:-webkit-transform 0.6s ease;transition:transform 0.6s ease;transition:transform 0.6s ease, -webkit-transform 0.6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}@media screen and (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports (-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}}.carousel-item-next,.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports (-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d){.carousel-item-next,.active.carousel-item-right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0)}}.carousel-item-prev,.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports (-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d){.carousel-item-prev,.active.carousel-item-left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0)}}.carousel-fade .carousel-item{opacity:0;-webkit-transition-duration:.6s;transition-duration:.6s;-webkit-transition-property:opacity;transition-property:opacity}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active,.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev{-webkit-transform:translateX(0);transform:translateX(0)}@supports (-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d){.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active,.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,0.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#325D88 !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#244463 !important}.bg-secondary{background-color:#8E8C84 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#74726b !important}.bg-success{background-color:#93C54B !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#79a736 !important}.bg-info{background-color:#29ABE0 !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#1b8dbb !important}.bg-warning{background-color:#F47C3C !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#ef5c0e !important}.bg-danger{background-color:#d9534f !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#c9302c !important}.bg-light{background-color:#F8F5F0 !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#e8decd !important}.bg-dark{background-color:#3E3F3A !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#242422 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #DFD7CA !important}.border-top{border-top:1px solid #DFD7CA !important}.border-right{border-right:1px solid #DFD7CA !important}.border-bottom{border-bottom:1px solid #DFD7CA !important}.border-left{border-left:1px solid #DFD7CA !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#325D88 !important}.border-secondary{border-color:#8E8C84 !important}.border-success{border-color:#93C54B !important}.border-info{border-color:#29ABE0 !important}.border-warning{border-color:#F47C3C !important}.border-danger{border-color:#d9534f !important}.border-light{border-color:#F8F5F0 !important}.border-dark{border-color:#3E3F3A !important}.border-white{border-color:#fff !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-circle{border-radius:50% !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}.text-justify{text-align:justify !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#325D88 !important}a.text-primary:hover,a.text-primary:focus{color:#244463 !important}.text-secondary{color:#8E8C84 !important}a.text-secondary:hover,a.text-secondary:focus{color:#74726b !important}.text-success{color:#93C54B !important}a.text-success:hover,a.text-success:focus{color:#79a736 !important}.text-info{color:#29ABE0 !important}a.text-info:hover,a.text-info:focus{color:#1b8dbb !important}.text-warning{color:#F47C3C !important}a.text-warning:hover,a.text-warning:focus{color:#ef5c0e !important}.text-danger{color:#d9534f !important}a.text-danger:hover,a.text-danger:focus{color:#c9302c !important}.text-light{color:#F8F5F0 !important}a.text-light:hover,a.text-light:focus{color:#e8decd !important}.text-dark{color:#3E3F3A !important}a.text-dark:hover,a.text-dark:focus{color:#242422 !important}.text-body{color:#3E3F3A !important}.text-muted{color:#8E8C84 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #98978B;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #DFD7CA !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#DFD7CA}.table .thead-dark th{color:inherit;border-color:#DFD7CA}}.bg-primary{background-color:#3E3F3A !important}.bg-dark{background-color:#8E8C84 !important}.bg-light{background-color:#F8F5F0 !important}.sandstone,.navbar .nav-link,.btn,.nav-tabs .nav-link,.nav-pills .nav-link,.breadcrumb,.pagination,.dropdown-menu .dropdown-item,.tooltip{font-size:11px;line-height:22px;font-weight:500;text-transform:uppercase}.navbar-form input,.navbar-form .form-control{border:none}.btn:hover{border-color:transparent}.btn-success,.btn-warning{color:#fff}.table .thead-dark th{background-color:#3E3F3A}.nav-tabs .nav-link{background-color:#F8F5F0;border-color:#DFD7CA}.nav-tabs .nav-link,.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{color:#8E8C84}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link.disabled:hover,.nav-tabs .nav-link.disabled:focus{background-color:#F8F5F0;border-color:#DFD7CA;color:#DFD7CA}.nav-pills .nav-link{border:1px solid transparent;color:#8E8C84}.nav-pills .nav-link.active,.nav-pills .nav-link:hover,.nav-pills .nav-link:focus{background-color:#F8F5F0;border-color:#DFD7CA}.nav-pills .nav-link.disabled,.nav-pills .nav-link.disabled:hover{background-color:transparent;border-color:transparent;color:#DFD7CA}.breadcrumb{border:1px solid #DFD7CA}.pagination a:hover{text-decoration:none}.alert{color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary,.alert-primary>th,.alert-primary>td{background-color:#325D88}.alert-secondary,.alert-secondary>th,.alert-secondary>td{background-color:#8E8C84}.alert-success,.alert-success>th,.alert-success>td{background-color:#93C54B}.alert-info,.alert-info>th,.alert-info>td{background-color:#29ABE0}.alert-danger,.alert-danger>th,.alert-danger>td{background-color:#d9534f}.alert-warning,.alert-warning>th,.alert-warning>td{background-color:#F47C3C}.alert-dark,.alert-dark>th,.alert-dark>td{background-color:#3E3F3A}.alert-light,.alert-light>th,.alert-light>td{background-color:#F8F5F0}.alert-light,.alert-light a:not(.btn),.alert-light .alert-link{color:#3E3F3A}.badge-success,.badge-warning{color:#fff}.close{color:#DFD7CA;opacity:1}.close:hover{color:#b9a78a} \ No newline at end of file diff --git a/blogContent/projects/steam/error.html b/blogContent/projects/steam/error.html new file mode 100644 index 0000000..cefe1e1 --- /dev/null +++ b/blogContent/projects/steam/error.html @@ -0,0 +1,143 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + + + +
+


+ +

My Steam websocket server down.

+

Sorry about that, you should try again some other day.

+ +

+
+ + + + + + + + + + + + + diff --git a/blogContent/projects/steam/exampleGraph.png b/blogContent/projects/steam/exampleGraph.png new file mode 100644 index 0000000..d5db88f Binary files /dev/null and b/blogContent/projects/steam/exampleGraph.png differ diff --git a/blogContent/projects/steam/faq.html b/blogContent/projects/steam/faq.html new file mode 100644 index 0000000..9df4594 --- /dev/null +++ b/blogContent/projects/steam/faq.html @@ -0,0 +1,275 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + + +
+


+

Frequently asked questions

+
+
+
+ +
+
+
Answer +
+

+ Option 1:
+ If you have steam open, simply click on the profile which you would like to view + from your friends list. The steam ID will be in the url that appears in the steam + browser. + +

+ +

+ Option 2:
+ If you only know the steam username, you can use steam ID look up sites like + STEAMID I/O. For this website, you will want + to use the steamID64 number. + +

+
+
+
+
+ +
+
+ +
+
+
Answer
+ +

Great question, I will get back to you on that one.

+
+
+
+
+ +
+
+ +
+
+
Answer
+ +

Contrary to popular belief, Java is not a terrible language. For this project + I needed something that was easy to work with a Gremlin database -- the tinkerpop framework makes + this very convenient. Overall, I needed a backend because I cannot dish out my steam api key to users. + Plus, this way I can cache steam friends making graph creation times faster.

+ + +
+
+
+
+ + +
+
+ +
+
+
Answer
+ +

Yes. You can read all about Steam's API usage here.

+
+
+
+
+ +
+
+ +
+
+
Answer
+ +

Go for it, all the code is on GitHub. I am going to be making a "comprehensive" docs which + should explain how to run this.

+
+
+
+
+ + +
+
+ +
+
+
Answer
+

Since the server caches all the friend requests in a local graph database, it is possible to + not see all your friends if you recently added them. I am working on a solution to this, + however, I don't want to excessively hammer the steam API if I don't have to.

+
+
+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/blogContent/projects/steam/graph.html b/blogContent/projects/steam/graph.html new file mode 100644 index 0000000..e8a51eb --- /dev/null +++ b/blogContent/projects/steam/graph.html @@ -0,0 +1,432 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + diff --git a/blogContent/projects/steam/graphs.html b/blogContent/projects/steam/graphs.html new file mode 100644 index 0000000..9a56320 --- /dev/null +++ b/blogContent/projects/steam/graphs.html @@ -0,0 +1,214 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + +
+


+ +
+ +
+
+

How To Make a Graph

+
+

Overview

+

Using the form on this page, you enter your steamID and select the + graph type you wish to use and then press generate. This will take you to a + new page where your graph will render. Rendering will take between 1-4 minutes depending + on how many friends you have on your graph. Once all the friends are on the graph, it will + shake bringing it to its "final form".

+

SteamID

+

+ Every steam user has an unique ID. This is NOT your username. + If you have steam open, simply click on the profile which you would like to view + from your friends list. The steam ID will be in the url that appears in the steam + browser. +
+ Steam id in steam browser +

+

Graph Types

+
    +
  • +
    Friends of Friends Graph
    +

    + This graph will display all of your friends in addition to all of their friends. +
    ex:
    + friends of friends steam graph +

    + +
  • +
  • +
    Common Friends Graph
    +

    + This graph will only display your friends, however, it will draw edges between your friends + if they are friends with each other. +
    ex:
    + Common friends steam graph +

    +
  • +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/blogContent/projects/steam/img/404.jpg b/blogContent/projects/steam/img/404.jpg new file mode 100644 index 0000000..2de142b Binary files /dev/null and b/blogContent/projects/steam/img/404.jpg differ diff --git a/blogContent/projects/steam/img/banner.png b/blogContent/projects/steam/img/banner.png new file mode 100644 index 0000000..91f18e4 Binary files /dev/null and b/blogContent/projects/steam/img/banner.png differ diff --git a/blogContent/projects/steam/img/banner2.png b/blogContent/projects/steam/img/banner2.png new file mode 100644 index 0000000..583ed80 Binary files /dev/null and b/blogContent/projects/steam/img/banner2.png differ diff --git a/blogContent/projects/steam/img/faq/faqGraph.png b/blogContent/projects/steam/img/faq/faqGraph.png new file mode 100644 index 0000000..b4cd34f Binary files /dev/null and b/blogContent/projects/steam/img/faq/faqGraph.png differ diff --git a/blogContent/projects/steam/img/faq/java.png b/blogContent/projects/steam/img/faq/java.png new file mode 100644 index 0000000..1fab362 Binary files /dev/null and b/blogContent/projects/steam/img/faq/java.png differ diff --git a/blogContent/projects/steam/img/faq/steamId.png b/blogContent/projects/steam/img/faq/steamId.png new file mode 100644 index 0000000..ea39f01 Binary files /dev/null and b/blogContent/projects/steam/img/faq/steamId.png differ diff --git a/blogContent/projects/steam/img/faq/steamIdOption2.png b/blogContent/projects/steam/img/faq/steamIdOption2.png new file mode 100644 index 0000000..4d6f8c2 Binary files /dev/null and b/blogContent/projects/steam/img/faq/steamIdOption2.png differ diff --git a/blogContent/projects/steam/img/jrtechs1.png b/blogContent/projects/steam/img/jrtechs1.png new file mode 100644 index 0000000..c5a29d7 Binary files /dev/null and b/blogContent/projects/steam/img/jrtechs1.png differ diff --git a/blogContent/projects/steam/img/jrtechs2.png b/blogContent/projects/steam/img/jrtechs2.png new file mode 100644 index 0000000..8e8e903 Binary files /dev/null and b/blogContent/projects/steam/img/jrtechs2.png differ diff --git a/blogContent/projects/steam/img/slider/img1.png b/blogContent/projects/steam/img/slider/img1.png new file mode 100644 index 0000000..db5ce6d Binary files /dev/null and b/blogContent/projects/steam/img/slider/img1.png differ diff --git a/blogContent/projects/steam/img/slider/img2.png b/blogContent/projects/steam/img/slider/img2.png new file mode 100644 index 0000000..aa7e386 Binary files /dev/null and b/blogContent/projects/steam/img/slider/img2.png differ diff --git a/blogContent/projects/steam/img/slider/img3.png b/blogContent/projects/steam/img/slider/img3.png new file mode 100644 index 0000000..acc227c Binary files /dev/null and b/blogContent/projects/steam/img/slider/img3.png differ diff --git a/blogContent/projects/steam/img/slider/img4.png b/blogContent/projects/steam/img/slider/img4.png new file mode 100644 index 0000000..701548e Binary files /dev/null and b/blogContent/projects/steam/img/slider/img4.png differ diff --git a/blogContent/projects/steam/img/slider/img5.png b/blogContent/projects/steam/img/slider/img5.png new file mode 100644 index 0000000..05c7959 Binary files /dev/null and b/blogContent/projects/steam/img/slider/img5.png differ diff --git a/blogContent/projects/steam/img/slider/img6.png b/blogContent/projects/steam/img/slider/img6.png new file mode 100644 index 0000000..bb16538 Binary files /dev/null and b/blogContent/projects/steam/img/slider/img6.png differ diff --git a/blogContent/projects/steam/index.html b/blogContent/projects/steam/index.html new file mode 100644 index 0000000..8ef5261 --- /dev/null +++ b/blogContent/projects/steam/index.html @@ -0,0 +1,249 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + + + + +
+


+ + + + + +
+

Steam Graph Analysis

+
+ +
+
+

This is a project that I threw together during a weekend to play around with + graph databases. This project uses the steam api to acquire people's friends lists; + this information is stored locally in a gremlin database. The client is sent information to + render on their javascript graph as it is pulled from the graph database via a web socket. +

+
+

Try it out!

+ Make a Graph +
+
+ +
+ Diagram +
+ +
+
+

+
+ + +
+
+

Future Additions

+ +
    +
  • Include a steam name to steam id lookup
  • + +
  • Dockerize this entire environment
  • + +
  • Connect the gremlin/janus server to a HBase server for persistent storage
  • + +
  • Get the java web socket to work with ssh -- currently does not work with wss
  • + +
  • Make more graphs to provide more insights + +
      + +
    • Most common friends of friends -- will show you people you may know
    • + +
    • Graph of a larger chunk of the steam community
    • + +
    • Graphs based on common games
    • + +
    • ...
    • +
    +
  • + +
  • Write more documentation on how the system as a whole works.
  • +
+ +

If you are interested in this project, or want to help work on it, check it out + on github.

+
+
+ + +
+ +
+
+ + + + + + + + + + + + + diff --git a/blogContent/projects/steam/profile.html b/blogContent/projects/steam/profile.html new file mode 100644 index 0000000..0c0d599 --- /dev/null +++ b/blogContent/projects/steam/profile.html @@ -0,0 +1,139 @@ + + + + + Jrtechs Steam Friend Graph Project + + + + + + + + + +
+
+ TDLTR + +
+ + + + + + + + + + + + + diff --git a/blogContent/projects/steam/src/RepoJS/README.md b/blogContent/projects/steam/src/RepoJS/README.md new file mode 100644 index 0000000..8b91d20 --- /dev/null +++ b/blogContent/projects/steam/src/RepoJS/README.md @@ -0,0 +1,37 @@ +Repo.js +======= + +[![Build Status](http://img.shields.io/travis/darcyclarke/Repo.js.svg?style=flat-square)](https://travis-ci.org/darcyclarke/Repo.js) +[![Dependency Status](https://david-dm.org/darcyclarke/repo.js/badges.svg?style=flat-square)](https://david-dm.org/darcyclarke/repo.js/badges) +[![devDependency Status](https://david-dm.org/darcyclarke/repo.js/badges/dev-status.svg?style=flat-square)](https://david-dm.org/darcyclarke/repo.js/badges#info=devDependencies) +[![Code Climate](http://img.shields.io/codeclimate/github/darcyclarke/Repo.js.svg?style=flat-square)](https://codeclimate.com/github/darcyclarke/Repo.js) +[![npm](https://img.shields.io/npm/v/repo.js.svg?maxAge=2592000&style=flat-square)](https://www.npmjs.com/package/repo.js) +[![npm](https://img.shields.io/npm/dt/repo.js.svg?maxAge=2592000&style=flat-square)](https://www.npmjs.com/package/repo.js) +[![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://darcyclarke.mit-license.org) +[![Join the chat at https://gitter.im/darcyclarke/Repo.js](http://img.shields.io/:Gitter-Join Chat-orange.svg?style=flat-square)](https://gitter.im/darcyclarke/Repo.js) + +Repo.js is a jQuery plugin that lets you easily embed a Github repo onto your site. This is great for other plugin or library authors that want to showcase the contents of a repo on their project pages. + +Repo.js uses [Markus Ekwall](https://twitter.com/#!/mekwall)'s [jQuery Vangogh](https://github.com/mekwall/jquery-vangogh) plugin for styling of file contents. Vangogh, subsequently, utilizes [highlight.js](https://github.com/isagalaev/highlight.js), written by [Ivan Sagalaev](https://github.com/isagalaev) for syntax highlighting. + +##Example Usage + +```javascript +$('body').repo({ user: 'darcyclarke', name: 'Repo.js' }); +```` + +You can also reference a specific branch if you want: + +```javascript +$('body').repo({ user: 'jquery', name: 'jquery', branch: 'strip_iife' }); +```` + +##License + +Copyright (c) 2016 Darcy Clarke + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/blogContent/projects/steam/src/RepoJS/bower.json b/blogContent/projects/steam/src/RepoJS/bower.json new file mode 100644 index 0000000..c9b7f01 --- /dev/null +++ b/blogContent/projects/steam/src/RepoJS/bower.json @@ -0,0 +1,5 @@ +{ + "name": "Repo.js", + "version": "1.0", + "main": "repo.js" +} diff --git a/blogContent/projects/steam/src/RepoJS/fonts/repo.eot b/blogContent/projects/steam/src/RepoJS/fonts/repo.eot new file mode 100644 index 0000000..2d33e2e Binary files /dev/null and b/blogContent/projects/steam/src/RepoJS/fonts/repo.eot differ diff --git a/blogContent/projects/steam/src/RepoJS/fonts/repo.svg b/blogContent/projects/steam/src/RepoJS/fonts/repo.svg new file mode 100644 index 0000000..b67e13b --- /dev/null +++ b/blogContent/projects/steam/src/RepoJS/fonts/repo.svg @@ -0,0 +1,18 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + \ No newline at end of file diff --git a/blogContent/projects/steam/src/RepoJS/fonts/repo.ttf b/blogContent/projects/steam/src/RepoJS/fonts/repo.ttf new file mode 100644 index 0000000..ce2fa8f Binary files /dev/null and b/blogContent/projects/steam/src/RepoJS/fonts/repo.ttf differ diff --git a/blogContent/projects/steam/src/RepoJS/fonts/repo.woff b/blogContent/projects/steam/src/RepoJS/fonts/repo.woff new file mode 100644 index 0000000..3bc406d Binary files /dev/null and b/blogContent/projects/steam/src/RepoJS/fonts/repo.woff differ diff --git a/blogContent/projects/steam/src/RepoJS/repo.js b/blogContent/projects/steam/src/RepoJS/repo.js new file mode 100644 index 0000000..ec6a720 --- /dev/null +++ b/blogContent/projects/steam/src/RepoJS/repo.js @@ -0,0 +1,296 @@ +/*! + * @mekwall's .vangogh() for Syntax Highlighting + * + * All code is open source and dual licensed under GPL and MIT. + * Check the individual licenses for more information. + * https://github.com/mekwall/jquery-vangogh/blob/master/GPL-LICENSE.txt + * https://github.com/mekwall/jquery-vangogh/blob/master/MIT-LICENSE.txt + */ +(function($,a,b){var c=1,d=!1,e=!1,f={get:function(b){var c=a.location.hash;if(c.length>0){var d=c.match(new RegExp(b+":{([a-zA-Z0-9,-]*)}"));if(d)return d[1].split(",")}return[]},set:function(b,c){var d=a.location.hash,e,f=b+":{"+c.join(",")+"}",g=d.indexOf(b+":{");if(c.length===0)return this.remove(b);g!==-1?e=d.replace(new RegExp("("+b+":{[a-zA-Z0-9,-]*})"),f):e=d.length>0?d+","+f:f,a.location.hash=e},remove:function(b){a.location.hash=a.location.hash.replace(new RegExp("([,]?"+b+":{[a-zA-Z0-9,-]*}[,]?)"),"")}},g={numberRange:/^([0-9]+)-([0-9]+)$/,pageNumber:/-([0-9]+)$/,multilineBegin:/(?:.[^<]*(?!<\/span>)|)$/ig,multilineEnd:/()?(?:.[^<]*)?(<\/span>)/ig};$.fn.vanGogh=function(h){function n(){if(d||e)setTimeout(n,100);else{k++;if(k>=10)return;if(h.source&&!l){e=!0,$.ajax({url:h.source,crossDomain:!0,dataType:"text",success:function(a){l=a},error:function(a,b){l="ERROR: "+b},complete:function(){e=!1,n()}});return}b=b||a.hljs;if(!b){d=!0,$.getScript(h.autoload,function(){d=!1,n()});return}j.filter("pre,code").each(function(){function r(b,c,e){var h=!1,i=a.find(".vg-line");c&&(a.find(".vg-highlight").removeClass("vg-highlight"),f.remove(d),k=[]),b=$.type(b)==="array"?b:[b],$.each(b,function(b,c){if(k.indexOf(c)<=-1){!isNaN(parseFloat(c,10))&&isFinite(c)&&(c=parseInt(c,10));if($.type(c)==="string"){var e=g.numberRange.exec(c);if(e){var f=e[1],h=e[2],j="";for(var b=f;b<=h;b++)j+=",#"+d+"-"+b,k.push(b);i.filter(j.substring(1)).addClass("vg-highlight")}else a.find(".vg-line:contains("+c+")").each(function(){var a=$(this).addClass("vg-highlight");a.html(a.html().replace(c,''+c+""))}),k.push(c)}else{var l=d+"-"+this,m=i.filter("#"+l);m.length&&(m.addClass("vg-highlight"),k.push(c))}}}),!e&&f.set(d,k)}var a=$(this).addClass("vg-container").attr("id",this.id||"vg-"+c++),d=this.id,e=a.find("code"),i=!1,j=!1,k=[];e.length||(e=a,i=!0),h.source&&l&&e.text(l);var n=e.text();b.highlightBlock(e[0],h.tab);var o=e.html().split("\n"),p="",q="";if(!i){var s={},t=0;$.each(o,function(a,b){var c=a+h.firstLine,e=d+"-"+c,f=b;h.numbers&&(p+=''+c+"");if(s[t]){var i=g.multilineEnd.exec(b);i&&!i[1]?(f=''+f,delete s[t],t--):f=''+f+""}var j=g.multilineBegin.exec(b);j&&(t++,s[t]=j[1]),q+='
'+f+"
"}),q=''+q+"",h.numbers&&(q='
'+p+"
"+q),a.html(q),e=a.find("code"),a.find(".vg-number").click(function(b){var c=$(this),e=c.attr("rel"),h=a.find(e);if(h.hasClass("vg-highlight")){h.removeClass("vg-highlight"),k.splice(k.indexOf(c.text()),1),f.set(d,k),j=!1;return!1}var i=j;j=parseInt(g.pageNumber.exec(e)[1],10),b.shiftKey&&j?r(iv){if(this.scrollLeft0){var y=a.find(".vg-line").height(),z=parseInt(e.css("paddingTop")),A=y*(h.maxLines+1)+z;a.css({minHeight:y+z,maxHeight:A})}h.highlight&&r(h.highlight,!0,!0);var B=f.get(d);B.length&&r(B,!1,!0)})}}function m(b){var c=a,d=a.document;if(d.body.createTextRange){var e=d.body.createTextRange();e.moveToElementText(b),e.select()}else if(d.createRange){var f=c.getSelection(),e=d.createRange();e.selectNodeContents(b),f.removeAllRanges(),f.addRange(e)}}var i={language:"auto",firstLine:1,maxLines:0,numbers:!0,highlight:null,animateGutter:!0,autoload:"http://softwaremaniacs.org/media/soft/highlight/highlight.pack.js",tab:" "};h=$.extend({},i,h);var j=this,k=0,l;n();return j}})(jQuery,this,typeof this.hljs!="undefined"?this.hljs:!1); + +/*! + * Repo.js + * @author Darcy Clarke + * + * Copyright (c) 2012 Darcy Clarke + * Dual licensed under the MIT and GPL licenses. + * http://darcyclarke.me/ + */ + (function($){ + + // Github repo + $.fn.repo = function( options ){ + + // Context and Base64 methods + var _this = this, + keyStr64 = "ABCDEFGHIJKLMNOP" + "QRSTUVWXYZabcdef" + "ghijklmnopqrstuv" + "wxyz0123456789+/" + "=", + encode64 = function(a){a=escape(a);var b="";var c,d,e="";var f,g,h,i="";var j=0;do{c=a.charCodeAt(j++);d=a.charCodeAt(j++);e=a.charCodeAt(j++);f=c>>2;g=(c&3)<<4|d>>4;h=(d&15)<<2|e>>6;i=e&63;if(isNaN(d)){h=i=64}else if(isNaN(e)){i=64}b=b+keyStr64.charAt(f)+keyStr64.charAt(g)+keyStr64.charAt(h)+keyStr64.charAt(i);c=d=e="";f=g=h=i=""}while(j>4;d=(g&15)<<4|h>>2;e=(h&3)<<6|i;b=b+String.fromCharCode(c);if(h!=64){b=b+String.fromCharCode(d)}if(i!=64){b=b+String.fromCharCode(e)}c=d=e="";f=g=h=i=""}while(j').html(_this.settings.css)); + + // Query Github Tree API + $.ajax({ + url: 'https://api.github.com/repos/' + _this.settings.user + '/' + _this.settings.name + '/git/trees/' + _this.settings.branch + '?recursive=1', + type: 'GET', + data: {}, + dataType: 'jsonp', + success: function(response){ + + var treeLength = response.data.tree.length; + $.each(response.data.tree, function(i){ + + // Setup if last element + if(!--treeLength){ + _this.container.addClass('loaded'); + // Add 10ms timeout here as some browsers require a bit of time before calculating height. + setTimeout( function(){ + transition(_this.container.find('.page').first(), 'left', true); + }, 10 ); + } + + // Return if data is not a file + if(this.type != 'blob') + return; + + // Setup defaults + var first = _this.container.find('.page').first() + ctx = _this.repo, + output = first, + path = this.path, + arr = path.split('/'), + file = arr[(arr.length - 1)], + id = ''; + + // Remove file from array + arr = arr.slice(0,-1); + id = _this.namespace; + + // Loop through folders + $.each(arr, function(i){ + + var name = String(this), + index = 0, + exists = false; + + id = id + '_split_' + name.replace('.','_dot_'); + + // Loop through folders and check names + $.each(ctx.folders, function(i){ + if(this.name == name){ + index = i; + exists = true; + } + }); + + // Create folder if it doesn't exist + if(!exists){ + + // Append folder to DOM + if(output !== first){ + output.find('ul li.back').after($('
  • ' + name +'
  • ')); + } else { + output.find('ul li').first().after($('
  • ' + name +'
  • ')); + } + + // Add folder to repo object + ctx.folders.push({ + name : name, + folders : [], + files : [], + element : $('
    ').appendTo(_this.container)[0] + }); + index = ctx.folders.length-1; + + } + + // Change context & output to the proper folder + output = $(ctx.folders[index].element); + ctx = ctx.folders[index]; + + }); + + // Append file to DOM + output.find('ul').append($('
  • ' + file +'
  • ')); + + // Add file to the repo object + ctx.files.push(file); + + }); + + // Bind to page links + _this.container.on('click', 'a', function(e){ + + e.preventDefault(); + + var link = $(this), + parent = link.parents('li'), + page = link.parents('.page'), + repo = link.parents('.repo'), + el = $('#' + link.data('id')); + + // Is link a file + if(parent.hasClass('file')){ + + el = $('#' + link.data('id')); + + if(el.legnth > 0){ + el.addClass('active'); + } else { + $.ajax({ + url: 'https://api.github.com/repos/' + _this.settings.user + '/' + _this.settings.name + '/contents/' + link.data('path') + '?ref=' + _this.settings.branch, + type: 'GET', + data: {}, + dataType: 'jsonp', + success: function(response){ + var fileContainer = $('
    '), + extension = response.data.name.split('.').pop().toLowerCase(), + mimeType = getMimeTypeByExtension(extension); + + if('image' === mimeType.split('/').shift()){ + el = fileContainer.append($('
    ')).appendTo(repo); + el.find('img') + .attr('src', 'data:' + mimeType + ';base64,' + response.data.content) + .attr('alt', response.data.name); + } + else { + el = fileContainer.append($('
    ')).appendTo(repo); + if(typeof _this.extensions[extension] != 'undefined') + el.find('code').addClass(_this.extensions[extension]); + el.find('code').html(String(decode64(response.data.content)).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')); + el.find('pre').vanGogh(); + } + + transition(el, 'left'); + }, + error: function(response){ + if(console && console.log) + console.log('Request Error:', e); + } + }); + } + + // Is link a folder + } else if(parent.hasClass('dir')) { + + _this.container + .find('h1') + .find('.active') + .removeClass('active') + .end() + .append('' + link.text() + ''); + transition(el, 'left'); + + // Is link a back link + } else if(parent.hasClass('back')){ + + _this.container.find('h1 a').last().remove(); + el = page[0].id.split('_split_').slice(0,-1).join('_split_'); + el = (el == _this.namespace) ? _this.container.find('.page').first() : $('#' + el); + transition(el, 'right'); + + // Is nav link + } else { + el = el.length ? el : _this.container.find('.page').eq(link.index()); + + if(link[0] !== _this.container.find('h1 a')[0]) + link.addClass('active'); + _this.container.find('h1 a').slice((link.index()+1),_this.container.find('h1 a').length).remove(); + transition(el, 'right'); + } + }); + }, + error : function(response){ + if(console && console.log) + console.log('Request Error:', response); + } + }); + + // Setup repo container + return this.each(function(){ + _this.container = $('').appendTo($(this)); + }); + }; + +})(jQuery); + diff --git a/blogContent/projects/steam/src/RepoJS/repo.min.js b/blogContent/projects/steam/src/RepoJS/repo.min.js new file mode 100644 index 0000000..5a3beb9 --- /dev/null +++ b/blogContent/projects/steam/src/RepoJS/repo.min.js @@ -0,0 +1,29 @@ +/*! + * @mekwall's .vangogh() for Syntax Highlighting + * + * All code is open source and dual licensed under GPL and MIT. + * Check the individual licenses for more information. + * https://github.com/mekwall/jquery-vangogh/blob/master/GPL-LICENSE.txt + * https://github.com/mekwall/jquery-vangogh/blob/master/MIT-LICENSE.txt + */ +(function($,a,b){var c=1,d=!1,e=!1,f={get:function(b){var c=a.location.hash;if(c.length>0){var d=c.match(new RegExp(b+":{([a-zA-Z0-9,-]*)}"));if(d)return d[1].split(",")}return[]},set:function(b,c){var d=a.location.hash,e,f=b+":{"+c.join(",")+"}",g=d.indexOf(b+":{");if(c.length===0)return this.remove(b);g!==-1?e=d.replace(new RegExp("("+b+":{[a-zA-Z0-9,-]*})"),f):e=d.length>0?d+","+f:f,a.location.hash=e},remove:function(b){a.location.hash=a.location.hash.replace(new RegExp("([,]?"+b+":{[a-zA-Z0-9,-]*}[,]?)"),"")}},g={numberRange:/^([0-9]+)-([0-9]+)$/,pageNumber:/-([0-9]+)$/,multilineBegin:/(?:.[^<]*(?!<\/span>)|)$/ig,multilineEnd:/()?(?:.[^<]*)?(<\/span>)/ig};$.fn.vanGogh=function(h){function n(){if(d||e)setTimeout(n,100);else{k++;if(k>=10)return;if(h.source&&!l){e=!0,$.ajax({url:h.source,crossDomain:!0,dataType:"text",success:function(a){l=a},error:function(a,b){l="ERROR: "+b},complete:function(){e=!1,n()}});return}b=b||a.hljs;if(!b){d=!0,$.getScript(h.autoload,function(){d=!1,n()});return}j.filter("pre,code").each(function(){function r(b,c,e){var h=!1,i=a.find(".vg-line");c&&(a.find(".vg-highlight").removeClass("vg-highlight"),f.remove(d),k=[]),b=$.type(b)==="array"?b:[b],$.each(b,function(b,c){if(k.indexOf(c)<=-1){!isNaN(parseFloat(c,10))&&isFinite(c)&&(c=parseInt(c,10));if($.type(c)==="string"){var e=g.numberRange.exec(c);if(e){var f=e[1],h=e[2],j="";for(var b=f;b<=h;b++)j+=",#"+d+"-"+b,k.push(b);i.filter(j.substring(1)).addClass("vg-highlight")}else a.find(".vg-line:contains("+c+")").each(function(){var a=$(this).addClass("vg-highlight");a.html(a.html().replace(c,''+c+""))}),k.push(c)}else{var l=d+"-"+this,m=i.filter("#"+l);m.length&&(m.addClass("vg-highlight"),k.push(c))}}}),!e&&f.set(d,k)}var a=$(this).addClass("vg-container").attr("id",this.id||"vg-"+c++),d=this.id,e=a.find("code"),i=!1,j=!1,k=[];e.length||(e=a,i=!0),h.source&&l&&e.text(l);var n=e.text();b.highlightBlock(e[0],h.tab);var o=e.html().split("\n"),p="",q="";if(!i){var s={},t=0;$.each(o,function(a,b){var c=a+h.firstLine,e=d+"-"+c,f=b;h.numbers&&(p+=''+c+"");if(s[t]){var i=g.multilineEnd.exec(b);i&&!i[1]?(f=''+f,delete s[t],t--):f=''+f+""}var j=g.multilineBegin.exec(b);j&&(t++,s[t]=j[1]),q+='
    '+f+"
    "}),q=''+q+"",h.numbers&&(q='
    '+p+"
    "+q),a.html(q),e=a.find("code"),a.find(".vg-number").click(function(b){var c=$(this),e=c.attr("rel"),h=a.find(e);if(h.hasClass("vg-highlight")){h.removeClass("vg-highlight"),k.splice(k.indexOf(c.text()),1),f.set(d,k),j=!1;return!1}var i=j;j=parseInt(g.pageNumber.exec(e)[1],10),b.shiftKey&&j?r(iv){if(this.scrollLeft0){var y=a.find(".vg-line").height(),z=parseInt(e.css("paddingTop")),A=y*(h.maxLines+1)+z;a.css({minHeight:y+z,maxHeight:A})}h.highlight&&r(h.highlight,!0,!0);var B=f.get(d);B.length&&r(B,!1,!0)})}}function m(b){var c=a,d=a.document;if(d.body.createTextRange){var e=d.body.createTextRange();e.moveToElementText(b),e.select()}else if(d.createRange){var f=c.getSelection(),e=d.createRange();e.selectNodeContents(b),f.removeAllRanges(),f.addRange(e)}}var i={language:"auto",firstLine:1,maxLines:0,numbers:!0,highlight:null,animateGutter:!0,autoload:"http://softwaremaniacs.org/media/soft/highlight/highlight.pack.js",tab:" "};h=$.extend({},i,h);var j=this,k=0,l;n();return j}})(jQuery,this,typeof this.hljs!="undefined"?this.hljs:!1); + +/*! + * Repo.js + * @author Darcy Clarke + * + * Copyright (c) 2012 Darcy Clarke + * Dual licensed under the MIT and GPL licenses. + * http://darcyclarke.me/ + */ +(function(a){a.fn.repo=function(j){var g=this,b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",i=function(t){t=escape(t);var s="";var r,q,p="";var o,n,m,l="";var k=0;do{r=t.charCodeAt(k++);q=t.charCodeAt(k++);p=t.charCodeAt(k++);o=r>>2;n=(r&3)<<4|q>>4;m=(q&15)<<2|p>>6;l=p&63;if(isNaN(q)){m=l=64}else{if(isNaN(p)){l=64}}s=s+b.charAt(o)+b.charAt(n)+b.charAt(m)+b.charAt(l);r=q=p="";o=n=m=l=""}while(k>4;s=(p&15)<<4|o>>2;r=(o&3)<<6|n;u=u+String.fromCharCode(t);if(o!=64){u=u+String.fromCharCode(s)}if(n!=64){u=u+String.fromCharCode(r)}t=s=r="";q=p=o=n=""}while(m').html(g.settings.css))}a.ajax({url:"https://api.github.com/repos/"+g.settings.user+"/"+g.settings.name+"/git/trees/"+g.settings.branch+"?recursive=1",type:"GET",data:{},dataType:"jsonp",success:function(k){var l=k.data.tree.length; +a.each(k.data.tree,function(m){if(!--l){g.container.addClass("loaded");setTimeout(function(){h(g.container.find(".page").first(),"left",true)},10)}if(this.type!="blob"){return}var n=g.container.find(".page").first();ctx=g.repo,output=n,path=this.path,arr=path.split("/"),file=arr[(arr.length-1)],id="";arr=arr.slice(0,-1);id=g.namespace;a.each(arr,function(q){var p=String(this),o=0,r=false;id=id+"_split_"+p.replace(".","_dot_");a.each(ctx.folders,function(s){if(this.name==p){o=s;r=true}});if(!r){if(output!==n){output.find("ul li.back").after(a('
  • '+p+"
  • ")) +}else{output.find("ul li").first().after(a('
  • '+p+"
  • "))}ctx.folders.push({name:p,folders:[],files:[],element:a('
    ').appendTo(g.container)[0]});o=ctx.folders.length-1}output=a(ctx.folders[o].element);ctx=ctx.folders[o]});output.find("ul").append(a('
  • '+file+"
  • ")); +ctx.files.push(file)});g.container.on("click","a",function(r){r.preventDefault();var p=a(this),o=p.parents("li"),q=p.parents(".page"),m=p.parents(".repo"),n=a("#"+p.data("id"));if(o.hasClass("file")){n=a("#"+p.data("id"));if(n.legnth>0){n.addClass("active")}else{a.ajax({url:"https://api.github.com/repos/"+g.settings.user+"/"+g.settings.name+"/contents/"+p.data("path")+"?ref="+g.settings.branch,type:"GET",data:{},dataType:"jsonp",success:function(t){var s=a('
    '),v=t.data.name.split(".").pop().toLowerCase(),u=e(v); +if("image"===u.split("/").shift()){n=s.append(a('
    ')).appendTo(m);n.find("img").attr("src","data:"+u+";base64,"+t.data.content).attr("alt",t.data.name)}else{n=s.append(a("
    ")).appendTo(m);if(typeof g.extensions[v]!="undefined"){n.find("code").addClass(g.extensions[v])}n.find("code").html(String(c(t.data.content)).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""));n.find("pre").vanGogh() +}h(n,"left")},error:function(s){if(console&&console.log){console.log("Request Error:",r)}}})}}else{if(o.hasClass("dir")){g.container.find("h1").find(".active").removeClass("active").end().append(''+p.text()+"");h(n,"left")}else{if(o.hasClass("back")){g.container.find("h1 a").last().remove();n=q[0].id.split("_split_").slice(0,-1).join("_split_");n=(n==g.namespace)?g.container.find(".page").first():a("#"+n);h(n,"right")}else{n=n.length?n:g.container.find(".page").eq(p.index()); +if(p[0]!==g.container.find("h1 a")[0]){p.addClass("active")}g.container.find("h1 a").slice((p.index()+1),g.container.find("h1 a").length).remove();h(n,"right")}}}})},error:function(k){if(console&&console.log){console.log("Request Error:",k)}}});return this.each(function(){g.container=a('').appendTo(a(this)) +})}})(jQuery); diff --git a/blogContent/projects/steam/src/captors/sigma.captors.mouse.js b/blogContent/projects/steam/src/captors/sigma.captors.mouse.js new file mode 100644 index 0000000..e3a40c5 --- /dev/null +++ b/blogContent/projects/steam/src/captors/sigma.captors.mouse.js @@ -0,0 +1,349 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.captors'); + + /** + * The user inputs default captor. It deals with mouse events, keyboards + * events and touch events. + * + * @param {DOMElement} target The DOM element where the listeners will be + * bound. + * @param {camera} camera The camera related to the target. + * @param {configurable} settings The settings function. + * @return {sigma.captor} The fresh new captor instance. + */ + sigma.captors.mouse = function(target, camera, settings) { + var _self = this, + _target = target, + _camera = camera, + _settings = settings, + + // CAMERA MANAGEMENT: + // ****************** + // The camera position when the user starts dragging: + _startCameraX, + _startCameraY, + _startCameraAngle, + + // The latest stage position: + _lastCameraX, + _lastCameraY, + _lastCameraAngle, + _lastCameraRatio, + + // MOUSE MANAGEMENT: + // ***************** + // The mouse position when the user starts dragging: + _startMouseX, + _startMouseY, + + _isMouseDown, + _isMoving, + _hasDragged, + _downStartTime, + _movingTimeoutId; + + sigma.classes.dispatcher.extend(this); + + sigma.utils.doubleClick(_target, 'click', _doubleClickHandler); + _target.addEventListener('DOMMouseScroll', _wheelHandler, false); + _target.addEventListener('mousewheel', _wheelHandler, false); + _target.addEventListener('mousemove', _moveHandler, false); + _target.addEventListener('mousedown', _downHandler, false); + _target.addEventListener('click', _clickHandler, false); + _target.addEventListener('mouseout', _outHandler, false); + document.addEventListener('mouseup', _upHandler, false); + + + + + /** + * This method unbinds every handlers that makes the captor work. + */ + this.kill = function() { + sigma.utils.unbindDoubleClick(_target, 'click'); + _target.removeEventListener('DOMMouseScroll', _wheelHandler); + _target.removeEventListener('mousewheel', _wheelHandler); + _target.removeEventListener('mousemove', _moveHandler); + _target.removeEventListener('mousedown', _downHandler); + _target.removeEventListener('click', _clickHandler); + _target.removeEventListener('mouseout', _outHandler); + document.removeEventListener('mouseup', _upHandler); + }; + + + + + // MOUSE EVENTS: + // ************* + + /** + * The handler listening to the 'move' mouse event. It will effectively + * drag the graph. + * + * @param {event} e A mouse event. + */ + function _moveHandler(e) { + var x, + y, + pos; + + // Dispatch event: + if (_settings('mouseEnabled')) { + _self.dispatchEvent('mousemove', + sigma.utils.mouseCoords(e)); + + if (_isMouseDown) { + _isMoving = true; + _hasDragged = true; + + if (_movingTimeoutId) + clearTimeout(_movingTimeoutId); + + _movingTimeoutId = setTimeout(function() { + _isMoving = false; + }, _settings('dragTimeout')); + + sigma.misc.animation.killAll(_camera); + + _camera.isMoving = true; + pos = _camera.cameraPosition( + sigma.utils.getX(e) - _startMouseX, + sigma.utils.getY(e) - _startMouseY, + true + ); + + x = _startCameraX - pos.x; + y = _startCameraY - pos.y; + + if (x !== _camera.x || y !== _camera.y) { + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + + _camera.goTo({ + x: x, + y: y + }); + } + + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + + e.stopPropagation(); + return false; + } + } + } + + /** + * The handler listening to the 'up' mouse event. It will stop dragging the + * graph. + * + * @param {event} e A mouse event. + */ + function _upHandler(e) { + if (_settings('mouseEnabled') && _isMouseDown) { + _isMouseDown = false; + if (_movingTimeoutId) + clearTimeout(_movingTimeoutId); + + _camera.isMoving = false; + + var x = sigma.utils.getX(e), + y = sigma.utils.getY(e); + + if (_isMoving) { + sigma.misc.animation.killAll(_camera); + sigma.misc.animation.camera( + _camera, + { + x: _camera.x + + _settings('mouseInertiaRatio') * (_camera.x - _lastCameraX), + y: _camera.y + + _settings('mouseInertiaRatio') * (_camera.y - _lastCameraY) + }, + { + easing: 'quadraticOut', + duration: _settings('mouseInertiaDuration') + } + ); + } else if ( + _startMouseX !== x || + _startMouseY !== y + ) + _camera.goTo({ + x: _camera.x, + y: _camera.y + }); + + _self.dispatchEvent('mouseup', + sigma.utils.mouseCoords(e)); + + // Update _isMoving flag: + _isMoving = false; + } + } + + /** + * The handler listening to the 'down' mouse event. It will start observing + * the mouse position for dragging the graph. + * + * @param {event} e A mouse event. + */ + function _downHandler(e) { + if (_settings('mouseEnabled')) { + _startCameraX = _camera.x; + _startCameraY = _camera.y; + + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + + _startMouseX = sigma.utils.getX(e); + _startMouseY = sigma.utils.getY(e); + + _hasDragged = false; + _downStartTime = (new Date()).getTime(); + + switch (e.which) { + case 2: + // Middle mouse button pressed + // Do nothing. + break; + case 3: + // Right mouse button pressed + _self.dispatchEvent('rightclick', + sigma.utils.mouseCoords(e, _startMouseX, _startMouseY)); + break; + // case 1: + default: + // Left mouse button pressed + _isMouseDown = true; + + _self.dispatchEvent('mousedown', + sigma.utils.mouseCoords(e, _startMouseX, _startMouseY)); + } + } + } + + /** + * The handler listening to the 'out' mouse event. It will just redispatch + * the event. + * + * @param {event} e A mouse event. + */ + function _outHandler(e) { + if (_settings('mouseEnabled')) + _self.dispatchEvent('mouseout'); + } + + /** + * The handler listening to the 'click' mouse event. It will redispatch the + * click event, but with normalized X and Y coordinates. + * + * @param {event} e A mouse event. + */ + function _clickHandler(e) { + if (_settings('mouseEnabled')) { + var event = sigma.utils.mouseCoords(e); + event.isDragging = + (((new Date()).getTime() - _downStartTime) > 100) && _hasDragged; + _self.dispatchEvent('click', event); + } + + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + + e.stopPropagation(); + return false; + } + + /** + * The handler listening to the double click custom event. It will + * basically zoom into the graph. + * + * @param {event} e A mouse event. + */ + function _doubleClickHandler(e) { + var pos, + ratio, + animation; + + if (_settings('mouseEnabled')) { + ratio = 1 / _settings('doubleClickZoomingRatio'); + + _self.dispatchEvent('doubleclick', + sigma.utils.mouseCoords(e, _startMouseX, _startMouseY)); + + if (_settings('doubleClickEnabled')) { + pos = _camera.cameraPosition( + sigma.utils.getX(e) - sigma.utils.getCenter(e).x, + sigma.utils.getY(e) - sigma.utils.getCenter(e).y, + true + ); + + animation = { + duration: _settings('doubleClickZoomDuration') + }; + + sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation); + } + + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + + e.stopPropagation(); + return false; + } + } + + /** + * The handler listening to the 'wheel' mouse event. It will basically zoom + * in or not into the graph. + * + * @param {event} e A mouse event. + */ + function _wheelHandler(e) { + var pos, + ratio, + animation, + wheelDelta = sigma.utils.getDelta(e); + + if (_settings('mouseEnabled') && _settings('mouseWheelEnabled') && wheelDelta !== 0) { + ratio = wheelDelta > 0 ? + 1 / _settings('zoomingRatio') : + _settings('zoomingRatio'); + + pos = _camera.cameraPosition( + sigma.utils.getX(e) - sigma.utils.getCenter(e).x, + sigma.utils.getY(e) - sigma.utils.getCenter(e).y, + true + ); + + animation = { + duration: _settings('mouseZoomDuration') + }; + + sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation); + + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + + e.stopPropagation(); + return false; + } + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/captors/sigma.captors.touch.js b/blogContent/projects/steam/src/captors/sigma.captors.touch.js new file mode 100644 index 0000000..0e4d987 --- /dev/null +++ b/blogContent/projects/steam/src/captors/sigma.captors.touch.js @@ -0,0 +1,410 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.captors'); + + /** + * The user inputs default captor. It deals with mouse events, keyboards + * events and touch events. + * + * @param {DOMElement} target The DOM element where the listeners will be + * bound. + * @param {camera} camera The camera related to the target. + * @param {configurable} settings The settings function. + * @return {sigma.captor} The fresh new captor instance. + */ + sigma.captors.touch = function(target, camera, settings) { + var _self = this, + _target = target, + _camera = camera, + _settings = settings, + + // CAMERA MANAGEMENT: + // ****************** + // The camera position when the user starts dragging: + _startCameraX, + _startCameraY, + _startCameraAngle, + _startCameraRatio, + + // The latest stage position: + _lastCameraX, + _lastCameraY, + _lastCameraAngle, + _lastCameraRatio, + + // TOUCH MANAGEMENT: + // ***************** + // Touches that are down: + _downTouches = [], + + _startTouchX0, + _startTouchY0, + _startTouchX1, + _startTouchY1, + _startTouchAngle, + _startTouchDistance, + + _touchMode, + + _isMoving, + _doubleTap, + _movingTimeoutId; + + sigma.classes.dispatcher.extend(this); + + sigma.utils.doubleClick(_target, 'touchstart', _doubleTapHandler); + _target.addEventListener('touchstart', _handleStart, false); + _target.addEventListener('touchend', _handleLeave, false); + _target.addEventListener('touchcancel', _handleLeave, false); + _target.addEventListener('touchleave', _handleLeave, false); + _target.addEventListener('touchmove', _handleMove, false); + + function position(e) { + var offset = sigma.utils.getOffset(_target); + + return { + x: e.pageX - offset.left, + y: e.pageY - offset.top + }; + } + + /** + * This method unbinds every handlers that makes the captor work. + */ + this.kill = function() { + sigma.utils.unbindDoubleClick(_target, 'touchstart'); + _target.addEventListener('touchstart', _handleStart); + _target.addEventListener('touchend', _handleLeave); + _target.addEventListener('touchcancel', _handleLeave); + _target.addEventListener('touchleave', _handleLeave); + _target.addEventListener('touchmove', _handleMove); + }; + + // TOUCH EVENTS: + // ************* + /** + * The handler listening to the 'touchstart' event. It will set the touch + * mode ("_touchMode") and start observing the user touch moves. + * + * @param {event} e A touch event. + */ + function _handleStart(e) { + if (_settings('touchEnabled')) { + var x0, + x1, + y0, + y1, + pos0, + pos1; + + _downTouches = e.touches; + + switch (_downTouches.length) { + case 1: + _camera.isMoving = true; + _touchMode = 1; + + _startCameraX = _camera.x; + _startCameraY = _camera.y; + + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + + pos0 = position(_downTouches[0]); + _startTouchX0 = pos0.x; + _startTouchY0 = pos0.y; + + break; + case 2: + _camera.isMoving = true; + _touchMode = 2; + + pos0 = position(_downTouches[0]); + pos1 = position(_downTouches[1]); + x0 = pos0.x; + y0 = pos0.y; + x1 = pos1.x; + y1 = pos1.y; + + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + + _startCameraAngle = _camera.angle; + _startCameraRatio = _camera.ratio; + + _startCameraX = _camera.x; + _startCameraY = _camera.y; + + _startTouchX0 = x0; + _startTouchY0 = y0; + _startTouchX1 = x1; + _startTouchY1 = y1; + + _startTouchAngle = Math.atan2( + _startTouchY1 - _startTouchY0, + _startTouchX1 - _startTouchX0 + ); + _startTouchDistance = Math.sqrt( + (_startTouchY1 - _startTouchY0) * + (_startTouchY1 - _startTouchY0) + + (_startTouchX1 - _startTouchX0) * + (_startTouchX1 - _startTouchX0) + ); + + e.preventDefault(); + return false; + } + } + } + + /** + * The handler listening to the 'touchend', 'touchcancel' and 'touchleave' + * event. It will update the touch mode if there are still at least one + * finger, and stop dragging else. + * + * @param {event} e A touch event. + */ + function _handleLeave(e) { + if (_settings('touchEnabled')) { + _downTouches = e.touches; + var inertiaRatio = _settings('touchInertiaRatio'); + + if (_movingTimeoutId) { + _isMoving = false; + clearTimeout(_movingTimeoutId); + } + + switch (_touchMode) { + case 2: + if (e.touches.length === 1) { + _handleStart(e); + + e.preventDefault(); + break; + } + /* falls through */ + case 1: + _camera.isMoving = false; + _self.dispatchEvent('stopDrag'); + + if (_isMoving) { + _doubleTap = false; + sigma.misc.animation.camera( + _camera, + { + x: _camera.x + + inertiaRatio * (_camera.x - _lastCameraX), + y: _camera.y + + inertiaRatio * (_camera.y - _lastCameraY) + }, + { + easing: 'quadraticOut', + duration: _settings('touchInertiaDuration') + } + ); + } + + _isMoving = false; + _touchMode = 0; + break; + } + } + } + + /** + * The handler listening to the 'touchmove' event. It will effectively drag + * the graph, and eventually zooms and turn it if the user is using two + * fingers. + * + * @param {event} e A touch event. + */ + function _handleMove(e) { + if (!_doubleTap && _settings('touchEnabled')) { + var x0, + x1, + y0, + y1, + cos, + sin, + end, + pos0, + pos1, + diff, + start, + dAngle, + dRatio, + newStageX, + newStageY, + newStageRatio, + newStageAngle; + + _downTouches = e.touches; + _isMoving = true; + + if (_movingTimeoutId) + clearTimeout(_movingTimeoutId); + + _movingTimeoutId = setTimeout(function() { + _isMoving = false; + }, _settings('dragTimeout')); + + switch (_touchMode) { + case 1: + pos0 = position(_downTouches[0]); + x0 = pos0.x; + y0 = pos0.y; + + diff = _camera.cameraPosition( + x0 - _startTouchX0, + y0 - _startTouchY0, + true + ); + + newStageX = _startCameraX - diff.x; + newStageY = _startCameraY - diff.y; + + if (newStageX !== _camera.x || newStageY !== _camera.y) { + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + + _camera.goTo({ + x: newStageX, + y: newStageY + }); + + _self.dispatchEvent('mousemove', + sigma.utils.mouseCoords(e, pos0.x, pos0.y)); + + _self.dispatchEvent('drag'); + } + break; + case 2: + pos0 = position(_downTouches[0]); + pos1 = position(_downTouches[1]); + x0 = pos0.x; + y0 = pos0.y; + x1 = pos1.x; + y1 = pos1.y; + + start = _camera.cameraPosition( + (_startTouchX0 + _startTouchX1) / 2 - + sigma.utils.getCenter(e).x, + (_startTouchY0 + _startTouchY1) / 2 - + sigma.utils.getCenter(e).y, + true + ); + end = _camera.cameraPosition( + (x0 + x1) / 2 - sigma.utils.getCenter(e).x, + (y0 + y1) / 2 - sigma.utils.getCenter(e).y, + true + ); + + dAngle = Math.atan2(y1 - y0, x1 - x0) - _startTouchAngle; + dRatio = Math.sqrt( + (y1 - y0) * (y1 - y0) + (x1 - x0) * (x1 - x0) + ) / _startTouchDistance; + + // Translation: + x0 = start.x; + y0 = start.y; + + // Homothetic transformation: + newStageRatio = _startCameraRatio / dRatio; + x0 = x0 * dRatio; + y0 = y0 * dRatio; + + // Rotation: + newStageAngle = _startCameraAngle - dAngle; + cos = Math.cos(-dAngle); + sin = Math.sin(-dAngle); + x1 = x0 * cos + y0 * sin; + y1 = y0 * cos - x0 * sin; + x0 = x1; + y0 = y1; + + // Finalize: + newStageX = x0 - end.x + _startCameraX; + newStageY = y0 - end.y + _startCameraY; + + if ( + newStageRatio !== _camera.ratio || + newStageAngle !== _camera.angle || + newStageX !== _camera.x || + newStageY !== _camera.y + ) { + _lastCameraX = _camera.x; + _lastCameraY = _camera.y; + _lastCameraAngle = _camera.angle; + _lastCameraRatio = _camera.ratio; + + _camera.goTo({ + x: newStageX, + y: newStageY, + angle: newStageAngle, + ratio: newStageRatio + }); + + _self.dispatchEvent('drag'); + } + + break; + } + + e.preventDefault(); + return false; + } + } + + /** + * The handler listening to the double tap custom event. It will + * basically zoom into the graph. + * + * @param {event} e A touch event. + */ + function _doubleTapHandler(e) { + var pos, + ratio, + animation; + + if (e.touches && e.touches.length === 1 && _settings('touchEnabled')) { + _doubleTap = true; + + ratio = 1 / _settings('doubleClickZoomingRatio'); + + pos = position(e.touches[0]); + _self.dispatchEvent('doubleclick', + sigma.utils.mouseCoords(e, pos.x, pos.y)); + + if (_settings('doubleClickEnabled')) { + pos = _camera.cameraPosition( + pos.x - sigma.utils.getCenter(e).x, + pos.y - sigma.utils.getCenter(e).y, + true + ); + + animation = { + duration: _settings('doubleClickZoomDuration'), + onComplete: function() { + _doubleTap = false; + } + }; + + sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation); + } + + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; + + e.stopPropagation(); + return false; + } + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.camera.js b/blogContent/projects/steam/src/classes/sigma.classes.camera.js new file mode 100644 index 0000000..dcc4c52 --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.camera.js @@ -0,0 +1,240 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.utils.pkg('sigma.classes'); + + /** + * The camera constructor. It just initializes its attributes and methods. + * + * @param {string} id The id. + * @param {sigma.classes.graph} graph The graph. + * @param {configurable} settings The settings function. + * @param {?object} options Eventually some overriding options. + * @return {camera} Returns the fresh new camera instance. + */ + sigma.classes.camera = function(id, graph, settings, options) { + sigma.classes.dispatcher.extend(this); + + Object.defineProperty(this, 'graph', { + value: graph + }); + Object.defineProperty(this, 'id', { + value: id + }); + Object.defineProperty(this, 'readPrefix', { + value: 'read_cam' + id + ':' + }); + Object.defineProperty(this, 'prefix', { + value: 'cam' + id + ':' + }); + + this.x = 0; + this.y = 0; + this.ratio = 1; + this.angle = 0; + this.isAnimated = false; + this.settings = (typeof options === 'object' && options) ? + settings.embedObject(options) : + settings; + }; + + /** + * Updates the camera position. + * + * @param {object} coordinates The new coordinates object. + * @return {camera} Returns the camera. + */ + sigma.classes.camera.prototype.goTo = function(coordinates) { + if (!this.settings('enableCamera')) + return this; + + var i, + l, + c = coordinates || {}, + keys = ['x', 'y', 'ratio', 'angle']; + + for (i = 0, l = keys.length; i < l; i++) + if (c[keys[i]] !== undefined) { + if (typeof c[keys[i]] === 'number' && !isNaN(c[keys[i]])) + this[keys[i]] = c[keys[i]]; + else + throw 'Value for "' + keys[i] + '" is not a number.'; + } + + this.dispatchEvent('coordinatesUpdated'); + return this; + }; + + /** + * This method takes a graph and computes for each node and edges its + * coordinates relatively to the center of the camera. Basically, it will + * compute the coordinates that will be used by the graphic renderers. + * + * Since it should be possible to use different cameras and different + * renderers, it is possible to specify a prefix to put before the new + * coordinates (to get something like "node.camera1_x") + * + * @param {?string} read The prefix of the coordinates to read. + * @param {?string} write The prefix of the coordinates to write. + * @param {?object} options Eventually an object of options. Those can be: + * - A restricted nodes array. + * - A restricted edges array. + * - A width. + * - A height. + * @return {camera} Returns the camera. + */ + sigma.classes.camera.prototype.applyView = function(read, write, options) { + options = options || {}; + write = write !== undefined ? write : this.prefix; + read = read !== undefined ? read : this.readPrefix; + + var nodes = options.nodes || this.graph.nodes(), + edges = options.edges || this.graph.edges(); + + var i, + l, + node, + relCos = Math.cos(this.angle) / this.ratio, + relSin = Math.sin(this.angle) / this.ratio, + nodeRatio = Math.pow(this.ratio, this.settings('nodesPowRatio')), + edgeRatio = Math.pow(this.ratio, this.settings('edgesPowRatio')), + xOffset = (options.width || 0) / 2 - this.x * relCos - this.y * relSin, + yOffset = (options.height || 0) / 2 - this.y * relCos + this.x * relSin; + + for (i = 0, l = nodes.length; i < l; i++) { + node = nodes[i]; + node[write + 'x'] = + (node[read + 'x'] || 0) * relCos + + (node[read + 'y'] || 0) * relSin + + xOffset; + node[write + 'y'] = + (node[read + 'y'] || 0) * relCos - + (node[read + 'x'] || 0) * relSin + + yOffset; + node[write + 'size'] = + (node[read + 'size'] || 0) / + nodeRatio; + } + + for (i = 0, l = edges.length; i < l; i++) { + edges[i][write + 'size'] = + (edges[i][read + 'size'] || 0) / + edgeRatio; + } + + return this; + }; + + /** + * This function converts the coordinates of a point from the frame of the + * camera to the frame of the graph. + * + * @param {number} x The X coordinate of the point in the frame of the + * camera. + * @param {number} y The Y coordinate of the point in the frame of the + * camera. + * @return {object} The point coordinates in the frame of the graph. + */ + sigma.classes.camera.prototype.graphPosition = function(x, y, vector) { + var X = 0, + Y = 0, + cos = Math.cos(this.angle), + sin = Math.sin(this.angle); + + // Revert the origin differential vector: + if (!vector) { + X = - (this.x * cos + this.y * sin) / this.ratio; + Y = - (this.y * cos - this.x * sin) / this.ratio; + } + + return { + x: (x * cos + y * sin) / this.ratio + X, + y: (y * cos - x * sin) / this.ratio + Y + }; + }; + + /** + * This function converts the coordinates of a point from the frame of the + * graph to the frame of the camera. + * + * @param {number} x The X coordinate of the point in the frame of the + * graph. + * @param {number} y The Y coordinate of the point in the frame of the + * graph. + * @return {object} The point coordinates in the frame of the camera. + */ + sigma.classes.camera.prototype.cameraPosition = function(x, y, vector) { + var X = 0, + Y = 0, + cos = Math.cos(this.angle), + sin = Math.sin(this.angle); + + // Revert the origin differential vector: + if (!vector) { + X = - (this.x * cos + this.y * sin) / this.ratio; + Y = - (this.y * cos - this.x * sin) / this.ratio; + } + + return { + x: ((x - X) * cos - (y - Y) * sin) * this.ratio, + y: ((y - Y) * cos + (x - X) * sin) * this.ratio + }; + }; + + /** + * This method returns the transformation matrix of the camera. This is + * especially useful to apply the camera view directly in shaders, in case of + * WebGL rendering. + * + * @return {array} The transformation matrix. + */ + sigma.classes.camera.prototype.getMatrix = function() { + var scale = sigma.utils.matrices.scale(1 / this.ratio), + rotation = sigma.utils.matrices.rotation(this.angle), + translation = sigma.utils.matrices.translation(-this.x, -this.y), + matrix = sigma.utils.matrices.multiply( + translation, + sigma.utils.matrices.multiply( + rotation, + scale + ) + ); + + return matrix; + }; + + /** + * Taking a width and a height as parameters, this method returns the + * coordinates of the rectangle representing the camera on screen, in the + * graph's referentiel. + * + * To keep displaying labels of nodes going out of the screen, the method + * keeps a margin around the screen in the returned rectangle. + * + * @param {number} width The width of the screen. + * @param {number} height The height of the screen. + * @return {object} The rectangle as x1, y1, x2 and y2, representing + * two opposite points. + */ + sigma.classes.camera.prototype.getRectangle = function(width, height) { + var widthVect = this.cameraPosition(width, 0, true), + heightVect = this.cameraPosition(0, height, true), + centerVect = this.cameraPosition(width / 2, height / 2, true), + marginX = this.cameraPosition(width / 4, 0, true).x, + marginY = this.cameraPosition(0, height / 4, true).y; + + return { + x1: this.x - centerVect.x - marginX, + y1: this.y - centerVect.y - marginY, + x2: this.x - centerVect.x + marginX + widthVect.x, + y2: this.y - centerVect.y - marginY + widthVect.y, + height: Math.sqrt( + Math.pow(heightVect.x, 2) + + Math.pow(heightVect.y + 2 * marginY, 2) + ) + }; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.configurable.js b/blogContent/projects/steam/src/classes/sigma.classes.configurable.js new file mode 100644 index 0000000..09ce1f7 --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.configurable.js @@ -0,0 +1,116 @@ +;(function() { + 'use strict'; + + /** + * This utils aims to facilitate the manipulation of each instance setting. + * Using a function instead of an object brings two main advantages: First, + * it will be easier in the future to catch settings updates through a + * function than an object. Second, giving it a full object will "merge" it + * to the settings object properly, keeping us to have to always add a loop. + * + * @return {configurable} The "settings" function. + */ + var configurable = function() { + var i, + l, + data = {}, + datas = Array.prototype.slice.call(arguments, 0); + + /** + * The method to use to set or get any property of this instance. + * + * @param {string|object} a1 If it is a string and if a2 is undefined, + * then it will return the corresponding + * property. If it is a string and if a2 is + * set, then it will set a2 as the property + * corresponding to a1, and return this. If + * it is an object, then each pair string + + * object(or any other type) will be set as a + * property. + * @param {*?} a2 The new property corresponding to a1 if a1 + * is a string. + * @return {*|configurable} Returns itself or the corresponding + * property. + * + * Polymorphism: + * ************* + * Here are some basic use examples: + * + * > settings = new configurable(); + * > settings('mySetting', 42); + * > settings('mySetting'); // Logs: 42 + * > settings('mySetting', 123); + * > settings('mySetting'); // Logs: 123 + * > settings({mySetting: 456}); + * > settings('mySetting'); // Logs: 456 + * + * Also, it is possible to use the function as a fallback: + * > settings({mySetting: 'abc'}, 'mySetting'); // Logs: 'abc' + * > settings({hisSetting: 'abc'}, 'mySetting'); // Logs: 456 + */ + var settings = function(a1, a2) { + var o, + i, + l, + k; + + if (arguments.length === 1 && typeof a1 === 'string') { + if (data[a1] !== undefined) + return data[a1]; + for (i = 0, l = datas.length; i < l; i++) + if (datas[i][a1] !== undefined) + return datas[i][a1]; + return undefined; + } else if (typeof a1 === 'object' && typeof a2 === 'string') { + return (a1 || {})[a2] !== undefined ? a1[a2] : settings(a2); + } else { + o = (typeof a1 === 'object' && a2 === undefined) ? a1 : {}; + + if (typeof a1 === 'string') + o[a1] = a2; + + for (i = 0, k = Object.keys(o), l = k.length; i < l; i++) + data[k[i]] = o[k[i]]; + + return this; + } + }; + + /** + * This method returns a new configurable function, with new objects + * + * @param {object*} Any number of objects to search in. + * @return {function} Returns the function. Check its documentation to know + * more about how it works. + */ + settings.embedObjects = function() { + var args = datas.concat( + data + ).concat( + Array.prototype.splice.call(arguments, 0) + ); + + return configurable.apply({}, args); + }; + + // Initialize + for (i = 0, l = arguments.length; i < l; i++) + settings(arguments[i]); + + return settings; + }; + + /** + * EXPORT: + * ******* + */ + if (typeof this.sigma !== 'undefined') { + this.sigma.classes = this.sigma.classes || {}; + this.sigma.classes.configurable = configurable; + } else if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = configurable; + exports.configurable = configurable; + } else + this.configurable = configurable; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.dispatcher.js b/blogContent/projects/steam/src/classes/sigma.classes.dispatcher.js new file mode 100644 index 0000000..0ce7dca --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.dispatcher.js @@ -0,0 +1,204 @@ +;(function() { + 'use strict'; + + /** + * Dispatcher constructor. + * + * @return {dispatcher} The new dispatcher instance. + */ + var dispatcher = function() { + Object.defineProperty(this, '_handlers', { + value: {} + }); + }; + + + + + /** + * Will execute the handler everytime that the indicated event (or the + * indicated events) will be triggered. + * + * @param {string} events The name of the event (or the events + * separated by spaces). + * @param {function(Object)} handler The handler to bind. + * @return {dispatcher} Returns the instance itself. + */ + dispatcher.prototype.bind = function(events, handler) { + var i, + l, + event, + eArray; + + if ( + arguments.length === 1 && + typeof arguments[0] === 'object' + ) + for (events in arguments[0]) + this.bind(events, arguments[0][events]); + else if ( + arguments.length === 2 && + typeof arguments[1] === 'function' + ) { + eArray = typeof events === 'string' ? events.split(' ') : events; + + for (i = 0, l = eArray.length; i !== l; i += 1) { + event = eArray[i]; + + // Check that event is not '': + if (!event) + continue; + + if (!this._handlers[event]) + this._handlers[event] = []; + + // Using an object instead of directly the handler will make possible + // later to add flags + this._handlers[event].push({ + handler: handler + }); + } + } else + throw 'bind: Wrong arguments.'; + + return this; + }; + + /** + * Removes the handler from a specified event (or specified events). + * + * @param {?string} events The name of the event (or the events + * separated by spaces). If undefined, + * then all handlers are removed. + * @param {?function(object)} handler The handler to unbind. If undefined, + * each handler bound to the event or the + * events will be removed. + * @return {dispatcher} Returns the instance itself. + */ + dispatcher.prototype.unbind = function(events, handler) { + var i, + n, + j, + m, + k, + a, + event, + eArray = typeof events === 'string' ? events.split(' ') : events; + + if (!arguments.length) { + for (k in this._handlers) + delete this._handlers[k]; + return this; + } + + if (handler) { + for (i = 0, n = eArray.length; i !== n; i += 1) { + event = eArray[i]; + if (this._handlers[event]) { + a = []; + for (j = 0, m = this._handlers[event].length; j !== m; j += 1) + if (this._handlers[event][j].handler !== handler) + a.push(this._handlers[event][j]); + + this._handlers[event] = a; + } + + if (this._handlers[event] && this._handlers[event].length === 0) + delete this._handlers[event]; + } + } else + for (i = 0, n = eArray.length; i !== n; i += 1) + delete this._handlers[eArray[i]]; + + return this; + }; + + /** + * Executes each handler bound to the event + * + * @param {string} events The name of the event (or the events separated + * by spaces). + * @param {?object} data The content of the event (optional). + * @return {dispatcher} Returns the instance itself. + */ + dispatcher.prototype.dispatchEvent = function(events, data) { + var i, + n, + j, + m, + a, + event, + eventName, + self = this, + eArray = typeof events === 'string' ? events.split(' ') : events; + + data = data === undefined ? {} : data; + + for (i = 0, n = eArray.length; i !== n; i += 1) { + eventName = eArray[i]; + + if (this._handlers[eventName]) { + event = self.getEvent(eventName, data); + a = []; + + for (j = 0, m = this._handlers[eventName].length; j !== m; j += 1) { + this._handlers[eventName][j].handler(event); + if (!this._handlers[eventName][j].one) + a.push(this._handlers[eventName][j]); + } + + this._handlers[eventName] = a; + } + } + + return this; + }; + + /** + * Return an event object. + * + * @param {string} events The name of the event. + * @param {?object} data The content of the event (optional). + * @return {object} Returns the instance itself. + */ + dispatcher.prototype.getEvent = function(event, data) { + return { + type: event, + data: data || {}, + target: this + }; + }; + + /** + * A useful function to deal with inheritance. It will make the target + * inherit the prototype of the class dispatcher as well as its constructor. + * + * @param {object} target The target. + */ + dispatcher.extend = function(target, args) { + var k; + + for (k in dispatcher.prototype) + if (dispatcher.prototype.hasOwnProperty(k)) + target[k] = dispatcher.prototype[k]; + + dispatcher.apply(target, args); + }; + + + + + /** + * EXPORT: + * ******* + */ + if (typeof this.sigma !== 'undefined') { + this.sigma.classes = this.sigma.classes || {}; + this.sigma.classes.dispatcher = dispatcher; + } else if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = dispatcher; + exports.dispatcher = dispatcher; + } else + this.dispatcher = dispatcher; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.edgequad.js b/blogContent/projects/steam/src/classes/sigma.classes.edgequad.js new file mode 100644 index 0000000..fbc5b7f --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.edgequad.js @@ -0,0 +1,832 @@ +;(function(undefined) { + 'use strict'; + + /** + * Sigma Quadtree Module for edges + * =============================== + * + * Author: Sébastien Heymann, + * from the quad of Guillaume Plique (Yomguithereal) + * Version: 0.2 + */ + + + + /** + * Quad Geometric Operations + * ------------------------- + * + * A useful batch of geometric operations used by the quadtree. + */ + + var _geom = { + + /** + * Transforms a graph node with x, y and size into an + * axis-aligned square. + * + * @param {object} A graph node with at least a point (x, y) and a size. + * @return {object} A square: two points (x1, y1), (x2, y2) and height. + */ + pointToSquare: function(n) { + return { + x1: n.x - n.size, + y1: n.y - n.size, + x2: n.x + n.size, + y2: n.y - n.size, + height: n.size * 2 + }; + }, + + /** + * Transforms a graph edge with x1, y1, x2, y2 and size into an + * axis-aligned square. + * + * @param {object} A graph edge with at least two points + * (x1, y1), (x2, y2) and a size. + * @return {object} A square: two points (x1, y1), (x2, y2) and height. + */ + lineToSquare: function(e) { + if (e.y1 < e.y2) { + // (e.x1, e.y1) on top + if (e.x1 < e.x2) { + // (e.x1, e.y1) on left + return { + x1: e.x1 - e.size, + y1: e.y1 - e.size, + x2: e.x2 + e.size, + y2: e.y1 - e.size, + height: e.y2 - e.y1 + e.size * 2 + }; + } + // (e.x1, e.y1) on right + return { + x1: e.x2 - e.size, + y1: e.y1 - e.size, + x2: e.x1 + e.size, + y2: e.y1 - e.size, + height: e.y2 - e.y1 + e.size * 2 + }; + } + + // (e.x2, e.y2) on top + if (e.x1 < e.x2) { + // (e.x1, e.y1) on left + return { + x1: e.x1 - e.size, + y1: e.y2 - e.size, + x2: e.x2 + e.size, + y2: e.y2 - e.size, + height: e.y1 - e.y2 + e.size * 2 + }; + } + // (e.x2, e.y2) on right + return { + x1: e.x2 - e.size, + y1: e.y2 - e.size, + x2: e.x1 + e.size, + y2: e.y2 - e.size, + height: e.y1 - e.y2 + e.size * 2 + }; + }, + + /** + * Transforms a graph edge of type 'curve' with x1, y1, x2, y2, + * control point and size into an axis-aligned square. + * + * @param {object} e A graph edge with at least two points + * (x1, y1), (x2, y2) and a size. + * @param {object} cp A control point (x,y). + * @return {object} A square: two points (x1, y1), (x2, y2) and height. + */ + quadraticCurveToSquare: function(e, cp) { + var pt = sigma.utils.getPointOnQuadraticCurve( + 0.5, + e.x1, + e.y1, + e.x2, + e.y2, + cp.x, + cp.y + ); + + // Bounding box of the two points and the point at the middle of the + // curve: + var minX = Math.min(e.x1, e.x2, pt.x), + maxX = Math.max(e.x1, e.x2, pt.x), + minY = Math.min(e.y1, e.y2, pt.y), + maxY = Math.max(e.y1, e.y2, pt.y); + + return { + x1: minX - e.size, + y1: minY - e.size, + x2: maxX + e.size, + y2: minY - e.size, + height: maxY - minY + e.size * 2 + }; + }, + + /** + * Transforms a graph self loop into an axis-aligned square. + * + * @param {object} n A graph node with a point (x, y) and a size. + * @return {object} A square: two points (x1, y1), (x2, y2) and height. + */ + selfLoopToSquare: function(n) { + // Fitting to the curve is too costly, we compute a larger bounding box + // using the control points: + var cp = sigma.utils.getSelfLoopControlPoints(n.x, n.y, n.size); + + // Bounding box of the point and the two control points: + var minX = Math.min(n.x, cp.x1, cp.x2), + maxX = Math.max(n.x, cp.x1, cp.x2), + minY = Math.min(n.y, cp.y1, cp.y2), + maxY = Math.max(n.y, cp.y1, cp.y2); + + return { + x1: minX - n.size, + y1: minY - n.size, + x2: maxX + n.size, + y2: minY - n.size, + height: maxY - minY + n.size * 2 + }; + }, + + /** + * Checks whether a rectangle is axis-aligned. + * + * @param {object} A rectangle defined by two points + * (x1, y1) and (x2, y2). + * @return {boolean} True if the rectangle is axis-aligned. + */ + isAxisAligned: function(r) { + return r.x1 === r.x2 || r.y1 === r.y2; + }, + + /** + * Compute top points of an axis-aligned rectangle. This is useful in + * cases when the rectangle has been rotated (left, right or bottom up) and + * later operations need to know the top points. + * + * @param {object} An axis-aligned rectangle defined by two points + * (x1, y1), (x2, y2) and height. + * @return {object} A rectangle: two points (x1, y1), (x2, y2) and height. + */ + axisAlignedTopPoints: function(r) { + + // Basic + if (r.y1 === r.y2 && r.x1 < r.x2) + return r; + + // Rotated to right + if (r.x1 === r.x2 && r.y2 > r.y1) + return { + x1: r.x1 - r.height, y1: r.y1, + x2: r.x1, y2: r.y1, + height: r.height + }; + + // Rotated to left + if (r.x1 === r.x2 && r.y2 < r.y1) + return { + x1: r.x1, y1: r.y2, + x2: r.x2 + r.height, y2: r.y2, + height: r.height + }; + + // Bottom's up + return { + x1: r.x2, y1: r.y1 - r.height, + x2: r.x1, y2: r.y1 - r.height, + height: r.height + }; + }, + + /** + * Get coordinates of a rectangle's lower left corner from its top points. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @return {object} Coordinates of the corner (x, y). + */ + lowerLeftCoor: function(r) { + var width = ( + Math.sqrt( + Math.pow(r.x2 - r.x1, 2) + + Math.pow(r.y2 - r.y1, 2) + ) + ); + + return { + x: r.x1 - (r.y2 - r.y1) * r.height / width, + y: r.y1 + (r.x2 - r.x1) * r.height / width + }; + }, + + /** + * Get coordinates of a rectangle's lower right corner from its top points + * and its lower left corner. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @param {object} A corner's coordinates (x, y). + * @return {object} Coordinates of the corner (x, y). + */ + lowerRightCoor: function(r, llc) { + return { + x: llc.x - r.x1 + r.x2, + y: llc.y - r.y1 + r.y2 + }; + }, + + /** + * Get the coordinates of all the corners of a rectangle from its top point. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @return {array} An array of the four corners' coordinates (x, y). + */ + rectangleCorners: function(r) { + var llc = this.lowerLeftCoor(r), + lrc = this.lowerRightCoor(r, llc); + + return [ + {x: r.x1, y: r.y1}, + {x: r.x2, y: r.y2}, + {x: llc.x, y: llc.y}, + {x: lrc.x, y: lrc.y} + ]; + }, + + /** + * Split a square defined by its boundaries into four. + * + * @param {object} Boundaries of the square (x, y, width, height). + * @return {array} An array containing the four new squares, themselves + * defined by an array of their four corners (x, y). + */ + splitSquare: function(b) { + return [ + [ + {x: b.x, y: b.y}, + {x: b.x + b.width / 2, y: b.y}, + {x: b.x, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height / 2} + ], + [ + {x: b.x + b.width / 2, y: b.y}, + {x: b.x + b.width, y: b.y}, + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x + b.width, y: b.y + b.height / 2} + ], + [ + {x: b.x, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x, y: b.y + b.height}, + {x: b.x + b.width / 2, y: b.y + b.height} + ], + [ + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x + b.width, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height}, + {x: b.x + b.width, y: b.y + b.height} + ] + ]; + }, + + /** + * Compute the four axis between corners of rectangle A and corners of + * rectangle B. This is needed later to check an eventual collision. + * + * @param {array} An array of rectangle A's four corners (x, y). + * @param {array} An array of rectangle B's four corners (x, y). + * @return {array} An array of four axis defined by their coordinates (x,y). + */ + axis: function(c1, c2) { + return [ + {x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y}, + {x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y}, + {x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y}, + {x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y} + ]; + }, + + /** + * Project a rectangle's corner on an axis. + * + * @param {object} Coordinates of a corner (x, y). + * @param {object} Coordinates of an axis (x, y). + * @return {object} The projection defined by coordinates (x, y). + */ + projection: function(c, a) { + var l = ( + (c.x * a.x + c.y * a.y) / + (Math.pow(a.x, 2) + Math.pow(a.y, 2)) + ); + + return { + x: l * a.x, + y: l * a.y + }; + }, + + /** + * Check whether two rectangles collide on one particular axis. + * + * @param {object} An axis' coordinates (x, y). + * @param {array} Rectangle A's corners. + * @param {array} Rectangle B's corners. + * @return {boolean} True if the rectangles collide on the axis. + */ + axisCollision: function(a, c1, c2) { + var sc1 = [], + sc2 = []; + + for (var ci = 0; ci < 4; ci++) { + var p1 = this.projection(c1[ci], a), + p2 = this.projection(c2[ci], a); + + sc1.push(p1.x * a.x + p1.y * a.y); + sc2.push(p2.x * a.x + p2.y * a.y); + } + + var maxc1 = Math.max.apply(Math, sc1), + maxc2 = Math.max.apply(Math, sc2), + minc1 = Math.min.apply(Math, sc1), + minc2 = Math.min.apply(Math, sc2); + + return (minc2 <= maxc1 && maxc2 >= minc1); + }, + + /** + * Check whether two rectangles collide on each one of their four axis. If + * all axis collide, then the two rectangles do collide on the plane. + * + * @param {array} Rectangle A's corners. + * @param {array} Rectangle B's corners. + * @return {boolean} True if the rectangles collide. + */ + collision: function(c1, c2) { + var axis = this.axis(c1, c2), + col = true; + + for (var i = 0; i < 4; i++) + col = col && this.axisCollision(axis[i], c1, c2); + + return col; + } + }; + + + /** + * Quad Functions + * ------------ + * + * The Quadtree functions themselves. + * For each of those functions, we consider that in a splitted quad, the + * index of each node is the following: + * 0: top left + * 1: top right + * 2: bottom left + * 3: bottom right + * + * Moreover, the hereafter quad's philosophy is to consider that if an element + * collides with more than one nodes, this element belongs to each of the + * nodes it collides with where other would let it lie on a higher node. + */ + + /** + * Get the index of the node containing the point in the quad + * + * @param {object} point A point defined by coordinates (x, y). + * @param {object} quadBounds Boundaries of the quad (x, y, width, heigth). + * @return {integer} The index of the node containing the point. + */ + function _quadIndex(point, quadBounds) { + var xmp = quadBounds.x + quadBounds.width / 2, + ymp = quadBounds.y + quadBounds.height / 2, + top = (point.y < ymp), + left = (point.x < xmp); + + if (top) { + if (left) + return 0; + else + return 1; + } + else { + if (left) + return 2; + else + return 3; + } + } + + /** + * Get a list of indexes of nodes containing an axis-aligned rectangle + * + * @param {object} rectangle A rectangle defined by two points (x1, y1), + * (x2, y2) and height. + * @param {array} quadCorners An array of the quad nodes' corners. + * @return {array} An array of indexes containing one to + * four integers. + */ + function _quadIndexes(rectangle, quadCorners) { + var indexes = []; + + // Iterating through quads + for (var i = 0; i < 4; i++) + if ((rectangle.x2 >= quadCorners[i][0].x) && + (rectangle.x1 <= quadCorners[i][1].x) && + (rectangle.y1 + rectangle.height >= quadCorners[i][0].y) && + (rectangle.y1 <= quadCorners[i][2].y)) + indexes.push(i); + + return indexes; + } + + /** + * Get a list of indexes of nodes containing a non-axis-aligned rectangle + * + * @param {array} corners An array containing each corner of the + * rectangle defined by its coordinates (x, y). + * @param {array} quadCorners An array of the quad nodes' corners. + * @return {array} An array of indexes containing one to + * four integers. + */ + function _quadCollision(corners, quadCorners) { + var indexes = []; + + // Iterating through quads + for (var i = 0; i < 4; i++) + if (_geom.collision(corners, quadCorners[i])) + indexes.push(i); + + return indexes; + } + + /** + * Subdivide a quad by creating a node at a precise index. The function does + * not generate all four nodes not to potentially create unused nodes. + * + * @param {integer} index The index of the node to create. + * @param {object} quad The quad object to subdivide. + * @return {object} A new quad representing the node created. + */ + function _quadSubdivide(index, quad) { + var next = quad.level + 1, + subw = Math.round(quad.bounds.width / 2), + subh = Math.round(quad.bounds.height / 2), + qx = Math.round(quad.bounds.x), + qy = Math.round(quad.bounds.y), + x, + y; + + switch (index) { + case 0: + x = qx; + y = qy; + break; + case 1: + x = qx + subw; + y = qy; + break; + case 2: + x = qx; + y = qy + subh; + break; + case 3: + x = qx + subw; + y = qy + subh; + break; + } + + return _quadTree( + {x: x, y: y, width: subw, height: subh}, + next, + quad.maxElements, + quad.maxLevel + ); + } + + /** + * Recursively insert an element into the quadtree. Only points + * with size, i.e. axis-aligned squares, may be inserted with this + * method. + * + * @param {object} el The element to insert in the quadtree. + * @param {object} sizedPoint A sized point defined by two top points + * (x1, y1), (x2, y2) and height. + * @param {object} quad The quad in which to insert the element. + * @return {undefined} The function does not return anything. + */ + function _quadInsert(el, sizedPoint, quad) { + if (quad.level < quad.maxLevel) { + + // Searching appropriate quads + var indexes = _quadIndexes(sizedPoint, quad.corners); + + // Iterating + for (var i = 0, l = indexes.length; i < l; i++) { + + // Subdividing if necessary + if (quad.nodes[indexes[i]] === undefined) + quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad); + + // Recursion + _quadInsert(el, sizedPoint, quad.nodes[indexes[i]]); + } + } + else { + + // Pushing the element in a leaf node + quad.elements.push(el); + } + } + + /** + * Recursively retrieve every elements held by the node containing the + * searched point. + * + * @param {object} point The searched point (x, y). + * @param {object} quad The searched quad. + * @return {array} An array of elements contained in the relevant + * node. + */ + function _quadRetrievePoint(point, quad) { + if (quad.level < quad.maxLevel) { + var index = _quadIndex(point, quad.bounds); + + // If node does not exist we return an empty list + if (quad.nodes[index] !== undefined) { + return _quadRetrievePoint(point, quad.nodes[index]); + } + else { + return []; + } + } + else { + return quad.elements; + } + } + + /** + * Recursively retrieve every elements contained within an rectangular area + * that may or may not be axis-aligned. + * + * @param {object|array} rectData The searched area defined either by + * an array of four corners (x, y) in + * the case of a non-axis-aligned + * rectangle or an object with two top + * points (x1, y1), (x2, y2) and height. + * @param {object} quad The searched quad. + * @param {function} collisionFunc The collision function used to search + * for node indexes. + * @param {array?} els The retrieved elements. + * @return {array} An array of elements contained in the + * area. + */ + function _quadRetrieveArea(rectData, quad, collisionFunc, els) { + els = els || {}; + + if (quad.level < quad.maxLevel) { + var indexes = collisionFunc(rectData, quad.corners); + + for (var i = 0, l = indexes.length; i < l; i++) + if (quad.nodes[indexes[i]] !== undefined) + _quadRetrieveArea( + rectData, + quad.nodes[indexes[i]], + collisionFunc, + els + ); + } else + for (var j = 0, m = quad.elements.length; j < m; j++) + if (els[quad.elements[j].id] === undefined) + els[quad.elements[j].id] = quad.elements[j]; + + return els; + } + + /** + * Creates the quadtree object itself. + * + * @param {object} bounds The boundaries of the quad defined by an + * origin (x, y), width and heigth. + * @param {integer} level The level of the quad in the tree. + * @param {integer} maxElements The max number of element in a leaf node. + * @param {integer} maxLevel The max recursion level of the tree. + * @return {object} The quadtree object. + */ + function _quadTree(bounds, level, maxElements, maxLevel) { + return { + level: level || 0, + bounds: bounds, + corners: _geom.splitSquare(bounds), + maxElements: maxElements || 40, + maxLevel: maxLevel || 8, + elements: [], + nodes: [] + }; + } + + + /** + * Sigma Quad Constructor + * ---------------------- + * + * The edgequad API as exposed to sigma. + */ + + /** + * The edgequad core that will become the sigma interface with the quadtree. + * + * property {object} _tree Property holding the quadtree object. + * property {object} _geom Exposition of the _geom namespace for testing. + * property {object} _cache Cache for the area method. + * property {boolean} _enabled Can index and retreive elements. + */ + var edgequad = function() { + this._geom = _geom; + this._tree = null; + this._cache = { + query: false, + result: false + }; + this._enabled = true; + }; + + /** + * Index a graph by inserting its edges into the quadtree. + * + * @param {object} graph A graph instance. + * @param {object} params An object of parameters with at least the quad + * bounds. + * @return {object} The quadtree object. + * + * Parameters: + * ---------- + * bounds: {object} boundaries of the quad defined by its origin (x, y) + * width and heigth. + * prefix: {string?} a prefix for edge geometric attributes. + * maxElements: {integer?} the max number of elements in a leaf node. + * maxLevel: {integer?} the max recursion level of the tree. + */ + edgequad.prototype.index = function(graph, params) { + if (!this._enabled) + return this._tree; + + // Enforcing presence of boundaries + if (!params.bounds) + throw 'sigma.classes.edgequad.index: bounds information not given.'; + + // Prefix + var prefix = params.prefix || '', + cp, + source, + target, + n, + e; + + // Building the tree + this._tree = _quadTree( + params.bounds, + 0, + params.maxElements, + params.maxLevel + ); + + var edges = graph.edges(); + + // Inserting graph edges into the tree + for (var i = 0, l = edges.length; i < l; i++) { + source = graph.nodes(edges[i].source); + target = graph.nodes(edges[i].target); + e = { + x1: source[prefix + 'x'], + y1: source[prefix + 'y'], + x2: target[prefix + 'x'], + y2: target[prefix + 'y'], + size: edges[i][prefix + 'size'] || 0 + }; + + // Inserting edge + if (edges[i].type === 'curve' || edges[i].type === 'curvedArrow') { + if (source.id === target.id) { + n = { + x: source[prefix + 'x'], + y: source[prefix + 'y'], + size: source[prefix + 'size'] || 0 + }; + _quadInsert( + edges[i], + _geom.selfLoopToSquare(n), + this._tree); + } + else { + cp = sigma.utils.getQuadraticControlPoint(e.x1, e.y1, e.x2, e.y2); + _quadInsert( + edges[i], + _geom.quadraticCurveToSquare(e, cp), + this._tree); + } + } + else { + _quadInsert( + edges[i], + _geom.lineToSquare(e), + this._tree); + } + } + + // Reset cache: + this._cache = { + query: false, + result: false + }; + + // remove? + return this._tree; + }; + + /** + * Retrieve every graph edges held by the quadtree node containing the + * searched point. + * + * @param {number} x of the point. + * @param {number} y of the point. + * @return {array} An array of edges retrieved. + */ + edgequad.prototype.point = function(x, y) { + if (!this._enabled) + return []; + + return this._tree ? + _quadRetrievePoint({x: x, y: y}, this._tree) || [] : + []; + }; + + /** + * Retrieve every graph edges within a rectangular area. The methods keep the + * last area queried in cache for optimization reason and will act differently + * for the same reason if the area is axis-aligned or not. + * + * @param {object} A rectangle defined by two top points (x1, y1), (x2, y2) + * and height. + * @return {array} An array of edges retrieved. + */ + edgequad.prototype.area = function(rect) { + if (!this._enabled) + return []; + + var serialized = JSON.stringify(rect), + collisionFunc, + rectData; + + // Returning cache? + if (this._cache.query === serialized) + return this._cache.result; + + // Axis aligned ? + if (_geom.isAxisAligned(rect)) { + collisionFunc = _quadIndexes; + rectData = _geom.axisAlignedTopPoints(rect); + } + else { + collisionFunc = _quadCollision; + rectData = _geom.rectangleCorners(rect); + } + + // Retrieving edges + var edges = this._tree ? + _quadRetrieveArea( + rectData, + this._tree, + collisionFunc + ) : + []; + + // Object to array + var edgesArray = []; + for (var i in edges) + edgesArray.push(edges[i]); + + // Caching + this._cache.query = serialized; + this._cache.result = edgesArray; + + return edgesArray; + }; + + + /** + * EXPORT: + * ******* + */ + if (typeof this.sigma !== 'undefined') { + this.sigma.classes = this.sigma.classes || {}; + this.sigma.classes.edgequad = edgequad; + } else if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = edgequad; + exports.edgequad = edgequad; + } else + this.edgequad = edgequad; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.graph.js b/blogContent/projects/steam/src/classes/sigma.classes.graph.js new file mode 100644 index 0000000..c7ebe9d --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.graph.js @@ -0,0 +1,859 @@ +;(function(undefined) { + 'use strict'; + + var _methods = Object.create(null), + _indexes = Object.create(null), + _initBindings = Object.create(null), + _methodBindings = Object.create(null), + _methodBeforeBindings = Object.create(null), + _defaultSettings = { + immutable: true, + clone: true + }, + _defaultSettingsFunction = function(key) { + return _defaultSettings[key]; + }; + + /** + * The graph constructor. It initializes the data and the indexes, and binds + * the custom indexes and methods to its own scope. + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters in the settings + * object: + * + * {boolean} clone Indicates if the data have to be cloned in methods + * to add nodes or edges. + * {boolean} immutable Indicates if nodes "id" values and edges "id", + * "source" and "target" values must be set as + * immutable. + * + * @param {?configurable} settings Eventually a settings function. + * @return {graph} The new graph instance. + */ + var graph = function(settings) { + var k, + fn, + data; + + /** + * DATA: + * ***** + * Every data that is callable from graph methods are stored in this "data" + * object. This object will be served as context for all these methods, + * and it is possible to add other type of data in it. + */ + data = { + /** + * SETTINGS FUNCTION: + * ****************** + */ + settings: settings || _defaultSettingsFunction, + + /** + * MAIN DATA: + * ********** + */ + nodesArray: [], + edgesArray: [], + + /** + * GLOBAL INDEXES: + * *************** + * These indexes just index data by ids. + */ + nodesIndex: Object.create(null), + edgesIndex: Object.create(null), + + /** + * LOCAL INDEXES: + * ************** + * These indexes refer from node to nodes. Each key is an id, and each + * value is the array of the ids of related nodes. + */ + inNeighborsIndex: Object.create(null), + outNeighborsIndex: Object.create(null), + allNeighborsIndex: Object.create(null), + + inNeighborsCount: Object.create(null), + outNeighborsCount: Object.create(null), + allNeighborsCount: Object.create(null) + }; + + // Execute bindings: + for (k in _initBindings) + _initBindings[k].call(data); + + // Add methods to both the scope and the data objects: + for (k in _methods) { + fn = __bindGraphMethod(k, data, _methods[k]); + this[k] = fn; + data[k] = fn; + } + }; + + + + + /** + * A custom tool to bind methods such that function that are bound to it will + * be executed anytime the method is called. + * + * @param {string} methodName The name of the method to bind. + * @param {object} scope The scope where the method must be executed. + * @param {function} fn The method itself. + * @return {function} The new method. + */ + function __bindGraphMethod(methodName, scope, fn) { + var result = function() { + var k, + res; + + // Execute "before" bound functions: + for (k in _methodBeforeBindings[methodName]) + _methodBeforeBindings[methodName][k].apply(scope, arguments); + + // Apply the method: + res = fn.apply(scope, arguments); + + // Execute bound functions: + for (k in _methodBindings[methodName]) + _methodBindings[methodName][k].apply(scope, arguments); + + // Return res: + return res; + }; + + return result; + } + + /** + * This custom tool function removes every pair key/value from an hash. The + * goal is to avoid creating a new object while some other references are + * still hanging in some scopes... + * + * @param {object} obj The object to empty. + * @return {object} The empty object. + */ + function __emptyObject(obj) { + var k; + + for (k in obj) + if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k)) + delete obj[k]; + + return obj; + } + + + + + /** + * This global method adds a method that will be bound to the futurly created + * graph instances. + * + * Since these methods will be bound to their scope when the instances are + * created, it does not use the prototype. Because of that, methods have to + * be added before instances are created to make them available. + * + * Here is an example: + * + * > graph.addMethod('getNodesCount', function() { + * > return this.nodesArray.length; + * > }); + * > + * > var myGraph = new graph(); + * > console.log(myGraph.getNodesCount()); // outputs 0 + * + * @param {string} methodName The name of the method. + * @param {function} fn The method itself. + * @return {object} The global graph constructor. + */ + graph.addMethod = function(methodName, fn) { + if ( + typeof methodName !== 'string' || + typeof fn !== 'function' || + arguments.length !== 2 + ) + throw 'addMethod: Wrong arguments.'; + + if (_methods[methodName] || graph[methodName]) + throw 'The method "' + methodName + '" already exists.'; + + _methods[methodName] = fn; + _methodBindings[methodName] = Object.create(null); + _methodBeforeBindings[methodName] = Object.create(null); + + return this; + }; + + /** + * This global method returns true if the method has already been added, and + * false else. + * + * Here are some examples: + * + * > graph.hasMethod('addNode'); // returns true + * > graph.hasMethod('hasMethod'); // returns true + * > graph.hasMethod('unexistingMethod'); // returns false + * + * @param {string} methodName The name of the method. + * @return {boolean} The result. + */ + graph.hasMethod = function(methodName) { + return !!(_methods[methodName] || graph[methodName]); + }; + + /** + * This global methods attaches a function to a method. Anytime the specified + * method is called, the attached function is called right after, with the + * same arguments and in the same scope. The attached function is called + * right before if the last argument is true, unless the method is the graph + * constructor. + * + * To attach a function to the graph constructor, use 'constructor' as the + * method name (first argument). + * + * The main idea is to have a clean way to keep custom indexes up to date, + * for instance: + * + * > var timesAddNodeCalled = 0; + * > graph.attach('addNode', 'timesAddNodeCalledInc', function() { + * > timesAddNodeCalled++; + * > }); + * > + * > var myGraph = new graph(); + * > console.log(timesAddNodeCalled); // outputs 0 + * > + * > myGraph.addNode({ id: '1' }).addNode({ id: '2' }); + * > console.log(timesAddNodeCalled); // outputs 2 + * + * The idea for calling a function before is to provide pre-processors, for + * instance: + * + * > var colorPalette = { Person: '#C3CBE1', Place: '#9BDEBD' }; + * > graph.attach('addNode', 'applyNodeColorPalette', function(n) { + * > n.color = colorPalette[n.category]; + * > }, true); + * > + * > var myGraph = new graph(); + * > myGraph.addNode({ id: 'n0', category: 'Person' }); + * > console.log(myGraph.nodes('n0').color); // outputs '#C3CBE1' + * + * @param {string} methodName The name of the related method or + * "constructor". + * @param {string} key The key to identify the function to attach. + * @param {function} fn The function to bind. + * @param {boolean} before If true the function is called right before. + * @return {object} The global graph constructor. + */ + graph.attach = function(methodName, key, fn, before) { + if ( + typeof methodName !== 'string' || + typeof key !== 'string' || + typeof fn !== 'function' || + arguments.length < 3 || + arguments.length > 4 + ) + throw 'attach: Wrong arguments.'; + + var bindings; + + if (methodName === 'constructor') + bindings = _initBindings; + else { + if (before) { + if (!_methodBeforeBindings[methodName]) + throw 'The method "' + methodName + '" does not exist.'; + + bindings = _methodBeforeBindings[methodName]; + } + else { + if (!_methodBindings[methodName]) + throw 'The method "' + methodName + '" does not exist.'; + + bindings = _methodBindings[methodName]; + } + } + + if (bindings[key]) + throw 'A function "' + key + '" is already attached ' + + 'to the method "' + methodName + '".'; + + bindings[key] = fn; + + return this; + }; + + /** + * Alias of attach(methodName, key, fn, true). + */ + graph.attachBefore = function(methodName, key, fn) { + return this.attach(methodName, key, fn, true); + }; + + /** + * This methods is just an helper to deal with custom indexes. It takes as + * arguments the name of the index and an object containing all the different + * functions to bind to the methods. + * + * Here is a basic example, that creates an index to keep the number of nodes + * in the current graph. It also adds a method to provide a getter on that + * new index: + * + * > sigma.classes.graph.addIndex('nodesCount', { + * > constructor: function() { + * > this.nodesCount = 0; + * > }, + * > addNode: function() { + * > this.nodesCount++; + * > }, + * > dropNode: function() { + * > this.nodesCount--; + * > } + * > }); + * > + * > sigma.classes.graph.addMethod('getNodesCount', function() { + * > return this.nodesCount; + * > }); + * > + * > var myGraph = new sigma.classes.graph(); + * > console.log(myGraph.getNodesCount()); // outputs 0 + * > + * > myGraph.addNode({ id: '1' }).addNode({ id: '2' }); + * > console.log(myGraph.getNodesCount()); // outputs 2 + * + * @param {string} name The name of the index. + * @param {object} bindings The object containing the functions to bind. + * @return {object} The global graph constructor. + */ + graph.addIndex = function(name, bindings) { + if ( + typeof name !== 'string' || + Object(bindings) !== bindings || + arguments.length !== 2 + ) + throw 'addIndex: Wrong arguments.'; + + if (_indexes[name]) + throw 'The index "' + name + '" already exists.'; + + var k; + + // Store the bindings: + _indexes[name] = bindings; + + // Attach the bindings: + for (k in bindings) + if (typeof bindings[k] !== 'function') + throw 'The bindings must be functions.'; + else + graph.attach(k, name, bindings[k]); + + return this; + }; + + + + + /** + * This method adds a node to the graph. The node must be an object, with a + * string under the key "id". Except for this, it is possible to add any + * other attribute, that will be preserved all along the manipulations. + * + * If the graph option "clone" has a truthy value, the node will be cloned + * when added to the graph. Also, if the graph option "immutable" has a + * truthy value, its id will be defined as immutable. + * + * @param {object} node The node to add. + * @return {object} The graph instance. + */ + graph.addMethod('addNode', function(node) { + // Check that the node is an object and has an id: + if (Object(node) !== node || arguments.length !== 1) + throw 'addNode: Wrong arguments.'; + + if (typeof node.id !== 'string' && typeof node.id !== 'number') + throw 'The node must have a string or number id.'; + + if (this.nodesIndex[node.id]) + throw 'The node "' + node.id + '" already exists.'; + + var k, + id = node.id, + validNode = Object.create(null); + + // Check the "clone" option: + if (this.settings('clone')) { + for (k in node) + if (k !== 'id') + validNode[k] = node[k]; + } else + validNode = node; + + // Check the "immutable" option: + if (this.settings('immutable')) + Object.defineProperty(validNode, 'id', { + value: id, + enumerable: true + }); + else + validNode.id = id; + + // Add empty containers for edges indexes: + this.inNeighborsIndex[id] = Object.create(null); + this.outNeighborsIndex[id] = Object.create(null); + this.allNeighborsIndex[id] = Object.create(null); + + this.inNeighborsCount[id] = 0; + this.outNeighborsCount[id] = 0; + this.allNeighborsCount[id] = 0; + + // Add the node to indexes: + this.nodesArray.push(validNode); + this.nodesIndex[validNode.id] = validNode; + + // Return the current instance: + return this; + }); + + /** + * This method adds an edge to the graph. The edge must be an object, with a + * string under the key "id", and strings under the keys "source" and + * "target" that design existing nodes. Except for this, it is possible to + * add any other attribute, that will be preserved all along the + * manipulations. + * + * If the graph option "clone" has a truthy value, the edge will be cloned + * when added to the graph. Also, if the graph option "immutable" has a + * truthy value, its id, source and target will be defined as immutable. + * + * @param {object} edge The edge to add. + * @return {object} The graph instance. + */ + graph.addMethod('addEdge', function(edge) { + // Check that the edge is an object and has an id: + if (Object(edge) !== edge || arguments.length !== 1) + throw 'addEdge: Wrong arguments.'; + + if (typeof edge.id !== 'string' && typeof edge.id !== 'number') + throw 'The edge must have a string or number id.'; + + if ((typeof edge.source !== 'string' && typeof edge.source !== 'number') || + !this.nodesIndex[edge.source]) + throw 'The edge source must have an existing node id.'; + + if ((typeof edge.target !== 'string' && typeof edge.target !== 'number') || + !this.nodesIndex[edge.target]) + throw 'The edge target must have an existing node id.'; + + if (this.edgesIndex[edge.id]) + throw 'The edge "' + edge.id + '" already exists.'; + + var k, + validEdge = Object.create(null); + + // Check the "clone" option: + if (this.settings('clone')) { + for (k in edge) + if (k !== 'id' && k !== 'source' && k !== 'target') + validEdge[k] = edge[k]; + } else + validEdge = edge; + + // Check the "immutable" option: + if (this.settings('immutable')) { + Object.defineProperty(validEdge, 'id', { + value: edge.id, + enumerable: true + }); + + Object.defineProperty(validEdge, 'source', { + value: edge.source, + enumerable: true + }); + + Object.defineProperty(validEdge, 'target', { + value: edge.target, + enumerable: true + }); + } else { + validEdge.id = edge.id; + validEdge.source = edge.source; + validEdge.target = edge.target; + } + + // Add the edge to indexes: + this.edgesArray.push(validEdge); + this.edgesIndex[validEdge.id] = validEdge; + + if (!this.inNeighborsIndex[validEdge.target][validEdge.source]) + this.inNeighborsIndex[validEdge.target][validEdge.source] = + Object.create(null); + this.inNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] = + validEdge; + + if (!this.outNeighborsIndex[validEdge.source][validEdge.target]) + this.outNeighborsIndex[validEdge.source][validEdge.target] = + Object.create(null); + this.outNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] = + validEdge; + + if (!this.allNeighborsIndex[validEdge.source][validEdge.target]) + this.allNeighborsIndex[validEdge.source][validEdge.target] = + Object.create(null); + this.allNeighborsIndex[validEdge.source][validEdge.target][validEdge.id] = + validEdge; + + if (validEdge.target !== validEdge.source) { + if (!this.allNeighborsIndex[validEdge.target][validEdge.source]) + this.allNeighborsIndex[validEdge.target][validEdge.source] = + Object.create(null); + this.allNeighborsIndex[validEdge.target][validEdge.source][validEdge.id] = + validEdge; + } + + // Keep counts up to date: + this.inNeighborsCount[validEdge.target]++; + this.outNeighborsCount[validEdge.source]++; + this.allNeighborsCount[validEdge.target]++; + this.allNeighborsCount[validEdge.source]++; + + return this; + }); + + /** + * This method drops a node from the graph. It also removes each edge that is + * bound to it, through the dropEdge method. An error is thrown if the node + * does not exist. + * + * @param {string} id The node id. + * @return {object} The graph instance. + */ + graph.addMethod('dropNode', function(id) { + // Check that the arguments are valid: + if ((typeof id !== 'string' && typeof id !== 'number') || + arguments.length !== 1) + throw 'dropNode: Wrong arguments.'; + + if (!this.nodesIndex[id]) + throw 'The node "' + id + '" does not exist.'; + + var i, k, l; + + // Remove the node from indexes: + delete this.nodesIndex[id]; + for (i = 0, l = this.nodesArray.length; i < l; i++) + if (this.nodesArray[i].id === id) { + this.nodesArray.splice(i, 1); + break; + } + + // Remove related edges: + for (i = this.edgesArray.length - 1; i >= 0; i--) + if (this.edgesArray[i].source === id || this.edgesArray[i].target === id) + this.dropEdge(this.edgesArray[i].id); + + // Remove related edge indexes: + delete this.inNeighborsIndex[id]; + delete this.outNeighborsIndex[id]; + delete this.allNeighborsIndex[id]; + + delete this.inNeighborsCount[id]; + delete this.outNeighborsCount[id]; + delete this.allNeighborsCount[id]; + + for (k in this.nodesIndex) { + delete this.inNeighborsIndex[k][id]; + delete this.outNeighborsIndex[k][id]; + delete this.allNeighborsIndex[k][id]; + } + + return this; + }); + + /** + * This method drops an edge from the graph. An error is thrown if the edge + * does not exist. + * + * @param {string} id The edge id. + * @return {object} The graph instance. + */ + graph.addMethod('dropEdge', function(id) { + // Check that the arguments are valid: + if ((typeof id !== 'string' && typeof id !== 'number') || + arguments.length !== 1) + throw 'dropEdge: Wrong arguments.'; + + if (!this.edgesIndex[id]) + throw 'The edge "' + id + '" does not exist.'; + + var i, l, edge; + + // Remove the edge from indexes: + edge = this.edgesIndex[id]; + delete this.edgesIndex[id]; + for (i = 0, l = this.edgesArray.length; i < l; i++) + if (this.edgesArray[i].id === id) { + this.edgesArray.splice(i, 1); + break; + } + + delete this.inNeighborsIndex[edge.target][edge.source][edge.id]; + if (!Object.keys(this.inNeighborsIndex[edge.target][edge.source]).length) + delete this.inNeighborsIndex[edge.target][edge.source]; + + delete this.outNeighborsIndex[edge.source][edge.target][edge.id]; + if (!Object.keys(this.outNeighborsIndex[edge.source][edge.target]).length) + delete this.outNeighborsIndex[edge.source][edge.target]; + + delete this.allNeighborsIndex[edge.source][edge.target][edge.id]; + if (!Object.keys(this.allNeighborsIndex[edge.source][edge.target]).length) + delete this.allNeighborsIndex[edge.source][edge.target]; + + if (edge.target !== edge.source) { + delete this.allNeighborsIndex[edge.target][edge.source][edge.id]; + if (!Object.keys(this.allNeighborsIndex[edge.target][edge.source]).length) + delete this.allNeighborsIndex[edge.target][edge.source]; + } + + this.inNeighborsCount[edge.target]--; + this.outNeighborsCount[edge.source]--; + this.allNeighborsCount[edge.source]--; + this.allNeighborsCount[edge.target]--; + + return this; + }); + + /** + * This method destroys the current instance. It basically empties each index + * and methods attached to the graph. + */ + graph.addMethod('kill', function() { + // Delete arrays: + this.nodesArray.length = 0; + this.edgesArray.length = 0; + delete this.nodesArray; + delete this.edgesArray; + + // Delete indexes: + delete this.nodesIndex; + delete this.edgesIndex; + delete this.inNeighborsIndex; + delete this.outNeighborsIndex; + delete this.allNeighborsIndex; + delete this.inNeighborsCount; + delete this.outNeighborsCount; + delete this.allNeighborsCount; + }); + + /** + * This method empties the nodes and edges arrays, as well as the different + * indexes. + * + * @return {object} The graph instance. + */ + graph.addMethod('clear', function() { + this.nodesArray.length = 0; + this.edgesArray.length = 0; + + // Due to GC issues, I prefer not to create new object. These objects are + // only available from the methods and attached functions, but still, it is + // better to prevent ghost references to unrelevant data... + __emptyObject(this.nodesIndex); + __emptyObject(this.edgesIndex); + __emptyObject(this.nodesIndex); + __emptyObject(this.inNeighborsIndex); + __emptyObject(this.outNeighborsIndex); + __emptyObject(this.allNeighborsIndex); + __emptyObject(this.inNeighborsCount); + __emptyObject(this.outNeighborsCount); + __emptyObject(this.allNeighborsCount); + + return this; + }); + + /** + * This method reads an object and adds the nodes and edges, through the + * proper methods "addNode" and "addEdge". + * + * Here is an example: + * + * > var myGraph = new graph(); + * > myGraph.read({ + * > nodes: [ + * > { id: 'n0' }, + * > { id: 'n1' } + * > ], + * > edges: [ + * > { + * > id: 'e0', + * > source: 'n0', + * > target: 'n1' + * > } + * > ] + * > }); + * > + * > console.log( + * > myGraph.nodes().length, + * > myGraph.edges().length + * > ); // outputs 2 1 + * + * @param {object} g The graph object. + * @return {object} The graph instance. + */ + graph.addMethod('read', function(g) { + var i, + a, + l; + + a = g.nodes || []; + for (i = 0, l = a.length; i < l; i++) + this.addNode(a[i]); + + a = g.edges || []; + for (i = 0, l = a.length; i < l; i++) + this.addEdge(a[i]); + + return this; + }); + + /** + * This methods returns one or several nodes, depending on how it is called. + * + * To get the array of nodes, call "nodes" without argument. To get a + * specific node, call it with the id of the node. The get multiple node, + * call it with an array of ids, and it will return the array of nodes, in + * the same order. + * + * @param {?(string|array)} v Eventually one id, an array of ids. + * @return {object|array} The related node or array of nodes. + */ + graph.addMethod('nodes', function(v) { + // Clone the array of nodes and return it: + if (!arguments.length) + return this.nodesArray.slice(0); + + // Return the related node: + if (arguments.length === 1 && + (typeof v === 'string' || typeof v === 'number')) + return this.nodesIndex[v]; + + // Return an array of the related node: + if ( + arguments.length === 1 && + Object.prototype.toString.call(v) === '[object Array]' + ) { + var i, + l, + a = []; + + for (i = 0, l = v.length; i < l; i++) + if (typeof v[i] === 'string' || typeof v[i] === 'number') + a.push(this.nodesIndex[v[i]]); + else + throw 'nodes: Wrong arguments.'; + + return a; + } + + throw 'nodes: Wrong arguments.'; + }); + + /** + * This methods returns the degree of one or several nodes, depending on how + * it is called. It is also possible to get incoming or outcoming degrees + * instead by specifying 'in' or 'out' as a second argument. + * + * @param {string|array} v One id, an array of ids. + * @param {?string} which Which degree is required. Values are 'in', + * 'out', and by default the normal degree. + * @return {number|array} The related degree or array of degrees. + */ + graph.addMethod('degree', function(v, which) { + // Check which degree is required: + which = { + 'in': this.inNeighborsCount, + 'out': this.outNeighborsCount + }[which || ''] || this.allNeighborsCount; + + // Return the related node: + if (typeof v === 'string' || typeof v === 'number') + return which[v]; + + // Return an array of the related node: + if (Object.prototype.toString.call(v) === '[object Array]') { + var i, + l, + a = []; + + for (i = 0, l = v.length; i < l; i++) + if (typeof v[i] === 'string' || typeof v[i] === 'number') + a.push(which[v[i]]); + else + throw 'degree: Wrong arguments.'; + + return a; + } + + throw 'degree: Wrong arguments.'; + }); + + /** + * This methods returns one or several edges, depending on how it is called. + * + * To get the array of edges, call "edges" without argument. To get a + * specific edge, call it with the id of the edge. The get multiple edge, + * call it with an array of ids, and it will return the array of edges, in + * the same order. + * + * @param {?(string|array)} v Eventually one id, an array of ids. + * @return {object|array} The related edge or array of edges. + */ + graph.addMethod('edges', function(v) { + // Clone the array of edges and return it: + if (!arguments.length) + return this.edgesArray.slice(0); + + // Return the related edge: + if (arguments.length === 1 && + (typeof v === 'string' || typeof v === 'number')) + return this.edgesIndex[v]; + + // Return an array of the related edge: + if ( + arguments.length === 1 && + Object.prototype.toString.call(v) === '[object Array]' + ) { + var i, + l, + a = []; + + for (i = 0, l = v.length; i < l; i++) + if (typeof v[i] === 'string' || typeof v[i] === 'number') + a.push(this.edgesIndex[v[i]]); + else + throw 'edges: Wrong arguments.'; + + return a; + } + + throw 'edges: Wrong arguments.'; + }); + + + /** + * EXPORT: + * ******* + */ + if (typeof sigma !== 'undefined') { + sigma.classes = sigma.classes || Object.create(null); + sigma.classes.graph = graph; + } else if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = graph; + exports.graph = graph; + } else + this.graph = graph; +}).call(this); diff --git a/blogContent/projects/steam/src/classes/sigma.classes.quad.js b/blogContent/projects/steam/src/classes/sigma.classes.quad.js new file mode 100644 index 0000000..fb11386 --- /dev/null +++ b/blogContent/projects/steam/src/classes/sigma.classes.quad.js @@ -0,0 +1,674 @@ +;(function(undefined) { + 'use strict'; + + /** + * Sigma Quadtree Module + * ===================== + * + * Author: Guillaume Plique (Yomguithereal) + * Version: 0.2 + */ + + + + /** + * Quad Geometric Operations + * ------------------------- + * + * A useful batch of geometric operations used by the quadtree. + */ + + var _geom = { + + /** + * Transforms a graph node with x, y and size into an + * axis-aligned square. + * + * @param {object} A graph node with at least a point (x, y) and a size. + * @return {object} A square: two points (x1, y1), (x2, y2) and height. + */ + pointToSquare: function(n) { + return { + x1: n.x - n.size, + y1: n.y - n.size, + x2: n.x + n.size, + y2: n.y - n.size, + height: n.size * 2 + }; + }, + + /** + * Checks whether a rectangle is axis-aligned. + * + * @param {object} A rectangle defined by two points + * (x1, y1) and (x2, y2). + * @return {boolean} True if the rectangle is axis-aligned. + */ + isAxisAligned: function(r) { + return r.x1 === r.x2 || r.y1 === r.y2; + }, + + /** + * Compute top points of an axis-aligned rectangle. This is useful in + * cases when the rectangle has been rotated (left, right or bottom up) and + * later operations need to know the top points. + * + * @param {object} An axis-aligned rectangle defined by two points + * (x1, y1), (x2, y2) and height. + * @return {object} A rectangle: two points (x1, y1), (x2, y2) and height. + */ + axisAlignedTopPoints: function(r) { + + // Basic + if (r.y1 === r.y2 && r.x1 < r.x2) + return r; + + // Rotated to right + if (r.x1 === r.x2 && r.y2 > r.y1) + return { + x1: r.x1 - r.height, y1: r.y1, + x2: r.x1, y2: r.y1, + height: r.height + }; + + // Rotated to left + if (r.x1 === r.x2 && r.y2 < r.y1) + return { + x1: r.x1, y1: r.y2, + x2: r.x2 + r.height, y2: r.y2, + height: r.height + }; + + // Bottom's up + return { + x1: r.x2, y1: r.y1 - r.height, + x2: r.x1, y2: r.y1 - r.height, + height: r.height + }; + }, + + /** + * Get coordinates of a rectangle's lower left corner from its top points. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @return {object} Coordinates of the corner (x, y). + */ + lowerLeftCoor: function(r) { + var width = ( + Math.sqrt( + Math.pow(r.x2 - r.x1, 2) + + Math.pow(r.y2 - r.y1, 2) + ) + ); + + return { + x: r.x1 - (r.y2 - r.y1) * r.height / width, + y: r.y1 + (r.x2 - r.x1) * r.height / width + }; + }, + + /** + * Get coordinates of a rectangle's lower right corner from its top points + * and its lower left corner. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @param {object} A corner's coordinates (x, y). + * @return {object} Coordinates of the corner (x, y). + */ + lowerRightCoor: function(r, llc) { + return { + x: llc.x - r.x1 + r.x2, + y: llc.y - r.y1 + r.y2 + }; + }, + + /** + * Get the coordinates of all the corners of a rectangle from its top point. + * + * @param {object} A rectangle defined by two points (x1, y1) and (x2, y2). + * @return {array} An array of the four corners' coordinates (x, y). + */ + rectangleCorners: function(r) { + var llc = this.lowerLeftCoor(r), + lrc = this.lowerRightCoor(r, llc); + + return [ + {x: r.x1, y: r.y1}, + {x: r.x2, y: r.y2}, + {x: llc.x, y: llc.y}, + {x: lrc.x, y: lrc.y} + ]; + }, + + /** + * Split a square defined by its boundaries into four. + * + * @param {object} Boundaries of the square (x, y, width, height). + * @return {array} An array containing the four new squares, themselves + * defined by an array of their four corners (x, y). + */ + splitSquare: function(b) { + return [ + [ + {x: b.x, y: b.y}, + {x: b.x + b.width / 2, y: b.y}, + {x: b.x, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height / 2} + ], + [ + {x: b.x + b.width / 2, y: b.y}, + {x: b.x + b.width, y: b.y}, + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x + b.width, y: b.y + b.height / 2} + ], + [ + {x: b.x, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x, y: b.y + b.height}, + {x: b.x + b.width / 2, y: b.y + b.height} + ], + [ + {x: b.x + b.width / 2, y: b.y + b.height / 2}, + {x: b.x + b.width, y: b.y + b.height / 2}, + {x: b.x + b.width / 2, y: b.y + b.height}, + {x: b.x + b.width, y: b.y + b.height} + ] + ]; + }, + + /** + * Compute the four axis between corners of rectangle A and corners of + * rectangle B. This is needed later to check an eventual collision. + * + * @param {array} An array of rectangle A's four corners (x, y). + * @param {array} An array of rectangle B's four corners (x, y). + * @return {array} An array of four axis defined by their coordinates (x,y). + */ + axis: function(c1, c2) { + return [ + {x: c1[1].x - c1[0].x, y: c1[1].y - c1[0].y}, + {x: c1[1].x - c1[3].x, y: c1[1].y - c1[3].y}, + {x: c2[0].x - c2[2].x, y: c2[0].y - c2[2].y}, + {x: c2[0].x - c2[1].x, y: c2[0].y - c2[1].y} + ]; + }, + + /** + * Project a rectangle's corner on an axis. + * + * @param {object} Coordinates of a corner (x, y). + * @param {object} Coordinates of an axis (x, y). + * @return {object} The projection defined by coordinates (x, y). + */ + projection: function(c, a) { + var l = ( + (c.x * a.x + c.y * a.y) / + (Math.pow(a.x, 2) + Math.pow(a.y, 2)) + ); + + return { + x: l * a.x, + y: l * a.y + }; + }, + + /** + * Check whether two rectangles collide on one particular axis. + * + * @param {object} An axis' coordinates (x, y). + * @param {array} Rectangle A's corners. + * @param {array} Rectangle B's corners. + * @return {boolean} True if the rectangles collide on the axis. + */ + axisCollision: function(a, c1, c2) { + var sc1 = [], + sc2 = []; + + for (var ci = 0; ci < 4; ci++) { + var p1 = this.projection(c1[ci], a), + p2 = this.projection(c2[ci], a); + + sc1.push(p1.x * a.x + p1.y * a.y); + sc2.push(p2.x * a.x + p2.y * a.y); + } + + var maxc1 = Math.max.apply(Math, sc1), + maxc2 = Math.max.apply(Math, sc2), + minc1 = Math.min.apply(Math, sc1), + minc2 = Math.min.apply(Math, sc2); + + return (minc2 <= maxc1 && maxc2 >= minc1); + }, + + /** + * Check whether two rectangles collide on each one of their four axis. If + * all axis collide, then the two rectangles do collide on the plane. + * + * @param {array} Rectangle A's corners. + * @param {array} Rectangle B's corners. + * @return {boolean} True if the rectangles collide. + */ + collision: function(c1, c2) { + var axis = this.axis(c1, c2), + col = true; + + for (var i = 0; i < 4; i++) + col = col && this.axisCollision(axis[i], c1, c2); + + return col; + } + }; + + + /** + * Quad Functions + * ------------ + * + * The Quadtree functions themselves. + * For each of those functions, we consider that in a splitted quad, the + * index of each node is the following: + * 0: top left + * 1: top right + * 2: bottom left + * 3: bottom right + * + * Moreover, the hereafter quad's philosophy is to consider that if an element + * collides with more than one nodes, this element belongs to each of the + * nodes it collides with where other would let it lie on a higher node. + */ + + /** + * Get the index of the node containing the point in the quad + * + * @param {object} point A point defined by coordinates (x, y). + * @param {object} quadBounds Boundaries of the quad (x, y, width, heigth). + * @return {integer} The index of the node containing the point. + */ + function _quadIndex(point, quadBounds) { + var xmp = quadBounds.x + quadBounds.width / 2, + ymp = quadBounds.y + quadBounds.height / 2, + top = (point.y < ymp), + left = (point.x < xmp); + + if (top) { + if (left) + return 0; + else + return 1; + } + else { + if (left) + return 2; + else + return 3; + } + } + + /** + * Get a list of indexes of nodes containing an axis-aligned rectangle + * + * @param {object} rectangle A rectangle defined by two points (x1, y1), + * (x2, y2) and height. + * @param {array} quadCorners An array of the quad nodes' corners. + * @return {array} An array of indexes containing one to + * four integers. + */ + function _quadIndexes(rectangle, quadCorners) { + var indexes = []; + + // Iterating through quads + for (var i = 0; i < 4; i++) + if ((rectangle.x2 >= quadCorners[i][0].x) && + (rectangle.x1 <= quadCorners[i][1].x) && + (rectangle.y1 + rectangle.height >= quadCorners[i][0].y) && + (rectangle.y1 <= quadCorners[i][2].y)) + indexes.push(i); + + return indexes; + } + + /** + * Get a list of indexes of nodes containing a non-axis-aligned rectangle + * + * @param {array} corners An array containing each corner of the + * rectangle defined by its coordinates (x, y). + * @param {array} quadCorners An array of the quad nodes' corners. + * @return {array} An array of indexes containing one to + * four integers. + */ + function _quadCollision(corners, quadCorners) { + var indexes = []; + + // Iterating through quads + for (var i = 0; i < 4; i++) + if (_geom.collision(corners, quadCorners[i])) + indexes.push(i); + + return indexes; + } + + /** + * Subdivide a quad by creating a node at a precise index. The function does + * not generate all four nodes not to potentially create unused nodes. + * + * @param {integer} index The index of the node to create. + * @param {object} quad The quad object to subdivide. + * @return {object} A new quad representing the node created. + */ + function _quadSubdivide(index, quad) { + var next = quad.level + 1, + subw = Math.round(quad.bounds.width / 2), + subh = Math.round(quad.bounds.height / 2), + qx = Math.round(quad.bounds.x), + qy = Math.round(quad.bounds.y), + x, + y; + + switch (index) { + case 0: + x = qx; + y = qy; + break; + case 1: + x = qx + subw; + y = qy; + break; + case 2: + x = qx; + y = qy + subh; + break; + case 3: + x = qx + subw; + y = qy + subh; + break; + } + + return _quadTree( + {x: x, y: y, width: subw, height: subh}, + next, + quad.maxElements, + quad.maxLevel + ); + } + + /** + * Recursively insert an element into the quadtree. Only points + * with size, i.e. axis-aligned squares, may be inserted with this + * method. + * + * @param {object} el The element to insert in the quadtree. + * @param {object} sizedPoint A sized point defined by two top points + * (x1, y1), (x2, y2) and height. + * @param {object} quad The quad in which to insert the element. + * @return {undefined} The function does not return anything. + */ + function _quadInsert(el, sizedPoint, quad) { + if (quad.level < quad.maxLevel) { + + // Searching appropriate quads + var indexes = _quadIndexes(sizedPoint, quad.corners); + + // Iterating + for (var i = 0, l = indexes.length; i < l; i++) { + + // Subdividing if necessary + if (quad.nodes[indexes[i]] === undefined) + quad.nodes[indexes[i]] = _quadSubdivide(indexes[i], quad); + + // Recursion + _quadInsert(el, sizedPoint, quad.nodes[indexes[i]]); + } + } + else { + + // Pushing the element in a leaf node + quad.elements.push(el); + } + } + + /** + * Recursively retrieve every elements held by the node containing the + * searched point. + * + * @param {object} point The searched point (x, y). + * @param {object} quad The searched quad. + * @return {array} An array of elements contained in the relevant + * node. + */ + function _quadRetrievePoint(point, quad) { + if (quad.level < quad.maxLevel) { + var index = _quadIndex(point, quad.bounds); + + // If node does not exist we return an empty list + if (quad.nodes[index] !== undefined) { + return _quadRetrievePoint(point, quad.nodes[index]); + } + else { + return []; + } + } + else { + return quad.elements; + } + } + + /** + * Recursively retrieve every elements contained within an rectangular area + * that may or may not be axis-aligned. + * + * @param {object|array} rectData The searched area defined either by + * an array of four corners (x, y) in + * the case of a non-axis-aligned + * rectangle or an object with two top + * points (x1, y1), (x2, y2) and height. + * @param {object} quad The searched quad. + * @param {function} collisionFunc The collision function used to search + * for node indexes. + * @param {array?} els The retrieved elements. + * @return {array} An array of elements contained in the + * area. + */ + function _quadRetrieveArea(rectData, quad, collisionFunc, els) { + els = els || {}; + + if (quad.level < quad.maxLevel) { + var indexes = collisionFunc(rectData, quad.corners); + + for (var i = 0, l = indexes.length; i < l; i++) + if (quad.nodes[indexes[i]] !== undefined) + _quadRetrieveArea( + rectData, + quad.nodes[indexes[i]], + collisionFunc, + els + ); + } else + for (var j = 0, m = quad.elements.length; j < m; j++) + if (els[quad.elements[j].id] === undefined) + els[quad.elements[j].id] = quad.elements[j]; + + return els; + } + + /** + * Creates the quadtree object itself. + * + * @param {object} bounds The boundaries of the quad defined by an + * origin (x, y), width and heigth. + * @param {integer} level The level of the quad in the tree. + * @param {integer} maxElements The max number of element in a leaf node. + * @param {integer} maxLevel The max recursion level of the tree. + * @return {object} The quadtree object. + */ + function _quadTree(bounds, level, maxElements, maxLevel) { + return { + level: level || 0, + bounds: bounds, + corners: _geom.splitSquare(bounds), + maxElements: maxElements || 20, + maxLevel: maxLevel || 4, + elements: [], + nodes: [] + }; + } + + + /** + * Sigma Quad Constructor + * ---------------------- + * + * The quad API as exposed to sigma. + */ + + /** + * The quad core that will become the sigma interface with the quadtree. + * + * property {object} _tree Property holding the quadtree object. + * property {object} _geom Exposition of the _geom namespace for testing. + * property {object} _cache Cache for the area method. + */ + var quad = function() { + this._geom = _geom; + this._tree = null; + this._cache = { + query: false, + result: false + }; + }; + + /** + * Index a graph by inserting its nodes into the quadtree. + * + * @param {array} nodes An array of nodes to index. + * @param {object} params An object of parameters with at least the quad + * bounds. + * @return {object} The quadtree object. + * + * Parameters: + * ---------- + * bounds: {object} boundaries of the quad defined by its origin (x, y) + * width and heigth. + * prefix: {string?} a prefix for node geometric attributes. + * maxElements: {integer?} the max number of elements in a leaf node. + * maxLevel: {integer?} the max recursion level of the tree. + */ + quad.prototype.index = function(nodes, params) { + + // Enforcing presence of boundaries + if (!params.bounds) + throw 'sigma.classes.quad.index: bounds information not given.'; + + // Prefix + var prefix = params.prefix || ''; + + // Building the tree + this._tree = _quadTree( + params.bounds, + 0, + params.maxElements, + params.maxLevel + ); + + // Inserting graph nodes into the tree + for (var i = 0, l = nodes.length; i < l; i++) { + + // Inserting node + _quadInsert( + nodes[i], + _geom.pointToSquare({ + x: nodes[i][prefix + 'x'], + y: nodes[i][prefix + 'y'], + size: nodes[i][prefix + 'size'] + }), + this._tree + ); + } + + // Reset cache: + this._cache = { + query: false, + result: false + }; + + // remove? + return this._tree; + }; + + /** + * Retrieve every graph nodes held by the quadtree node containing the + * searched point. + * + * @param {number} x of the point. + * @param {number} y of the point. + * @return {array} An array of nodes retrieved. + */ + quad.prototype.point = function(x, y) { + return this._tree ? + _quadRetrievePoint({x: x, y: y}, this._tree) || [] : + []; + }; + + /** + * Retrieve every graph nodes within a rectangular area. The methods keep the + * last area queried in cache for optimization reason and will act differently + * for the same reason if the area is axis-aligned or not. + * + * @param {object} A rectangle defined by two top points (x1, y1), (x2, y2) + * and height. + * @return {array} An array of nodes retrieved. + */ + quad.prototype.area = function(rect) { + var serialized = JSON.stringify(rect), + collisionFunc, + rectData; + + // Returning cache? + if (this._cache.query === serialized) + return this._cache.result; + + // Axis aligned ? + if (_geom.isAxisAligned(rect)) { + collisionFunc = _quadIndexes; + rectData = _geom.axisAlignedTopPoints(rect); + } + else { + collisionFunc = _quadCollision; + rectData = _geom.rectangleCorners(rect); + } + + // Retrieving nodes + var nodes = this._tree ? + _quadRetrieveArea( + rectData, + this._tree, + collisionFunc + ) : + []; + + // Object to array + var nodesArray = []; + for (var i in nodes) + nodesArray.push(nodes[i]); + + // Caching + this._cache.query = serialized; + this._cache.result = nodesArray; + + return nodesArray; + }; + + + /** + * EXPORT: + * ******* + */ + if (typeof this.sigma !== 'undefined') { + this.sigma.classes = this.sigma.classes || {}; + this.sigma.classes.quad = quad; + } else if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = quad; + exports.quad = quad; + } else + this.quad = quad; +}).call(this); diff --git a/blogContent/projects/steam/src/conrad.js b/blogContent/projects/steam/src/conrad.js new file mode 100644 index 0000000..bdb6610 --- /dev/null +++ b/blogContent/projects/steam/src/conrad.js @@ -0,0 +1,984 @@ +/** + * conrad.js is a tiny JavaScript jobs scheduler, + * + * Version: 0.1.0 + * Sources: http://github.com/jacomyal/conrad.js + * Doc: http://github.com/jacomyal/conrad.js#readme + * + * License: + * -------- + * Copyright © 2013 Alexis Jacomy, Sciences-Po médialab + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising + * from, out of or in connection with the software or the use or other dealings + * in the Software. + */ +(function(global) { + 'use strict'; + + // Check that conrad.js has not been loaded yet: + if (global.conrad) + throw new Error('conrad already exists'); + + + /** + * PRIVATE VARIABLES: + * ****************** + */ + + /** + * A flag indicating whether conrad is running or not. + * + * @type {Number} + */ + var _lastFrameTime; + + /** + * A flag indicating whether conrad is running or not. + * + * @type {Boolean} + */ + var _isRunning = false; + + /** + * The hash of registered jobs. Each job must at least have a unique ID + * under the key "id" and a function under the key "job". This hash + * contains each running job and each waiting job. + * + * @type {Object} + */ + var _jobs = {}; + + /** + * The hash of currently running jobs. + * + * @type {Object} + */ + var _runningJobs = {}; + + /** + * The array of currently running jobs, sorted by priority. + * + * @type {Array} + */ + var _sortedByPriorityJobs = []; + + /** + * The array of currently waiting jobs. + * + * @type {Object} + */ + var _waitingJobs = {}; + + /** + * The array of finished jobs. They are stored in an array, since two jobs + * with the same "id" can happen at two different times. + * + * @type {Array} + */ + var _doneJobs = []; + + /** + * A dirty flag to keep conrad from starting: Indeed, when addJob() is called + * with several jobs, conrad must be started only at the end. This flag keeps + * me from duplicating the code that effectively adds a job. + * + * @type {Boolean} + */ + var _noStart = false; + + /** + * An hash containing some global settings about how conrad.js should + * behave. + * + * @type {Object} + */ + var _parameters = { + frameDuration: 20, + history: true + }; + + /** + * This object contains every handlers bound to conrad events. It does not + * requirea any DOM implementation, since the events are all JavaScript. + * + * @type {Object} + */ + var _handlers = Object.create(null); + + + /** + * PRIVATE FUNCTIONS: + * ****************** + */ + + /** + * Will execute the handler everytime that the indicated event (or the + * indicated events) will be triggered. + * + * @param {string|array|object} events The name of the event (or the events + * separated by spaces). + * @param {function(Object)} handler The handler to bind. + * @return {Object} Returns conrad. + */ + function _bind(events, handler) { + var i, + i_end, + event, + eArray; + + if (!arguments.length) + return; + else if ( + arguments.length === 1 && + Object(arguments[0]) === arguments[0] + ) + for (events in arguments[0]) + _bind(events, arguments[0][events]); + else if (arguments.length > 1) { + eArray = + Array.isArray(events) ? + events : + events.split(/ /); + + for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { + event = eArray[i]; + + if (!_handlers[event]) + _handlers[event] = []; + + // Using an object instead of directly the handler will make possible + // later to add flags + _handlers[event].push({ + handler: handler + }); + } + } + } + + /** + * Removes the handler from a specified event (or specified events). + * + * @param {?string} events The name of the event (or the events + * separated by spaces). If undefined, + * then all handlers are removed. + * @param {?function(Object)} handler The handler to unbind. If undefined, + * each handler bound to the event or the + * events will be removed. + * @return {Object} Returns conrad. + */ + function _unbind(events, handler) { + var i, + i_end, + j, + j_end, + a, + event, + eArray = Array.isArray(events) ? + events : + events.split(/ /); + + if (!arguments.length) + _handlers = Object.create(null); + else if (handler) { + for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { + event = eArray[i]; + if (_handlers[event]) { + a = []; + for (j = 0, j_end = _handlers[event].length; j !== j_end; j += 1) + if (_handlers[event][j].handler !== handler) + a.push(_handlers[event][j]); + + _handlers[event] = a; + } + + if (_handlers[event] && _handlers[event].length === 0) + delete _handlers[event]; + } + } else + for (i = 0, i_end = eArray.length; i !== i_end; i += 1) + delete _handlers[eArray[i]]; + } + + /** + * Executes each handler bound to the event. + * + * @param {string} events The name of the event (or the events separated + * by spaces). + * @param {?Object} data The content of the event (optional). + * @return {Object} Returns conrad. + */ + function _dispatch(events, data) { + var i, + j, + i_end, + j_end, + event, + eventName, + eArray = Array.isArray(events) ? + events : + events.split(/ /); + + data = data === undefined ? {} : data; + + for (i = 0, i_end = eArray.length; i !== i_end; i += 1) { + eventName = eArray[i]; + + if (_handlers[eventName]) { + event = { + type: eventName, + data: data || {} + }; + + for (j = 0, j_end = _handlers[eventName].length; j !== j_end; j += 1) + try { + _handlers[eventName][j].handler(event); + } catch (e) {} + } + } + } + + /** + * Executes the most prioritary job once, and deals with filling the stats + * (done, time, averageTime, currentTime, etc...). + * + * @return {?Object} Returns the job object if it has to be killed, null else. + */ + function _executeFirstJob() { + var i, + l, + test, + kill, + pushed = false, + time = __dateNow(), + job = _sortedByPriorityJobs.shift(); + + // Execute the job and look at the result: + test = job.job(); + + // Deal with stats: + time = __dateNow() - time; + job.done++; + job.time += time; + job.currentTime += time; + job.weightTime = job.currentTime / (job.weight || 1); + job.averageTime = job.time / job.done; + + // Check if the job has to be killed: + kill = job.count ? (job.count <= job.done) : !test; + + // Reset priorities: + if (!kill) { + for (i = 0, l = _sortedByPriorityJobs.length; i < l; i++) + if (_sortedByPriorityJobs[i].weightTime > job.weightTime) { + _sortedByPriorityJobs.splice(i, 0, job); + pushed = true; + break; + } + + if (!pushed) + _sortedByPriorityJobs.push(job); + } + + return kill ? job : null; + } + + /** + * Activates a job, by adding it to the _runningJobs object and the + * _sortedByPriorityJobs array. It also initializes its currentTime value. + * + * @param {Object} job The job to activate. + */ + function _activateJob(job) { + var l = _sortedByPriorityJobs.length; + + // Add the job to the running jobs: + _runningJobs[job.id] = job; + job.status = 'running'; + + // Add the job to the priorities: + if (l) { + job.weightTime = _sortedByPriorityJobs[l - 1].weightTime; + job.currentTime = job.weightTime * (job.weight || 1); + } + + // Initialize the job and dispatch: + job.startTime = __dateNow(); + _dispatch('jobStarted', __clone(job)); + + _sortedByPriorityJobs.push(job); + } + + /** + * The main loop of conrad.js: + * . It executes job such that they all occupate the same processing time. + * . It stops jobs that do not need to be executed anymore. + * . It triggers callbacks when it is relevant. + * . It starts waiting jobs when they need to be started. + * . It injects frames to keep a constant frapes per second ratio. + * . It stops itself when there are no more jobs to execute. + */ + function _loop() { + var k, + o, + l, + job, + time, + deadJob; + + // Deal with the newly added jobs (the _jobs object): + for (k in _jobs) { + job = _jobs[k]; + + if (job.after) + _waitingJobs[k] = job; + else + _activateJob(job); + + delete _jobs[k]; + } + + // Set the _isRunning flag to false if there are no running job: + _isRunning = !!_sortedByPriorityJobs.length; + + // Deal with the running jobs (the _runningJobs object): + while ( + _sortedByPriorityJobs.length && + __dateNow() - _lastFrameTime < _parameters.frameDuration + ) { + deadJob = _executeFirstJob(); + + // Deal with the case where the job has ended: + if (deadJob) { + _killJob(deadJob.id); + + // Check for waiting jobs: + for (k in _waitingJobs) + if (_waitingJobs[k].after === deadJob.id) { + _activateJob(_waitingJobs[k]); + delete _waitingJobs[k]; + } + } + } + + // Check if conrad still has jobs to deal with, and kill it if not: + if (_isRunning) { + // Update the _lastFrameTime: + _lastFrameTime = __dateNow(); + + _dispatch('enterFrame'); + setTimeout(_loop, 0); + } else + _dispatch('stop'); + } + + /** + * Adds one or more jobs, and starts the loop if no job was running before. A + * job is at least a unique string "id" and a function, and there are some + * parameters that you can specify for each job to modify the way conrad will + * execute it. If a job is added with the "id" of another job that is waiting + * or still running, an error will be thrown. + * + * When a job is added, it is referenced in the _jobs object, by its id. + * Then, if it has to be executed right now, it will be also referenced in + * the _runningJobs object. If it has to wait, then it will be added into the + * _waitingJobs object, until it can start. + * + * Keep reading this documentation to see how to call this method. + * + * @return {Object} Returns conrad. + * + * Adding one job: + * *************** + * Basically, a job is defined by its string id and a function (the job). It + * is also possible to add some parameters: + * + * > conrad.addJob('myJobId', myJobFunction); + * > conrad.addJob('myJobId', { + * > job: myJobFunction, + * > someParameter: someValue + * > }); + * > conrad.addJob({ + * > id: 'myJobId', + * > job: myJobFunction, + * > someParameter: someValue + * > }); + * + * Adding several jobs: + * ******************** + * When adding several jobs at the same time, it is possible to specify + * parameters for each one individually or for all: + * + * > conrad.addJob([ + * > { + * > id: 'myJobId1', + * > job: myJobFunction1, + * > someParameter1: someValue1 + * > }, + * > { + * > id: 'myJobId2', + * > job: myJobFunction2, + * > someParameter2: someValue2 + * > } + * > ], { + * > someCommonParameter: someCommonValue + * > }); + * > conrad.addJob({ + * > myJobId1: {, + * > job: myJobFunction1, + * > someParameter1: someValue1 + * > }, + * > myJobId2: {, + * > job: myJobFunction2, + * > someParameter2: someValue2 + * > } + * > }, { + * > someCommonParameter: someCommonValue + * > }); + * > conrad.addJob({ + * > myJobId1: myJobFunction1, + * > myJobId2: myJobFunction2 + * > }, { + * > someCommonParameter: someCommonValue + * > }); + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters: + * + * {?Function} end A callback to execute when the job is ended. It is + * not executed if the job is killed instead of ended + * "naturally". + * {?Integer} count The number of time the job has to be executed. + * {?Number} weight If specified, the job will be executed as it was + * added "weight" times. + * {?String} after The id of another job (eventually not added yet). + * If specified, this job will start only when the + * specified "after" job is ended. + */ + function _addJob(v1, v2) { + var i, + l, + o; + + // Array of jobs: + if (Array.isArray(v1)) { + // Keep conrad to start until the last job is added: + _noStart = true; + + for (i = 0, l = v1.length; i < l; i++) + _addJob(v1[i].id, __extend(v1[i], v2)); + + _noStart = false; + if (!_isRunning) { + // Update the _lastFrameTime: + _lastFrameTime = __dateNow(); + + _dispatch('start'); + _loop(); + } + } else if (typeof v1 === 'object') { + // One job (object): + if (typeof v1.id === 'string') + _addJob(v1.id, v1); + + // Hash of jobs: + else { + // Keep conrad to start until the last job is added: + _noStart = true; + + for (i in v1) + if (typeof v1[i] === 'function') + _addJob(i, __extend({ + job: v1[i] + }, v2)); + else + _addJob(i, __extend(v1[i], v2)); + + _noStart = false; + if (!_isRunning) { + // Update the _lastFrameTime: + _lastFrameTime = __dateNow(); + + _dispatch('start'); + _loop(); + } + } + + // One job (string, *): + } else if (typeof v1 === 'string') { + if (_hasJob(v1)) + throw new Error( + '[conrad.addJob] Job with id "' + v1 + '" already exists.' + ); + + // One job (string, function): + if (typeof v2 === 'function') { + o = { + id: v1, + done: 0, + time: 0, + status: 'waiting', + currentTime: 0, + averageTime: 0, + weightTime: 0, + job: v2 + }; + + // One job (string, object): + } else if (typeof v2 === 'object') { + o = __extend( + { + id: v1, + done: 0, + time: 0, + status: 'waiting', + currentTime: 0, + averageTime: 0, + weightTime: 0 + }, + v2 + ); + + // If none of those cases, throw an error: + } else + throw new Error('[conrad.addJob] Wrong arguments.'); + + // Effectively add the job: + _jobs[v1] = o; + _dispatch('jobAdded', __clone(o)); + + // Check if the loop has to be started: + if (!_isRunning && !_noStart) { + // Update the _lastFrameTime: + _lastFrameTime = __dateNow(); + + _dispatch('start'); + _loop(); + } + + // If none of those cases, throw an error: + } else + throw new Error('[conrad.addJob] Wrong arguments.'); + + return this; + } + + /** + * Kills one or more jobs, indicated by their ids. It is only possible to + * kill running jobs or waiting jobs. If you try to kill a job that does not + * exist or that is already killed, a warning will be thrown. + * + * @param {Array|String} v1 A string job id or an array of job ids. + * @return {Object} Returns conrad. + */ + function _killJob(v1) { + var i, + l, + k, + a, + job, + found = false; + + // Array of job ids: + if (Array.isArray(v1)) + for (i = 0, l = v1.length; i < l; i++) + _killJob(v1[i]); + + // One job's id: + else if (typeof v1 === 'string') { + a = [_runningJobs, _waitingJobs, _jobs]; + + // Remove the job from the hashes: + for (i = 0, l = a.length; i < l; i++) + if (v1 in a[i]) { + job = a[i][v1]; + + if (_parameters.history) { + job.status = 'done'; + _doneJobs.push(job); + } + + _dispatch('jobEnded', __clone(job)); + delete a[i][v1]; + + if (typeof job.end === 'function') + job.end(); + + found = true; + } + + // Remove the priorities array: + a = _sortedByPriorityJobs; + for (i = 0, l = a.length; i < l; i++) + if (a[i].id === v1) { + a.splice(i, 1); + break; + } + + if (!found) + throw new Error('[conrad.killJob] Job "' + v1 + '" not found.'); + + // If none of those cases, throw an error: + } else + throw new Error('[conrad.killJob] Wrong arguments.'); + + return this; + } + + /** + * Kills every running, waiting, and just added jobs. + * + * @return {Object} Returns conrad. + */ + function _killAll() { + var k, + jobs = __extend(_jobs, _runningJobs, _waitingJobs); + + // Take every jobs and push them into the _doneJobs object: + if (_parameters.history) + for (k in jobs) { + jobs[k].status = 'done'; + _doneJobs.push(jobs[k]); + + if (typeof jobs[k].end === 'function') + jobs[k].end(); + } + + // Reinitialize the different jobs lists: + _jobs = {}; + _waitingJobs = {}; + _runningJobs = {}; + _sortedByPriorityJobs = []; + + // In case some jobs are added right after the kill: + _isRunning = false; + + return this; + } + + /** + * Returns true if a job with the specified id is currently running or + * waiting, and false else. + * + * @param {String} id The id of the job. + * @return {?Object} Returns the job object if it exists. + */ + function _hasJob(id) { + var job = _jobs[id] || _runningJobs[id] || _waitingJobs[id]; + return job ? __extend(job) : null; + } + + /** + * This method will set the setting specified by "v1" to the value specified + * by "v2" if both are given, and else return the current value of the + * settings "v1". + * + * @param {String} v1 The name of the property. + * @param {?*} v2 Eventually, a value to set to the specified + * property. + * @return {Object|*} Returns the specified settings value if "v2" is not + * given, and conrad else. + */ + function _settings(v1, v2) { + var o; + + if (typeof a1 === 'string' && arguments.length === 1) + return _parameters[a1]; + else { + o = (typeof a1 === 'object' && arguments.length === 1) ? + a1 || {} : + {}; + if (typeof a1 === 'string') + o[a1] = a2; + + for (var k in o) + if (o[k] !== undefined) + _parameters[k] = o[k]; + else + delete _parameters[k]; + + return this; + } + } + + /** + * Returns true if conrad is currently running, and false else. + * + * @return {Boolean} Returns _isRunning. + */ + function _getIsRunning() { + return _isRunning; + } + + /** + * Unreference every job that is stored in the _doneJobs object. It will + * not be possible anymore to get stats about these jobs, but it will release + * the memory. + * + * @return {Object} Returns conrad. + */ + function _clearHistory() { + _doneJobs = []; + return this; + } + + /** + * Returns a snapshot of every data about jobs that wait to be started, are + * currently running or are done. + * + * It is possible to get only running, waiting or done jobs by giving + * "running", "waiting" or "done" as fist argument. + * + * It is also possible to get every job with a specified id by giving it as + * first argument. Also, using a RegExp instead of an id will return every + * jobs whose ids match the RegExp. And these two last use cases work as well + * by giving before "running", "waiting" or "done". + * + * @return {Array} The array of the matching jobs. + * + * Some call examples: + * ******************* + * > conrad.getStats('running') + * > conrad.getStats('waiting') + * > conrad.getStats('done') + * > conrad.getStats('myJob') + * > conrad.getStats(/test/) + * > conrad.getStats('running', 'myRunningJob') + * > conrad.getStats('running', /test/) + */ + function _getStats(v1, v2) { + var a, + k, + i, + l, + stats, + pattern, + isPatternString; + + if (!arguments.length) { + stats = []; + + for (k in _jobs) + stats.push(_jobs[k]); + + for (k in _waitingJobs) + stats.push(_waitingJobs[k]); + + for (k in _runningJobs) + stats.push(_runningJobs[k]); + + stats = stats.concat(_doneJobs); + } + + if (typeof v1 === 'string') + switch (v1) { + case 'waiting': + stats = __objectValues(_waitingJobs); + break; + case 'running': + stats = __objectValues(_runningJobs); + break; + case 'done': + stats = _doneJobs; + break; + default: + pattern = v1; + } + + if (v1 instanceof RegExp) + pattern = v1; + + if (!pattern && (typeof v2 === 'string' || v2 instanceof RegExp)) + pattern = v2; + + // Filter jobs if a pattern is given: + if (pattern) { + isPatternString = typeof pattern === 'string'; + + if (stats instanceof Array) { + a = stats; + } else if (typeof stats === 'object') { + a = []; + + for (k in stats) + a = a.concat(stats[k]); + } else { + a = []; + + for (k in _jobs) + a.push(_jobs[k]); + + for (k in _waitingJobs) + a.push(_waitingJobs[k]); + + for (k in _runningJobs) + a.push(_runningJobs[k]); + + a = a.concat(_doneJobs); + } + + stats = []; + for (i = 0, l = a.length; i < l; i++) + if (isPatternString ? a[i].id === pattern : a[i].id.match(pattern)) + stats.push(a[i]); + } + + return __clone(stats); + } + + + /** + * TOOLS FUNCTIONS: + * **************** + */ + + /** + * This function takes any number of objects as arguments, copies from each + * of these objects each pair key/value into a new object, and finally + * returns this object. + * + * The arguments are parsed from the last one to the first one, such that + * when two objects have keys in common, the "earliest" object wins. + * + * Example: + * ******** + * > var o1 = { + * > a: 1, + * > b: 2, + * > c: '3' + * > }, + * > o2 = { + * > c: '4', + * > d: [ 5 ] + * > }; + * > __extend(o1, o2); + * > // Returns: { + * > // a: 1, + * > // b: 2, + * > // c: '3', + * > // d: [ 5 ] + * > // }; + * + * @param {Object+} Any number of objects. + * @return {Object} The merged object. + */ + function __extend() { + var i, + k, + res = {}, + l = arguments.length; + + for (i = l - 1; i >= 0; i--) + for (k in arguments[i]) + res[k] = arguments[i][k]; + + return res; + } + + /** + * This function simply clones an object. This object must contain only + * objects, arrays and immutable values. Since it is not public, it does not + * deal with cyclic references, DOM elements and instantiated objects - so + * use it carefully. + * + * @param {Object} The object to clone. + * @return {Object} The clone. + */ + function __clone(item) { + var result, i, k, l; + + if (!item) + return item; + + if (Array.isArray(item)) { + result = []; + for (i = 0, l = item.length; i < l; i++) + result.push(__clone(item[i])); + } else if (typeof item === 'object') { + result = {}; + for (i in item) + result[i] = __clone(item[i]); + } else + result = item; + + return result; + } + + /** + * Returns an array containing the values of an object. + * + * @param {Object} The object. + * @return {Array} The array of values. + */ + function __objectValues(o) { + var k, + a = []; + + for (k in o) + a.push(o[k]); + + return a; + } + + /** + * A short "Date.now()" polyfill. + * + * @return {Number} The current time (in ms). + */ + function __dateNow() { + return Date.now ? Date.now() : new Date().getTime(); + } + + /** + * Polyfill for the Array.isArray function: + */ + if (!Array.isArray) + Array.isArray = function(v) { + return Object.prototype.toString.call(v) === '[object Array]'; + }; + + + /** + * EXPORT PUBLIC API: + * ****************** + */ + var conrad = { + hasJob: _hasJob, + addJob: _addJob, + killJob: _killJob, + killAll: _killAll, + settings: _settings, + getStats: _getStats, + isRunning: _getIsRunning, + clearHistory: _clearHistory, + + // Events management: + bind: _bind, + unbind: _unbind, + + // Version: + version: '0.1.0' + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = conrad; + exports.conrad = conrad; + } + global.conrad = conrad; +})(this); diff --git a/blogContent/projects/steam/src/middlewares/sigma.middlewares.copy.js b/blogContent/projects/steam/src/middlewares/sigma.middlewares.copy.js new file mode 100644 index 0000000..8f1b2a9 --- /dev/null +++ b/blogContent/projects/steam/src/middlewares/sigma.middlewares.copy.js @@ -0,0 +1,35 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.middlewares'); + + /** + * This middleware will just copy the graphic properties. + * + * @param {?string} readPrefix The read prefix. + * @param {?string} writePrefix The write prefix. + */ + sigma.middlewares.copy = function(readPrefix, writePrefix) { + var i, + l, + a; + + if (writePrefix + '' === readPrefix + '') + return; + + a = this.graph.nodes(); + for (i = 0, l = a.length; i < l; i++) { + a[i][writePrefix + 'x'] = a[i][readPrefix + 'x']; + a[i][writePrefix + 'y'] = a[i][readPrefix + 'y']; + a[i][writePrefix + 'size'] = a[i][readPrefix + 'size']; + } + + a = this.graph.edges(); + for (i = 0, l = a.length; i < l; i++) + a[i][writePrefix + 'size'] = a[i][readPrefix + 'size']; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/middlewares/sigma.middlewares.rescale.js b/blogContent/projects/steam/src/middlewares/sigma.middlewares.rescale.js new file mode 100644 index 0000000..85460ec --- /dev/null +++ b/blogContent/projects/steam/src/middlewares/sigma.middlewares.rescale.js @@ -0,0 +1,189 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.middlewares'); + sigma.utils.pkg('sigma.utils'); + + /** + * This middleware will rescale the graph such that it takes an optimal space + * on the renderer. + * + * As each middleware, this function is executed in the scope of the sigma + * instance. + * + * @param {?string} readPrefix The read prefix. + * @param {?string} writePrefix The write prefix. + * @param {object} options The parameters. + */ + sigma.middlewares.rescale = function(readPrefix, writePrefix, options) { + var i, + l, + a, + b, + c, + d, + scale, + margin, + n = this.graph.nodes(), + e = this.graph.edges(), + settings = this.settings.embedObjects(options || {}), + bounds = settings('bounds') || sigma.utils.getBoundaries( + this.graph, + readPrefix, + true + ), + minX = bounds.minX, + minY = bounds.minY, + maxX = bounds.maxX, + maxY = bounds.maxY, + sizeMax = bounds.sizeMax, + weightMax = bounds.weightMax, + w = settings('width') || 1, + h = settings('height') || 1, + rescaleSettings = settings('autoRescale'), + validSettings = { + nodePosition: 1, + nodeSize: 1, + edgeSize: 1 + }; + + /** + * What elements should we rescale? + */ + if (!(rescaleSettings instanceof Array)) + rescaleSettings = ['nodePosition', 'nodeSize', 'edgeSize']; + + for (i = 0, l = rescaleSettings.length; i < l; i++) + if (!validSettings[rescaleSettings[i]]) + throw new Error( + 'The rescale setting "' + rescaleSettings[i] + '" is not recognized.' + ); + + var np = ~rescaleSettings.indexOf('nodePosition'), + ns = ~rescaleSettings.indexOf('nodeSize'), + es = ~rescaleSettings.indexOf('edgeSize'); + + /** + * First, we compute the scaling ratio, without considering the sizes + * of the nodes : Each node will have its center in the canvas, but might + * be partially out of it. + */ + scale = settings('scalingMode') === 'outside' ? + Math.max( + w / Math.max(maxX - minX, 1), + h / Math.max(maxY - minY, 1) + ) : + Math.min( + w / Math.max(maxX - minX, 1), + h / Math.max(maxY - minY, 1) + ); + + /** + * Then, we correct that scaling ratio considering a margin, which is + * basically the size of the biggest node. + * This has to be done as a correction since to compare the size of the + * biggest node to the X and Y values, we have to first get an + * approximation of the scaling ratio. + **/ + margin = + ( + settings('rescaleIgnoreSize') ? + 0 : + (settings('maxNodeSize') || sizeMax) / scale + ) + + (settings('sideMargin') || 0); + maxX += margin; + minX -= margin; + maxY += margin; + minY -= margin; + + // Fix the scaling with the new extrema: + scale = settings('scalingMode') === 'outside' ? + Math.max( + w / Math.max(maxX - minX, 1), + h / Math.max(maxY - minY, 1) + ) : + Math.min( + w / Math.max(maxX - minX, 1), + h / Math.max(maxY - minY, 1) + ); + + // Size homothetic parameters: + if (!settings('maxNodeSize') && !settings('minNodeSize')) { + a = 1; + b = 0; + } else if (settings('maxNodeSize') === settings('minNodeSize')) { + a = 0; + b = +settings('maxNodeSize'); + } else { + a = (settings('maxNodeSize') - settings('minNodeSize')) / sizeMax; + b = +settings('minNodeSize'); + } + + if (!settings('maxEdgeSize') && !settings('minEdgeSize')) { + c = 1; + d = 0; + } else if (settings('maxEdgeSize') === settings('minEdgeSize')) { + c = 0; + d = +settings('minEdgeSize'); + } else { + c = (settings('maxEdgeSize') - settings('minEdgeSize')) / weightMax; + d = +settings('minEdgeSize'); + } + + // Rescale the nodes and edges: + for (i = 0, l = e.length; i < l; i++) + e[i][writePrefix + 'size'] = + e[i][readPrefix + 'size'] * (es ? c : 1) + (es ? d : 0); + + for (i = 0, l = n.length; i < l; i++) { + n[i][writePrefix + 'size'] = + n[i][readPrefix + 'size'] * (ns ? a : 1) + (ns ? b : 0); + n[i][writePrefix + 'x'] = + (n[i][readPrefix + 'x'] - (maxX + minX) / 2) * (np ? scale : 1); + n[i][writePrefix + 'y'] = + (n[i][readPrefix + 'y'] - (maxY + minY) / 2) * (np ? scale : 1); + } + }; + + sigma.utils.getBoundaries = function(graph, prefix, doEdges) { + var i, + l, + e = graph.edges(), + n = graph.nodes(), + weightMax = -Infinity, + sizeMax = -Infinity, + minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + + if (doEdges) + for (i = 0, l = e.length; i < l; i++) + weightMax = Math.max(e[i][prefix + 'size'], weightMax); + + for (i = 0, l = n.length; i < l; i++) { + sizeMax = Math.max(n[i][prefix + 'size'], sizeMax); + maxX = Math.max(n[i][prefix + 'x'], maxX); + minX = Math.min(n[i][prefix + 'x'], minX); + maxY = Math.max(n[i][prefix + 'y'], maxY); + minY = Math.min(n[i][prefix + 'y'], minY); + } + + weightMax = weightMax || 1; + sizeMax = sizeMax || 1; + + return { + weightMax: weightMax, + sizeMax: sizeMax, + minX: minX, + minY: minY, + maxX: maxX, + maxY: maxY + }; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/misc/sigma.misc.animation.js b/blogContent/projects/steam/src/misc/sigma.misc.animation.js new file mode 100644 index 0000000..299f00f --- /dev/null +++ b/blogContent/projects/steam/src/misc/sigma.misc.animation.js @@ -0,0 +1,239 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.misc.animation.running'); + + /** + * Generates a unique ID for the animation. + * + * @return {string} Returns the new ID. + */ + var _getID = (function() { + var id = 0; + return function() { + return '' + (++id); + }; + })(); + + /** + * This function animates a camera. It has to be called with the camera to + * animate, the values of the coordinates to reach and eventually some + * options. It returns a number id, that you can use to kill the animation, + * with the method sigma.misc.animation.kill(id). + * + * The available options are: + * + * {?number} duration The duration of the animation. + * {?function} onNewFrame A callback to execute when the animation + * enter a new frame. + * {?function} onComplete A callback to execute when the animation + * is completed or killed. + * {?(string|function)} easing The name of a function from the package + * sigma.utils.easings, or a custom easing + * function. + * + * @param {camera} camera The camera to animate. + * @param {object} target The coordinates to reach. + * @param {?object} options Eventually an object to specify some options to + * the function. The available options are + * presented in the description of the function. + * @return {number} The animation id, to make it easy to kill + * through the method "sigma.misc.animation.kill". + */ + sigma.misc.animation.camera = function(camera, val, options) { + if ( + !(camera instanceof sigma.classes.camera) || + typeof val !== 'object' || + !val + ) + throw 'animation.camera: Wrong arguments.'; + + if ( + typeof val.x !== 'number' && + typeof val.y !== 'number' && + typeof val.ratio !== 'number' && + typeof val.angle !== 'number' + ) + throw 'There must be at least one valid coordinate in the given val.'; + + var fn, + id, + anim, + easing, + duration, + initialVal, + o = options || {}, + start = sigma.utils.dateNow(); + + // Store initial values: + initialVal = { + x: camera.x, + y: camera.y, + ratio: camera.ratio, + angle: camera.angle + }; + + duration = o.duration; + easing = typeof o.easing !== 'function' ? + sigma.utils.easings[o.easing || 'quadraticInOut'] : + o.easing; + + fn = function() { + var coef, + t = o.duration ? (sigma.utils.dateNow() - start) / o.duration : 1; + + // If the animation is over: + if (t >= 1) { + camera.isAnimated = false; + camera.goTo({ + x: val.x !== undefined ? val.x : initialVal.x, + y: val.y !== undefined ? val.y : initialVal.y, + ratio: val.ratio !== undefined ? val.ratio : initialVal.ratio, + angle: val.angle !== undefined ? val.angle : initialVal.angle + }); + + cancelAnimationFrame(id); + delete sigma.misc.animation.running[id]; + + // Check callbacks: + if (typeof o.onComplete === 'function') + o.onComplete(); + + // Else, let's keep going: + } else { + coef = easing(t); + camera.isAnimated = true; + camera.goTo({ + x: val.x !== undefined ? + initialVal.x + (val.x - initialVal.x) * coef : + initialVal.x, + y: val.y !== undefined ? + initialVal.y + (val.y - initialVal.y) * coef : + initialVal.y, + ratio: val.ratio !== undefined ? + initialVal.ratio + (val.ratio - initialVal.ratio) * coef : + initialVal.ratio, + angle: val.angle !== undefined ? + initialVal.angle + (val.angle - initialVal.angle) * coef : + initialVal.angle + }); + + // Check callbacks: + if (typeof o.onNewFrame === 'function') + o.onNewFrame(); + + anim.frameId = requestAnimationFrame(fn); + } + }; + + id = _getID(); + anim = { + frameId: requestAnimationFrame(fn), + target: camera, + type: 'camera', + options: o, + fn: fn + }; + sigma.misc.animation.running[id] = anim; + + return id; + }; + + /** + * Kills a running animation. It triggers the eventual onComplete callback. + * + * @param {number} id The id of the animation to kill. + * @return {object} Returns the sigma.misc.animation package. + */ + sigma.misc.animation.kill = function(id) { + if (arguments.length !== 1 || typeof id !== 'number') + throw 'animation.kill: Wrong arguments.'; + + var o = sigma.misc.animation.running[id]; + + if (o) { + cancelAnimationFrame(id); + delete sigma.misc.animation.running[o.frameId]; + + if (o.type === 'camera') + o.target.isAnimated = false; + + // Check callbacks: + if (typeof (o.options || {}).onComplete === 'function') + o.options.onComplete(); + } + + return this; + }; + + /** + * Kills every running animations, or only the one with the specified type, + * if a string parameter is given. + * + * @param {?(string|object)} filter A string to filter the animations to kill + * on their type (example: "camera"), or an + * object to filter on their target. + * @return {number} Returns the number of animations killed + * that way. + */ + sigma.misc.animation.killAll = function(filter) { + var o, + id, + count = 0, + type = typeof filter === 'string' ? filter : null, + target = typeof filter === 'object' ? filter : null, + running = sigma.misc.animation.running; + + for (id in running) + if ( + (!type || running[id].type === type) && + (!target || running[id].target === target) + ) { + o = sigma.misc.animation.running[id]; + cancelAnimationFrame(o.frameId); + delete sigma.misc.animation.running[id]; + + if (o.type === 'camera') + o.target.isAnimated = false; + + // Increment counter: + count++; + + // Check callbacks: + if (typeof (o.options || {}).onComplete === 'function') + o.options.onComplete(); + } + + return count; + }; + + /** + * Returns "true" if any animation that is currently still running matches + * the filter given to the function. + * + * @param {string|object} filter A string to filter the animations to kill + * on their type (example: "camera"), or an + * object to filter on their target. + * @return {boolean} Returns true if any running animation + * matches. + */ + sigma.misc.animation.has = function(filter) { + var id, + type = typeof filter === 'string' ? filter : null, + target = typeof filter === 'object' ? filter : null, + running = sigma.misc.animation.running; + + for (id in running) + if ( + (!type || running[id].type === type) && + (!target || running[id].target === target) + ) + return true; + + return false; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/misc/sigma.misc.bindDOMEvents.js b/blogContent/projects/steam/src/misc/sigma.misc.bindDOMEvents.js new file mode 100644 index 0000000..1e758e8 --- /dev/null +++ b/blogContent/projects/steam/src/misc/sigma.misc.bindDOMEvents.js @@ -0,0 +1,156 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.misc'); + + /** + * This helper will bind any DOM renderer (for instance svg) + * to its captors, to properly dispatch the good events to the sigma instance + * to manage clicking, hovering etc... + * + * It has to be called in the scope of the related renderer. + */ + sigma.misc.bindDOMEvents = function(container) { + var self = this, + graph = this.graph; + + // DOMElement abstraction + function Element(domElement) { + + // Helpers + this.attr = function(attrName) { + return domElement.getAttributeNS(null, attrName); + }; + + // Properties + this.tag = domElement.tagName; + this.class = this.attr('class'); + this.id = this.attr('id'); + + // Methods + this.isNode = function() { + return !!~this.class.indexOf(self.settings('classPrefix') + '-node'); + }; + + this.isEdge = function() { + return !!~this.class.indexOf(self.settings('classPrefix') + '-edge'); + }; + + this.isHover = function() { + return !!~this.class.indexOf(self.settings('classPrefix') + '-hover'); + }; + } + + // Click + function click(e) { + if (!self.settings('eventsEnabled')) + return; + + // Generic event + self.dispatchEvent('click', e); + + // Are we on a node? + var element = new Element(e.target); + + if (element.isNode()) + self.dispatchEvent('clickNode', { + node: graph.nodes(element.attr('data-node-id')) + }); + else + self.dispatchEvent('clickStage'); + + e.preventDefault(); + e.stopPropagation(); + } + + // Double click + function doubleClick(e) { + if (!self.settings('eventsEnabled')) + return; + + // Generic event + self.dispatchEvent('doubleClick', e); + + // Are we on a node? + var element = new Element(e.target); + + if (element.isNode()) + self.dispatchEvent('doubleClickNode', { + node: graph.nodes(element.attr('data-node-id')) + }); + else + self.dispatchEvent('doubleClickStage'); + + e.preventDefault(); + e.stopPropagation(); + } + + // On over + function onOver(e) { + var target = e.toElement || e.target; + + if (!self.settings('eventsEnabled') || !target) + return; + + var el = new Element(target); + + if (el.isNode()) { + self.dispatchEvent('overNode', { + node: graph.nodes(el.attr('data-node-id')) + }); + } + else if (el.isEdge()) { + var edge = graph.edges(el.attr('data-edge-id')); + self.dispatchEvent('overEdge', { + edge: edge, + source: graph.nodes(edge.source), + target: graph.nodes(edge.target) + }); + } + } + + // On out + function onOut(e) { + var target = e.fromElement || e.originalTarget; + + if (!self.settings('eventsEnabled')) + return; + + var el = new Element(target); + + if (el.isNode()) { + self.dispatchEvent('outNode', { + node: graph.nodes(el.attr('data-node-id')) + }); + } + else if (el.isEdge()) { + var edge = graph.edges(el.attr('data-edge-id')); + self.dispatchEvent('outEdge', { + edge: edge, + source: graph.nodes(edge.source), + target: graph.nodes(edge.target) + }); + } + } + + // Registering Events: + + // Click + container.addEventListener('click', click, false); + sigma.utils.doubleClick(container, 'click', doubleClick); + + // Touch counterparts + container.addEventListener('touchstart', click, false); + sigma.utils.doubleClick(container, 'touchstart', doubleClick); + + // Mouseover + container.addEventListener('mouseover', onOver, true); + + // Mouseout + container.addEventListener('mouseout', onOut, true); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/misc/sigma.misc.bindEvents.js b/blogContent/projects/steam/src/misc/sigma.misc.bindEvents.js new file mode 100644 index 0000000..e87ad02 --- /dev/null +++ b/blogContent/projects/steam/src/misc/sigma.misc.bindEvents.js @@ -0,0 +1,509 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.misc'); + + /** + * This helper will bind any no-DOM renderer (for instance canvas or WebGL) + * to its captors, to properly dispatch the good events to the sigma instance + * to manage clicking, hovering etc... + * + * It has to be called in the scope of the related renderer. + */ + sigma.misc.bindEvents = function(prefix) { + var i, + l, + mX, + mY, + captor, + self = this; + + function getNodes(e) { + if (e) { + mX = 'x' in e.data ? e.data.x : mX; + mY = 'y' in e.data ? e.data.y : mY; + } + + var i, + j, + l, + n, + x, + y, + s, + inserted, + selected = [], + modifiedX = mX + self.width / 2, + modifiedY = mY + self.height / 2, + point = self.camera.cameraPosition( + mX, + mY + ), + nodes = self.camera.quadtree.point( + point.x, + point.y + ); + + if (nodes.length) + for (i = 0, l = nodes.length; i < l; i++) { + n = nodes[i]; + x = n[prefix + 'x']; + y = n[prefix + 'y']; + s = n[prefix + 'size']; + + if ( + !n.hidden && + modifiedX > x - s && + modifiedX < x + s && + modifiedY > y - s && + modifiedY < y + s && + Math.sqrt( + Math.pow(modifiedX - x, 2) + + Math.pow(modifiedY - y, 2) + ) < s + ) { + // Insert the node: + inserted = false; + + for (j = 0; j < selected.length; j++) + if (n.size > selected[j].size) { + selected.splice(j, 0, n); + inserted = true; + break; + } + + if (!inserted) + selected.push(n); + } + } + + return selected; + } + + + function getEdges(e) { + if (!self.settings('enableEdgeHovering')) { + // No event if the setting is off: + return []; + } + + var isCanvas = ( + sigma.renderers.canvas && self instanceof sigma.renderers.canvas); + + if (!isCanvas) { + // A quick hardcoded rule to prevent people from using this feature + // with the WebGL renderer (which is not good enough at the moment): + throw new Error( + 'The edge events feature is not compatible with the WebGL renderer' + ); + } + + if (e) { + mX = 'x' in e.data ? e.data.x : mX; + mY = 'y' in e.data ? e.data.y : mY; + } + + var i, + j, + l, + a, + edge, + s, + maxEpsilon = self.settings('edgeHoverPrecision'), + source, + target, + cp, + nodeIndex = {}, + inserted, + selected = [], + modifiedX = mX + self.width / 2, + modifiedY = mY + self.height / 2, + point = self.camera.cameraPosition( + mX, + mY + ), + edges = []; + + if (isCanvas) { + var nodesOnScreen = self.camera.quadtree.area( + self.camera.getRectangle(self.width, self.height) + ); + for (a = nodesOnScreen, i = 0, l = a.length; i < l; i++) + nodeIndex[a[i].id] = a[i]; + } + + if (self.camera.edgequadtree !== undefined) { + edges = self.camera.edgequadtree.point( + point.x, + point.y + ); + } + + function insertEdge(selected, edge) { + inserted = false; + + for (j = 0; j < selected.length; j++) + if (edge.size > selected[j].size) { + selected.splice(j, 0, edge); + inserted = true; + break; + } + + if (!inserted) + selected.push(edge); + } + + if (edges.length) + for (i = 0, l = edges.length; i < l; i++) { + edge = edges[i]; + source = self.graph.nodes(edge.source); + target = self.graph.nodes(edge.target); + // (HACK) we can't get edge[prefix + 'size'] on WebGL renderer: + s = edge[prefix + 'size'] || + edge['read_' + prefix + 'size']; + + // First, let's identify which edges are drawn. To do this, we keep + // every edges that have at least one extremity displayed according to + // the quadtree and the "hidden" attribute. We also do not keep hidden + // edges. + // Then, let's check if the mouse is on the edge (we suppose that it + // is a line segment). + + if ( + !edge.hidden && + !source.hidden && !target.hidden && + (!isCanvas || + (nodeIndex[edge.source] || nodeIndex[edge.target])) && + sigma.utils.getDistance( + source[prefix + 'x'], + source[prefix + 'y'], + modifiedX, + modifiedY) > source[prefix + 'size'] && + sigma.utils.getDistance( + target[prefix + 'x'], + target[prefix + 'y'], + modifiedX, + modifiedY) > target[prefix + 'size'] + ) { + if (edge.type == 'curve' || edge.type == 'curvedArrow') { + if (source.id === target.id) { + cp = sigma.utils.getSelfLoopControlPoints( + source[prefix + 'x'], + source[prefix + 'y'], + source[prefix + 'size'] + ); + if ( + sigma.utils.isPointOnBezierCurve( + modifiedX, + modifiedY, + source[prefix + 'x'], + source[prefix + 'y'], + target[prefix + 'x'], + target[prefix + 'y'], + cp.x1, + cp.y1, + cp.x2, + cp.y2, + Math.max(s, maxEpsilon) + )) { + insertEdge(selected, edge); + } + } + else { + cp = sigma.utils.getQuadraticControlPoint( + source[prefix + 'x'], + source[prefix + 'y'], + target[prefix + 'x'], + target[prefix + 'y']); + if ( + sigma.utils.isPointOnQuadraticCurve( + modifiedX, + modifiedY, + source[prefix + 'x'], + source[prefix + 'y'], + target[prefix + 'x'], + target[prefix + 'y'], + cp.x, + cp.y, + Math.max(s, maxEpsilon) + )) { + insertEdge(selected, edge); + } + } + } else if ( + sigma.utils.isPointOnSegment( + modifiedX, + modifiedY, + source[prefix + 'x'], + source[prefix + 'y'], + target[prefix + 'x'], + target[prefix + 'y'], + Math.max(s, maxEpsilon) + )) { + insertEdge(selected, edge); + } + } + } + + return selected; + } + + + function bindCaptor(captor) { + var nodes, + edges, + overNodes = {}, + overEdges = {}; + + function onClick(e) { + if (!self.settings('eventsEnabled')) + return; + + self.dispatchEvent('click', e.data); + + nodes = getNodes(e); + edges = getEdges(e); + + if (nodes.length) { + self.dispatchEvent('clickNode', { + node: nodes[0], + captor: e.data + }); + self.dispatchEvent('clickNodes', { + node: nodes, + captor: e.data + }); + } else if (edges.length) { + self.dispatchEvent('clickEdge', { + edge: edges[0], + captor: e.data + }); + self.dispatchEvent('clickEdges', { + edge: edges, + captor: e.data + }); + } else + self.dispatchEvent('clickStage', {captor: e.data}); + } + + function onDoubleClick(e) { + if (!self.settings('eventsEnabled')) + return; + + self.dispatchEvent('doubleClick', e.data); + + nodes = getNodes(e); + edges = getEdges(e); + + if (nodes.length) { + self.dispatchEvent('doubleClickNode', { + node: nodes[0], + captor: e.data + }); + self.dispatchEvent('doubleClickNodes', { + node: nodes, + captor: e.data + }); + } else if (edges.length) { + self.dispatchEvent('doubleClickEdge', { + edge: edges[0], + captor: e.data + }); + self.dispatchEvent('doubleClickEdges', { + edge: edges, + captor: e.data + }); + } else + self.dispatchEvent('doubleClickStage', {captor: e.data}); + } + + function onRightClick(e) { + if (!self.settings('eventsEnabled')) + return; + + self.dispatchEvent('rightClick', e.data); + + nodes = getNodes(e); + edges = getEdges(e); + + if (nodes.length) { + self.dispatchEvent('rightClickNode', { + node: nodes[0], + captor: e.data + }); + self.dispatchEvent('rightClickNodes', { + node: nodes, + captor: e.data + }); + } else if (edges.length) { + self.dispatchEvent('rightClickEdge', { + edge: edges[0], + captor: e.data + }); + self.dispatchEvent('rightClickEdges', { + edge: edges, + captor: e.data + }); + } else + self.dispatchEvent('rightClickStage', {captor: e.data}); + } + + function onOut(e) { + if (!self.settings('eventsEnabled')) + return; + + var k, + i, + l, + le, + outNodes = [], + outEdges = []; + + for (k in overNodes) + outNodes.push(overNodes[k]); + + overNodes = {}; + // Dispatch both single and multi events: + for (i = 0, l = outNodes.length; i < l; i++) + self.dispatchEvent('outNode', { + node: outNodes[i], + captor: e.data + }); + if (outNodes.length) + self.dispatchEvent('outNodes', { + nodes: outNodes, + captor: e.data + }); + + overEdges = {}; + // Dispatch both single and multi events: + for (i = 0, le = outEdges.length; i < le; i++) + self.dispatchEvent('outEdge', { + edge: outEdges[i], + captor: e.data + }); + if (outEdges.length) + self.dispatchEvent('outEdges', { + edges: outEdges, + captor: e.data + }); + } + + function onMove(e) { + if (!self.settings('eventsEnabled')) + return; + + nodes = getNodes(e); + edges = getEdges(e); + + var i, + k, + node, + edge, + newOutNodes = [], + newOverNodes = [], + currentOverNodes = {}, + l = nodes.length, + newOutEdges = [], + newOverEdges = [], + currentOverEdges = {}, + le = edges.length; + + // Check newly overred nodes: + for (i = 0; i < l; i++) { + node = nodes[i]; + currentOverNodes[node.id] = node; + if (!overNodes[node.id]) { + newOverNodes.push(node); + overNodes[node.id] = node; + } + } + + // Check no more overred nodes: + for (k in overNodes) + if (!currentOverNodes[k]) { + newOutNodes.push(overNodes[k]); + delete overNodes[k]; + } + + // Dispatch both single and multi events: + for (i = 0, l = newOverNodes.length; i < l; i++) + self.dispatchEvent('overNode', { + node: newOverNodes[i], + captor: e.data + }); + for (i = 0, l = newOutNodes.length; i < l; i++) + self.dispatchEvent('outNode', { + node: newOutNodes[i], + captor: e.data + }); + if (newOverNodes.length) + self.dispatchEvent('overNodes', { + nodes: newOverNodes, + captor: e.data + }); + if (newOutNodes.length) + self.dispatchEvent('outNodes', { + nodes: newOutNodes, + captor: e.data + }); + + // Check newly overred edges: + for (i = 0; i < le; i++) { + edge = edges[i]; + currentOverEdges[edge.id] = edge; + if (!overEdges[edge.id]) { + newOverEdges.push(edge); + overEdges[edge.id] = edge; + } + } + + // Check no more overred edges: + for (k in overEdges) + if (!currentOverEdges[k]) { + newOutEdges.push(overEdges[k]); + delete overEdges[k]; + } + + // Dispatch both single and multi events: + for (i = 0, le = newOverEdges.length; i < le; i++) + self.dispatchEvent('overEdge', { + edge: newOverEdges[i], + captor: e.data + }); + for (i = 0, le = newOutEdges.length; i < le; i++) + self.dispatchEvent('outEdge', { + edge: newOutEdges[i], + captor: e.data + }); + if (newOverEdges.length) + self.dispatchEvent('overEdges', { + edges: newOverEdges, + captor: e.data + }); + if (newOutEdges.length) + self.dispatchEvent('outEdges', { + edges: newOutEdges, + captor: e.data + }); + } + + // Bind events: + captor.bind('click', onClick); + captor.bind('mousedown', onMove); + captor.bind('mouseup', onMove); + captor.bind('mousemove', onMove); + captor.bind('mouseout', onOut); + captor.bind('doubleclick', onDoubleClick); + captor.bind('rightclick', onRightClick); + self.bind('render', onMove); + } + + for (i = 0, l = this.captors.length; i < l; i++) + bindCaptor(this.captors[i]); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/misc/sigma.misc.drawHovers.js b/blogContent/projects/steam/src/misc/sigma.misc.drawHovers.js new file mode 100644 index 0000000..82fafec --- /dev/null +++ b/blogContent/projects/steam/src/misc/sigma.misc.drawHovers.js @@ -0,0 +1,222 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.misc'); + + /** + * This method listens to "overNode", "outNode", "overEdge" and "outEdge" + * events from a renderer and renders the nodes differently on the top layer. + * The goal is to make any node label readable with the mouse, and to + * highlight hovered nodes and edges. + * + * It has to be called in the scope of the related renderer. + */ + sigma.misc.drawHovers = function(prefix) { + var self = this, + hoveredNodes = {}, + hoveredEdges = {}; + + this.bind('overNode', function(event) { + var node = event.data.node; + if (!node.hidden) { + hoveredNodes[node.id] = node; + draw(); + } + }); + + this.bind('outNode', function(event) { + delete hoveredNodes[event.data.node.id]; + draw(); + }); + + this.bind('overEdge', function(event) { + var edge = event.data.edge; + if (!edge.hidden) { + hoveredEdges[edge.id] = edge; + draw(); + } + }); + + this.bind('outEdge', function(event) { + delete hoveredEdges[event.data.edge.id]; + draw(); + }); + + this.bind('render', function(event) { + draw(); + }); + + function draw() { + + var k, + source, + target, + hoveredNode, + hoveredEdge, + c = self.contexts.hover.canvas, + defaultNodeType = self.settings('defaultNodeType'), + defaultEdgeType = self.settings('defaultEdgeType'), + nodeRenderers = sigma.canvas.hovers, + edgeRenderers = sigma.canvas.edgehovers, + extremitiesRenderers = sigma.canvas.extremities, + embedSettings = self.settings.embedObjects({ + prefix: prefix + }); + + // Clear self.contexts.hover: + self.contexts.hover.clearRect(0, 0, c.width, c.height); + + // Node render: single hover + if ( + embedSettings('enableHovering') && + embedSettings('singleHover') && + Object.keys(hoveredNodes).length + ) { + hoveredNode = hoveredNodes[Object.keys(hoveredNodes)[0]]; + ( + nodeRenderers[hoveredNode.type] || + nodeRenderers[defaultNodeType] || + nodeRenderers.def + )( + hoveredNode, + self.contexts.hover, + embedSettings + ); + } + + // Node render: multiple hover + if ( + embedSettings('enableHovering') && + !embedSettings('singleHover') + ) + for (k in hoveredNodes) + ( + nodeRenderers[hoveredNodes[k].type] || + nodeRenderers[defaultNodeType] || + nodeRenderers.def + )( + hoveredNodes[k], + self.contexts.hover, + embedSettings + ); + + // Edge render: single hover + if ( + embedSettings('enableEdgeHovering') && + embedSettings('singleHover') && + Object.keys(hoveredEdges).length + ) { + hoveredEdge = hoveredEdges[Object.keys(hoveredEdges)[0]]; + source = self.graph.nodes(hoveredEdge.source); + target = self.graph.nodes(hoveredEdge.target); + + if (! hoveredEdge.hidden) { + ( + edgeRenderers[hoveredEdge.type] || + edgeRenderers[defaultEdgeType] || + edgeRenderers.def + ) ( + hoveredEdge, + source, + target, + self.contexts.hover, + embedSettings + ); + + if (embedSettings('edgeHoverExtremities')) { + ( + extremitiesRenderers[hoveredEdge.type] || + extremitiesRenderers.def + )( + hoveredEdge, + source, + target, + self.contexts.hover, + embedSettings + ); + + } else { + // Avoid edges rendered over nodes: + ( + sigma.canvas.nodes[source.type] || + sigma.canvas.nodes.def + ) ( + source, + self.contexts.hover, + embedSettings + ); + ( + sigma.canvas.nodes[target.type] || + sigma.canvas.nodes.def + ) ( + target, + self.contexts.hover, + embedSettings + ); + } + } + } + + // Edge render: multiple hover + if ( + embedSettings('enableEdgeHovering') && + !embedSettings('singleHover') + ) { + for (k in hoveredEdges) { + hoveredEdge = hoveredEdges[k]; + source = self.graph.nodes(hoveredEdge.source); + target = self.graph.nodes(hoveredEdge.target); + + if (!hoveredEdge.hidden) { + ( + edgeRenderers[hoveredEdge.type] || + edgeRenderers[defaultEdgeType] || + edgeRenderers.def + ) ( + hoveredEdge, + source, + target, + self.contexts.hover, + embedSettings + ); + + if (embedSettings('edgeHoverExtremities')) { + ( + extremitiesRenderers[hoveredEdge.type] || + extremitiesRenderers.def + )( + hoveredEdge, + source, + target, + self.contexts.hover, + embedSettings + ); + } else { + // Avoid edges rendered over nodes: + ( + sigma.canvas.nodes[source.type] || + sigma.canvas.nodes.def + ) ( + source, + self.contexts.hover, + embedSettings + ); + ( + sigma.canvas.nodes[target.type] || + sigma.canvas.nodes.def + ) ( + target, + self.contexts.hover, + embedSettings + ); + } + } + } + } + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.exporters.svg/README.md b/blogContent/projects/steam/src/plugins/sigma.exporters.svg/README.md new file mode 100644 index 0000000..bc188e6 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.exporters.svg/README.md @@ -0,0 +1,41 @@ +sigma.exporters.svg +======================== + +Plugin by [Guillaume Plique](https://github.com/Yomguithereal). + +--- + +This plugin aims at providing an easy way to export a graph as a SVG file. + +*Basic usage* + +```js +// Retrieving the svg file as a string +var svgString = sigInst.toSVG(); + +// Dowload the svg file +sigInst.toSVG({download: true, filename: 'my-fancy-graph.svg'}); +``` + +*Complex usage* + +```js +sigInst.toSVG({ + labels: true, + classes: false, + data: true, + download: true, + filename: 'hello.svg' +}); +``` + +*Parameters* + +* **size** *?integer* [`1000`]: size of the svg canvas in pixels. +* **height** *?integer* [`1000`]: height of the svg canvas in pixels (useful only if you want a height different from the width). +* **width** *?integer* [`1000`]: width of the svg canvas in pixels (useful only if you want a width different from the height). +* **classes** *?boolean* [`true`]: should the exporter try to optimize the svg document by creating classes? +* **labels** *?boolean* [`false`]: should the labels be included in the svg file? +* **data** *?boolean* [`false`]: should additional data (node ids for instance) be included in the svg file? +* **download** *?boolean* [`false`]: should the exporter make the browser download the svg file? +* **filename** *?string* [`'graph.svg'`]: filename of the file to download. diff --git a/blogContent/projects/steam/src/plugins/sigma.exporters.svg/sigma.exporters.svg.js b/blogContent/projects/steam/src/plugins/sigma.exporters.svg/sigma.exporters.svg.js new file mode 100644 index 0000000..58c83e4 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.exporters.svg/sigma.exporters.svg.js @@ -0,0 +1,225 @@ +;(function(undefined) { + 'use strict'; + + /** + * Sigma SVG Exporter + * =================== + * + * This plugin is designed to export a graph to a svg file that can be + * downloaded or just used elsewhere. + * + * Author: Guillaume Plique (Yomguithereal) + * Version: 0.0.1 + */ + + // Terminating if sigma were not to be found + if (typeof sigma === 'undefined') + throw 'sigma.renderers.snapshot: sigma not in scope.'; + + + /** + * Polyfills + */ + var URL = this.URL || this.webkitURL || this; + + + /** + * Utilities + */ + function createBlob(data) { + return new Blob( + [data], + {type: 'image/svg+xml;charset=utf-8'} + ); + } + + function download(string, filename) { + + // Creating blob href + var blob = createBlob(string); + + // Anchor + var o = {}; + o.anchor = document.createElement('a'); + o.anchor.setAttribute('href', URL.createObjectURL(blob)); + o.anchor.setAttribute('download', filename); + + // Click event + var event = document.createEvent('MouseEvent'); + event.initMouseEvent('click', true, false, window, 0, 0, 0 ,0, 0, + false, false, false, false, 0, null); + + URL.revokeObjectURL(blob); + + o.anchor.dispatchEvent(event); + delete o.anchor; + } + + + /** + * Defaults + */ + var DEFAULTS = { + size: '1000', + width: '1000', + height: '1000', + classes: true, + labels: true, + data: false, + download: false, + filename: 'graph.svg' + }; + + var XMLNS = 'http://www.w3.org/2000/svg'; + + + /** + * Subprocesses + */ + function optimize(svg, prefix, params) { + var nodeColorIndex = {}, + edgeColorIndex = {}, + count = 0, + color, + style, + styleText = '', + f, + i, + l; + + // Creating style tag if needed + if (params.classes) { + style = document.createElementNS(XMLNS, 'style'); + style.setAttribute('type', 'text/css') + svg.insertBefore(style, svg.firstChild); + } + + // Iterating over nodes + var nodes = svg.querySelectorAll('[id="' + prefix + '-group-nodes"] > [class="' + prefix + '-node"]'); + + for (i = 0, l = nodes.length, f = true; i < l; i++) { + color = nodes[i].getAttribute('fill'); + + if (!params.data) + nodes[i].removeAttribute('data-node-id'); + + if (params.classes) { + + if (!(color in nodeColorIndex)) { + nodeColorIndex[color] = (f ? prefix + '-node' : 'c-' + (count++)); + styleText += '.' + nodeColorIndex[color] + '{fill: ' + color + '}'; + } + + if (nodeColorIndex[color] !== prefix + '-node') + nodes[i].setAttribute('class', nodes[i].getAttribute('class') + ' ' + nodeColorIndex[color]); + nodes[i].removeAttribute('fill'); + } + + f = false; + } + + // Iterating over edges + var edges = svg.querySelectorAll('[id="' + prefix + '-group-edges"] > [class="' + prefix + '-edge"]'); + + for (i = 0, l = edges.length, f = true; i < l; i++) { + color = edges[i].getAttribute('stroke'); + + if (!params.data) + edges[i].removeAttribute('data-edge-id'); + + if (params.classes) { + + if (!(color in edgeColorIndex)) { + edgeColorIndex[color] = (f ? prefix + '-edge' : 'c-' + (count++)); + styleText += '.' + edgeColorIndex[color] + '{stroke: ' + color + '}'; + } + + if (edgeColorIndex[color] !== prefix + '-edge') + edges[i].setAttribute('class', edges[i].getAttribute('class') + ' ' + edgeColorIndex[color]); + edges[i].removeAttribute('stroke'); + } + + f = false; + } + + if (params.classes) + style.appendChild(document.createTextNode(styleText)); + } + + + /** + * Extending prototype + */ + sigma.prototype.toSVG = function(params) { + params = params || {}; + + var prefix = this.settings('classPrefix'), + w = params.size || params.width || DEFAULTS.size, + h = params.size || params.height || DEFAULTS.size; + + // Creating a dummy container + var container = document.createElement('div'); + container.setAttribute('width', w); + container.setAttribute('height', h); + container.setAttribute('style', 'position:absolute; top: 0px; left:0px; width: ' + w + 'px; height: ' + h + 'px;'); + + // Creating a camera + var camera = this.addCamera(); + + // Creating a svg renderer + var renderer = this.addRenderer({ + camera: camera, + container: container, + type: 'svg', + forceLabels: !!params.labels + }); + + // Refreshing + renderer.resize(w, h); + this.refresh(); + + // Dropping camera and renderers before something nasty happens + this.killRenderer(renderer); + this.killCamera(camera); + + // Retrieving svg + var svg = container.querySelector('svg'); + svg.removeAttribute('style'); + svg.setAttribute('width', w + 'px'); + svg.setAttribute('height', h + 'px'); + svg.setAttribute('x', '0px'); + svg.setAttribute('y', '0px'); + // svg.setAttribute('viewBox', '0 0 1000 1000'); + + // Dropping labels + if (!params.labels) { + var labelGroup = svg.querySelector('[id="' + prefix + '-group-labels"]'); + svg.removeChild(labelGroup); + } + + // Dropping hovers + var hoverGroup = svg.querySelector('[id="' + prefix + '-group-hovers"]'); + svg.removeChild(hoverGroup); + + // Optims? + params.classes = (params.classes !== false); + if (!params.data || params.classes) + optimize(svg, prefix, params); + + // Retrieving svg string + var svgString = svg.outerHTML; + + // Paranoid cleanup + container = null; + + // Output string + var output = '\n'; + output += '\n'; + output += svgString; + + if (params.download) + download(output, params.filename || DEFAULTS.filename); + + return output; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/Gruntfile.js b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/Gruntfile.js new file mode 100644 index 0000000..28d60be --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/Gruntfile.js @@ -0,0 +1,28 @@ +module.exports = function(grunt) { + + + // Setting grunt base as sigma's root directory + grunt.file.setBase('../../'); + + // Registering needed files + var files = ['supervisor.js', 'worker.js'].map(function(p) { + return __dirname + '/' + p; + }); + + // Project configuration: + grunt.initConfig({ + forceAtlas2: { + prod: { + files: { + 'build/plugins/sigma.layout.forceAtlas2.min.js': files + } + } + } + }); + + // Loading tasks + grunt.loadTasks(__dirname + '/tasks'); + + // By default, we will crush and then minify + grunt.registerTask('default', ['forceAtlas2:prod']); +}; diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/README.md b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/README.md new file mode 100644 index 0000000..942e3e3 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/README.md @@ -0,0 +1,79 @@ +sigma.layout.forceAtlas2 +======================== + +Algorithm by [Mathieu Jacomy](https://github.com/jacomyma). + +Plugin by [Guillaume Plique](https://github.com/Yomguithereal). + +--- + +This plugin implements [ForceAtlas2](http://www.plosone.org/article/info%3Adoi%2F10.1371%2Fjournal.pone.0098679), a force-directed layout algorithm. + +For optimization purposes, the algorithm's computations are delegated to a web worker. + +## Methods + +**sigma.startForceAtlas2** + +Starts or unpauses the layout. It is possible to pass a configuration if this is the first time you start the layout. + +```js +sigmaInstance.startForceAtlas2(config); +``` + +**sigma.stopForceAtlas2** + +Pauses the layout. + +```js +sigmaInstance.stopForceAtlas2(); +``` + +**sigma.configForceAtlas2** + +Changes the layout's configuration. + +```js +sigmaInstance.configForceAtlas2(config); +``` + +**sigma.killForceAtlas2** + +Completely stops the layout and terminates the assiociated worker. You can still restart it later, but a new worker will have to initialize. + +```js +sigmaInstance.killForceAtlas2(); +``` + +**sigma.isForceAtlas2Running** + +Returns whether ForceAtlas2 is running. + +```js +sigmaInstance.isForceAtlas2Running(); +``` + +## Configuration + +*Algorithm configuration* + +* **linLogMode** *boolean* `false`: switch ForceAtlas' model from lin-lin to lin-log (tribute to Andreas Noack). Makes clusters more tight. +* **outboundAttractionDistribution** *boolean* `false` +* **adjustSizes** *boolean* `false` +* **edgeWeightInfluence** *number* `0`: how much influence you give to the edges weight. 0 is "no influence" and 1 is "normal". +* **scalingRatio** *number* `1`: how much repulsion you want. More makes a more sparse graph. +* **strongGravityMode** *boolean* `false` +* **gravity** *number* `1`: attracts nodes to the center. Prevents islands from drifting away. +* **barnesHutOptimize** *boolean* `true`: should we use the algorithm's Barnes-Hut to improve repulsion's scalability (`O(n²)` to `O(nlog(n))`)? This is useful for large graph but harmful to small ones. +* **barnesHutTheta** *number* `0.5` +* **slowDown** *number* `1` +* **startingIterations** *integer* `1`: number of iterations to be run before the first render. +* **iterationsPerRender** *integer* `1`: number of iterations to be run before each render. + +*Supervisor configuration* + +* **worker** *boolean* `true`: should the layout use a web worker? +* **workerUrl** *string* : path to the worker file if needed because your browser does not support blob workers. + +## Notes +1. The layout won't stop by itself, so if you want it to stop, you will have to trigger it explicitly. diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/supervisor.js b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/supervisor.js new file mode 100644 index 0000000..57f9b94 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/supervisor.js @@ -0,0 +1,340 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + /** + * Sigma ForceAtlas2.5 Supervisor + * =============================== + * + * Author: Guillaume Plique (Yomguithereal) + * Version: 0.1 + */ + var _root = this; + + /** + * Feature detection + * ------------------ + */ + var webWorkers = 'Worker' in _root; + + /** + * Supervisor Object + * ------------------ + */ + function Supervisor(sigInst, options) { + var _this = this, + workerFn = sigInst.getForceAtlas2Worker && + sigInst.getForceAtlas2Worker(); + + options = options || {}; + + // _root URL Polyfill + _root.URL = _root.URL || _root.webkitURL; + + // Properties + this.sigInst = sigInst; + this.graph = this.sigInst.graph; + this.ppn = 10; + this.ppe = 3; + this.config = {}; + this.shouldUseWorker = + options.worker === false ? false : true && webWorkers; + this.workerUrl = options.workerUrl; + + // State + this.started = false; + this.running = false; + + // Web worker or classic DOM events? + if (this.shouldUseWorker) { + if (!this.workerUrl) { + var blob = this.makeBlob(workerFn); + this.worker = new Worker(URL.createObjectURL(blob)); + } + else { + this.worker = new Worker(this.workerUrl); + } + + // Post Message Polyfill + this.worker.postMessage = + this.worker.webkitPostMessage || this.worker.postMessage; + } + else { + + eval(workerFn); + } + + // Worker message receiver + this.msgName = (this.worker) ? 'message' : 'newCoords'; + this.listener = function(e) { + + // Retrieving data + _this.nodesByteArray = new Float32Array(e.data.nodes); + + // If ForceAtlas2 is running, we act accordingly + if (_this.running) { + + // Applying layout + _this.applyLayoutChanges(); + + // Send data back to worker and loop + _this.sendByteArrayToWorker(); + + // Rendering graph + _this.sigInst.refresh(); + } + }; + + (this.worker || document).addEventListener(this.msgName, this.listener); + + // Filling byteArrays + this.graphToByteArrays(); + + // Binding on kill to properly terminate layout when parent is killed + sigInst.bind('kill', function() { + sigInst.killForceAtlas2(); + }); + } + + Supervisor.prototype.makeBlob = function(workerFn) { + var blob; + + try { + blob = new Blob([workerFn], {type: 'application/javascript'}); + } + catch (e) { + _root.BlobBuilder = _root.BlobBuilder || + _root.WebKitBlobBuilder || + _root.MozBlobBuilder; + + blob = new BlobBuilder(); + blob.append(workerFn); + blob = blob.getBlob(); + } + + return blob; + }; + + Supervisor.prototype.graphToByteArrays = function() { + var nodes = this.graph.nodes(), + edges = this.graph.edges(), + nbytes = nodes.length * this.ppn, + ebytes = edges.length * this.ppe, + nIndex = {}, + i, + j, + l; + + // Allocating Byte arrays with correct nb of bytes + this.nodesByteArray = new Float32Array(nbytes); + this.edgesByteArray = new Float32Array(ebytes); + + // Iterate through nodes + for (i = j = 0, l = nodes.length; i < l; i++) { + + // Populating index + nIndex[nodes[i].id] = j; + + // Populating byte array + this.nodesByteArray[j] = nodes[i].x; + this.nodesByteArray[j + 1] = nodes[i].y; + this.nodesByteArray[j + 2] = 0; + this.nodesByteArray[j + 3] = 0; + this.nodesByteArray[j + 4] = 0; + this.nodesByteArray[j + 5] = 0; + this.nodesByteArray[j + 6] = 1 + this.graph.degree(nodes[i].id); + this.nodesByteArray[j + 7] = 1; + this.nodesByteArray[j + 8] = nodes[i].size; + this.nodesByteArray[j + 9] = 0; + j += this.ppn; + } + + // Iterate through edges + for (i = j = 0, l = edges.length; i < l; i++) { + this.edgesByteArray[j] = nIndex[edges[i].source]; + this.edgesByteArray[j + 1] = nIndex[edges[i].target]; + this.edgesByteArray[j + 2] = edges[i].weight || 0; + j += this.ppe; + } + }; + + // TODO: make a better send function + Supervisor.prototype.applyLayoutChanges = function() { + var nodes = this.graph.nodes(), + j = 0, + realIndex; + + // Moving nodes + for (var i = 0, l = this.nodesByteArray.length; i < l; i += this.ppn) { + nodes[j].x = this.nodesByteArray[i]; + nodes[j].y = this.nodesByteArray[i + 1]; + j++; + } + }; + + Supervisor.prototype.sendByteArrayToWorker = function(action) { + var content = { + action: action || 'loop', + nodes: this.nodesByteArray.buffer + }; + + var buffers = [this.nodesByteArray.buffer]; + + if (action === 'start') { + content.config = this.config || {}; + content.edges = this.edgesByteArray.buffer; + buffers.push(this.edgesByteArray.buffer); + } + + if (this.shouldUseWorker) + this.worker.postMessage(content, buffers); + else + _root.postMessage(content, '*'); + }; + + Supervisor.prototype.start = function() { + if (this.running) + return; + + this.running = true; + + // Do not refresh edgequadtree during layout: + var k, + c; + for (k in this.sigInst.cameras) { + c = this.sigInst.cameras[k]; + c.edgequadtree._enabled = false; + } + + if (!this.started) { + + // Sending init message to worker + this.sendByteArrayToWorker('start'); + this.started = true; + } + else { + this.sendByteArrayToWorker(); + } + }; + + Supervisor.prototype.stop = function() { + if (!this.running) + return; + + // Allow to refresh edgequadtree: + var k, + c, + bounds; + for (k in this.sigInst.cameras) { + c = this.sigInst.cameras[k]; + c.edgequadtree._enabled = true; + + // Find graph boundaries: + bounds = sigma.utils.getBoundaries( + this.graph, + c.readPrefix + ); + + // Refresh edgequadtree: + if (c.settings('drawEdges') && c.settings('enableEdgeHovering')) + c.edgequadtree.index(this.sigInst.graph, { + prefix: c.readPrefix, + bounds: { + x: bounds.minX, + y: bounds.minY, + width: bounds.maxX - bounds.minX, + height: bounds.maxY - bounds.minY + } + }); + } + + this.running = false; + }; + + Supervisor.prototype.killWorker = function() { + if (this.worker) { + this.worker.terminate(); + } + else { + _root.postMessage({action: 'kill'}, '*'); + document.removeEventListener(this.msgName, this.listener); + } + }; + + Supervisor.prototype.configure = function(config) { + + // Setting configuration + this.config = config; + + if (!this.started) + return; + + var data = {action: 'config', config: this.config}; + + if (this.shouldUseWorker) + this.worker.postMessage(data); + else + _root.postMessage(data, '*'); + }; + + /** + * Interface + * ---------- + */ + sigma.prototype.startForceAtlas2 = function(config) { + + // Create supervisor if undefined + if (!this.supervisor) + this.supervisor = new Supervisor(this, config); + + // Configuration provided? + if (config) + this.supervisor.configure(config); + + // Start algorithm + this.supervisor.start(); + + return this; + }; + + sigma.prototype.stopForceAtlas2 = function() { + if (!this.supervisor) + return this; + + // Pause algorithm + this.supervisor.stop(); + + return this; + }; + + sigma.prototype.killForceAtlas2 = function() { + if (!this.supervisor) + return this; + + // Stop Algorithm + this.supervisor.stop(); + + // Kill Worker + this.supervisor.killWorker(); + + // Kill supervisor + this.supervisor = null; + + return this; + }; + + sigma.prototype.configForceAtlas2 = function(config) { + if (!this.supervisor) + this.supervisor = new Supervisor(this, config); + + this.supervisor.configure(config); + + return this; + }; + + sigma.prototype.isForceAtlas2Running = function(config) { + return !!this.supervisor && this.supervisor.running; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/tasks/forceAtlas2.js b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/tasks/forceAtlas2.js new file mode 100644 index 0000000..1bb0062 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/tasks/forceAtlas2.js @@ -0,0 +1,127 @@ +/* + * grunt-forceAtlas2 + * + * This task crush and minify Force Atlas 2 code. + */ +var uglify = require('uglify-js'); + +// Shorteners +function minify(string) { + return uglify.minify(string, {fromString: true}).code; +} + +// Crushing function +function crush(fnString) { + var pattern, + i, + l; + + var np = [ + 'x', + 'y', + 'dx', + 'dy', + 'old_dx', + 'old_dy', + 'mass', + 'convergence', + 'size', + 'fixed' + ]; + + var ep = [ + 'source', + 'target', + 'weight' + ]; + + var rp = [ + 'node', + 'centerX', + 'centerY', + 'size', + 'nextSibling', + 'firstChild', + 'mass', + 'massCenterX', + 'massCenterY' + ]; + + // Replacing matrix accessors by incremented indexes + for (i = 0, l = rp.length; i < l; i++) { + pattern = new RegExp('rp\\(([^,]*), \'' + rp[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + for (i = 0, l = np.length; i < l; i++) { + pattern = new RegExp('np\\(([^,]*), \'' + np[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + for (i = 0, l = ep.length; i < l; i++) { + pattern = new RegExp('ep\\(([^,]*), \'' + ep[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + return fnString; +} + +// Cleaning function +function clean(string) { + return string.replace( + /function crush\(fnString\)/, + 'var crush = null; function no_crush(fnString)' + ); +} + +module.exports = function(grunt) { + + // Force atlas grunt multitask + function multitask() { + + // Merge task-specific and/or target-specific options with these defaults. + var options = this.options({}); + + // Iterate over all specified file groups. + this.files.forEach(function(f) { + // Concat specified files. + var src = f.src.filter(function(filepath) { + // Warn on and remove invalid source files (if nonull was set). + if (!grunt.file.exists(filepath)) { + grunt.log.warn('Source file "' + filepath + '" not found.'); + return false; + } else { + return true; + } + }).map(function(filepath) { + // Read file source. + return grunt.file.read(filepath); + }).join('\n'); + + // Crushing, cleaning and minifying + src = minify(clean(crush(src))); + + // Write the destination file. + grunt.file.write(f.dest, src); + + // Print a success message. + grunt.log.writeln('File "' + f.dest + '" created.'); + }); + } + + // Registering the task + grunt.registerMultiTask( + 'forceAtlas2', + 'A grunt task to crush and minify ForceAtlas2.', + multitask + ); +}; diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/worker.js b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/worker.js new file mode 100644 index 0000000..adc0b29 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.forceAtlas2/worker.js @@ -0,0 +1,1129 @@ +;(function(undefined) { + 'use strict'; + + /** + * Sigma ForceAtlas2.5 Webworker + * ============================== + * + * Author: Guillaume Plique (Yomguithereal) + * Algorithm author: Mathieu Jacomy @ Sciences Po Medialab & WebAtlas + * Version: 1.0.3 + */ + + var _root = this, + inWebWorker = !('document' in _root); + + /** + * Worker Function Wrapper + * ------------------------ + * + * The worker has to be wrapped into a single stringified function + * to be passed afterwards as a BLOB object to the supervisor. + */ + var Worker = function(undefined) { + 'use strict'; + + /** + * Worker settings and properties + */ + var W = { + + // Properties + ppn: 10, + ppe: 3, + ppr: 9, + maxForce: 10, + iterations: 0, + converged: false, + + // Possible to change through config + settings: { + linLogMode: false, + outboundAttractionDistribution: false, + adjustSizes: false, + edgeWeightInfluence: 0, + scalingRatio: 1, + strongGravityMode: false, + gravity: 1, + slowDown: 1, + barnesHutOptimize: false, + barnesHutTheta: 0.5, + startingIterations: 1, + iterationsPerRender: 1 + } + }; + + var NodeMatrix, + EdgeMatrix, + RegionMatrix; + + /** + * Helpers + */ + function extend() { + var i, + k, + res = {}, + l = arguments.length; + + for (i = l - 1; i >= 0; i--) + for (k in arguments[i]) + res[k] = arguments[i][k]; + return res; + } + + function __emptyObject(obj) { + var k; + + for (k in obj) + if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k)) + delete obj[k]; + + return obj; + } + + /** + * Matrices properties accessors + */ + var nodeProperties = { + x: 0, + y: 1, + dx: 2, + dy: 3, + old_dx: 4, + old_dy: 5, + mass: 6, + convergence: 7, + size: 8, + fixed: 9 + }; + + var edgeProperties = { + source: 0, + target: 1, + weight: 2 + }; + + var regionProperties = { + node: 0, + centerX: 1, + centerY: 2, + size: 3, + nextSibling: 4, + firstChild: 5, + mass: 6, + massCenterX: 7, + massCenterY: 8 + }; + + function np(i, p) { + + // DEBUG: safeguards + if ((i % W.ppn) !== 0) + throw 'np: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'np: non int.'; + + if (p in nodeProperties) + return i + nodeProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant node property given (' + p + ').'; + } + + function ep(i, p) { + + // DEBUG: safeguards + if ((i % W.ppe) !== 0) + throw 'ep: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'ep: non int.'; + + if (p in edgeProperties) + return i + edgeProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant edge property given (' + p + ').'; + } + + function rp(i, p) { + + // DEBUG: safeguards + if ((i % W.ppr) !== 0) + throw 'rp: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'rp: non int.'; + + if (p in regionProperties) + return i + regionProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant region property given (' + p + ').'; + } + + // DEBUG + function nan(v) { + if (isNaN(v)) + throw 'NaN alert!'; + } + + + /** + * Algorithm initialization + */ + + function init(nodes, edges, config) { + config = config || {}; + var i, l; + + // Matrices + NodeMatrix = nodes; + EdgeMatrix = edges; + + // Length + W.nodesLength = NodeMatrix.length; + W.edgesLength = EdgeMatrix.length; + + // Merging configuration + configure(config); + } + + function configure(o) { + W.settings = extend(o, W.settings); + } + + /** + * Algorithm pass + */ + + // MATH: get distances stuff and power 2 issues + function pass() { + var a, i, j, l, r, n, n1, n2, e, w, g, k, m; + + var outboundAttCompensation, + coefficient, + xDist, + yDist, + ewc, + mass, + distance, + size, + factor; + + // 1) Initializing layout data + //----------------------------- + + // Resetting positions & computing max values + for (n = 0; n < W.nodesLength; n += W.ppn) { + NodeMatrix[np(n, 'old_dx')] = NodeMatrix[np(n, 'dx')]; + NodeMatrix[np(n, 'old_dy')] = NodeMatrix[np(n, 'dy')]; + NodeMatrix[np(n, 'dx')] = 0; + NodeMatrix[np(n, 'dy')] = 0; + } + + // If outbound attraction distribution, compensate + if (W.settings.outboundAttractionDistribution) { + outboundAttCompensation = 0; + for (n = 0; n < W.nodesLength; n += W.ppn) { + outboundAttCompensation += NodeMatrix[np(n, 'mass')]; + } + + outboundAttCompensation /= W.nodesLength; + } + + + // 1.bis) Barnes-Hut computation + //------------------------------ + + if (W.settings.barnesHutOptimize) { + + var minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity, + q, q0, q1, q2, q3; + + // Setting up + // RegionMatrix = new Float32Array(W.nodesLength / W.ppn * 4 * W.ppr); + RegionMatrix = []; + + // Computing min and max values + for (n = 0; n < W.nodesLength; n += W.ppn) { + minX = Math.min(minX, NodeMatrix[np(n, 'x')]); + maxX = Math.max(maxX, NodeMatrix[np(n, 'x')]); + minY = Math.min(minY, NodeMatrix[np(n, 'y')]); + maxY = Math.max(maxY, NodeMatrix[np(n, 'y')]); + } + + // Build the Barnes Hut root region + RegionMatrix[rp(0, 'node')] = -1; + RegionMatrix[rp(0, 'centerX')] = (minX + maxX) / 2; + RegionMatrix[rp(0, 'centerY')] = (minY + maxY) / 2; + RegionMatrix[rp(0, 'size')] = Math.max(maxX - minX, maxY - minY); + RegionMatrix[rp(0, 'nextSibling')] = -1; + RegionMatrix[rp(0, 'firstChild')] = -1; + RegionMatrix[rp(0, 'mass')] = 0; + RegionMatrix[rp(0, 'massCenterX')] = 0; + RegionMatrix[rp(0, 'massCenterY')] = 0; + + // Add each node in the tree + l = 1; + for (n = 0; n < W.nodesLength; n += W.ppn) { + + // Current region, starting with root + r = 0; + + while (true) { + // Are there sub-regions? + + // We look at first child index + if (RegionMatrix[rp(r, 'firstChild')] >= 0) { + + // There are sub-regions + + // We just iterate to find a "leave" of the tree + // that is an empty region or a region with a single node + // (see next case) + + // Find the quadrant of n + if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { + + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q = RegionMatrix[rp(r, 'firstChild')]; + } + else { + + // Bottom Left quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + // Update center of mass and mass (we only do it for non-leave regions) + RegionMatrix[rp(r, 'massCenterX')] = + (RegionMatrix[rp(r, 'massCenterX')] * RegionMatrix[rp(r, 'mass')] + + NodeMatrix[np(n, 'x')] * NodeMatrix[np(n, 'mass')]) / + (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); + + RegionMatrix[rp(r, 'massCenterY')] = + (RegionMatrix[rp(r, 'massCenterY')] * RegionMatrix[rp(r, 'mass')] + + NodeMatrix[np(n, 'y')] * NodeMatrix[np(n, 'mass')]) / + (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); + + RegionMatrix[rp(r, 'mass')] += NodeMatrix[np(n, 'mass')]; + + // Iterate on the right quadrant + r = q; + continue; + } + else { + + // There are no sub-regions: we are in a "leave" + + // Is there a node in this leave? + if (RegionMatrix[rp(r, 'node')] < 0) { + + // There is no node in region: + // we record node n and go on + RegionMatrix[rp(r, 'node')] = n; + break; + } + else { + + // There is a node in this region + + // We will need to create sub-regions, stick the two + // nodes (the old one r[0] and the new one n) in two + // subregions. If they fall in the same quadrant, + // we will iterate. + + // Create sub-regions + RegionMatrix[rp(r, 'firstChild')] = l * W.ppr; + w = RegionMatrix[rp(r, 'size')] / 2; // new size (half) + + // NOTE: we use screen coordinates + // from Top Left to Bottom Right + + // Top Left sub-region + g = RegionMatrix[rp(r, 'firstChild')]; + + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Bottom Left sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Top Right sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Bottom Right sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = RegionMatrix[rp(r, 'nextSibling')]; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + l += 4; + + // Now the goal is to find two different sub-regions + // for the two nodes: the one previously recorded (r[0]) + // and the one we want to add (n) + + // Find the quadrant of the old node + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')] < RegionMatrix[rp(r, 'centerX')]) { + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q = RegionMatrix[rp(r, 'firstChild')]; + } + else { + + // Bottom Left quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + // We remove r[0] from the region r, add its mass to r and record it in q + RegionMatrix[rp(r, 'mass')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')]; + RegionMatrix[rp(r, 'massCenterX')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; + RegionMatrix[rp(r, 'massCenterY')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; + + RegionMatrix[rp(q, 'node')] = RegionMatrix[rp(r, 'node')]; + RegionMatrix[rp(r, 'node')] = -1; + + // Find the quadrant of n + if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q2 = RegionMatrix[rp(r, 'firstChild')]; + } + else { + // Bottom Left quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if(NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + if (q === q2) { + + // If both nodes are in the same quadrant, + // we have to try it again on this quadrant + r = q; + continue; + } + + // If both quadrants are different, we record n + // in its quadrant + RegionMatrix[rp(q2, 'node')] = n; + break; + } + } + } + } + } + + + // 2) Repulsion + //-------------- + // NOTES: adjustSizes = antiCollision & scalingRatio = coefficient + + if (W.settings.barnesHutOptimize) { + coefficient = W.settings.scalingRatio; + + // Applying repulsion through regions + for (n = 0; n < W.nodesLength; n += W.ppn) { + + // Computing leaf quad nodes iteration + + r = 0; // Starting with root region + while (true) { + + if (RegionMatrix[rp(r, 'firstChild')] >= 0) { + + // The region has sub-regions + + // We run the Barnes Hut test to see if we are at the right distance + distance = Math.sqrt( + (Math.pow(NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')], 2)) + + (Math.pow(NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')], 2)) + ); + + if (2 * RegionMatrix[rp(r, 'size')] / distance < W.settings.barnesHutTheta) { + + // We treat the region as a single body, and we repulse + + xDist = NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')]; + yDist = NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')]; + + if (W.settings.adjustSizes) { + + //-- Linear Anti-collision Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = -coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + else { + + //-- Linear Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + + // When this is done, we iterate. We have to look at the next sibling. + if (RegionMatrix[rp(r, 'nextSibling')] < 0) + break; // No next sibling: we have finished the tree + r = RegionMatrix[rp(r, 'nextSibling')]; + continue; + + } + else { + + // The region is too close and we have to look at sub-regions + r = RegionMatrix[rp(r, 'firstChild')]; + continue; + } + + } + else { + + // The region has no sub-region + // If there is a node r[0] and it is not n, then repulse + + if (RegionMatrix[rp(r, 'node')] >= 0 && RegionMatrix[rp(r, 'node')] !== n) { + xDist = NodeMatrix[np(n, 'x')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; + yDist = NodeMatrix[np(n, 'y')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; + + distance = Math.sqrt(xDist * xDist + yDist * yDist); + + if (W.settings.adjustSizes) { + + //-- Linear Anti-collision Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = -coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + else { + + //-- Linear Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + + } + + // When this is done, we iterate. We have to look at the next sibling. + if (RegionMatrix[rp(r, 'nextSibling')] < 0) + break; // No next sibling: we have finished the tree + r = RegionMatrix[rp(r, 'nextSibling')]; + continue; + } + } + } + } + else { + coefficient = W.settings.scalingRatio; + + // Square iteration + for (n1 = 0; n1 < W.nodesLength; n1 += W.ppn) { + for (n2 = 0; n2 < n1; n2 += W.ppn) { + + // Common to both methods + xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; + yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; + + if (W.settings.adjustSizes) { + + //-- Anticollision Linear Repulsion + distance = Math.sqrt(xDist * xDist + yDist * yDist) - + NodeMatrix[np(n1, 'size')] - + NodeMatrix[np(n2, 'size')]; + + if (distance > 0) { + factor = coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')] / + distance / distance; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] += xDist * factor; + NodeMatrix[np(n2, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = 100 * coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')]; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + else { + + //-- Linear Repulsion + distance = Math.sqrt(xDist * xDist + yDist * yDist); + + if (distance > 0) { + factor = coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')] / + distance / distance; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + } + } + } + + + // 3) Gravity + //------------ + g = W.settings.gravity / W.settings.scalingRatio; + coefficient = W.settings.scalingRatio; + for (n = 0; n < W.nodesLength; n += W.ppn) { + factor = 0; + + // Common to both methods + xDist = NodeMatrix[np(n, 'x')]; + yDist = NodeMatrix[np(n, 'y')]; + distance = Math.sqrt( + Math.pow(xDist, 2) + Math.pow(yDist, 2) + ); + + if (W.settings.strongGravityMode) { + + //-- Strong gravity + if (distance > 0) + factor = coefficient * NodeMatrix[np(n, 'mass')] * g; + } + else { + + //-- Linear Anti-collision Repulsion n + if (distance > 0) + factor = coefficient * NodeMatrix[np(n, 'mass')] * g / distance; + } + + // Updating node's dx and dy + NodeMatrix[np(n, 'dx')] -= xDist * factor; + NodeMatrix[np(n, 'dy')] -= yDist * factor; + } + + + + // 4) Attraction + //--------------- + coefficient = 1 * + (W.settings.outboundAttractionDistribution ? + outboundAttCompensation : + 1); + + // TODO: simplify distance + // TODO: coefficient is always used as -c --> optimize? + for (e = 0; e < W.edgesLength; e += W.ppe) { + n1 = EdgeMatrix[ep(e, 'source')]; + n2 = EdgeMatrix[ep(e, 'target')]; + w = EdgeMatrix[ep(e, 'weight')]; + + // Edge weight influence + ewc = Math.pow(w, W.settings.edgeWeightInfluence); + + // Common measures + xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; + yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; + + // Applying attraction to nodes + if (W.settings.adjustSizes) { + + distance = Math.sqrt( + (Math.pow(xDist, 2) + Math.pow(yDist, 2)) - + NodeMatrix[np(n1, 'size')] - + NodeMatrix[np(n2, 'size')] + ); + + if (W.settings.linLogMode) { + if (W.settings.outboundAttractionDistribution) { + + //-- LinLog Degree Distributed Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / + distance / + NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- LinLog Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / distance; + } + } + } + else { + if (W.settings.outboundAttractionDistribution) { + + //-- Linear Degree Distributed Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- Linear Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc; + } + } + } + } + else { + + distance = Math.sqrt( + Math.pow(xDist, 2) + Math.pow(yDist, 2) + ); + + if (W.settings.linLogMode) { + if (W.settings.outboundAttractionDistribution) { + + //-- LinLog Degree Distributed Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / + distance / + NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- LinLog Attraction + if (distance > 0) + factor = -coefficient * ewc * Math.log(1 + distance) / distance; + } + } + else { + if (W.settings.outboundAttractionDistribution) { + + //-- Linear Attraction Mass Distributed + // NOTE: Distance is set to 1 to override next condition + distance = 1; + factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; + } + else { + + //-- Linear Attraction + // NOTE: Distance is set to 1 to override next condition + distance = 1; + factor = -coefficient * ewc; + } + } + } + + // Updating nodes' dx and dy + // TODO: if condition or factor = 1? + if (distance > 0) { + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + + + // 5) Apply Forces + //----------------- + var force, + swinging, + traction, + nodespeed; + + // MATH: sqrt and square distances + if (W.settings.adjustSizes) { + + for (n = 0; n < W.nodesLength; n += W.ppn) { + if (!NodeMatrix[np(n, 'fixed')]) { + force = Math.sqrt( + Math.pow(NodeMatrix[np(n, 'dx')], 2) + + Math.pow(NodeMatrix[np(n, 'dy')], 2) + ); + + if (force > W.maxForce) { + NodeMatrix[np(n, 'dx')] = + NodeMatrix[np(n, 'dx')] * W.maxForce / force; + NodeMatrix[np(n, 'dy')] = + NodeMatrix[np(n, 'dy')] * W.maxForce / force; + } + + swinging = NodeMatrix[np(n, 'mass')] * + Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) + ); + + traction = Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) + ) / 2; + + nodespeed = + 0.1 * Math.log(1 + traction) / (1 + Math.sqrt(swinging)); + + // Updating node's positon + NodeMatrix[np(n, 'x')] = + NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * + (nodespeed / W.settings.slowDown); + NodeMatrix[np(n, 'y')] = + NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * + (nodespeed / W.settings.slowDown); + } + } + } + else { + + for (n = 0; n < W.nodesLength; n += W.ppn) { + if (!NodeMatrix[np(n, 'fixed')]) { + + swinging = NodeMatrix[np(n, 'mass')] * + Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) + ); + + traction = Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) + ) / 2; + + nodespeed = NodeMatrix[np(n, 'convergence')] * + Math.log(1 + traction) / (1 + Math.sqrt(swinging)); + + // Updating node convergence + NodeMatrix[np(n, 'convergence')] = + Math.min(1, Math.sqrt( + nodespeed * + (Math.pow(NodeMatrix[np(n, 'dx')], 2) + + Math.pow(NodeMatrix[np(n, 'dy')], 2)) / + (1 + Math.sqrt(swinging)) + )); + + // Updating node's positon + NodeMatrix[np(n, 'x')] = + NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * + (nodespeed / W.settings.slowDown); + NodeMatrix[np(n, 'y')] = + NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * + (nodespeed / W.settings.slowDown); + } + } + } + + // Counting one more iteration + W.iterations++; + } + + /** + * Message reception & sending + */ + + // Sending data back to the supervisor + var sendNewCoords; + + if (typeof window !== 'undefined' && window.document) { + + // From same document as sigma + sendNewCoords = function() { + var e; + + if (document.createEvent) { + e = document.createEvent('Event'); + e.initEvent('newCoords', true, false); + } + else { + e = document.createEventObject(); + e.eventType = 'newCoords'; + } + + e.eventName = 'newCoords'; + e.data = { + nodes: NodeMatrix.buffer + }; + requestAnimationFrame(function() { + document.dispatchEvent(e); + }); + }; + } + else { + + // From a WebWorker + sendNewCoords = function() { + self.postMessage( + {nodes: NodeMatrix.buffer}, + [NodeMatrix.buffer] + ); + }; + } + + // Algorithm run + function run(n) { + for (var i = 0; i < n; i++) + pass(); + sendNewCoords(); + } + + // On supervisor message + var listener = function(e) { + switch (e.data.action) { + case 'start': + init( + new Float32Array(e.data.nodes), + new Float32Array(e.data.edges), + e.data.config + ); + + // First iteration(s) + run(W.settings.startingIterations); + break; + + case 'loop': + NodeMatrix = new Float32Array(e.data.nodes); + run(W.settings.iterationsPerRender); + break; + + case 'config': + + // Merging new settings + configure(e.data.config); + break; + + case 'kill': + + // Deleting context for garbage collection + __emptyObject(W); + NodeMatrix = null; + EdgeMatrix = null; + RegionMatrix = null; + self.removeEventListener('message', listener); + break; + + default: + } + }; + + // Adding event listener + self.addEventListener('message', listener); + }; + + + /** + * Exporting + * ---------- + * + * Crush the worker function and make it accessible by sigma's instances so + * the supervisor can call it. + */ + function crush(fnString) { + var pattern, + i, + l; + + var np = [ + 'x', + 'y', + 'dx', + 'dy', + 'old_dx', + 'old_dy', + 'mass', + 'convergence', + 'size', + 'fixed' + ]; + + var ep = [ + 'source', + 'target', + 'weight' + ]; + + var rp = [ + 'node', + 'centerX', + 'centerY', + 'size', + 'nextSibling', + 'firstChild', + 'mass', + 'massCenterX', + 'massCenterY' + ]; + + // rp + // NOTE: Must go first + for (i = 0, l = rp.length; i < l; i++) { + pattern = new RegExp('rp\\(([^,]*), \'' + rp[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + // np + for (i = 0, l = np.length; i < l; i++) { + pattern = new RegExp('np\\(([^,]*), \'' + np[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + // ep + for (i = 0, l = ep.length; i < l; i++) { + pattern = new RegExp('ep\\(([^,]*), \'' + ep[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + return fnString; + } + + // Exporting + function getWorkerFn() { + var fnString = crush ? crush(Worker.toString()) : Worker.toString(); + return ';(' + fnString + ').call(this);'; + } + + if (inWebWorker) { + + // We are in a webworker, so we launch the Worker function + eval(getWorkerFn()); + } + else { + + // We are requesting the worker from sigma, we retrieve it therefore + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.prototype.getForceAtlas2Worker = getWorkerFn; + } +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/README.md b/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/README.md new file mode 100644 index 0000000..7b954aa --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/README.md @@ -0,0 +1,87 @@ +sigma.layout.noverlap +======================== + +Plugin developed by [Andrew Pitts](https://github.com/apitts) and published under the [MIT](LICENSE) license. Original algorithm by [Mathieu Jacomy](https://github.com/jacomyma) and ported to sigma.js with permission. + +--- + +This plugin runs an algorithm which distributes nodes in the network, ensuring that they do not overlap and providing a margin where specified. + +## Methods + +**configure** + +Changes the layout's configuration. + +```js +var listener = s.configNoverlap(config); +``` + +**start** + +Starts the layout. It is possible to pass a configuration if this is the first time you start the layout. + +```js +s.startNoverlap(); +``` + +**isRunning** + +Returns whether the layout is running. + +```js +s.isNoverlapRunning(); +``` + +## Configuration + +* **nodes**: *array*: the subset of nodes to apply the layout. + +*Algorithm configuration* + +* **nodeMargin**: *number* `5.0`: The additional minimum space to apply around each and every node. +* **scaleNodes**: *number* `1.2`: A multiplier to apply to nodes such that larger nodes will have more space around them if this multiplier is greater than zero. +* **gridSize**: *integer* `20`: The number of rows and columns to use when dividing the nodes up into cells which the algorithm is applied to. Use more rows and columns for larger graphs for a more efficient algorithm. +* **permittedExpansion** *number* `1.1`: At every step, this is the maximum ratio to apply to the bounding box, i.e. the maximum by which the network is permitted to expand. +* **rendererIndex** *integer* `0`: The index of the renderer to use to compute overlap and collisions of the nodes. +* **speed** *number* `2`: A larger value increases the speed with which the algorithm will convergence at the cost of precision. +* **maxIterations** *number* `500`: The maximum number of iterations to run the algorithm for before stopping it. + +*Easing configuration* + +* **easing** *string*: if specified, ease the transition between nodes positions if background is `true`. The duration is specified by the Sigma settings `animationsTime`. See [sigma.utils.easing](../../src/utils/sigma.utils.js#L723) for available values. +* **duration** *number*: duration of the transition for the easing method. Default value is Sigma setting `animationsTime`. + +## Events + +The plugin dispatches the following events: + +- `start`: on layout start. +- `interpolate`: at the beginning of the layout animation if an *easing* function is specified and the layout is ran on background. +- `stop`: on layout stop, will be dispatched after `interpolate`. + +Example: + +```js + +s = new sigma({ + graph: g, + container: 'graph-container' +}); + +var config = { + nodeMargin: 3.0, + scaleNodes: 1.3 +}; + +// Configure the algorithm +var listener = s.configNoverlap(config); + +// Bind all events: +listener.bind('start stop interpolate', function(event) { + console.log(event.type); +}); + +// Start the algorithm: +s.startNoverlap(); +``` \ No newline at end of file diff --git a/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/sigma.layout.noverlap.js b/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/sigma.layout.noverlap.js new file mode 100644 index 0000000..6d4b3a4 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.layout.noverlap/sigma.layout.noverlap.js @@ -0,0 +1,408 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw new Error('sigma is not declared'); + + // Initialize package: + sigma.utils.pkg('sigma.layout.noverlap'); + + /** + * Noverlap Layout + * =============================== + * + * Author: @apitts / Andrew Pitts + * Algorithm: @jacomyma / Mathieu Jacomy (originally contributed to Gephi and ported to sigma.js under the MIT license by @andpitts with permission) + * Acknowledgement: @sheyman / Sébastien Heymann (some inspiration has been taken from other MIT licensed layout algorithms authored by @sheyman) + * Version: 0.1 + */ + + var settings = { + speed: 3, + scaleNodes: 1.2, + nodeMargin: 5.0, + gridSize: 20, + permittedExpansion: 1.1, + rendererIndex: 0, + maxIterations: 500 + }; + + var _instance = {}; + + /** + * Event emitter Object + * ------------------ + */ + var _eventEmitter = {}; + + /** + * Noverlap Object + * ------------------ + */ + function Noverlap() { + var self = this; + + this.init = function (sigInst, options) { + options = options || {}; + + // Properties + this.sigInst = sigInst; + this.config = sigma.utils.extend(options, settings); + this.easing = options.easing; + this.duration = options.duration; + + if (options.nodes) { + this.nodes = options.nodes; + delete options.nodes; + } + + if (!sigma.plugins || typeof sigma.plugins.animate === 'undefined') { + throw new Error('sigma.plugins.animate is not declared'); + } + + // State + this.running = false; + }; + + /** + * Single layout iteration. + */ + this.atomicGo = function () { + if (!this.running || this.iterCount < 1) return false; + + var nodes = this.nodes || this.sigInst.graph.nodes(), + nodesCount = nodes.length, + i, + n, + n1, + n2, + xmin = Infinity, + xmax = -Infinity, + ymin = Infinity, + ymax = -Infinity, + xwidth, + yheight, + xcenter, + ycenter, + grid, + row, + col, + minXBox, + maxXBox, + minYBox, + maxYBox, + adjacentNodes, + subRow, + subCol, + nxmin, + nxmax, + nymin, + nymax; + + this.iterCount--; + this.running = false; + + for (i=0; i < nodesCount; i++) { + n = nodes[i]; + n.dn.dx = 0; + n.dn.dy = 0; + + //Find the min and max for both x and y across all nodes + xmin = Math.min(xmin, n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) ); + xmax = Math.max(xmax, n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) ); + ymin = Math.min(ymin, n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) ); + ymax = Math.max(ymax, n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) ); + + } + + xwidth = xmax - xmin; + yheight = ymax - ymin; + xcenter = (xmin + xmax) / 2; + ycenter = (ymin + ymax) / 2; + xmin = xcenter - self.config.permittedExpansion*xwidth / 2; + xmax = xcenter + self.config.permittedExpansion*xwidth / 2; + ymin = ycenter - self.config.permittedExpansion*yheight / 2; + ymax = ycenter + self.config.permittedExpansion*yheight / 2; + + grid = {}; //An object of objects where grid[row][col] is an array of node ids representing nodes that fall in that grid. Nodes can fall in more than one grid + + for(row = 0; row < self.config.gridSize; row++) { + grid[row] = {}; + for(col = 0; col < self.config.gridSize; col++) { + grid[row][col] = []; + } + } + + //Place nodes in grid + for (i=0; i < nodesCount; i++) { + n = nodes[i]; + + nxmin = n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin); + nxmax = n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin); + nymin = n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin); + nymax = n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin); + + minXBox = Math.floor(self.config.gridSize* (nxmin - xmin) / (xmax - xmin) ); + maxXBox = Math.floor(self.config.gridSize* (nxmax - xmin) / (xmax - xmin) ); + minYBox = Math.floor(self.config.gridSize* (nymin - ymin) / (ymax - ymin) ); + maxYBox = Math.floor(self.config.gridSize* (nymax - ymin) / (ymax - ymin) ); + for(col = minXBox; col <= maxXBox; col++) { + for(row = minYBox; row <= maxYBox; row++) { + grid[row][col].push(n.id); + } + } + } + + + adjacentNodes = {}; //An object that stores the node ids of adjacent nodes (either in same grid box or adjacent grid box) for all nodes + + for(row = 0; row < self.config.gridSize; row++) { + for(col = 0; col < self.config.gridSize; col++) { + grid[row][col].forEach(function(nodeId) { + if(!adjacentNodes[nodeId]) { + adjacentNodes[nodeId] = []; + } + for(subRow = Math.max(0, row - 1); subRow <= Math.min(row + 1, self.config.gridSize - 1); subRow++) { + for(subCol = Math.max(0, col - 1); subCol <= Math.min(col + 1, self.config.gridSize - 1); subCol++) { + grid[subRow][subCol].forEach(function(subNodeId) { + if(subNodeId !== nodeId && adjacentNodes[nodeId].indexOf(subNodeId) === -1) { + adjacentNodes[nodeId].push(subNodeId); + } + }); + } + } + }); + } + } + + //If two nodes overlap then repulse them + for (i=0; i < nodesCount; i++) { + n1 = nodes[i]; + adjacentNodes[n1.id].forEach(function(nodeId) { + var n2 = self.sigInst.graph.nodes(nodeId); + var xDist = n2.dn_x - n1.dn_x; + var yDist = n2.dn_y - n1.dn_y; + var dist = Math.sqrt(xDist*xDist + yDist*yDist); + var collision = (dist < ((n1.dn_size*self.config.scaleNodes + self.config.nodeMargin) + (n2.dn_size*self.config.scaleNodes + self.config.nodeMargin))); + if(collision) { + self.running = true; + if(dist > 0) { + n2.dn.dx += xDist / dist * (1 + n1.dn_size); + n2.dn.dy += yDist / dist * (1 + n1.dn_size); + } else { + n2.dn.dx += xwidth * 0.01 * (0.5 - Math.random()); + n2.dn.dy += yheight * 0.01 * (0.5 - Math.random()); + } + } + }); + } + + for (i=0; i < nodesCount; i++) { + n = nodes[i]; + if(!n.fixed) { + n.dn_x = n.dn_x + n.dn.dx * 0.1 * self.config.speed; + n.dn_y = n.dn_y + n.dn.dy * 0.1 * self.config.speed; + } + } + + if(this.running && this.iterCount < 1) { + this.running = false; + } + + return this.running; + }; + + this.go = function () { + this.iterCount = this.config.maxIterations; + + while (this.running) { + this.atomicGo(); + }; + + this.stop(); + }; + + this.start = function() { + if (this.running) return; + + var nodes = this.sigInst.graph.nodes(); + + var prefix = this.sigInst.renderers[self.config.rendererIndex].options.prefix; + + this.running = true; + + // Init nodes + for (var i = 0; i < nodes.length; i++) { + nodes[i].dn_x = nodes[i][prefix + 'x']; + nodes[i].dn_y = nodes[i][prefix + 'y']; + nodes[i].dn_size = nodes[i][prefix + 'size']; + nodes[i].dn = { + dx: 0, + dy: 0 + }; + } + _eventEmitter[self.sigInst.id].dispatchEvent('start'); + this.go(); + }; + + this.stop = function() { + var nodes = this.sigInst.graph.nodes(); + + this.running = false; + + if (this.easing) { + _eventEmitter[self.sigInst.id].dispatchEvent('interpolate'); + sigma.plugins.animate( + self.sigInst, + { + x: 'dn_x', + y: 'dn_y' + }, + { + easing: self.easing, + onComplete: function() { + self.sigInst.refresh(); + for (var i = 0; i < nodes.length; i++) { + nodes[i].dn = null; + nodes[i].dn_x = null; + nodes[i].dn_y = null; + } + _eventEmitter[self.sigInst.id].dispatchEvent('stop'); + }, + duration: self.duration + } + ); + } + else { + // Apply changes + for (var i = 0; i < nodes.length; i++) { + nodes[i].x = nodes[i].dn_x; + nodes[i].y = nodes[i].dn_y; + } + + this.sigInst.refresh(); + + for (var i = 0; i < nodes.length; i++) { + nodes[i].dn = null; + nodes[i].dn_x = null; + nodes[i].dn_y = null; + } + _eventEmitter[self.sigInst.id].dispatchEvent('stop'); + } + }; + + this.kill = function() { + this.sigInst = null; + this.config = null; + this.easing = null; + }; + }; + + /** + * Interface + * ---------- + */ + + /** + * Configure the layout algorithm. + + * Recognized options: + * ********************** + * Here is the exhaustive list of every accepted parameter in the settings + * object: + * + * {?number} speed A larger value increases the convergence speed at the cost of precision + * {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes + * {?number} nodeMargin A fixed margin to apply around nodes regardless of size + * {?number} maxIterations The maximum number of iterations to perform before the layout completes. + * {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation + * {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration + * {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero. + * {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the + * quadraticInOut easing from this package will be used instead. + * {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead. + * + * + * @param {object} config The optional configuration object. + * + * @return {sigma.classes.dispatcher} Returns an event emitter. + */ + sigma.prototype.configNoverlap = function(config) { + + var sigInst = this; + + if (!config) throw new Error('Missing argument: "config"'); + + // Create instance if undefined + if (!_instance[sigInst.id]) { + _instance[sigInst.id] = new Noverlap(); + + _eventEmitter[sigInst.id] = {}; + sigma.classes.dispatcher.extend(_eventEmitter[sigInst.id]); + + // Binding on kill to clear the references + sigInst.bind('kill', function() { + _instance[sigInst.id].kill(); + _instance[sigInst.id] = null; + _eventEmitter[sigInst.id] = null; + }); + } + + _instance[sigInst.id].init(sigInst, config); + + return _eventEmitter[sigInst.id]; + }; + + /** + * Start the layout algorithm. It will use the existing configuration if no + * new configuration is passed. + + * Recognized options: + * ********************** + * Here is the exhaustive list of every accepted parameter in the settings + * object + * + * {?number} speed A larger value increases the convergence speed at the cost of precision + * {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes + * {?number} nodeMargin A fixed margin to apply around nodes regardless of size + * {?number} maxIterations The maximum number of iterations to perform before the layout completes. + * {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation + * {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration + * {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero. + * {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the + * quadraticInOut easing from this package will be used instead. + * {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead. + * + * + * + * @param {object} config The optional configuration object. + * + * @return {sigma.classes.dispatcher} Returns an event emitter. + */ + + sigma.prototype.startNoverlap = function(config) { + + var sigInst = this; + + if (config) { + this.configNoverlap(sigInst, config); + } + + _instance[sigInst.id].start(); + + return _eventEmitter[sigInst.id]; + }; + + /** + * Returns true if the layout has started and is not completed. + * + * @return {boolean} + */ + sigma.prototype.isNoverlapRunning = function() { + + var sigInst = this; + + return !!_instance[sigInst.id] && _instance[sigInst.id].running; + }; + +}).call(this); \ No newline at end of file diff --git a/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/LICENSE b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/LICENSE new file mode 100644 index 0000000..c9f3b20 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/LICENSE @@ -0,0 +1,553 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +Preamble +The GNU General Public License is a free, copyleft license for +software and other kinds of works. +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. +The precise terms and conditions for copying, distribution and +modification follow. +TERMS AND CONDITIONS +0. Definitions. +"This License" refers to version 3 of the GNU General Public License. +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. +A "covered work" means either the unmodified Program or a work based +on the Program. +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. +1. Source Code. +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. +The Corresponding Source for a work in source code form is that +same work. +2. Basic Permissions. +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. +8. Termination. +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. +11. Patents. +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. +END OF TERMS AND CONDITIONS +How to Apply These Terms to Your New Programs +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. +{one line to give the program's name and a brief idea of what it does.} +Copyright (C) {year} {name of author} +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program. If not, see . +Also add information on how to contact you by electronic and paper mail. +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: +{project} Copyright (C) {year} {fullname} +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/README.md b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/README.md new file mode 100644 index 0000000..f55c70a --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/README.md @@ -0,0 +1,58 @@ +sigma.neo4j.cypher +==================== + +Plugin developed by [Benoît Simard](https://github.com/sim51). + +--- + +This plugin provides a simple function, `sigma.neo4j.cypher()`, that will run a cypher query on a neo4j instance, parse the response, eventually instantiate sigma and fill the graph with the `graph.read()` method. + +Nodes are created with the following structure : + * id -> Neo4j node id + * label -> Neo4j node id + * neo4j_labels -> Labels of Neo4j node + * neo4j_data -> All the properties of the neo4j node + +Edges are created with the following structure : + * id -> Neo4j edge id + * label -> Neo4j edge type + * neo4j_type -> Neo4j edge type + * neo4j_data -> All the properties of Neo4j relationship + +The most basic way to use this helper is to call it with a neo4j server url and a cypher query. It will then instantiate sigma, but after having added the graph into the config object. + +For neo4j < 2.2 +````javascript +sigma.neo4j.cypher( + 'http://localhost:7474', + 'MATCH (n) OPTIONAL MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 100', + { container: 'myContainer' } +); +```` + +For neo4j >= 2.2, you must pass a neo4j user with its password. So instead of the neo4j url, you have to pass a neo4j server object like this : +````javascript +sigma.neo4j.cypher( + { url: 'http://localhost:7474', user:'neo4j', password:'admin' }, + 'MATCH (n) OPTIONAL MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 100', + { container: 'myContainer' } +); +```` + +It is also possible to update an existing instance's graph instead. + +````javascript +sigma.neo4j.cypher( + { url: 'http://localhost:7474', user:'neo4j', password:'admin' }, + 'MATCH (n) OPTIONAL MATCH (n)-[r]->(m) RETURN n,r,m LIMIT 100', + myExistingInstance, + function() { + myExistingInstance.refresh(); + } +); +```` + +There is two additional functions provided by the plugin : + + * ```sigma.neo4j.getTypes({ url: 'http://localhost:7474', user:'neo4j', password:'admin' }, callback)``` : Return all relationship type of the database + * ```sigma.neo4j.getLabels({ url: 'http://localhost:7474', user:'neo4j', password:'admin' }, callback)``` : Return all node label of the database diff --git a/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/sigma.neo4j.cypher.js b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/sigma.neo4j.cypher.js new file mode 100644 index 0000000..0e23742 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.neo4j.cypher/sigma.neo4j.cypher.js @@ -0,0 +1,218 @@ +;(function (undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Declare neo4j package + sigma.utils.pkg("sigma.neo4j"); + + // Initialize package: + sigma.utils.pkg('sigma.utils'); + + + /** + * This function is an helper for the neo4j communication. + * + * @param {string|object} neo4j The URL of neo4j server or a neo4j server object. + * @param {string} endpoint Endpoint of the neo4j server + * @param {string} method The calling method for the endpoint : 'GET' or 'POST' + * @param {object|string} data Data that will be send to the server + * @param {function} callback The callback function + */ + sigma.neo4j.send = function(neo4j, endpoint, method, data, callback) { + var xhr = sigma.utils.xhr(), + url, user, password; + + // if neo4j arg is not an object + url = neo4j; + if(typeof neo4j === 'object') { + url = neo4j.url; + user = neo4j.user; + password = neo4j.password; + } + + if (!xhr) + throw 'XMLHttpRequest not supported, cannot load the file.'; + + // Construct the endpoint url + url += endpoint; + + xhr.open(method, url, true); + if( user && password) { + xhr.setRequestHeader('Authorization', 'Basic ' + btoa(user + ':' + password)); + } + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + // Call the callback if specified: + callback(JSON.parse(xhr.responseText)); + } + }; + xhr.send(data); + }; + + /** + * This function parse a neo4j cypher query result, and transform it into + * a sigma graph object. + * + * @param {object} result The server response of a cypher query. + * + * @return A graph object + */ + sigma.neo4j.cypher_parse = function(result) { + var graph = { nodes: [], edges: [] }, + nodesMap = {}, + edgesMap = {}, + key; + + // Iteration on all result data + result.results[0].data.forEach(function (data) { + + // iteration on graph for all node + data.graph.nodes.forEach(function (node) { + + var sigmaNode = { + id : node.id, + label : node.id, + x : Math.random(), + y : Math.random(), + size : 1, + color : '#000000', + neo4j_labels : node.labels, + neo4j_data : node.properties + }; + + if (sigmaNode.id in nodesMap) { + // do nothing + } else { + nodesMap[sigmaNode.id] = sigmaNode; + } + }); + + // iteration on graph for all node + data.graph.relationships.forEach(function (edge) { + var sigmaEdge = { + id : edge.id, + label : edge.type, + source : edge.startNode, + target : edge.endNode, + color : '#7D7C8E', + neo4j_type : edge.type, + neo4j_data : edge.properties + }; + + if (sigmaEdge.id in edgesMap) { + // do nothing + } else { + edgesMap[sigmaEdge.id] = sigmaEdge; + } + }); + + }); + + // construct sigma nodes + for (key in nodesMap) { + graph.nodes.push(nodesMap[key]); + } + // construct sigma nodes + for (key in edgesMap) { + graph.edges.push(edgesMap[key]); + } + + return graph; + }; + + + /** + * This function execute a cypher and create a new sigma instance or + * updates the graph of a given instance. It is possible to give a callback + * that will be executed at the end of the process. + * + * @param {object|string} neo4j The URL of neo4j server or a neo4j server object. + * @param {string} cypher The cypher query + * @param {?object|?sigma} sig A sigma configuration object or a sigma instance. + * @param {?function} callback Eventually a callback to execute after + * having parsed the file. It will be called + * with the related sigma instance as + * parameter. + */ + sigma.neo4j.cypher = function (neo4j, cypher, sig, callback) { + var endpoint = '/db/data/transaction/commit', + data, cypherCallback; + + // Data that will be send to the server + data = JSON.stringify({ + "statements": [ + { + "statement": cypher, + "resultDataContents": ["graph"], + "includeStats": false + } + ] + }); + + // Callback method after server response + cypherCallback = function (callback) { + + return function (response) { + + var graph = { nodes: [], edges: [] }; + + graph = sigma.neo4j.cypher_parse(response); + + // Update the instance's graph: + if (sig instanceof sigma) { + sig.graph.clear(); + sig.graph.read(graph); + + // ...or instantiate sigma if needed: + } else if (typeof sig === 'object') { + sig = new sigma(sig); + sig.graph.read(graph); + sig.refresh(); + + // ...or it's finally the callback: + } else if (typeof sig === 'function') { + callback = sig; + sig = null; + } + + // Call the callback if specified: + if (callback) + callback(sig || graph); + }; + }; + + // Let's call neo4j + sigma.neo4j.send(neo4j, endpoint, 'POST', data, cypherCallback(callback)); + }; + + /** + * This function call neo4j to get all labels of the graph. + * + * @param {string} neo4j The URL of neo4j server or an object with the url, user & password. + * @param {function} callback The callback function + * + * @return An array of label + */ + sigma.neo4j.getLabels = function(neo4j, callback) { + sigma.neo4j.send(neo4j, '/db/data/labels', 'GET', null, callback); + }; + + /** + * This function parse a neo4j cypher query result. + * + * @param {string} neo4j The URL of neo4j server or an object with the url, user & password. + * @param {function} callback The callback function + * + * @return An array of relationship type + */ + sigma.neo4j.getTypes = function(neo4j, callback) { + sigma.neo4j.send(neo4j, '/db/data/relationship/types', 'GET', null, callback); + }; + +}).call(this); + + diff --git a/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/README.md b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/README.md new file mode 100644 index 0000000..d9ba5e7 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/README.md @@ -0,0 +1,29 @@ +sigma.parsers.gexf +================== + +Plugin developed by [Alexis Jacomy](https://github.com/jacomyal), on top of [gexf-parser](https://github.com/Yomguithereal/gexf-parser), developed by [Guillaume Plique](https://github.com/Yomguithereal). + +--- + +This plugin provides a single function, `sigma.parsers.gexf()`, that will load a GEXF encoded file, parse it, and instantiate sigma. + +The most basic way to use this helper is to call it with a path and a sigma configuration object. It will then instantiate sigma, but after having added the graph into the config object. + +````javascript +sigma.parsers.gexf( + 'myGraph.gexf', + { container: 'myContainer' } +); +```` + +It is also possible to update an existing instance's graph instead. + +````javascript +sigma.parsers.gexf( + 'myGraph.gexf', + myExistingInstance, + function() { + myExistingInstance.refresh(); + } +); +```` diff --git a/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/gexf-parser.js b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/gexf-parser.js new file mode 100644 index 0000000..6ff9424 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/gexf-parser.js @@ -0,0 +1,551 @@ +;(function(undefined) { + 'use strict'; + + /** + * GEXF Library + * ============= + * + * Author: PLIQUE Guillaume (Yomguithereal) + * URL: https://github.com/Yomguithereal/gexf-parser + * Version: 0.1.1 + */ + + /** + * Helper Namespace + * ----------------- + * + * A useful batch of function dealing with DOM operations and types. + */ + var _helpers = { + getModelTags: function(xml) { + var attributesTags = xml.getElementsByTagName('attributes'), + modelTags = {}, + l = attributesTags.length, + i; + + for (i = 0; i < l; i++) + modelTags[attributesTags[i].getAttribute('class')] = + attributesTags[i].childNodes; + + return modelTags; + }, + nodeListToArray: function(nodeList) { + + // Return array + var children = []; + + // Iterating + for (var i = 0, len = nodeList.length; i < len; ++i) { + if (nodeList[i].nodeName !== '#text') + children.push(nodeList[i]); + } + + return children; + }, + nodeListEach: function(nodeList, func) { + + // Iterating + for (var i = 0, len = nodeList.length; i < len; ++i) { + if (nodeList[i].nodeName !== '#text') + func(nodeList[i]); + } + }, + nodeListToHash: function(nodeList, filter) { + + // Return object + var children = {}; + + // Iterating + for (var i = 0; i < nodeList.length; i++) { + if (nodeList[i].nodeName !== '#text') { + var prop = filter(nodeList[i]); + children[prop.key] = prop.value; + } + } + + return children; + }, + namedNodeMapToObject: function(nodeMap) { + + // Return object + var attributes = {}; + + // Iterating + for (var i = 0; i < nodeMap.length; i++) { + attributes[nodeMap[i].name] = nodeMap[i].value; + } + + return attributes; + }, + getFirstElementByTagNS: function(node, ns, tag) { + var el = node.getElementsByTagName(ns + ':' + tag)[0]; + + if (!el) + el = node.getElementsByTagNameNS(ns, tag)[0]; + + if (!el) + el = node.getElementsByTagName(tag)[0]; + + return el; + }, + getAttributeNS: function(node, ns, attribute) { + var attr_value = node.getAttribute(ns + ':' + attribute); + + if (attr_value === undefined) + attr_value = node.getAttributeNS(ns, attribute); + + if (attr_value === undefined) + attr_value = node.getAttribute(attribute); + + return attr_value; + }, + enforceType: function(type, value) { + + switch (type) { + case 'boolean': + value = (value === 'true'); + break; + + case 'integer': + case 'long': + case 'float': + case 'double': + value = +value; + break; + + case 'liststring': + value = value ? value.split('|') : []; + break; + } + + return value; + }, + getRGB: function(values) { + return (values[3]) ? + 'rgba(' + values.join(',') + ')' : + 'rgb(' + values.slice(0, -1).join(',') + ')'; + } + }; + + + /** + * Parser Core Functions + * ---------------------- + * + * The XML parser's functions themselves. + */ + + /** + * Node structure. + * A function returning an object guarded with default value. + * + * @param {object} properties The node properties. + * @return {object} The guarded node object. + */ + function Node(properties) { + + // Possible Properties + var node = { + id: properties.id, + label: properties.label + }; + + if (properties.viz) + node.viz = properties.viz; + + if (properties.attributes) + node.attributes = properties.attributes; + + return node; + } + + + /** + * Edge structure. + * A function returning an object guarded with default value. + * + * @param {object} properties The edge properties. + * @return {object} The guarded edge object. + */ + function Edge(properties) { + + // Possible Properties + var edge = { + id: properties.id, + type: properties.type || 'undirected', + label: properties.label || '', + source: properties.source, + target: properties.target, + weight: +properties.weight || 1.0 + }; + + if (properties.viz) + edge.viz = properties.viz; + + if (properties.attributes) + edge.attributes = properties.attributes; + + return edge; + } + + /** + * Graph parser. + * This structure parse a gexf string and return an object containing the + * parsed graph. + * + * @param {string} xml The xml string of the gexf file to parse. + * @return {object} The parsed graph. + */ + function Graph(xml) { + var _xml = {}; + + // Basic Properties + //------------------ + _xml.els = { + root: xml.getElementsByTagName('gexf')[0], + graph: xml.getElementsByTagName('graph')[0], + meta: xml.getElementsByTagName('meta')[0], + nodes: xml.getElementsByTagName('node'), + edges: xml.getElementsByTagName('edge'), + model: _helpers.getModelTags(xml) + }; + + // Information + _xml.hasViz = !!_helpers.getAttributeNS(_xml.els.root, 'xmlns', 'viz'); + _xml.version = _xml.els.root.getAttribute('version') || '1.0'; + _xml.mode = _xml.els.graph.getAttribute('mode') || 'static'; + + var edgeType = _xml.els.graph.getAttribute('defaultedgetype'); + _xml.defaultEdgetype = edgeType || 'undirected'; + + // Parser Functions + //------------------ + + // Meta Data + function _metaData() { + + var metas = {}; + if (!_xml.els.meta) + return metas; + + // Last modified date + metas.lastmodifieddate = _xml.els.meta.getAttribute('lastmodifieddate'); + + // Other information + _helpers.nodeListEach(_xml.els.meta.childNodes, function(child) { + metas[child.tagName.toLowerCase()] = child.textContent; + }); + + return metas; + } + + // Model + function _model(cls) { + var attributes = []; + + // Iterating through attributes + if (_xml.els.model[cls]) + _helpers.nodeListEach(_xml.els.model[cls], function(attr) { + + // Properties + var properties = { + id: attr.getAttribute('id') || attr.getAttribute('for'), + type: attr.getAttribute('type') || 'string', + title: attr.getAttribute('title') || '' + }; + + // Defaults + var default_el = _helpers.nodeListToArray(attr.childNodes); + + if (default_el.length > 0) + properties.defaultValue = default_el[0].textContent; + + // Creating attribute + attributes.push(properties); + }); + + return attributes.length > 0 ? attributes : false; + } + + // Data from nodes or edges + function _data(model, node_or_edge) { + + var data = {}; + var attvalues_els = node_or_edge.getElementsByTagName('attvalue'); + + // Getting Node Indicated Attributes + var ah = _helpers.nodeListToHash(attvalues_els, function(el) { + var attributes = _helpers.namedNodeMapToObject(el.attributes); + var key = attributes.id || attributes['for']; + + // Returning object + return {key: key, value: attributes.value}; + }); + + + // Iterating through model + model.map(function(a) { + + // Default value? + data[a.id] = !(a.id in ah) && 'defaultValue' in a ? + _helpers.enforceType(a.type, a.defaultValue) : + _helpers.enforceType(a.type, ah[a.id]); + + }); + + return data; + } + + // Nodes + function _nodes(model) { + var nodes = []; + + // Iteration through nodes + _helpers.nodeListEach(_xml.els.nodes, function(n) { + + // Basic properties + var properties = { + id: n.getAttribute('id'), + label: n.getAttribute('label') || '' + }; + + // Retrieving data from nodes if any + if (model) + properties.attributes = _data(model, n); + + // Retrieving viz information + if (_xml.hasViz) + properties.viz = _nodeViz(n); + + // Pushing node + nodes.push(Node(properties)); + }); + + return nodes; + } + + // Viz information from nodes + function _nodeViz(node) { + var viz = {}; + + // Color + var color_el = _helpers.getFirstElementByTagNS(node, 'viz', 'color'); + + if (color_el) { + var color = ['r', 'g', 'b', 'a'].map(function(c) { + return color_el.getAttribute(c); + }); + + viz.color = _helpers.getRGB(color); + } + + // Position + var pos_el = _helpers.getFirstElementByTagNS(node, 'viz', 'position'); + + if (pos_el) { + viz.position = {}; + + ['x', 'y', 'z'].map(function(p) { + viz.position[p] = +pos_el.getAttribute(p); + }); + } + + // Size + var size_el = _helpers.getFirstElementByTagNS(node, 'viz', 'size'); + if (size_el) + viz.size = +size_el.getAttribute('value'); + + // Shape + var shape_el = _helpers.getFirstElementByTagNS(node, 'viz', 'shape'); + if (shape_el) + viz.shape = shape_el.getAttribute('value'); + + return viz; + } + + // Edges + function _edges(model, default_type) { + var edges = []; + + // Iteration through edges + _helpers.nodeListEach(_xml.els.edges, function(e) { + + // Creating the edge + var properties = _helpers.namedNodeMapToObject(e.attributes); + if (!('type' in properties)) { + properties.type = default_type; + } + + // Retrieving edge data + if (model) + properties.attributes = _data(model, e); + + + // Retrieving viz information + if (_xml.hasViz) + properties.viz = _edgeViz(e); + + edges.push(Edge(properties)); + }); + + return edges; + } + + // Viz information from edges + function _edgeViz(edge) { + var viz = {}; + + // Color + var color_el = _helpers.getFirstElementByTagNS(edge, 'viz', 'color'); + + if (color_el) { + var color = ['r', 'g', 'b', 'a'].map(function(c) { + return color_el.getAttribute(c); + }); + + viz.color = _helpers.getRGB(color); + } + + // Shape + var shape_el = _helpers.getFirstElementByTagNS(edge, 'viz', 'shape'); + if (shape_el) + viz.shape = shape_el.getAttribute('value'); + + // Thickness + var thick_el = _helpers.getFirstElementByTagNS(edge, 'viz', 'thickness'); + if (thick_el) + viz.thickness = +thick_el.getAttribute('value'); + + return viz; + } + + + // Returning the Graph + //--------------------- + var nodeModel = _model('node'), + edgeModel = _model('edge'); + + var graph = { + version: _xml.version, + mode: _xml.mode, + defaultEdgeType: _xml.defaultEdgetype, + meta: _metaData(), + model: {}, + nodes: _nodes(nodeModel), + edges: _edges(edgeModel, _xml.defaultEdgetype) + }; + + if (nodeModel) + graph.model.node = nodeModel; + if (edgeModel) + graph.model.edge = edgeModel; + + return graph; + } + + + /** + * Public API + * ----------- + * + * User-accessible functions. + */ + + // Fetching GEXF with XHR + function fetch(gexf_url, callback) { + var xhr = (function() { + if (window.XMLHttpRequest) + return new XMLHttpRequest(); + + var names, + i; + + if (window.ActiveXObject) { + names = [ + 'Msxml2.XMLHTTP.6.0', + 'Msxml2.XMLHTTP.3.0', + 'Msxml2.XMLHTTP', + 'Microsoft.XMLHTTP' + ]; + + for (i in names) + try { + return new ActiveXObject(names[i]); + } catch (e) {} + } + + return null; + })(); + + if (!xhr) + throw 'XMLHttpRequest not supported, cannot load the file.'; + + // Async? + var async = (typeof callback === 'function'), + getResult; + + // If we can't override MIME type, we are on IE 9 + // We'll be parsing the response string then. + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/xml'); + getResult = function(r) { + return r.responseXML; + }; + } + else { + getResult = function(r) { + var p = new DOMParser(); + return p.parseFromString(r.responseText, 'application/xml'); + }; + } + + xhr.open('GET', gexf_url, async); + + if (async) + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) + callback(getResult(xhr)); + }; + + xhr.send(); + + return (async) ? xhr : getResult(xhr); + } + + // Parsing the GEXF File + function parse(gexf) { + return Graph(gexf); + } + + // Fetch and parse the GEXF File + function fetchAndParse(gexf_url, callback) { + if (typeof callback === 'function') { + return fetch(gexf_url, function(gexf) { + callback(Graph(gexf)); + }); + } else + return Graph(fetch(gexf_url)); + } + + + /** + * Exporting + * ---------- + */ + if (typeof this.gexf !== 'undefined') + throw 'gexf: error - a variable called "gexf" already ' + + 'exists in the global scope'; + + this.gexf = { + + // Functions + parse: parse, + fetch: fetchAndParse, + + // Version + version: '0.1.1' + }; + + if (typeof exports !== 'undefined' && this.exports !== exports) + module.exports = this.gexf; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/sigma.parsers.gexf.js b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/sigma.parsers.gexf.js new file mode 100644 index 0000000..943d313 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.parsers.gexf/sigma.parsers.gexf.js @@ -0,0 +1,112 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize package: + sigma.utils.pkg('sigma.parsers'); + + // Just a basic ID generator: + var _id = 0; + function edgeId() { + return 'e' + (_id++); + } + + /** + * If the first arguments is a valid URL, this function loads a GEXF file and + * creates a new sigma instance or updates the graph of a given instance. It + * is possible to give a callback that will be executed at the end of the + * process. And if the first argument is a DOM element, it will skip the + * loading step and parse the given XML tree to fill the graph. + * + * @param {string|DOMElement} target The URL of the GEXF file or a valid + * GEXF tree. + * @param {object|sigma} sig A sigma configuration object or a + * sigma instance. + * @param {?function} callback Eventually a callback to execute + * after having parsed the file. It will + * be called with the related sigma + * instance as parameter. + */ + sigma.parsers.gexf = function(target, sig, callback) { + var i, + l, + arr, + obj; + + function parse(graph) { + // Adapt the graph: + arr = graph.nodes; + for (i = 0, l = arr.length; i < l; i++) { + obj = arr[i]; + + obj.id = obj.id; + if (obj.viz && typeof obj.viz === 'object') { + if (obj.viz.position && typeof obj.viz.position === 'object') { + obj.x = obj.viz.position.x; + obj.y = -obj.viz.position.y; // Needed otherwise it's up side down + } + obj.size = obj.viz.size; + obj.color = obj.viz.color; + } + } + + arr = graph.edges; + for (i = 0, l = arr.length; i < l; i++) { + obj = arr[i]; + + obj.id = typeof obj.id === 'string' ? obj.id : edgeId(); + obj.source = '' + obj.source; + obj.target = '' + obj.target; + + if (obj.viz && typeof obj.viz === 'object') { + obj.color = obj.viz.color; + obj.size = obj.viz.thickness; + } + + // Weight over viz.thickness? + obj.size = obj.weight; + + // Changing type to be direction so it won't mess with sigma's naming + obj.direction = obj.type; + delete obj.type; + } + + // Update the instance's graph: + if (sig instanceof sigma) { + sig.graph.clear(); + + arr = graph.nodes; + for (i = 0, l = arr.length; i < l; i++) + sig.graph.addNode(arr[i]); + + arr = graph.edges; + for (i = 0, l = arr.length; i < l; i++) + sig.graph.addEdge(arr[i]); + + // ...or instantiate sigma if needed: + } else if (typeof sig === 'object') { + sig.graph = graph; + sig = new sigma(sig); + + // ...or it's finally the callback: + } else if (typeof sig === 'function') { + callback = sig; + sig = null; + } + + // Call the callback if specified: + if (callback) { + callback(sig || graph); + return; + } else + return graph; + } + + if (typeof target === 'string') + gexf.fetch(target, parse); + else if (typeof target === 'object') + return parse(gexf.parse(target)); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.parsers.json/README.md b/blogContent/projects/steam/src/plugins/sigma.parsers.json/README.md new file mode 100644 index 0000000..8c9be75 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.parsers.json/README.md @@ -0,0 +1,29 @@ +sigma.parsers.json +================== + +Plugin developed by [Alexis Jacomy](https://github.com/jacomyal). + +--- + +This plugin provides a single function, `sigma.parsers.json()`, that will load a JSON encoded file, parse it, eventually instantiate sigma and fill the graph with the `graph.read()` method. The main goal is to avoid using jQuery only to load an external JSON file. + +The most basic way to use this helper is to call it with a path and a sigma configuration object. It will then instanciate sigma, but after having added the graph into the config object. + +````javascript +sigma.parsers.json( + 'myGraph.json', + { container: 'myContainer' } +); +```` + +It is also possible to update an existing instance's graph instead. + +````javascript +sigma.parsers.json( + 'myGraph.json', + myExistingInstance, + function() { + myExistingInstance.refresh(); + } +); +```` diff --git a/blogContent/projects/steam/src/plugins/sigma.parsers.json/sigma.parsers.json.js b/blogContent/projects/steam/src/plugins/sigma.parsers.json/sigma.parsers.json.js new file mode 100644 index 0000000..b71903d --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.parsers.json/sigma.parsers.json.js @@ -0,0 +1,88 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize package: + sigma.utils.pkg('sigma.parsers'); + sigma.utils.pkg('sigma.utils'); + + /** + * Just an XmlHttpRequest polyfill for different IE versions. + * + * @return {*} The XHR like object. + */ + sigma.utils.xhr = function() { + if (window.XMLHttpRequest) + return new XMLHttpRequest(); + + var names, + i; + + if (window.ActiveXObject) { + names = [ + 'Msxml2.XMLHTTP.6.0', + 'Msxml2.XMLHTTP.3.0', + 'Msxml2.XMLHTTP', + 'Microsoft.XMLHTTP' + ]; + + for (i in names) + try { + return new ActiveXObject(names[i]); + } catch (e) {} + } + + return null; + }; + + /** + * This function loads a JSON file and creates a new sigma instance or + * updates the graph of a given instance. It is possible to give a callback + * that will be executed at the end of the process. + * + * @param {string} url The URL of the JSON file. + * @param {object|sigma} sig A sigma configuration object or a sigma + * instance. + * @param {?function} callback Eventually a callback to execute after + * having parsed the file. It will be called + * with the related sigma instance as + * parameter. + */ + sigma.parsers.json = function(url, sig, callback) { + var graph, + xhr = sigma.utils.xhr(); + + if (!xhr) + throw 'XMLHttpRequest not supported, cannot load the file.'; + + xhr.open('GET', url, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + graph = JSON.parse(xhr.responseText); + + // Update the instance's graph: + if (sig instanceof sigma) { + sig.graph.clear(); + sig.graph.read(graph); + + // ...or instantiate sigma if needed: + } else if (typeof sig === 'object') { + sig.graph = graph; + sig = new sigma(sig); + + // ...or it's finally the callback: + } else if (typeof sig === 'function') { + callback = sig; + sig = null; + } + + // Call the callback if specified: + if (callback) + callback(sig || graph); + } + }; + xhr.send(); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/LICENSE b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/LICENSE new file mode 100644 index 0000000..a84c395 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/LICENSE @@ -0,0 +1,25 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + diff --git a/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/README.md b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/README.md new file mode 100644 index 0000000..68d5363 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/README.md @@ -0,0 +1,27 @@ +sigma.pathfinding.astar.js — v1.0.0 +=================================== + +> Plugin author: [@A----](https://github.com/A----) +> Main repository for this plugin is here: https://github.com/A----/sigma-pathfinding-astar +> Please report issues, make PR, there. +> This project is released under Public Domain license (see LICENSE for more information). + + +*sigma.pathfinding.astar.js* is a plugin for [sigma.js](http://sigmajs.org) that computes path in a graph +using a naive implementation of the [A*](http://en.wikipedia.org/wiki/A*_search_algorithm) algorithm. + +## Usage + +Either download a tarball, `git clone` the repository or `npm install` it. Then it's pretty straight-forward. + +It adds a method to your `sigma.graph` called `astar(srcId, destId[, options])`. +- `srcId`, identifier of the start node; +- `destId`, identification of the destination node; +- `options` (optional), an object containing one or more of those properties: + - `undirected` (default: `false`), if set to `true`, consider the graph as non-oriented (will explore all edges, including the inbound ones); + - `pathLengthFunction` (default is the geometrical distance), a `function(node1, node2, previousLength)` that should return the new path length between the start node and `node2`, knowing that the path length between the start node and `node1` is contained in `previousLength`. + - `heuristicLengthFunction` (default: `undefined`), a `function(node1, node2)` guesses the path length between `node1` and `node2` (`node2` actually is the destination node). If left undefined, takes the `pathLengthFunction` option (third parameter will be left undefined). + +Return value is either: +- `undefined`: no path could be found between the source node and the destination node; +- `[srcNode, …, destNode ]`: an array of nodes, including source and destination node. diff --git a/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/sigma.pathfinding.astar.js b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/sigma.pathfinding.astar.js new file mode 100644 index 0000000..47d3d09 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.pathfinding.astar/sigma.pathfinding.astar.js @@ -0,0 +1,134 @@ +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') { + throw 'sigma is not declared'; + } + + // Default function to compute path length between two nodes: + // the euclidian + var defaultPathLengthFunction = function(node1, node2, previousPathLength) { + var isEverythingDefined = + node1 != undefined && + node2 != undefined && + node1.x != undefined && + node1.y != undefined && + node2.x != undefined && + node2.y != undefined; + if(!isEverythingDefined) { + return undefined; + } + + return (previousPathLength || 0) + Math.sqrt( + Math.pow((node2.y - node1.y), 2) + Math.pow((node2.x - node1.x), 2) + ); + }; + + sigma.classes.graph.addMethod( + 'astar', + function(srcId, destId, settings) { + var currentSettings = new sigma.classes.configurable( + // Default settings + { + // Graph is directed, affects which edges are taken into account + undirected: false, + // Function to compute the distance between two connected node + pathLengthFunction: defaultPathLengthFunction, + // Function to compute an distance between two nodes + // if undefined, uses pathLengthFunction + heuristicLengthFunction: undefined + }, + settings || {}); + + var pathLengthFunction = currentSettings("pathLengthFunction"), + heuristicLengthFunction = currentSettings("heuristicLengthFunction") || pathLengthFunction; + + var srcNode = this.nodes(srcId), + destNode = this.nodes(destId); + + var closedList = {}, + openList = []; + + var addToLists = function(node, previousNode, pathLength, heuristicLength) { + var nodeId = node.id; + var newItem = { + pathLength: pathLength, + heuristicLength: heuristicLength, + node: node, + nodeId: nodeId, + previousNode: previousNode + }; + + if(closedList[nodeId] == undefined || closedList[nodeId].pathLength > pathLength) { + closedList[nodeId] = newItem; + + var item; + var i; + for(i = 0; i < openList.length; i++) { + item = openList[i]; + if(item.heuristicLength > heuristicLength) { + break; + } + } + + openList.splice(i, 0, newItem); + } + }; + + addToLists(srcNode, null, 0, 0); + + var pathFound = false; + + // Depending of the type of graph (directed or not), + // the neighbors are either the out neighbors or all. + var allNeighbors; + if(currentSettings("undirected")) { + allNeighbors = this.allNeighborsIndex; + } + else { + allNeighbors = this.outNeighborsIndex; + } + + + var inspectedItem, + neighbors, + neighbor, + pathLength, + heuristicLength, + i; + while(openList.length > 0) { + inspectedItem = openList.shift(); + + // We reached the destination node + if(inspectedItem.nodeId == destId) { + pathFound = true; + break; + } + + neighbors = Object.keys(allNeighbors[inspectedItem.nodeId]); + for(i = 0; i < neighbors.length; i++) { + neighbor = this.nodes(neighbors[i]); + pathLength = pathLengthFunction(inspectedItem.node, neighbor, inspectedItem.pathLength); + heuristicLength = heuristicLengthFunction(neighbor, destNode); + addToLists(neighbor, inspectedItem.node, pathLength, heuristicLength); + } + } + + if(pathFound) { + // Rebuilding path + var path = [], + currentNode = destNode; + + while(currentNode) { + path.unshift(currentNode); + currentNode = closedList[currentNode.id].previousNode; + } + + return path; + } + else { + return undefined; + } + } + ); +}).call(window); diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.animate/README.md b/blogContent/projects/steam/src/plugins/sigma.plugins.animate/README.md new file mode 100644 index 0000000..57ece93 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.animate/README.md @@ -0,0 +1,71 @@ +sigma.plugins.animate +===================== + +Plugin developed by [Alexis Jacomy](https://github.com/jacomyal). + +--- + +This plugin provides a method to animate a sigma instance by interpolating some node properties. Check the `sigma.plugins.animate` function doc or the `examples/animate.html` code sample to know more. + +Interpolate coordinates as follows: + +```js +sigma.plugins.animate( + s, + { + x: 'target_x', + y: 'target_y', + } + ); +``` + +Interpolate colors and sizes as follows: + +```js +sigma.plugins.animate( + s, + { + size: 'target_size', + color: 'target_color' + } + ); +``` + +Animate a subset of nodes as follows: + +```js +sigma.plugins.animate( + s, + { + x: 'to_x', + y: 'to_y', + size: 'to_size', + color: 'to_color' + }, + { + nodes: ['n0', 'n1', 'n2'] + } + ); +``` + +Example using all options: + +```js +sigma.plugins.animate( + s, + { + x: 'to_x', + y: 'to_y', + size: 'to_size', + color: 'to_color' + }, + { + nodes: ['n0', 'n1', 'n2'], + easing: 'cubicInOut', + duration: 300, + onComplete: function() { + // do stuff here after animation is complete + } + } + ); +``` diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.animate/sigma.plugins.animate.js b/blogContent/projects/steam/src/plugins/sigma.plugins.animate/sigma.plugins.animate.js new file mode 100644 index 0000000..763463f --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.animate/sigma.plugins.animate.js @@ -0,0 +1,204 @@ +/** + * This plugin provides a method to animate a sigma instance by interpolating + * some node properties. Check the sigma.plugins.animate function doc or the + * examples/animate.html code sample to know more. + */ +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.utils.pkg('sigma.plugins'); + + var _id = 0, + _cache = {}; + + // TOOLING FUNCTIONS: + // ****************** + function parseColor(val) { + if (_cache[val]) + return _cache[val]; + + var result = [0, 0, 0]; + + if (val.match(/^#/)) { + val = (val || '').replace(/^#/, ''); + result = (val.length === 3) ? + [ + parseInt(val.charAt(0) + val.charAt(0), 16), + parseInt(val.charAt(1) + val.charAt(1), 16), + parseInt(val.charAt(2) + val.charAt(2), 16) + ] : + [ + parseInt(val.charAt(0) + val.charAt(1), 16), + parseInt(val.charAt(2) + val.charAt(3), 16), + parseInt(val.charAt(4) + val.charAt(5), 16) + ]; + } else if (val.match(/^ *rgba? *\(/)) { + val = val.match( + /^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.*)?\) *$/ + ); + result = [ + +val[1], + +val[2], + +val[3] + ]; + } + + _cache[val] = { + r: result[0], + g: result[1], + b: result[2] + }; + + return _cache[val]; + } + + function interpolateColors(c1, c2, p) { + c1 = parseColor(c1); + c2 = parseColor(c2); + + var c = { + r: c1.r * (1 - p) + c2.r * p, + g: c1.g * (1 - p) + c2.g * p, + b: c1.b * (1 - p) + c2.b * p + }; + + return 'rgb(' + [c.r | 0, c.g | 0, c.b | 0].join(',') + ')'; + } + + /** + * This function will animate some specified node properties. It will + * basically call requestAnimationFrame, interpolate the values and call the + * refresh method during a specified duration. + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters in the settings + * object: + * + * {?array} nodes An array of node objects or node ids. If + * not specified, all nodes of the graph + * will be animated. + * {?(function|string)} easing Either the name of an easing in the + * sigma.utils.easings package or a + * function. If not specified, the + * quadraticInOut easing from this package + * will be used instead. + * {?number} duration The duration of the animation. If not + * specified, the "animationsTime" setting + * value of the sigma instance will be used + * instead. + * {?function} onComplete Eventually a function to call when the + * animation is ended. + * + * @param {sigma} s The related sigma instance. + * @param {object} animate An hash with the keys being the node properties + * to interpolate, and the values being the related + * target values. + * @param {?object} options Eventually an object with options. + */ + sigma.plugins.animate = function(s, animate, options) { + var o = options || {}, + id = ++_id, + duration = o.duration || s.settings('animationsTime'), + easing = typeof o.easing === 'string' ? + sigma.utils.easings[o.easing] : + typeof o.easing === 'function' ? + o.easing : + sigma.utils.easings.quadraticInOut, + start = sigma.utils.dateNow(), + nodes, + startPositions; + + if (o.nodes && o.nodes.length) { + if (typeof o.nodes[0] === 'object') + nodes = o.nodes; + else + nodes = s.graph.nodes(o.nodes); // argument is an array of IDs + } + else + nodes = s.graph.nodes(); + + // Store initial positions: + startPositions = nodes.reduce(function(res, node) { + var k; + res[node.id] = {}; + for (k in animate) + if (k in node) + res[node.id][k] = node[k]; + return res; + }, {}); + + s.animations = s.animations || Object.create({}); + sigma.plugins.kill(s); + + // Do not refresh edgequadtree during drag: + var k, + c; + for (k in s.cameras) { + c = s.cameras[k]; + c.edgequadtree._enabled = false; + } + + function step() { + var p = (sigma.utils.dateNow() - start) / duration; + + if (p >= 1) { + nodes.forEach(function(node) { + for (var k in animate) + if (k in animate) + node[k] = node[animate[k]]; + }); + + // Allow to refresh edgequadtree: + var k, + c; + for (k in s.cameras) { + c = s.cameras[k]; + c.edgequadtree._enabled = true; + } + + s.refresh(); + if (typeof o.onComplete === 'function') + o.onComplete(); + } else { + p = easing(p); + nodes.forEach(function(node) { + for (var k in animate) + if (k in animate) { + if (k.match(/color$/)) + node[k] = interpolateColors( + startPositions[node.id][k], + node[animate[k]], + p + ); + else + node[k] = + node[animate[k]] * p + + startPositions[node.id][k] * (1 - p); + } + }); + + s.refresh(); + s.animations[id] = requestAnimationFrame(step); + } + } + + step(); + }; + + sigma.plugins.kill = function(s) { + for (var k in (s.animations || {})) + cancelAnimationFrame(s.animations[k]); + + // Allow to refresh edgequadtree: + var k, + c; + for (k in s.cameras) { + c = s.cameras[k]; + c.edgequadtree._enabled = true; + } + }; +}).call(window); diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/README.md b/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/README.md new file mode 100644 index 0000000..3ed734d --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/README.md @@ -0,0 +1,36 @@ +sigma.plugins.dragNodes +===================== + +Plugin developed by [José M. Camacho](https://github.com/josemazo), events by [Sébastien Heymann](https://github.com/sheymann) for [Linkurious](https://github.com/Linkurious). + +--- + +This plugin provides a method to drag & drop nodes. At the moment, this plugin is not compatible with the WebGL renderer. Check the sigma.plugins.dragNodes function doc or the [example code](../../examples/drag-nodes.html) to know more. + +To use, include all .js files under this folder. Then initialize it as follows: + +````javascript +var dragListener = new sigma.plugins.dragNodes(sigInst, renderer); +```` + +Kill the plugin as follows: + +````javascript +sigma.plugins.killDragNodes(sigInst); +```` + +## Events + +This plugin provides the following events fired by the instance of the plugin: +* `startdrag`: fired at the beginning of the drag +* `drag`: fired while the node is dragged +* `drop`: fired at the end of the drag if the node has been dragged +* `dragend`: fired at the end of the drag + +Exemple of event binding: + +````javascript +dragListener.bind('startdrag', function(event) { + console.log(event); +}); +```` diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/sigma.plugins.dragNodes.js b/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/sigma.plugins.dragNodes.js new file mode 100644 index 0000000..fd0ed90 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.dragNodes/sigma.plugins.dragNodes.js @@ -0,0 +1,326 @@ +/** + * This plugin provides a method to drag & drop nodes. Check the + * sigma.plugins.dragNodes function doc or the examples/basic.html & + * examples/api-candy.html code samples to know more. + */ +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.utils.pkg('sigma.plugins'); + + + /** + * This function will add `mousedown`, `mouseup` & `mousemove` events to the + * nodes in the `overNode`event to perform drag & drop operations. It uses + * `linear interpolation` [http://en.wikipedia.org/wiki/Linear_interpolation] + * and `rotation matrix` [http://en.wikipedia.org/wiki/Rotation_matrix] to + * calculate the X and Y coordinates from the `cam` or `renderer` node + * attributes. These attributes represent the coordinates of the nodes in + * the real container, not in canvas. + * + * Fired events: + * ************* + * startdrag Fired at the beginning of the drag. + * drag Fired while the node is dragged. + * drop Fired at the end of the drag if the node has been dragged. + * dragend Fired at the end of the drag. + * + * Recognized parameters: + * ********************** + * @param {sigma} s The related sigma instance. + * @param {renderer} renderer The related renderer instance. + */ + function DragNodes(s, renderer) { + sigma.classes.dispatcher.extend(this); + + // A quick hardcoded rule to prevent people from using this plugin with the + // WebGL renderer (which is impossible at the moment): + // if ( + // sigma.renderers.webgl && + // renderer instanceof sigma.renderers.webgl + // ) + // throw new Error( + // 'The sigma.plugins.dragNodes is not compatible with the WebGL renderer' + // ); + + // Init variables: + var _self = this, + _s = s, + _body = document.body, + _renderer = renderer, + _mouse = renderer.container.lastChild, + _camera = renderer.camera, + _node = null, + _prefix = '', + _hoverStack = [], + _hoverIndex = {}, + _isMouseDown = false, + _isMouseOverCanvas = false, + _drag = false; + + if (renderer instanceof sigma.renderers.svg) { + _mouse = renderer.container.firstChild; + } + + // It removes the initial substring ('read_') if it's a WegGL renderer. + if (renderer instanceof sigma.renderers.webgl) { + _prefix = renderer.options.prefix.substr(5); + } else { + _prefix = renderer.options.prefix; + } + + renderer.bind('overNode', nodeMouseOver); + renderer.bind('outNode', treatOutNode); + renderer.bind('click', click); + + _s.bind('kill', function() { + _self.unbindAll(); + }); + + /** + * Unbind all event listeners. + */ + this.unbindAll = function() { + _mouse.removeEventListener('mousedown', nodeMouseDown); + _body.removeEventListener('mousemove', nodeMouseMove); + _body.removeEventListener('mouseup', nodeMouseUp); + _renderer.unbind('overNode', nodeMouseOver); + _renderer.unbind('outNode', treatOutNode); + } + + // Calculates the global offset of the given element more accurately than + // element.offsetTop and element.offsetLeft. + function calculateOffset(element) { + var style = window.getComputedStyle(element); + var getCssProperty = function(prop) { + return parseInt(style.getPropertyValue(prop).replace('px', '')) || 0; + }; + return { + left: element.getBoundingClientRect().left + getCssProperty('padding-left'), + top: element.getBoundingClientRect().top + getCssProperty('padding-top') + }; + }; + + function click(event) { + // event triggered at the end of the click + _isMouseDown = false; + _body.removeEventListener('mousemove', nodeMouseMove); + _body.removeEventListener('mouseup', nodeMouseUp); + + if (!_hoverStack.length) { + _node = null; + } + }; + + function nodeMouseOver(event) { + // Don't treat the node if it is already registered + if (_hoverIndex[event.data.node.id]) { + return; + } + + // Add node to array of current nodes over + _hoverStack.push(event.data.node); + _hoverIndex[event.data.node.id] = true; + + if(_hoverStack.length && ! _isMouseDown) { + // Set the current node to be the last one in the array + _node = _hoverStack[_hoverStack.length - 1]; + _mouse.addEventListener('mousedown', nodeMouseDown); + } + }; + + function treatOutNode(event) { + // Remove the node from the array + var indexCheck = _hoverStack.map(function(e) { return e; }).indexOf(event.data.node); + _hoverStack.splice(indexCheck, 1); + delete _hoverIndex[event.data.node.id]; + + if(_hoverStack.length && ! _isMouseDown) { + // On out, set the current node to be the next stated in array + _node = _hoverStack[_hoverStack.length - 1]; + } else { + _mouse.removeEventListener('mousedown', nodeMouseDown); + } + }; + + function nodeMouseDown(event) { + _isMouseDown = true; + var size = _s.graph.nodes().length; + + // when there is only node in the graph, the plugin cannot apply + // linear interpolation. So treat it as if a user is dragging + // the graph + if (_node && size > 1) { + _mouse.removeEventListener('mousedown', nodeMouseDown); + _body.addEventListener('mousemove', nodeMouseMove); + _body.addEventListener('mouseup', nodeMouseUp); + + // Do not refresh edgequadtree during drag: + var k, + c; + for (k in _s.cameras) { + c = _s.cameras[k]; + if (c.edgequadtree !== undefined) { + c.edgequadtree._enabled = false; + } + } + + // Deactivate drag graph. + _renderer.settings({mouseEnabled: false, enableHovering: false}); + _s.refresh(); + + _self.dispatchEvent('startdrag', { + node: _node, + captor: event, + renderer: _renderer + }); + } + }; + + function nodeMouseUp(event) { + _isMouseDown = false; + _mouse.addEventListener('mousedown', nodeMouseDown); + _body.removeEventListener('mousemove', nodeMouseMove); + _body.removeEventListener('mouseup', nodeMouseUp); + + // Allow to refresh edgequadtree: + var k, + c; + for (k in _s.cameras) { + c = _s.cameras[k]; + if (c.edgequadtree !== undefined) { + c.edgequadtree._enabled = true; + } + } + + // Activate drag graph. + _renderer.settings({mouseEnabled: true, enableHovering: true}); + _s.refresh(); + + if (_drag) { + _self.dispatchEvent('drop', { + node: _node, + captor: event, + renderer: _renderer + }); + } + _self.dispatchEvent('dragend', { + node: _node, + captor: event, + renderer: _renderer + }); + + _drag = false; + _node = null; + }; + + function nodeMouseMove(event) { + if(navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { + clearTimeout(timeOut); + var timeOut = setTimeout(executeNodeMouseMove, 0); + } else { + executeNodeMouseMove(); + } + + function executeNodeMouseMove() { + var offset = calculateOffset(_renderer.container), + x = event.clientX - offset.left, + y = event.clientY - offset.top, + cos = Math.cos(_camera.angle), + sin = Math.sin(_camera.angle), + nodes = _s.graph.nodes(), + ref = []; + + // Getting and derotating the reference coordinates. + for (var i = 0; i < 2; i++) { + var n = nodes[i]; + var aux = { + x: n.x * cos + n.y * sin, + y: n.y * cos - n.x * sin, + renX: n[_prefix + 'x'], + renY: n[_prefix + 'y'], + }; + ref.push(aux); + } + + // Applying linear interpolation. + // if the nodes are on top of each other, we use the camera ratio to interpolate + if (ref[0].x === ref[1].x && ref[0].y === ref[1].y) { + var xRatio = (ref[0].renX === 0) ? 1 : ref[0].renX; + var yRatio = (ref[0].renY === 0) ? 1 : ref[0].renY; + x = (ref[0].x / xRatio) * (x - ref[0].renX) + ref[0].x; + y = (ref[0].y / yRatio) * (y - ref[0].renY) + ref[0].y; + } else { + var xRatio = (ref[1].renX - ref[0].renX) / (ref[1].x - ref[0].x); + var yRatio = (ref[1].renY - ref[0].renY) / (ref[1].y - ref[0].y); + + // if the coordinates are the same, we use the other ratio to interpolate + if (ref[1].x === ref[0].x) { + xRatio = yRatio; + } + + if (ref[1].y === ref[0].y) { + yRatio = xRatio; + } + + x = (x - ref[0].renX) / xRatio + ref[0].x; + y = (y - ref[0].renY) / yRatio + ref[0].y; + } + + // Rotating the coordinates. + _node.x = x * cos - y * sin; + _node.y = y * cos + x * sin; + + _s.refresh(); + + _drag = true; + _self.dispatchEvent('drag', { + node: _node, + captor: event, + renderer: _renderer + }); + } + }; + }; + + /** + * Interface + * ------------------ + * + * > var dragNodesListener = sigma.plugins.dragNodes(s, s.renderers[0]); + */ + var _instance = {}; + + /** + * @param {sigma} s The related sigma instance. + * @param {renderer} renderer The related renderer instance. + */ + sigma.plugins.dragNodes = function(s, renderer) { + // Create object if undefined + if (!_instance[s.id]) { + _instance[s.id] = new DragNodes(s, renderer); + } + + s.bind('kill', function() { + sigma.plugins.killDragNodes(s); + }); + + return _instance[s.id]; + }; + + /** + * This method removes the event listeners and kills the dragNodes instance. + * + * @param {sigma} s The related sigma instance. + */ + sigma.plugins.killDragNodes = function(s) { + if (_instance[s.id] instanceof DragNodes) { + _instance[s.id].unbindAll(); + delete _instance[s.id]; + } + }; + +}).call(window); diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.filter/README.md b/blogContent/projects/steam/src/plugins/sigma.plugins.filter/README.md new file mode 100644 index 0000000..acbba72 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.filter/README.md @@ -0,0 +1,187 @@ +sigma.plugins.filter +================== + +Plugin developed by [Sébastien Heymann](sheymann) for [Linkurious](https://github.com/Linkurious). + +--- +## General +This plugin filters nodes and edges in a fancy manner: +- Define your own filters on nodes and edges using the `nodesBy` and `edgesBy` methods, or execute more complex filters using the `neighborsOf` method. +- Register multiple filters before applying them anytime at once. +- Undo any filter while preserving the execution order. +- Chain all methods for concise style. + +See the following [example code](../../examples/filters.html) and [unit tests](../../test/unit.plugins.filter.js) for full usage. + +To use, include all .js files under this folder. Then initialize it as follows: + +````javascript +var filter = new sigma.plugins.filter(sigInst); +```` + +## Predicates +Predicates are truth tests (i.e. functions which return a boolean) on a single node or a single edge. They return true if the element should be visible. For instance: + +````javascript +// Only edges of size above one should be visible: +function(e) { + return e.size > 1; +} +```` + +In this example, notice that if the size attribute is undefined, the edge will be hidden. If you still want to display edges with no size attribute defined, you have to modify the predicate a bit: + +````javascript +// Only edges of size above one should be visible: +function(e) { + return e.size === undefined || e.size > 1; +} +```` + +Predicates are applied by predicate processors. + +## Predicate processors +Predicate processors are functions which wrap one predicate and apply it to the graph. Three predicate processors are available: +- `nodesBy` +- `edgesBy` +- `neighborsOf` + +For each node of the graph, the `nodesBy` processor sets the attribute `hidden` to false if the predicate is true for the node. It also sets the `hidden` attribute of edges to true if one of the edge's extremities is hidden. For instance: + +````javascript +// Only connected nodes (i.e. nodes of positive degree) should be visible: +filter.nodesBy(function(n) { + return this.degree(n.id) > 0; +}, 'non-isolates'); +```` + +For each edge of the graph, the `edgesBy` processor sets the attribute `hidden` to false if the predicate is true for the edge. For instance: + +````javascript +// Only edges of size above one should be visible: +filter.edgesBy(function(e) { + return e.size > 1; +}, 'edge-size-above-one'); +```` + +For each neighbor node of a specified node, the `neighborsOf` processor sets the attribute `hidden` to true if it is not directly connected to the node. It also sets the `hidden` attribute of edges to true if one of the edge's extremities is hidden. For instance: + +````javascript +// Only neighbors of the node 'n0' should be visible: +filter.neighborsOf('n0'); +```` + +Processors instanciated with a predicate are called filters. **Filters are not applied until the `apply` method is called.** + +## Filters chain +Combining filters is easy! Declare one filter after another, then call the `apply` method to execute them on the graph in that order. For instance: + +````javascript +// graph = { +// nodes: [{id:'n0'}, {id:'n1'}, {id:'n2'}, {id:'n3'}], +// edges: [ +// {id:'e0', source:'n0', target:'n1', size:1}, +// {id:'e1', source:'n1', target:'n2', size:0.5}, +// {id:'e2', source:'n1', target:'n2'}] +// } +filter + .nodesBy(function(n) { + return this.degree(n.id) > 0; + }) + .edgesBy(function(e) { + return e.size >= 1; + }) + .apply(); +// n3.hidden == true +// e1.hidden == true +// e2.hidden == true +```` + +Combined filters work like if there was an 'AND' operator between them. Be careful not to create mutually exclusive filters, for instance: + +````javascript +filter + .nodesBy(function(n) { + return n.attributes.animal === 'pony'; + }) + .nodesBy(function(n) { + return n.attributes.animal !== 'pony'; + }) + .apply(); +// all nodes are hidden +```` + +Filters are internally stored in an array called the `chain`. + +## Undo filters +Undoing filters means to remove them from the `chain`. Filters can be undone easily. Choose which filter(s) to undo, or undo all of them at once. + +Filters can be associated with keys at declaration, where keys are any string you give. For instance, the following filter has the key *node-animal*: + +````javascript +filter.nodesBy(function(n) { + return n.attributes.animal === 'pony'; +}, 'node-animal'); +```` + +Manually undo this filter as follows: + +````javascript +filter + .undo('node-animal') + .apply(); // we want it applied now +```` + +Multiple filters can be undone at once, for instance: + +````javascript +filter.undo('node-animal', 'edge-size', 'high-node-degree'); +// don't forget to call `apply()` anytime! +```` + +Alternative syntax: + +````javascript +var a = ['node-animal', 'edge-size', 'high-node-degree']; +filter.undo(a); +// don't forget to call `apply()` anytime! +```` + +Finally, undo all filters (with or without keys) as follows: + +````javascript +filter.undo(); +// don't forget to call `apply()` anytime! +```` + +Warning: you can't declare two filters with the same key, or it will throw an exception. + +## Export the chain +The exported chain is an array of objects. Each object represents a filter by a triplet *(?key, processor, predicate)*. The processor value is the internal name of the processor: `filter.processors.nodes`, `filter.processors.edges`, `filter.processors.neighbors`. The predicate value is a copy of the predicate function. Dump the `chain` using the `export` method as follows: + +````javascript +var chain = filter.export(); +// chain == [ +// { +// key: '...', +// processor: '...', +// predicate: function() {...} +// }, ... +// ] +```` + +## Import a chain +You can load a filters chain using the `import` method: + +````javascript +var chain = [ + { + key: 'my-filter', + predicate: function(n) { return this.degree(n.id) > 0; }, + processor: 'filter.processors.nodes' + } +]; +filter + .import(chain) + .apply(); +```` diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.filter/sigma.plugins.filter.js b/blogContent/projects/steam/src/plugins/sigma.plugins.filter/sigma.plugins.filter.js new file mode 100644 index 0000000..c3f4c4e --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.filter/sigma.plugins.filter.js @@ -0,0 +1,504 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize package: + sigma.utils.pkg('sigma.plugins'); + + // Add custom graph methods: + /** + * This methods returns an array of nodes that are adjacent to a node. + * + * @param {string} id The node id. + * @return {array} The array of adjacent nodes. + */ + if (!sigma.classes.graph.hasMethod('adjacentNodes')) + sigma.classes.graph.addMethod('adjacentNodes', function(id) { + if (typeof id !== 'string') + throw 'adjacentNodes: the node id must be a string.'; + + var target, + nodes = []; + for(target in this.allNeighborsIndex[id]) { + nodes.push(this.nodesIndex[target]); + } + return nodes; + }); + + /** + * This methods returns an array of edges that are adjacent to a node. + * + * @param {string} id The node id. + * @return {array} The array of adjacent edges. + */ + if (!sigma.classes.graph.hasMethod('adjacentEdges')) + sigma.classes.graph.addMethod('adjacentEdges', function(id) { + if (typeof id !== 'string') + throw 'adjacentEdges: the node id must be a string.'; + + var a = this.allNeighborsIndex[id], + eid, + target, + edges = []; + for(target in a) { + for(eid in a[target]) { + edges.push(a[target][eid]); + } + } + return edges; + }); + + /** + * Sigma Filter + * ============================= + * + * @author Sébastien Heymann (Linkurious) + * @version 0.1 + */ + + var _g = undefined, + _s = undefined, + _chain = [], // chain of wrapped filters + _keysIndex = Object.create(null), + Processors = {}; // available predicate processors + + + /** + * Library of processors + * ------------------ + */ + + /** + * + * @param {function} fn The predicate. + */ + Processors.nodes = function nodes(fn) { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length; + + // hide node, or keep former value + while(ln--) + n[ln].hidden = !fn.call(_g, n[ln]) || n[ln].hidden; + + while(le--) + if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) + e[le].hidden = true; + }; + + /** + * + * @param {function} fn The predicate. + */ + Processors.edges = function edges(fn) { + var e = _g.edges(), + le = e.length; + + // hide edge, or keep former value + while(le--) + e[le].hidden = !fn.call(_g, e[le]) || e[le].hidden; + }; + + /** + * + * @param {string} id The center node. + */ + Processors.neighbors = function neighbors(id) { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length, + neighbors = _g.adjacentNodes(id), + nn = neighbors.length, + no = {}; + + while(nn--) + no[neighbors[nn].id] = true; + + while(ln--) + if (n[ln].id !== id && !(n[ln].id in no)) + n[ln].hidden = true; + + while(le--) + if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden) + e[le].hidden = true; + }; + + + /** + * This function adds a filter to the chain of filters. + * + * @param {function} fn The filter (i.e. predicate processor). + * @param {function} p The predicate. + * @param {?string} key The key to identify the filter. + */ + function register(fn, p, key) { + if (key != undefined && typeof key !== 'string') + throw 'The filter key "'+ key.toString() +'" must be a string.'; + + if (key != undefined && !key.length) + throw 'The filter key must be a non-empty string.'; + + if (typeof fn !== 'function') + throw 'The predicate of key "'+ key +'" must be a function.'; + + if ('undo' === key) + throw '"undo" is a reserved key.'; + + if (_keysIndex[key]) + throw 'The filter "' + key + '" already exists.'; + + if (key) + _keysIndex[key] = true; + + _chain.push({ + 'key': key, + 'processor': fn, + 'predicate': p + }); + }; + + /** + * This function removes a set of filters from the chain. + * + * @param {object} o The filter keys. + */ + function unregister (o) { + _chain = _chain.filter(function(a) { + return !(a.key in o); + }); + + for(var key in o) + delete _keysIndex[key]; + }; + + + + + /** + * Filter Object + * ------------------ + * @param {sigma} s The related sigma instance. + */ + function Filter(s) { + _s = s; + _g = s.graph; + }; + + + /** + * This method is used to filter the nodes. The method must be called with + * the predicate, which is a function that takes a node as argument and + * returns a boolean. It may take an identifier as argument to undo the + * filter later. The method wraps the predicate into an anonymous function + * that looks through each node in the graph. When executed, the anonymous + * function hides the nodes that fail a truth test (predicate). The method + * adds the anonymous function to the chain of filters. The filter is not + * executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull'); + * + * @param {function} fn The filter predicate. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.nodesBy = function(fn, key) { + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.nodes, fn, key); + + return this; + }; + + /** + * This method is used to filter the edges. The method must be called with + * the predicate, which is a function that takes a node as argument and + * returns a boolean. It may take an identifier as argument to undo the + * filter later. The method wraps the predicate into an anonymous function + * that looks through each edge in the graph. When executed, the anonymous + * function hides the edges that fail a truth test (predicate). The method + * adds the anonymous function to the chain of filters. The filter is not + * executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.edgesBy(function(e) { + * > return e.size > 1; + * > }, 'edgeSize'); + * + * @param {function} fn The filter predicate. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.edgesBy = function(fn, key) { + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.edges, fn, key); + + return this; + }; + + /** + * This method is used to filter the nodes which are not direct connections + * of a given node. The method must be called with the node identifier. It + * may take an identifier as argument to undo the filter later. The filter + * is not executed until the apply() method is called. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.neighborsOf('n0'); + * + * @param {string} id The node id. + * @param {?string} key The key to identify the filter. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.neighborsOf = function(id, key) { + if (typeof id !== 'string') + throw 'The node id "'+ id.toString() +'" must be a string.'; + if (!id.length) + throw 'The node id must be a non-empty string.'; + + // Wrap the predicate to be applied on the graph and add it to the chain. + register(Processors.neighbors, id, key); + + return this; + }; + + /** + * This method is used to execute the chain of filters and to refresh the + * display. + * + * > var filter = new sigma.plugins.filter(s); + * > filter + * > .nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull') + * > .apply(); + * + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.apply = function() { + for (var i = 0, len = _chain.length; i < len; ++i) { + _chain[i].processor(_chain[i].predicate); + }; + + if (_chain[0] && 'undo' === _chain[0].key) { + _chain.shift(); + } + + _s.refresh(); + + return this; + }; + + /** + * This method undoes one or several filters, depending on how it is called. + * + * To undo all filters, call "undo" without argument. To undo a specific + * filter, call it with the key of the filter. To undo multiple filters, call + * it with an array of keys or multiple arguments, and it will undo each + * filter, in the same order. The undo is not executed until the apply() + * method is called. For instance: + * + * > var filter = new sigma.plugins.filter(s); + * > filter + * > .nodesBy(function(n) { + * > return this.degree(n.id) > 0; + * > }, 'degreeNotNull'); + * > .edgesBy(function(e) { + * > return e.size > 1; + * > }, 'edgeSize') + * > .undo(); + * + * Other examples: + * > filter.undo(); + * > filter.undo('myfilter'); + * > filter.undo(['myfilter1', 'myfilter2']); + * > filter.undo('myfilter1', 'myfilter2'); + * + * @param {?(string|array|*string))} v Eventually one key, an array of keys. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.undo = function(v) { + var q = Object.create(null), + la = arguments.length; + + // find removable filters + if (la === 1) { + if (Object.prototype.toString.call(v) === '[object Array]') + for (var i = 0, len = v.length; i < len; i++) + q[v[i]] = true; + + else // 1 filter key + q[v] = true; + + } else if (la > 1) { + for (var i = 0; i < la; i++) + q[arguments[i]] = true; + } + else + this.clear(); + + unregister(q); + + function processor() { + var n = _g.nodes(), + ln = n.length, + e = _g.edges(), + le = e.length; + + while(ln--) + n[ln].hidden = false; + + while(le--) + e[le].hidden = false; + }; + + _chain.unshift({ + 'key': 'undo', + 'processor': processor + }); + + return this; + }; + + // fast deep copy function + function deepCopy(o) { + var copy = Object.create(null); + for (var i in o) { + if (typeof o[i] === "object" && o[i] !== null) { + copy[i] = deepCopy(o[i]); + } + else if (typeof o[i] === "function" && o[i] !== null) { + // clone function: + eval(" copy[i] = " + o[i].toString()); + //copy[i] = o[i].bind(_g); + } + + else + copy[i] = o[i]; + } + return copy; + }; + + function cloneChain(chain) { + // Clone the array of filters: + var copy = chain.slice(0); + for (var i = 0, len = copy.length; i < len; i++) { + copy[i] = deepCopy(copy[i]); + if (typeof copy[i].processor === "function") + copy[i].processor = 'filter.processors.' + copy[i].processor.name; + }; + return copy; + } + + /** + * This method is used to empty the chain of filters. + * Prefer the undo() method to reset filters. + * + * > var filter = new sigma.plugins.filter(s); + * > filter.clear(); + * + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.clear = function() { + _chain.length = 0; // clear the array + _keysIndex = Object.create(null); + return this; + }; + + /** + * This method clones the filter chain and return the copy. + * + * > var filter = new sigma.plugins.filter(s); + * > var chain = filter.export(); + * + * @return {object} The cloned chain of filters. + */ + Filter.prototype.export = function() { + var c = cloneChain(_chain); + return c; + }; + + /** + * This method sets the chain of filters with the specified chain. + * + * > var filter = new sigma.plugins.filter(s); + * > var chain = [ + * > { + * > key: 'my-filter', + * > predicate: function(n) {...}, + * > processor: 'filter.processors.nodes' + * > }, ... + * > ]; + * > filter.import(chain); + * + * @param {array} chain The chain of filters. + * @return {sigma.plugins.filter} Returns the instance. + */ + Filter.prototype.import = function(chain) { + if (chain === undefined) + throw 'Wrong arguments.'; + + if (Object.prototype.toString.call(chain) !== '[object Array]') + throw 'The chain" must be an array.'; + + var copy = cloneChain(chain); + + for (var i = 0, len = copy.length; i < len; i++) { + if (copy[i].predicate === undefined || copy[i].processor === undefined) + throw 'Wrong arguments.'; + + if (copy[i].key != undefined && typeof copy[i].key !== 'string') + throw 'The filter key "'+ copy[i].key.toString() +'" must be a string.'; + + if (typeof copy[i].predicate !== 'function') + throw 'The predicate of key "'+ copy[i].key +'" must be a function.'; + + if (typeof copy[i].processor !== 'string') + throw 'The processor of key "'+ copy[i].key +'" must be a string.'; + + // Replace the processor name by the corresponding function: + switch(copy[i].processor) { + case 'filter.processors.nodes': + copy[i].processor = Processors.nodes; + break; + case 'filter.processors.edges': + copy[i].processor = Processors.edges; + break; + case 'filter.processors.neighbors': + copy[i].processor = Processors.neighbors; + break; + default: + throw 'Unknown processor ' + copy[i].processor; + } + }; + + _chain = copy; + + return this; + }; + + + /** + * Interface + * ------------------ + * + * > var filter = new sigma.plugins.filter(s); + */ + var filter = null; + + /** + * @param {sigma} s The related sigma instance. + */ + sigma.plugins.filter = function(s) { + // Create filter if undefined + if (!filter) { + filter = new Filter(s); + } + return filter; + }; + +}).call(this); diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/README.md b/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/README.md new file mode 100644 index 0000000..69ceb99 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/README.md @@ -0,0 +1,24 @@ +sigma.plugins.neighborhood +========================== + +Plugin developed by [Alexis Jacomy](https://github.com/jacomyal). + +--- + +This plugin provides a method to retrieve the neighborhood of a node. Basically, it loads a graph and stores it in a headless `sigma.classes.graph` instance, that you can query to retrieve neighborhoods. + +It is useful for people who want to provide a neighborhoods navigation inside a big graph instead of just displaying it, and without having to deploy an API or the list of every neighborhoods. But please note that this plugin is here as an example of what you can do with the graph model, and do not hesitate to try customizing your navigation through graphs. + +This plugin also adds to the graph model a method called "neighborhood". Check the code for more information. + +Here is how to use it: + +````javascript +var db = new sigma.plugins.neighborhoods(); +db.load('path/to/my/graph.json', function() { + var nodeId = 'anyNodeID'; + mySigmaInstance + .read(db.neighborhood(nodeId)) + .refresh(); +}); +```` \ No newline at end of file diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/sigma.plugins.neighborhoods.js b/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/sigma.plugins.neighborhoods.js new file mode 100644 index 0000000..8c4dd83 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.neighborhoods/sigma.plugins.neighborhoods.js @@ -0,0 +1,186 @@ +/** + * This plugin provides a method to retrieve the neighborhood of a node. + * Basically, it loads a graph and stores it in a headless sigma.classes.graph + * instance, that you can query to retrieve neighborhoods. + * + * It is useful for people who want to provide a neighborhoods navigation + * inside a big graph instead of just displaying it, and without having to + * deploy an API or the list of every neighborhoods. + * + * This plugin also adds to the graph model a method called "neighborhood". + * Check the code for more information. + * + * Here is how to use it: + * + * > var db = new sigma.plugins.neighborhoods(); + * > db.load('path/to/my/graph.json', function() { + * > var nodeId = 'anyNodeID'; + * > mySigmaInstance + * > .read(db.neighborhood(nodeId)) + * > .refresh(); + * > }); + */ +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + /** + * This method takes the ID of node as argument and returns the graph of the + * specified node, with every other nodes that are connected to it and every + * edges that connect two of the previously cited nodes. It uses the built-in + * indexes from sigma's graph model to search in the graph. + * + * @param {string} centerId The ID of the center node. + * @return {object} The graph, as a simple descriptive object, in + * the format required by the "read" graph method. + */ + sigma.classes.graph.addMethod( + 'neighborhood', + function(centerId) { + var k1, + k2, + k3, + node, + center, + // Those two local indexes are here just to avoid duplicates: + localNodesIndex = {}, + localEdgesIndex = {}, + // And here is the resulted graph, empty at the moment: + graph = { + nodes: [], + edges: [] + }; + + // Check that the exists: + if (!this.nodes(centerId)) + return graph; + + // Add center. It has to be cloned to add it the "center" attribute + // without altering the current graph: + node = this.nodes(centerId); + center = {}; + center.center = true; + for (k1 in node) + center[k1] = node[k1]; + + localNodesIndex[centerId] = true; + graph.nodes.push(center); + + // Add neighbors and edges between the center and the neighbors: + for (k1 in this.allNeighborsIndex[centerId]) { + if (!localNodesIndex[k1]) { + localNodesIndex[k1] = true; + graph.nodes.push(this.nodesIndex[k1]); + } + + for (k2 in this.allNeighborsIndex[centerId][k1]) + if (!localEdgesIndex[k2]) { + localEdgesIndex[k2] = true; + graph.edges.push(this.edgesIndex[k2]); + } + } + + // Add edges connecting two neighbors: + for (k1 in localNodesIndex) + if (k1 !== centerId) + for (k2 in localNodesIndex) + if ( + k2 !== centerId && + k1 !== k2 && + this.allNeighborsIndex[k1][k2] + ) + for (k3 in this.allNeighborsIndex[k1][k2]) + if (!localEdgesIndex[k3]) { + localEdgesIndex[k3] = true; + graph.edges.push(this.edgesIndex[k3]); + } + + // Finally, let's return the final graph: + return graph; + } + ); + + sigma.utils.pkg('sigma.plugins'); + + /** + * sigma.plugins.neighborhoods constructor. + */ + sigma.plugins.neighborhoods = function() { + var ready = false, + readyCallbacks = [], + graph = new sigma.classes.graph(); + + /** + * This method just returns the neighborhood of a node. + * + * @param {string} centerNodeID The ID of the center node. + * @return {object} Returns the neighborhood. + */ + this.neighborhood = function(centerNodeID) { + return graph.neighborhood(centerNodeID); + }; + + /** + * This method loads the JSON graph at "path", stores it in the local graph + * instance, and executes the callback. + * + * @param {string} path The path of the JSON graph file. + * @param {?function} callback Eventually a callback to execute. + */ + this.load = function(path, callback) { + // Quick XHR polyfill: + var xhr = (function() { + if (window.XMLHttpRequest) + return new XMLHttpRequest(); + + var names, + i; + + if (window.ActiveXObject) { + names = [ + 'Msxml2.XMLHTTP.6.0', + 'Msxml2.XMLHTTP.3.0', + 'Msxml2.XMLHTTP', + 'Microsoft.XMLHTTP' + ]; + + for (i in names) + try { + return new ActiveXObject(names[i]); + } catch (e) {} + } + + return null; + })(); + + if (!xhr) + throw 'XMLHttpRequest not supported, cannot load the data.'; + + xhr.open('GET', path, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + graph.clear().read(JSON.parse(xhr.responseText)); + + if (callback) + callback(); + } + }; + + // Start loading the file: + xhr.send(); + + return this; + }; + + /** + * This method cleans the graph instance "reads" a graph into it. + * + * @param {object} g The graph object to read. + */ + this.read = function(g) { + graph.clear().read(g); + }; + }; +}).call(window); diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/README.md b/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/README.md new file mode 100644 index 0000000..ec287d0 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/README.md @@ -0,0 +1,8 @@ +sigma.plugins.relativeSize +===================== + +Plugin developed by [Anatoliy Stegniy](https://github.com/tsdaemon). + +--- + +This plugin provides a method to change nodes size depending to their degree (number of relationships) diff --git a/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/sigma.plugins.relativeSize.js b/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/sigma.plugins.relativeSize.js new file mode 100644 index 0000000..f56ded0 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.plugins.relativeSize/sigma.plugins.relativeSize.js @@ -0,0 +1,28 @@ +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.utils.pkg('sigma.plugins'); + + var _id = 0, + _cache = {}; + + /** + * This function will change size for all nodes depending to their degree + * + * @param {sigma} s The related sigma instance. + * @param {object} initialSize Start size property + */ + sigma.plugins.relativeSize = function(s, initialSize) { + var nodes = s.graph.nodes(); + + // second create size for every node + for(var i = 0; i < nodes.length; i++) { + var degree = s.graph.degree(nodes[i].id); + nodes[i].size = initialSize * Math.sqrt(degree); + } + s.refresh(); + }; +}).call(window); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/README.md b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/README.md new file mode 100644 index 0000000..9a379b3 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/README.md @@ -0,0 +1,21 @@ +sigma.renderers.customEdgeShapes +================== + +Plugin developed by [Sébastien Heymann](https://github.com/sheymann) for [Linkurious](https://github.com/Linkurious). + +Contact: seb@linkurio.us + +--- +## General +This plugin registers custom edge shape renderers. See the following [example code](../../examples/plugin-customEdgeShapes.html) for full usage. + +To use, include all .js files under this folder. + +## Shapes +The plugin implements the following shapes: + * `dashed` + * `dotted` + * `parallel`: two solid parallel lines representing an edge aggregating multiple edges in the original graph. + * `tapered` (see Danny Holten, Petra Isenberg, Jean-Daniel Fekete, and J. Van Wijk (2010) Performance Evaluation of Tapered, Curved, and Animated Directed-Edge Representations in Node-Link Graphs. Research Report, Sep 2010.) + +To assign a shape renderer to an edge, simply set `edge.type='shape-name'` e.g. `edge.type='dotted'`. The default renderer implemented by sigma.js is named `def` (alias `line`) - see also [generic custom edge renderer example](../../examples/custom-edge-renderer.html). diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dashed.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dashed.js new file mode 100644 index 0000000..ad18000 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dashed.js @@ -0,0 +1,64 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.dashed = + function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + size *= settings('edgeHoverSizeRatio'); + + context.save(); + + context.setLineDash([8,3]); + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dotted.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dotted.js new file mode 100644 index 0000000..6152825 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.dotted.js @@ -0,0 +1,64 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.dotted = + function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + size *= settings('edgeHoverSizeRatio'); + + context.save(); + + context.setLineDash([2]); + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.parallel.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.parallel.js new file mode 100644 index 0000000..011caff --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.parallel.js @@ -0,0 +1,77 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.parallel = + function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + c, + d, + dist = sigma.utils.getDistance(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + size *= settings('edgeHoverSizeRatio'); + + // Intersection points of the source node circle: + c = sigma.utils.getCircleIntersection(sX, sY, size, tX, tY, dist); + + // Intersection points of the target node circle: + d = sigma.utils.getCircleIntersection(tX, tY, size, sX, sY, dist); + + context.save(); + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(c.xi, c.yi); + context.lineTo(d.xi_prime, d.yi_prime); + context.closePath(); + context.stroke(); + + context.beginPath(); + context.moveTo(c.xi_prime, c.yi_prime); + context.lineTo(d.xi, d.yi); + context.closePath(); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.tapered.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.tapered.js new file mode 100644 index 0000000..aa1773a --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edgehovers.tapered.js @@ -0,0 +1,74 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.tapered = + function(edge, source, target, context, settings) { + // The goal is to draw a triangle where the target node is a point of + // the triangle, and the two other points are the intersection of the + // source circle and the circle (target, distance(source, target)). + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + prefix = settings('prefix') || '', + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + dist = sigma.utils.getDistance(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + size *= settings('edgeHoverSizeRatio'); + + // Intersection points: + var c = sigma.utils.getCircleIntersection(sX, sY, size, tX, tY, dist); + + context.save(); + + // Turn transparency on: + context.globalAlpha = 0.65; + + // Draw the triangle: + context.fillStyle = color; + context.beginPath(); + context.moveTo(tX, tY); + context.lineTo(c.xi, c.yi); + context.lineTo(c.xi_prime, c.yi_prime); + context.closePath(); + context.fill(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dashed.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dashed.js new file mode 100644 index 0000000..cbcc31e --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dashed.js @@ -0,0 +1,64 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This method renders the edge as a dashed line. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.dashed = function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.save(); + + if (edge.active) { + context.strokeStyle = settings('edgeActiveColor') === 'edge' ? + (color || defaultEdgeColor) : + settings('defaultEdgeActiveColor'); + } + else { + context.strokeStyle = color; + } + + context.setLineDash([8,3]); + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dotted.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dotted.js new file mode 100644 index 0000000..d82f430 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.dotted.js @@ -0,0 +1,64 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This method renders the edge as a dotted line. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.dotted = function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.save(); + + if (edge.active) { + context.strokeStyle = settings('edgeActiveColor') === 'edge' ? + (color || defaultEdgeColor) : + settings('defaultEdgeActiveColor'); + } + else { + context.strokeStyle = color; + } + + context.setLineDash([2]); + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.parallel.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.parallel.js new file mode 100644 index 0000000..7264b62 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.parallel.js @@ -0,0 +1,77 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This method renders the edge as two parallel lines. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.parallel = function(edge, source, target, context, settings) { + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + c, + d, + dist = sigma.utils.getDistance(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + // Intersection points of the source node circle: + c = sigma.utils.getCircleIntersection(sX, sY, size, tX, tY, dist); + + // Intersection points of the target node circle: + d = sigma.utils.getCircleIntersection(tX, tY, size, sX, sY, dist); + + context.save(); + + if (edge.active) { + context.strokeStyle = settings('edgeActiveColor') === 'edge' ? + (color || defaultEdgeColor) : + settings('defaultEdgeActiveColor'); + } + else { + context.strokeStyle = color; + } + + context.lineWidth = size; + context.beginPath(); + context.moveTo(c.xi, c.yi); + context.lineTo(d.xi_prime, d.yi_prime); + context.closePath(); + context.stroke(); + + context.beginPath(); + context.moveTo(c.xi_prime, c.yi_prime); + context.lineTo(d.xi, d.yi); + context.closePath(); + context.stroke(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.tapered.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.tapered.js new file mode 100644 index 0000000..b862892 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customEdgeShapes/sigma.canvas.edges.tapered.js @@ -0,0 +1,77 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This method renders the edge as a tapered line. + * Danny Holten, Petra Isenberg, Jean-Daniel Fekete, and J. Van Wijk (2010) + * Performance Evaluation of Tapered, Curved, and Animated Directed-Edge + * Representations in Node-Link Graphs. Research Report, Sep 2010. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.tapered = function(edge, source, target, context, settings) { + // The goal is to draw a triangle where the target node is a point of + // the triangle, and the two other points are the intersection of the + // source circle and the circle (target, distance(source, target)). + var color = edge.active ? + edge.active_color || settings('defaultEdgeActiveColor') : + edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + prefix = settings('prefix') || '', + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + dist = sigma.utils.getDistance(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + // Intersection points: + var c = sigma.utils.getCircleIntersection(sX, sY, size, tX, tY, dist); + + context.save(); + + if (edge.active) { + context.fillStyle = settings('edgeActiveColor') === 'edge' ? + (color || defaultEdgeColor) : + settings('defaultEdgeActiveColor'); + } + else { + context.fillStyle = color; + } + + // Turn transparency on: + context.globalAlpha = 0.65; + + // Draw the triangle: + context.beginPath(); + context.moveTo(tX, tY); + context.lineTo(c.xi, c.yi); + context.lineTo(c.xi_prime, c.yi_prime); + context.closePath(); + context.fill(); + + context.restore(); + }; +})(); diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/README.md b/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/README.md new file mode 100644 index 0000000..8e0054a --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/README.md @@ -0,0 +1,61 @@ +sigma.renderers.customShapes +================== + +Plugin developed by [Ron Peleg](https://github.com/rpeleg1970). + +--- +## General +This plugin registers custom node shape renderers, and allows adding scaled images on top of them. See the following [example code](../../examples/plugin-customShapes.html) for full usage. + +To use, include all .js files under this folder. + +The plugin implements the `node.borderColor` property to allow control of the (surprise) border color. + +## Shapes +The plugin implements the following shapes. To set a shape renderer, you simply set `node.type='shape-name'` e.g. `node.type='star'`. The default renderer implemented by sigma.js is named `def` - see also [generic custom node renderer example](../../examples/custom-node-renderer.html) + * `circle`: similar to the `def` renderer, but also allows images + * `square` + * `diamond` + * `equilateral`: equilateral polygon. you can control additional properties in this polygon by setting more values as follows: +````javascript + node.equilateral = { + rotate: /* rotate right, value in deg */, + numPoints: /* default 5, integer */ + } +```` + * `star`: you can control additional properties in this star by setting more as follows: +````javascript + node.star = { + numPoints: /* default 5, integer */, + innerRatio: /* ratio of inner radius in star, compared to node.size */ + } +```` + * `cross`: plus shape. you can control additional properties in this polygon by setting more values as follows: +````javascript + node.cross = { + lineWeight: /* width of cross arms */, + } +```` + * `pacman`: an example of a more exotic renderer + +The list of available renderer types can be obtained by calling `ShapeLibrary.enumerate()` + +## Images +You can add an image to any node, simply by adding `node.image` property, with the following content: +````javascript +node.image = { + url: /* mandatory image URL */, + clip: /* Ratio of image clipping disk compared to node size (def 1.0) - see example to how we adapt this to differenmt shapes */, + scale: /* Ratio of how to scale the image, compared to node size, default 1.0 */, + w: /* numeric width - important for correct scaling if w/h ratio is not 1.0 */, + h: /* numeric height - important for correct scaling if w/h ratio is not 1.0 */ +} +```` +Because the plug-in calls the sigma instance `refresh()` method on image loading, you MUST init as follows or you will not see rendered images: +````javascript + s = new sigma({ + ... + }); + CustomShapes.init(s); + s.refresh(); +```` diff --git a/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/shape-library.js b/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/shape-library.js new file mode 100644 index 0000000..875c190 --- /dev/null +++ b/blogContent/projects/steam/src/plugins/sigma.renderers.customShapes/shape-library.js @@ -0,0 +1,162 @@ +;(function(undefined) { + 'use strict'; + + var shapes = []; + var register = function(name,drawShape,drawBorder) { + shapes.push({ + 'name': name, + 'drawShape': drawShape, + 'drawBorder': drawBorder + }); + } + + var enumerateShapes = function() { + return shapes; + } + + /** + * For the standard closed shapes - the shape fill and border are drawn the + * same, with some minor differences for fill and border. To facilitate this we + * create the generic draw functions, that take a shape drawing func and + * return a shape-renderer/border-renderer + * ---------- + */ + var genericDrawShape = function(shapeFunc) { + return function(node,x,y,size,color,context) { + context.fillStyle = color; + context.beginPath(); + shapeFunc(node,x,y,size,context); + context.closePath(); + context.fill(); + }; + } + + var genericDrawBorder = function(shapeFunc) { + return function(node,x,y,size,color,context) { + context.strokeStyle = color; + context.lineWidth = size / 5; + context.beginPath(); + shapeFunc(node,x,y,size,context); + context.closePath(); + context.stroke(); + }; + } + + /** + * We now proced to use the generics to define our standard shape/border + * drawers: square, diamond, equilateral (polygon), and star + * ---------- + */ + var drawSquare = function(node,x,y,size,context) { + var rotate = Math.PI*45/180; // 45 deg rotation of a diamond shape + context.moveTo(x+size*Math.sin(rotate), y-size*Math.cos(rotate)); // first point on outer radius, dwangle 'rotate' + for(var i=1; i<4; i++) { + context.lineTo(x+Math.sin(rotate+2*Math.PI*i/4)*size, y-Math.cos(rotate+2*Math.PI*i/4)*size); + } + } + register("square",genericDrawShape(drawSquare),genericDrawBorder(drawSquare)); + + var drawCircle = function(node,x,y,size,context) { + context.arc(x,y,size,0,Math.PI*2,true); + } + register("circle",genericDrawShape(drawCircle),genericDrawBorder(drawCircle)); + + var drawDiamond = function(node,x,y,size,context) { + context.moveTo(x-size, y); + context.lineTo(x, y-size); + context.lineTo(x+size, y); + context.lineTo(x, y+size); + } + register("diamond",genericDrawShape(drawDiamond),genericDrawBorder(drawDiamond)); + + var drawCross = function(node,x,y,size,context) { + var lineWeight = (node.cross && node.cross.lineWeight) || 5; + context.moveTo(x-size, y-lineWeight); + context.lineTo(x-size, y+lineWeight); + context.lineTo(x-lineWeight, y+lineWeight); + context.lineTo(x-lineWeight, y+size); + context.lineTo(x+lineWeight, y+size); + context.lineTo(x+lineWeight, y+lineWeight); + context.lineTo(x+size, y+lineWeight); + context.lineTo(x+size, y-lineWeight); + context.lineTo(x+lineWeight, y-lineWeight); + context.lineTo(x+lineWeight, y-size); + context.lineTo(x-lineWeight, y-size); + context.lineTo(x-lineWeight, y-lineWeight); + } + register("cross",genericDrawShape(drawCross),genericDrawBorder(drawCross)); + + var drawEquilateral = function(node,x,y,size,context) { + var pcount = (node.equilateral && node.equilateral.numPoints) || 5; + var rotate = ((node.equilateral && node.equilateral.rotate) || 0)*Math.PI/180; + var radius = size; + context.moveTo(x+radius*Math.sin(rotate), y-radius*Math.cos(rotate)); // first point on outer radius, angle 'rotate' + for(var i=1; i doesn't work + // HACKHACK: IE <=9 does not respect the HTML base element in SVG. + // They don't need the current URL in the clip path reference. + var absolutePath = /MSIE [5-9]/.test(navigator.userAgent) ? + "" : document.location.href; + // To fix cases where an anchor tag was used + absolutePath = absolutePath.split("#")[0]; + image.setAttributeNS(null, 'class', + settings('classPrefix') + '-node-image'); + image.setAttributeNS(null, 'clip-path', + 'url(' + absolutePath + '#' + clipPathId + ')'); + image.setAttributeNS(null, 'pointer-events', 'none'); + image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', + node.image.url); + group.appendChild(def); + group.appendChild(image); + } + } + + var register = function(name,drawShape,drawBorder) { + sigma.canvas.nodes[name] = function(node, context, settings) { + var args = arguments, + prefix = settings('prefix') || '', + size = node[prefix + 'size'], + color = node.color || settings('defaultNodeColor'), + borderColor = node.borderColor || color, + x = node[prefix + 'x'], + y = node[prefix + 'y']; + + context.save(); + + if(drawShape) { + drawShape(node,x,y,size,color,context); + } + + if(drawBorder) { + drawBorder(node,x,y,size,borderColor,context); + } + + drawImage(node,x,y,size,context); + + context.restore(); + }; + + sigma.svg.nodes[name] = { + create: function(node, settings) { + var group = document.createElementNS(settings('xmlns'), 'g'), + circle = document.createElementNS(settings('xmlns'), 'circle'); + + group.setAttributeNS(null, 'class', + settings('classPrefix') + '-node-group'); + group.setAttributeNS(null, 'data-node-id', node.id); + // Defining the node's circle + circle.setAttributeNS(null, 'data-node-id', node.id); + circle.setAttributeNS(null, 'class', + settings('classPrefix') + '-node'); + circle.setAttributeNS(null, 'fill', + node.color || settings('defaultNodeColor')); + + group.appendChild(circle); + drawSVGImage(node, group, settings); + return group; + }, + update: function(node, group, settings) { + var classPrefix = settings('classPrefix'), + clip = node.image.clip || 1, + // 1 is arbitrary, anyway only the ratio counts + ih = node.image.h || 1, + iw = node.image.w || 1, + prefix = settings('prefix') || '', + scale = node.image.scale || 1, + size = node[prefix + 'size'], + x = node[prefix + 'x'], + y = node[prefix + 'y']; + + var r = scale * size, + xratio = (iw, Sebastien Heymann , Dual-licensed under GPL v3 and CDDL) +* https://github.com/Mango-information-systems/gephi/blob/fix-hits/modules/StatisticsPlugin/src/main/java/org/gephi/statistics/plugin/Hits.java +* +* Bugs in Gephi implementation should not be found in this implementation. +* Tests have been put in place based on a test plan used to test implementation in Gephi, cf. discussion here: https://github.com/jacomyal/sigma.js/issues/309 +* No guarantee is provided regarding the correctness of the calculations. Plugin author did not control the validity of the test scenarii. +* +* Warning: tricky edge-case. Hubs and authorities for nodes without any edge are only reliable in an undirected graph calculation mode. +* +* Check the code for more information. +* +* Here is how to use it: +* +* > // directed graph +* > var stats = s.graph.HITS() +* > // returns an object indexed by node Id with the authority and hub measures +* > // like { "n0": {"authority": 0.00343, "hub": 0.023975}, "n1": [...]* +* +* > // undirected graph: pass 'true' as function parameter +* > var stats = s.graph.HITS(true) +* > // returns an object indexed by node Id with the authority and hub measures +* > // like { "n0": {"authority": 0.00343, "hub": 0.023975}, "n1": [...] +*/ + +(function() { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + +/** +* This method takes a graph instance and returns authority and hub measures computed for each node. It uses the built-in +* indexes from sigma's graph model to search in the graph. +* +* @param {boolean} isUndirected flag informing whether the graph is directed or not. Default false: directed graph. +* @return {object} object indexed by node Ids, containing authority and hub measures for each node of the graph. +*/ + + sigma.classes.graph.addMethod( + 'HITS', + function(isUndirected) { + var res = {} + , epsilon = 0.0001 + , hubList = [] + , authList = [] + , nodes = this.nodes() + , nodesCount = nodes.length + , tempRes = {} + + if (!isUndirected) + isUndirected = false + + for (var i in nodes) { + + if (isUndirected) { + hubList.push(nodes[i]) + authList.push(nodes[i]) + } + else { + if (this.degree(nodes[i].id, 'out') > 0) + hubList.push(nodes[i]) + + if (this.degree(nodes[i].id, 'in') > 0) + authList.push(nodes[i]) + } + + res[nodes[i].id] = { authority : 1, hub: 1 } + } + + var done + + while (true) { + done = true + var authSum = 0 + , hubSum = 0 + + for (var i in authList) { + + tempRes[authList[i].id] = {authority : 1, hub:0 } + + var connectedNodes = [] + + if (isUndirected) + connectedNodes = this.allNeighborsIndex[authList[i].id] + else + connectedNodes = this.inNeighborsIndex[authList[i].id] + + for (var j in connectedNodes) { + if (j != authList[i].id) + tempRes[authList[i].id].authority += res[j].hub + } + + authSum += tempRes[authList[i].id].authority + + } + + for (var i in hubList) { + + if (tempRes[hubList[i].id]) + tempRes[hubList[i].id].hub = 1 + else + tempRes[hubList[i].id] = {authority: 0, hub : 1 } + + var connectedNodes = [] + + if (isUndirected) + connectedNodes = this.allNeighborsIndex[hubList[i].id] + else + connectedNodes = this.outNeighborsIndex[hubList[i].id] + + for (var j in connectedNodes) { + if (j != hubList[i].id) + tempRes[hubList[i].id].hub += res[j].authority + } + + hubSum += tempRes[hubList[i].id].hub + + } + + for (var i in authList) { + tempRes[authList[i].id].authority /= authSum + + if (Math.abs((tempRes[authList[i].id].authority - res[authList[i].id].authority) / res[authList[i].id].authority) >= epsilon) + done = false + } + + for (var i in hubList) { + tempRes[hubList[i].id].hub /= hubSum + + if (Math.abs((tempRes[hubList[i].id].hub - res[hubList[i].id].hub) / res[hubList[i].id].hub) >= epsilon) + done = false + } + res = tempRes + + tempRes = {} + + if (done) + break + + } + + return res + + } + ) + +}).call(window) diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.arrow.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.arrow.js new file mode 100644 index 0000000..1be0cc4 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.arrow.js @@ -0,0 +1,76 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.arrow = + function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + size = edge[prefix + 'size'] || 1, + tSize = target[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y']; + + size = (edge.hover) ? + settings('edgeHoverSizeRatio') * size : size; + var aSize = size * 2.5, + d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)), + aX = sX + (tX - sX) * (d - aSize - tSize) / d, + aY = sY + (tY - sY) * (d - aSize - tSize) / d, + vX = (tX - sX) * aSize / d, + vY = (tY - sY) * aSize / d; + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + context.lineTo( + aX, + aY + ); + context.stroke(); + + context.fillStyle = color; + context.beginPath(); + context.moveTo(aX + vX, aY + vY); + context.lineTo(aX + vY * 0.6, aY - vX * 0.6); + context.lineTo(aX - vY * 0.6, aY + vX * 0.6); + context.lineTo(aX + vX, aY + vY); + context.closePath(); + context.fill(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curve.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curve.js new file mode 100644 index 0000000..f79abf8 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curve.js @@ -0,0 +1,64 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.curve = + function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1), + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + cp = {}, + sSize = source[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y']; + + cp = (source.id === target.id) ? + sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) : + sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + if (source.id === target.id) { + context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY); + } else { + context.quadraticCurveTo(cp.x, cp.y, tX, tY); + } + context.stroke(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js new file mode 100644 index 0000000..6a34b77 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.curvedArrow.js @@ -0,0 +1,96 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.curvedArrow = + function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + cp = {}, + size = settings('edgeHoverSizeRatio') * (edge[prefix + 'size'] || 1), + tSize = target[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + d, + aSize, + aX, + aY, + vX, + vY; + + cp = (source.id === target.id) ? + sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) : + sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY); + + if (source.id === target.id) { + d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2)); + aSize = size * 2.5; + aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d; + aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d; + vX = (tX - cp.x1) * aSize / d; + vY = (tY - cp.y1) * aSize / d; + } + else { + d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2)); + aSize = size * 2.5; + aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d; + aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d; + vX = (tX - cp.x) * aSize / d; + vY = (tY - cp.y) * aSize / d; + } + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + if (source.id === target.id) { + context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY); + } else { + context.quadraticCurveTo(cp.x, cp.y, aX, aY); + } + context.stroke(); + + context.fillStyle = color; + context.beginPath(); + context.moveTo(aX + vX, aY + vY); + context.lineTo(aX + vY * 0.6, aY - vX * 0.6); + context.lineTo(aX - vY * 0.6, aY + vX * 0.6); + context.lineTo(aX + vX, aY + vY); + context.closePath(); + context.fill(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.def.js new file mode 100644 index 0000000..d88ad38 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edgehovers.def.js @@ -0,0 +1,57 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edgehovers'); + + /** + * This hover renderer will display the edge with a different color or size. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edgehovers.def = + function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + if (settings('edgeHoverColor') === 'edge') { + color = edge.hover_color || color; + } else { + color = edge.hover_color || settings('defaultEdgeHoverColor') || color; + } + size *= settings('edgeHoverSizeRatio'); + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.arrow.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.arrow.js new file mode 100644 index 0000000..4f12977 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.arrow.js @@ -0,0 +1,66 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This edge renderer will display edges as arrows going from the source node + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.arrow = function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + size = edge[prefix + 'size'] || 1, + tSize = target[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + aSize = Math.max(size * 2.5, settings('minArrowSize')), + d = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2)), + aX = sX + (tX - sX) * (d - aSize - tSize) / d, + aY = sY + (tY - sY) * (d - aSize - tSize) / d, + vX = (tX - sX) * aSize / d, + vY = (tY - sY) * aSize / d; + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + context.lineTo( + aX, + aY + ); + context.stroke(); + + context.fillStyle = color; + context.beginPath(); + context.moveTo(aX + vX, aY + vY); + context.lineTo(aX + vY * 0.6, aY - vX * 0.6); + context.lineTo(aX - vY * 0.6, aY + vX * 0.6); + context.lineTo(aX + vX, aY + vY); + context.closePath(); + context.fill(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curve.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curve.js new file mode 100644 index 0000000..3e1502b --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curve.js @@ -0,0 +1,57 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This edge renderer will display edges as curves. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.curve = function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + cp = {}, + sSize = source[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y']; + + cp = (source.id === target.id) ? + sigma.utils.getSelfLoopControlPoints(sX, sY, sSize) : + sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + if (source.id === target.id) { + context.bezierCurveTo(cp.x1, cp.y1, cp.x2, cp.y2, tX, tY); + } else { + context.quadraticCurveTo(cp.x, cp.y, tX, tY); + } + context.stroke(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curvedArrow.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curvedArrow.js new file mode 100644 index 0000000..9c7b663 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.curvedArrow.js @@ -0,0 +1,88 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * This edge renderer will display edges as curves with arrow heading. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.curvedArrow = + function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'), + cp = {}, + size = edge[prefix + 'size'] || 1, + tSize = target[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + aSize = Math.max(size * 2.5, settings('minArrowSize')), + d, + aX, + aY, + vX, + vY; + + cp = (source.id === target.id) ? + sigma.utils.getSelfLoopControlPoints(sX, sY, tSize) : + sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY); + + if (source.id === target.id) { + d = Math.sqrt(Math.pow(tX - cp.x1, 2) + Math.pow(tY - cp.y1, 2)); + aX = cp.x1 + (tX - cp.x1) * (d - aSize - tSize) / d; + aY = cp.y1 + (tY - cp.y1) * (d - aSize - tSize) / d; + vX = (tX - cp.x1) * aSize / d; + vY = (tY - cp.y1) * aSize / d; + } + else { + d = Math.sqrt(Math.pow(tX - cp.x, 2) + Math.pow(tY - cp.y, 2)); + aX = cp.x + (tX - cp.x) * (d - aSize - tSize) / d; + aY = cp.y + (tY - cp.y) * (d - aSize - tSize) / d; + vX = (tX - cp.x) * aSize / d; + vY = (tY - cp.y) * aSize / d; + } + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo(sX, sY); + if (source.id === target.id) { + context.bezierCurveTo(cp.x2, cp.y2, cp.x1, cp.y1, aX, aY); + } else { + context.quadraticCurveTo(cp.x, cp.y, aX, aY); + } + context.stroke(); + + context.fillStyle = color; + context.beginPath(); + context.moveTo(aX + vX, aY + vY); + context.lineTo(aX + vY * 0.6, aY - vX * 0.6); + context.lineTo(aX - vY * 0.6, aY + vX * 0.6); + context.lineTo(aX + vX, aY + vY); + context.closePath(); + context.fill(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.def.js new file mode 100644 index 0000000..dd97b90 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.edges.def.js @@ -0,0 +1,49 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.edges'); + + /** + * The default edge renderer. It renders the edge as a simple line. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.def = function(edge, source, target, context, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1, + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + context.strokeStyle = color; + context.lineWidth = size; + context.beginPath(); + context.moveTo( + source[prefix + 'x'], + source[prefix + 'y'] + ); + context.lineTo( + target[prefix + 'x'], + target[prefix + 'y'] + ); + context.stroke(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.extremities.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.extremities.def.js new file mode 100644 index 0000000..7877dc2 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.extremities.def.js @@ -0,0 +1,38 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.extremities'); + + /** + * The default renderer for hovered edge extremities. It renders the edge + * extremities as hovered. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.extremities.def = + function(edge, source, target, context, settings) { + // Source Node: + ( + sigma.canvas.hovers[source.type] || + sigma.canvas.hovers.def + ) ( + source, context, settings + ); + + // Target Node: + ( + sigma.canvas.hovers[target.type] || + sigma.canvas.hovers.def + ) ( + target, context, settings + ); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.hovers.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.hovers.def.js new file mode 100644 index 0000000..00185c2 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.hovers.def.js @@ -0,0 +1,106 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.hovers'); + + /** + * This hover renderer will basically display the label with a background. + * + * @param {object} node The node object. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.hovers.def = function(node, context, settings) { + var x, + y, + w, + h, + e, + fontStyle = settings('hoverFontStyle') || settings('fontStyle'), + prefix = settings('prefix') || '', + size = node[prefix + 'size'], + fontSize = (settings('labelSize') === 'fixed') ? + settings('defaultLabelSize') : + settings('labelSizeRatio') * size; + + // Label background: + context.font = (fontStyle ? fontStyle + ' ' : '') + + fontSize + 'px ' + (settings('hoverFont') || settings('font')); + + context.beginPath(); + context.fillStyle = settings('labelHoverBGColor') === 'node' ? + (node.color || settings('defaultNodeColor')) : + settings('defaultHoverLabelBGColor'); + + if (node.label && settings('labelHoverShadow')) { + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 8; + context.shadowColor = settings('labelHoverShadowColor'); + } + + if (node.label && typeof node.label === 'string') { + x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2); + y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2); + w = Math.round( + context.measureText(node.label).width + fontSize / 2 + size + 7 + ); + h = Math.round(fontSize + 4); + e = Math.round(fontSize / 2 + 2); + + context.moveTo(x, y + e); + context.arcTo(x, y, x + e, y, e); + context.lineTo(x + w, y); + context.lineTo(x + w, y + h); + context.lineTo(x + e, y + h); + context.arcTo(x, y + h, x, y + h - e, e); + context.lineTo(x, y + e); + + context.closePath(); + context.fill(); + + context.shadowOffsetX = 0; + context.shadowOffsetY = 0; + context.shadowBlur = 0; + } + + // Node border: + if (settings('borderSize') > 0) { + context.beginPath(); + context.fillStyle = settings('nodeBorderColor') === 'node' ? + (node.color || settings('defaultNodeColor')) : + settings('defaultNodeBorderColor'); + context.arc( + node[prefix + 'x'], + node[prefix + 'y'], + size + settings('borderSize'), + 0, + Math.PI * 2, + true + ); + context.closePath(); + context.fill(); + } + + // Node: + var nodeRenderer = sigma.canvas.nodes[node.type] || sigma.canvas.nodes.def; + nodeRenderer(node, context, settings); + + // Display the label: + if (node.label && typeof node.label === 'string') { + context.fillStyle = (settings('labelHoverColor') === 'node') ? + (node.color || settings('defaultNodeColor')) : + settings('defaultLabelHoverColor'); + + context.fillText( + node.label, + Math.round(node[prefix + 'x'] + size + 3), + Math.round(node[prefix + 'y'] + fontSize / 3) + ); + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.labels.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.labels.def.js new file mode 100644 index 0000000..8a70d73 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.labels.def.js @@ -0,0 +1,44 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.labels'); + + /** + * This label renderer will just display the label on the right of the node. + * + * @param {object} node The node object. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.labels.def = function(node, context, settings) { + var fontSize, + prefix = settings('prefix') || '', + size = node[prefix + 'size']; + + if (size < settings('labelThreshold')) + return; + + if (!node.label || typeof node.label !== 'string') + return; + + fontSize = (settings('labelSize') === 'fixed') ? + settings('defaultLabelSize') : + settings('labelSizeRatio') * size; + + context.font = (settings('fontStyle') ? settings('fontStyle') + ' ' : '') + + fontSize + 'px ' + settings('font'); + context.fillStyle = (settings('labelColor') === 'node') ? + (node.color || settings('defaultNodeColor')) : + settings('defaultLabelColor'); + + context.fillText( + node.label, + Math.round(node[prefix + 'x'] + size + 3), + Math.round(node[prefix + 'y'] + fontSize / 3) + ); + }; +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.nodes.def.js b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.nodes.def.js new file mode 100644 index 0000000..ee499b0 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/canvas/sigma.canvas.nodes.def.js @@ -0,0 +1,30 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.canvas.nodes'); + + /** + * The default node renderer. It renders the node as a simple disc. + * + * @param {object} node The node object. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.nodes.def = function(node, context, settings) { + var prefix = settings('prefix') || ''; + + context.fillStyle = node.color || settings('defaultNodeColor'); + context.beginPath(); + context.arc( + node[prefix + 'x'], + node[prefix + 'y'], + node[prefix + 'size'], + 0, + Math.PI * 2, + true + ); + + context.closePath(); + context.fill(); + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/sigma.renderers.canvas.js b/blogContent/projects/steam/src/renderers/sigma.renderers.canvas.js new file mode 100644 index 0000000..963d7d0 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/sigma.renderers.canvas.js @@ -0,0 +1,442 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + if (typeof conrad === 'undefined') + throw 'conrad is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.renderers'); + + /** + * This function is the constructor of the canvas sigma's renderer. + * + * @param {sigma.classes.graph} graph The graph to render. + * @param {sigma.classes.camera} camera The camera. + * @param {configurable} settings The sigma instance settings + * function. + * @param {object} object The options object. + * @return {sigma.renderers.canvas} The renderer instance. + */ + sigma.renderers.canvas = function(graph, camera, settings, options) { + if (typeof options !== 'object') + throw 'sigma.renderers.canvas: Wrong arguments.'; + + if (!(options.container instanceof HTMLElement)) + throw 'Container not found.'; + + var k, + i, + l, + a, + fn, + self = this; + + sigma.classes.dispatcher.extend(this); + + // Initialize main attributes: + Object.defineProperty(this, 'conradId', { + value: sigma.utils.id() + }); + this.graph = graph; + this.camera = camera; + this.contexts = {}; + this.domElements = {}; + this.options = options; + this.container = this.options.container; + this.settings = ( + typeof options.settings === 'object' && + options.settings + ) ? + settings.embedObjects(options.settings) : + settings; + + // Node indexes: + this.nodesOnScreen = []; + this.edgesOnScreen = []; + + // Conrad related attributes: + this.jobs = {}; + + // Find the prefix: + this.options.prefix = 'renderer' + this.conradId + ':'; + + // Initialize the DOM elements: + if ( + !this.settings('batchEdgesDrawing') + ) { + this.initDOM('canvas', 'scene'); + this.contexts.edges = this.contexts.scene; + this.contexts.nodes = this.contexts.scene; + this.contexts.labels = this.contexts.scene; + } else { + this.initDOM('canvas', 'edges'); + this.initDOM('canvas', 'scene'); + this.contexts.nodes = this.contexts.scene; + this.contexts.labels = this.contexts.scene; + } + + this.initDOM('canvas', 'mouse'); + this.contexts.hover = this.contexts.mouse; + + // Initialize captors: + this.captors = []; + a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch]; + for (i = 0, l = a.length; i < l; i++) { + fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]]; + this.captors.push( + new fn( + this.domElements.mouse, + this.camera, + this.settings + ) + ); + } + + // Deal with sigma events: + sigma.misc.bindEvents.call(this, this.options.prefix); + sigma.misc.drawHovers.call(this, this.options.prefix); + + this.resize(false); + }; + + + + + /** + * This method renders the graph on the canvases. + * + * @param {?object} options Eventually an object of options. + * @return {sigma.renderers.canvas} Returns the instance itself. + */ + sigma.renderers.canvas.prototype.render = function(options) { + options = options || {}; + + var a, + i, + k, + l, + o, + id, + end, + job, + start, + edges, + renderers, + rendererType, + batchSize, + tempGCO, + index = {}, + graph = this.graph, + nodes = this.graph.nodes, + prefix = this.options.prefix || '', + drawEdges = this.settings(options, 'drawEdges'), + drawNodes = this.settings(options, 'drawNodes'), + drawLabels = this.settings(options, 'drawLabels'), + drawEdgeLabels = this.settings(options, 'drawEdgeLabels'), + embedSettings = this.settings.embedObjects(options, { + prefix: this.options.prefix + }); + + // Call the resize function: + this.resize(false); + + // Check the 'hideEdgesOnMove' setting: + if (this.settings(options, 'hideEdgesOnMove')) + if (this.camera.isAnimated || this.camera.isMoving) + drawEdges = false; + + // Apply the camera's view: + this.camera.applyView( + undefined, + this.options.prefix, + { + width: this.width, + height: this.height + } + ); + + // Clear canvases: + this.clear(); + + // Kill running jobs: + for (k in this.jobs) + if (conrad.hasJob(k)) + conrad.killJob(k); + + // Find which nodes are on screen: + this.edgesOnScreen = []; + this.nodesOnScreen = this.camera.quadtree.area( + this.camera.getRectangle(this.width, this.height) + ); + + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) + index[a[i].id] = a[i]; + + // Draw edges: + // - If settings('batchEdgesDrawing') is true, the edges are displayed per + // batches. If not, they are drawn in one frame. + if (drawEdges) { + // First, let's identify which edges to draw. To do this, we just keep + // every edges that have at least one extremity displayed according to + // the quadtree and the "hidden" attribute. We also do not keep hidden + // edges. + for (a = graph.edges(), i = 0, l = a.length; i < l; i++) { + o = a[i]; + if ( + (index[o.source] || index[o.target]) && + (!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden) + ) + this.edgesOnScreen.push(o); + } + + // If the "batchEdgesDrawing" settings is true, edges are batched: + if (this.settings(options, 'batchEdgesDrawing')) { + id = 'edges_' + this.conradId; + batchSize = embedSettings('canvasEdgesBatchSize'); + + edges = this.edgesOnScreen; + l = edges.length; + + start = 0; + end = Math.min(edges.length, start + batchSize); + + job = function() { + tempGCO = this.contexts.edges.globalCompositeOperation; + this.contexts.edges.globalCompositeOperation = 'destination-over'; + + renderers = sigma.canvas.edges; + for (i = start; i < end; i++) { + o = edges[i]; + (renderers[ + o.type || this.settings(options, 'defaultEdgeType') + ] || renderers.def)( + o, + graph.nodes(o.source), + graph.nodes(o.target), + this.contexts.edges, + embedSettings + ); + } + + // Draw edge labels: + if (drawEdgeLabels) { + renderers = sigma.canvas.edges.labels; + for (i = start; i < end; i++) { + o = edges[i]; + if (!o.hidden) + (renderers[ + o.type || this.settings(options, 'defaultEdgeType') + ] || renderers.def)( + o, + graph.nodes(o.source), + graph.nodes(o.target), + this.contexts.labels, + embedSettings + ); + } + } + + // Restore original globalCompositeOperation: + this.contexts.edges.globalCompositeOperation = tempGCO; + + // Catch job's end: + if (end === edges.length) { + delete this.jobs[id]; + return false; + } + + start = end + 1; + end = Math.min(edges.length, start + batchSize); + return true; + }; + + this.jobs[id] = job; + conrad.addJob(id, job.bind(this)); + + // If not, they are drawn in one frame: + } else { + renderers = sigma.canvas.edges; + for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) { + o = a[i]; + (renderers[ + o.type || this.settings(options, 'defaultEdgeType') + ] || renderers.def)( + o, + graph.nodes(o.source), + graph.nodes(o.target), + this.contexts.edges, + embedSettings + ); + } + + // Draw edge labels: + // - No batching + if (drawEdgeLabels) { + renderers = sigma.canvas.edges.labels; + for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) + if (!a[i].hidden) + (renderers[ + a[i].type || this.settings(options, 'defaultEdgeType') + ] || renderers.def)( + a[i], + graph.nodes(a[i].source), + graph.nodes(a[i].target), + this.contexts.labels, + embedSettings + ); + } + } + } + + // Draw nodes: + // - No batching + if (drawNodes) { + renderers = sigma.canvas.nodes; + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) + if (!a[i].hidden) + (renderers[ + a[i].type || this.settings(options, 'defaultNodeType') + ] || renderers.def)( + a[i], + this.contexts.nodes, + embedSettings + ); + } + + // Draw labels: + // - No batching + if (drawLabels) { + renderers = sigma.canvas.labels; + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) + if (!a[i].hidden) + (renderers[ + a[i].type || this.settings(options, 'defaultNodeType') + ] || renderers.def)( + a[i], + this.contexts.labels, + embedSettings + ); + } + + this.dispatchEvent('render'); + + return this; + }; + + /** + * This method creates a DOM element of the specified type, switches its + * position to "absolute", references it to the domElements attribute, and + * finally appends it to the container. + * + * @param {string} tag The label tag. + * @param {string} id The id of the element (to store it in "domElements"). + */ + sigma.renderers.canvas.prototype.initDOM = function(tag, id) { + var dom = document.createElement(tag); + + dom.style.position = 'absolute'; + dom.setAttribute('class', 'sigma-' + id); + + this.domElements[id] = dom; + this.container.appendChild(dom); + + if (tag.toLowerCase() === 'canvas') + this.contexts[id] = dom.getContext('2d'); + }; + + /** + * This method resizes each DOM elements in the container and stores the new + * dimensions. Then, it renders the graph. + * + * @param {?number} width The new width of the container. + * @param {?number} height The new height of the container. + * @return {sigma.renderers.canvas} Returns the instance itself. + */ + sigma.renderers.canvas.prototype.resize = function(w, h) { + var k, + oldWidth = this.width, + oldHeight = this.height, + pixelRatio = sigma.utils.getPixelRatio(); + + if (w !== undefined && h !== undefined) { + this.width = w; + this.height = h; + } else { + this.width = this.container.offsetWidth; + this.height = this.container.offsetHeight; + + w = this.width; + h = this.height; + } + + if (oldWidth !== this.width || oldHeight !== this.height) { + for (k in this.domElements) { + this.domElements[k].style.width = w + 'px'; + this.domElements[k].style.height = h + 'px'; + + if (this.domElements[k].tagName.toLowerCase() === 'canvas') { + this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px'); + this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px'); + + if (pixelRatio !== 1) + this.contexts[k].scale(pixelRatio, pixelRatio); + } + } + } + + return this; + }; + + /** + * This method clears each canvas. + * + * @return {sigma.renderers.canvas} Returns the instance itself. + */ + sigma.renderers.canvas.prototype.clear = function() { + for (var k in this.contexts) { + this.contexts[k].clearRect(0, 0, this.width, this.height); + } + + return this; + }; + + /** + * This method kills contexts and other attributes. + */ + sigma.renderers.canvas.prototype.kill = function() { + var k, + captor; + + // Kill captors: + while ((captor = this.captors.pop())) + captor.kill(); + delete this.captors; + + // Kill contexts: + for (k in this.domElements) { + this.domElements[k].parentNode.removeChild(this.domElements[k]); + delete this.domElements[k]; + delete this.contexts[k]; + } + delete this.domElements; + delete this.contexts; + }; + + + + + /** + * The labels, nodes and edges renderers are stored in the three following + * objects. When an element is drawn, its type will be checked and if a + * renderer with the same name exists, it will be used. If not found, the + * default renderer will be used instead. + * + * They are stored in different files, in the "./canvas" folder. + */ + sigma.utils.pkg('sigma.canvas.nodes'); + sigma.utils.pkg('sigma.canvas.edges'); + sigma.utils.pkg('sigma.canvas.labels'); +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/sigma.renderers.def.js b/blogContent/projects/steam/src/renderers/sigma.renderers.def.js new file mode 100644 index 0000000..b091d39 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/sigma.renderers.def.js @@ -0,0 +1,29 @@ +;(function(global) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.renderers'); + + // Check if WebGL is enabled: + var canvas, + webgl = !!global.WebGLRenderingContext; + if (webgl) { + canvas = document.createElement('canvas'); + try { + webgl = !!( + canvas.getContext('webgl') || + canvas.getContext('experimental-webgl') + ); + } catch (e) { + webgl = false; + } + } + + // Copy the good renderer: + sigma.renderers.def = webgl ? + sigma.renderers.webgl : + sigma.renderers.canvas; +})(this); diff --git a/blogContent/projects/steam/src/renderers/sigma.renderers.svg.js b/blogContent/projects/steam/src/renderers/sigma.renderers.svg.js new file mode 100644 index 0000000..ffec79e --- /dev/null +++ b/blogContent/projects/steam/src/renderers/sigma.renderers.svg.js @@ -0,0 +1,479 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + if (typeof conrad === 'undefined') + throw 'conrad is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.renderers'); + + /** + * This function is the constructor of the svg sigma's renderer. + * + * @param {sigma.classes.graph} graph The graph to render. + * @param {sigma.classes.camera} camera The camera. + * @param {configurable} settings The sigma instance settings + * function. + * @param {object} object The options object. + * @return {sigma.renderers.svg} The renderer instance. + */ + sigma.renderers.svg = function(graph, camera, settings, options) { + if (typeof options !== 'object') + throw 'sigma.renderers.svg: Wrong arguments.'; + + if (!(options.container instanceof HTMLElement)) + throw 'Container not found.'; + + var i, + l, + a, + fn, + self = this; + + sigma.classes.dispatcher.extend(this); + + // Initialize main attributes: + this.graph = graph; + this.camera = camera; + this.domElements = { + graph: null, + groups: {}, + nodes: {}, + edges: {}, + labels: {}, + hovers: {} + }; + this.measurementCanvas = null; + this.options = options; + this.container = this.options.container; + this.settings = ( + typeof options.settings === 'object' && + options.settings + ) ? + settings.embedObjects(options.settings) : + settings; + + // Is the renderer meant to be freestyle? + this.settings('freeStyle', !!this.options.freeStyle); + + // SVG xmlns + this.settings('xmlns', 'http://www.w3.org/2000/svg'); + + // Indexes: + this.nodesOnScreen = []; + this.edgesOnScreen = []; + + // Find the prefix: + this.options.prefix = 'renderer' + sigma.utils.id() + ':'; + + // Initialize the DOM elements + this.initDOM('svg'); + + // Initialize captors: + this.captors = []; + a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch]; + for (i = 0, l = a.length; i < l; i++) { + fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]]; + this.captors.push( + new fn( + this.domElements.graph, + this.camera, + this.settings + ) + ); + } + + // Bind resize: + window.addEventListener('resize', function() { + self.resize(); + }); + + // Deal with sigma events: + // TODO: keep an option to override the DOM events? + sigma.misc.bindDOMEvents.call(this, this.domElements.graph); + this.bindHovers(this.options.prefix); + + // Resize + this.resize(false); + }; + + /** + * This method renders the graph on the svg scene. + * + * @param {?object} options Eventually an object of options. + * @return {sigma.renderers.svg} Returns the instance itself. + */ + sigma.renderers.svg.prototype.render = function(options) { + options = options || {}; + + var a, + i, + k, + e, + l, + o, + source, + target, + start, + edges, + renderers, + subrenderers, + index = {}, + graph = this.graph, + nodes = this.graph.nodes, + prefix = this.options.prefix || '', + drawEdges = this.settings(options, 'drawEdges'), + drawNodes = this.settings(options, 'drawNodes'), + drawLabels = this.settings(options, 'drawLabels'), + embedSettings = this.settings.embedObjects(options, { + prefix: this.options.prefix, + forceLabels: this.options.forceLabels + }); + + // Check the 'hideEdgesOnMove' setting: + if (this.settings(options, 'hideEdgesOnMove')) + if (this.camera.isAnimated || this.camera.isMoving) + drawEdges = false; + + // Apply the camera's view: + this.camera.applyView( + undefined, + this.options.prefix, + { + width: this.width, + height: this.height + } + ); + + // Hiding everything + // TODO: find a more sensible way to perform this operation + this.hideDOMElements(this.domElements.nodes); + this.hideDOMElements(this.domElements.edges); + this.hideDOMElements(this.domElements.labels); + + // Find which nodes are on screen + this.edgesOnScreen = []; + this.nodesOnScreen = this.camera.quadtree.area( + this.camera.getRectangle(this.width, this.height) + ); + + // Node index + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) + index[a[i].id] = a[i]; + + // Find which edges are on screen + for (a = graph.edges(), i = 0, l = a.length; i < l; i++) { + o = a[i]; + if ( + (index[o.source] || index[o.target]) && + (!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden) + ) + this.edgesOnScreen.push(o); + } + + // Display nodes + //--------------- + renderers = sigma.svg.nodes; + subrenderers = sigma.svg.labels; + + //-- First we create the nodes which are not already created + if (drawNodes) + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) { + if (!a[i].hidden && !this.domElements.nodes[a[i].id]) { + + // Node + e = (renderers[a[i].type] || renderers.def).create( + a[i], + embedSettings + ); + + this.domElements.nodes[a[i].id] = e; + this.domElements.groups.nodes.appendChild(e); + + // Label + e = (subrenderers[a[i].type] || subrenderers.def).create( + a[i], + embedSettings + ); + + this.domElements.labels[a[i].id] = e; + this.domElements.groups.labels.appendChild(e); + } + } + + //-- Second we update the nodes + if (drawNodes) + for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) { + + if (a[i].hidden) + continue; + + // Node + (renderers[a[i].type] || renderers.def).update( + a[i], + this.domElements.nodes[a[i].id], + embedSettings + ); + + // Label + (subrenderers[a[i].type] || subrenderers.def).update( + a[i], + this.domElements.labels[a[i].id], + embedSettings + ); + } + + // Display edges + //--------------- + renderers = sigma.svg.edges; + + //-- First we create the edges which are not already created + if (drawEdges) + for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) { + if (!this.domElements.edges[a[i].id]) { + source = nodes(a[i].source); + target = nodes(a[i].target); + + e = (renderers[a[i].type] || renderers.def).create( + a[i], + source, + target, + embedSettings + ); + + this.domElements.edges[a[i].id] = e; + this.domElements.groups.edges.appendChild(e); + } + } + + //-- Second we update the edges + if (drawEdges) + for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) { + source = nodes(a[i].source); + target = nodes(a[i].target); + + (renderers[a[i].type] || renderers.def).update( + a[i], + this.domElements.edges[a[i].id], + source, + target, + embedSettings + ); + } + + this.dispatchEvent('render'); + + return this; + }; + + /** + * This method creates a DOM element of the specified type, switches its + * position to "absolute", references it to the domElements attribute, and + * finally appends it to the container. + * + * @param {string} tag The label tag. + * @param {string} id The id of the element (to store it in "domElements"). + */ + sigma.renderers.svg.prototype.initDOM = function(tag) { + var dom = document.createElementNS(this.settings('xmlns'), tag), + c = this.settings('classPrefix'), + g, + l, + i; + + dom.style.position = 'absolute'; + dom.setAttribute('class', c + '-svg'); + + // Setting SVG namespace + dom.setAttribute('xmlns', this.settings('xmlns')); + dom.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + dom.setAttribute('version', '1.1'); + + // Creating the measurement canvas + var canvas = document.createElement('canvas'); + canvas.setAttribute('class', c + '-measurement-canvas'); + + // Appending elements + this.domElements.graph = this.container.appendChild(dom); + + // Creating groups + var groups = ['edges', 'nodes', 'labels', 'hovers']; + for (i = 0, l = groups.length; i < l; i++) { + g = document.createElementNS(this.settings('xmlns'), 'g'); + + g.setAttributeNS(null, 'id', c + '-group-' + groups[i]); + g.setAttributeNS(null, 'class', c + '-group'); + + this.domElements.groups[groups[i]] = + this.domElements.graph.appendChild(g); + } + + // Appending measurement canvas + this.container.appendChild(canvas); + this.measurementCanvas = canvas.getContext('2d'); + }; + + /** + * This method hides a batch of SVG DOM elements. + * + * @param {array} elements An array of elements to hide. + * @param {object} renderer The renderer to use. + * @return {sigma.renderers.svg} Returns the instance itself. + */ + sigma.renderers.svg.prototype.hideDOMElements = function(elements) { + var o, + i; + + for (i in elements) { + o = elements[i]; + sigma.svg.utils.hide(o); + } + + return this; + }; + + /** + * This method binds the hover events to the renderer. + * + * @param {string} prefix The renderer prefix. + */ + // TODO: add option about whether to display hovers or not + sigma.renderers.svg.prototype.bindHovers = function(prefix) { + var renderers = sigma.svg.hovers, + self = this, + hoveredNode; + + function overNode(e) { + var node = e.data.node, + embedSettings = self.settings.embedObjects({ + prefix: prefix + }); + + if (!embedSettings('enableHovering')) + return; + + var hover = (renderers[node.type] || renderers.def).create( + node, + self.domElements.nodes[node.id], + self.measurementCanvas, + embedSettings + ); + + self.domElements.hovers[node.id] = hover; + + // Inserting the hover in the dom + self.domElements.groups.hovers.appendChild(hover); + hoveredNode = node; + } + + function outNode(e) { + var node = e.data.node, + embedSettings = self.settings.embedObjects({ + prefix: prefix + }); + + if (!embedSettings('enableHovering')) + return; + + // Deleting element + self.domElements.groups.hovers.removeChild( + self.domElements.hovers[node.id] + ); + hoveredNode = null; + delete self.domElements.hovers[node.id]; + + // Reinstate + self.domElements.groups.nodes.appendChild( + self.domElements.nodes[node.id] + ); + } + + // OPTIMIZE: perform a real update rather than a deletion + function update() { + if (!hoveredNode) + return; + + var embedSettings = self.settings.embedObjects({ + prefix: prefix + }); + + // Deleting element before update + self.domElements.groups.hovers.removeChild( + self.domElements.hovers[hoveredNode.id] + ); + delete self.domElements.hovers[hoveredNode.id]; + + var hover = (renderers[hoveredNode.type] || renderers.def).create( + hoveredNode, + self.domElements.nodes[hoveredNode.id], + self.measurementCanvas, + embedSettings + ); + + self.domElements.hovers[hoveredNode.id] = hover; + + // Inserting the hover in the dom + self.domElements.groups.hovers.appendChild(hover); + } + + // Binding events + this.bind('overNode', overNode); + this.bind('outNode', outNode); + + // Update on render + this.bind('render', update); + }; + + /** + * This method resizes each DOM elements in the container and stores the new + * dimensions. Then, it renders the graph. + * + * @param {?number} width The new width of the container. + * @param {?number} height The new height of the container. + * @return {sigma.renderers.svg} Returns the instance itself. + */ + sigma.renderers.svg.prototype.resize = function(w, h) { + var oldWidth = this.width, + oldHeight = this.height, + pixelRatio = 1; + + if (w !== undefined && h !== undefined) { + this.width = w; + this.height = h; + } else { + this.width = this.container.offsetWidth; + this.height = this.container.offsetHeight; + + w = this.width; + h = this.height; + } + + if (oldWidth !== this.width || oldHeight !== this.height) { + this.domElements.graph.style.width = w + 'px'; + this.domElements.graph.style.height = h + 'px'; + + if (this.domElements.graph.tagName.toLowerCase() === 'svg') { + this.domElements.graph.setAttribute('width', (w * pixelRatio)); + this.domElements.graph.setAttribute('height', (h * pixelRatio)); + } + } + + return this; + }; + + + /** + * The labels, nodes and edges renderers are stored in the three following + * objects. When an element is drawn, its type will be checked and if a + * renderer with the same name exists, it will be used. If not found, the + * default renderer will be used instead. + * + * They are stored in different files, in the "./svg" folder. + */ + sigma.utils.pkg('sigma.svg.nodes'); + sigma.utils.pkg('sigma.svg.edges'); + sigma.utils.pkg('sigma.svg.labels'); +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/sigma.renderers.webgl.js b/blogContent/projects/steam/src/renderers/sigma.renderers.webgl.js new file mode 100644 index 0000000..db58ff5 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/sigma.renderers.webgl.js @@ -0,0 +1,717 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.renderers'); + + /** + * This function is the constructor of the canvas sigma's renderer. + * + * @param {sigma.classes.graph} graph The graph to render. + * @param {sigma.classes.camera} camera The camera. + * @param {configurable} settings The sigma instance settings + * function. + * @param {object} object The options object. + * @return {sigma.renderers.canvas} The renderer instance. + */ + sigma.renderers.webgl = function(graph, camera, settings, options) { + if (typeof options !== 'object') + throw 'sigma.renderers.webgl: Wrong arguments.'; + + if (!(options.container instanceof HTMLElement)) + throw 'Container not found.'; + + var k, + i, + l, + a, + fn, + _self = this; + + sigma.classes.dispatcher.extend(this); + + // Conrad related attributes: + this.jobs = {}; + + Object.defineProperty(this, 'conradId', { + value: sigma.utils.id() + }); + + // Initialize main attributes: + this.graph = graph; + this.camera = camera; + this.contexts = {}; + this.domElements = {}; + this.options = options; + this.container = this.options.container; + this.settings = ( + typeof options.settings === 'object' && + options.settings + ) ? + settings.embedObjects(options.settings) : + settings; + + // Find the prefix: + this.options.prefix = this.camera.readPrefix; + + // Initialize programs hash + Object.defineProperty(this, 'nodePrograms', { + value: {} + }); + Object.defineProperty(this, 'edgePrograms', { + value: {} + }); + Object.defineProperty(this, 'nodeFloatArrays', { + value: {} + }); + Object.defineProperty(this, 'edgeFloatArrays', { + value: {} + }); + Object.defineProperty(this, 'edgeIndicesArrays', { + value: {} + }); + + // Initialize the DOM elements: + if (this.settings(options, 'batchEdgesDrawing')) { + this.initDOM('canvas', 'edges', true); + this.initDOM('canvas', 'nodes', true); + } else { + this.initDOM('canvas', 'scene', true); + this.contexts.nodes = this.contexts.scene; + this.contexts.edges = this.contexts.scene; + } + + this.initDOM('canvas', 'labels'); + this.initDOM('canvas', 'mouse'); + this.contexts.hover = this.contexts.mouse; + + // Initialize captors: + this.captors = []; + a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch]; + for (i = 0, l = a.length; i < l; i++) { + fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]]; + this.captors.push( + new fn( + this.domElements.mouse, + this.camera, + this.settings + ) + ); + } + + // Deal with sigma events: + sigma.misc.bindEvents.call(this, this.camera.prefix); + sigma.misc.drawHovers.call(this, this.camera.prefix); + + this.resize(); + }; + + + + + /** + * This method will generate the nodes and edges float arrays. This step is + * separated from the "render" method, because to keep WebGL efficient, since + * all the camera and middlewares are modelised as matrices and they do not + * require the float arrays to be regenerated. + * + * Basically, when the user moves the camera or applies some specific linear + * transformations, this process step will be skipped, and the "render" + * method will efficiently refresh the rendering. + * + * And when the user modifies the graph colors or positions (applying a new + * layout or filtering the colors, for instance), this "process" step will be + * required to regenerate the float arrays. + * + * @return {sigma.renderers.webgl} Returns the instance itself. + */ + sigma.renderers.webgl.prototype.process = function() { + var a, + i, + l, + k, + type, + renderer, + graph = this.graph, + options = sigma.utils.extend(options, this.options), + defaultEdgeType = this.settings(options, 'defaultEdgeType'), + defaultNodeType = this.settings(options, 'defaultNodeType'); + + // Empty float arrays: + for (k in this.nodeFloatArrays) + delete this.nodeFloatArrays[k]; + + for (k in this.edgeFloatArrays) + delete this.edgeFloatArrays[k]; + + for (k in this.edgeIndicesArrays) + delete this.edgeIndicesArrays[k]; + + // Sort edges and nodes per types: + for (a = graph.edges(), i = 0, l = a.length; i < l; i++) { + type = a[i].type || defaultEdgeType; + k = (type && sigma.webgl.edges[type]) ? type : 'def'; + + if (!this.edgeFloatArrays[k]) + this.edgeFloatArrays[k] = { + edges: [] + }; + + this.edgeFloatArrays[k].edges.push(a[i]); + } + + for (a = graph.nodes(), i = 0, l = a.length; i < l; i++) { + type = a[i].type || defaultNodeType; + k = (type && sigma.webgl.nodes[type]) ? type : 'def'; + + if (!this.nodeFloatArrays[k]) + this.nodeFloatArrays[k] = { + nodes: [] + }; + + this.nodeFloatArrays[k].nodes.push(a[i]); + } + + // Push edges: + for (k in this.edgeFloatArrays) { + renderer = sigma.webgl.edges[k]; + a = this.edgeFloatArrays[k].edges; + + // Creating the necessary arrays + this.edgeFloatArrays[k].array = new Float32Array( + a.length * renderer.POINTS * renderer.ATTRIBUTES + ); + + for (i = 0, l = a.length; i < l; i++) { + + // Just check that the edge and both its extremities are visible: + if ( + !a[i].hidden && + !graph.nodes(a[i].source).hidden && + !graph.nodes(a[i].target).hidden + ) + renderer.addEdge( + a[i], + graph.nodes(a[i].source), + graph.nodes(a[i].target), + this.edgeFloatArrays[k].array, + i * renderer.POINTS * renderer.ATTRIBUTES, + options.prefix, + this.settings + ); + } + + if (typeof renderer.computeIndices === 'function') + this.edgeIndicesArrays[k] = renderer.computeIndices( + this.edgeFloatArrays[k].array + ); + } + + // Push nodes: + for (k in this.nodeFloatArrays) { + renderer = sigma.webgl.nodes[k]; + a = this.nodeFloatArrays[k].nodes; + + // Creating the necessary arrays + this.nodeFloatArrays[k].array = new Float32Array( + a.length * renderer.POINTS * renderer.ATTRIBUTES + ); + + for (i = 0, l = a.length; i < l; i++) { + if (!this.nodeFloatArrays[k].array) + this.nodeFloatArrays[k].array = new Float32Array( + a.length * renderer.POINTS * renderer.ATTRIBUTES + ); + + // Just check that the edge and both its extremities are visible: + if ( + !a[i].hidden + ) + renderer.addNode( + a[i], + this.nodeFloatArrays[k].array, + i * renderer.POINTS * renderer.ATTRIBUTES, + options.prefix, + this.settings + ); + } + } + + return this; + }; + + + + + /** + * This method renders the graph. It basically calls each program (and + * generate them if they do not exist yet) to render nodes and edges, batched + * per renderer. + * + * As in the canvas renderer, it is possible to display edges, nodes and / or + * labels in batches, to make the whole thing way more scalable. + * + * @param {?object} params Eventually an object of options. + * @return {sigma.renderers.webgl} Returns the instance itself. + */ + sigma.renderers.webgl.prototype.render = function(params) { + var a, + i, + l, + k, + o, + program, + renderer, + self = this, + graph = this.graph, + nodesGl = this.contexts.nodes, + edgesGl = this.contexts.edges, + matrix = this.camera.getMatrix(), + options = sigma.utils.extend(params, this.options), + drawLabels = this.settings(options, 'drawLabels'), + drawEdges = this.settings(options, 'drawEdges'), + drawNodes = this.settings(options, 'drawNodes'); + + // Call the resize function: + this.resize(false); + + // Check the 'hideEdgesOnMove' setting: + if (this.settings(options, 'hideEdgesOnMove')) + if (this.camera.isAnimated || this.camera.isMoving) + drawEdges = false; + + // Clear canvases: + this.clear(); + + // Translate matrix to [width/2, height/2]: + matrix = sigma.utils.matrices.multiply( + matrix, + sigma.utils.matrices.translation(this.width / 2, this.height / 2) + ); + + // Kill running jobs: + for (k in this.jobs) + if (conrad.hasJob(k)) + conrad.killJob(k); + + if (drawEdges) { + if (this.settings(options, 'batchEdgesDrawing')) + (function() { + var a, + k, + i, + id, + job, + arr, + end, + start, + indices, + renderer, + batchSize, + currentProgram; + + id = 'edges_' + this.conradId; + batchSize = this.settings(options, 'webglEdgesBatchSize'); + a = Object.keys(this.edgeFloatArrays); + + if (!a.length) + return; + i = 0; + renderer = sigma.webgl.edges[a[i]]; + arr = this.edgeFloatArrays[a[i]].array; + indices = this.edgeIndicesArrays[a[i]]; + start = 0; + end = Math.min( + start + batchSize * renderer.POINTS, + arr.length / renderer.ATTRIBUTES + ); + + job = function() { + // Check program: + if (!this.edgePrograms[a[i]]) + this.edgePrograms[a[i]] = renderer.initProgram(edgesGl); + + if (start < end) { + edgesGl.useProgram(this.edgePrograms[a[i]]); + renderer.render( + edgesGl, + this.edgePrograms[a[i]], + arr, + { + settings: this.settings, + matrix: matrix, + width: this.width, + height: this.height, + ratio: this.camera.ratio, + scalingRatio: this.settings( + options, + 'webglOversamplingRatio' + ), + start: start, + count: end - start, + indicesData: indices + } + ); + } + + // Catch job's end: + if ( + end >= arr.length / renderer.ATTRIBUTES && + i === a.length - 1 + ) { + delete this.jobs[id]; + return false; + } + + if (end >= arr.length / renderer.ATTRIBUTES) { + i++; + arr = this.edgeFloatArrays[a[i]].array; + renderer = sigma.webgl.edges[a[i]]; + start = 0; + end = Math.min( + start + batchSize * renderer.POINTS, + arr.length / renderer.ATTRIBUTES + ); + } else { + start = end; + end = Math.min( + start + batchSize * renderer.POINTS, + arr.length / renderer.ATTRIBUTES + ); + } + + return true; + }; + + this.jobs[id] = job; + conrad.addJob(id, job.bind(this)); + }).call(this); + else { + for (k in this.edgeFloatArrays) { + renderer = sigma.webgl.edges[k]; + + // Check program: + if (!this.edgePrograms[k]) + this.edgePrograms[k] = renderer.initProgram(edgesGl); + + // Render + if (this.edgeFloatArrays[k]) { + edgesGl.useProgram(this.edgePrograms[k]); + renderer.render( + edgesGl, + this.edgePrograms[k], + this.edgeFloatArrays[k].array, + { + settings: this.settings, + matrix: matrix, + width: this.width, + height: this.height, + ratio: this.camera.ratio, + scalingRatio: this.settings(options, 'webglOversamplingRatio'), + indicesData: this.edgeIndicesArrays[k] + } + ); + } + } + } + } + + if (drawNodes) { + // Enable blending: + nodesGl.blendFunc(nodesGl.SRC_ALPHA, nodesGl.ONE_MINUS_SRC_ALPHA); + nodesGl.enable(nodesGl.BLEND); + + for (k in this.nodeFloatArrays) { + renderer = sigma.webgl.nodes[k]; + + // Check program: + if (!this.nodePrograms[k]) + this.nodePrograms[k] = renderer.initProgram(nodesGl); + + // Render + if (this.nodeFloatArrays[k]) { + nodesGl.useProgram(this.nodePrograms[k]); + renderer.render( + nodesGl, + this.nodePrograms[k], + this.nodeFloatArrays[k].array, + { + settings: this.settings, + matrix: matrix, + width: this.width, + height: this.height, + ratio: this.camera.ratio, + scalingRatio: this.settings(options, 'webglOversamplingRatio') + } + ); + } + } + } + + if (drawLabels) { + a = this.camera.quadtree.area( + this.camera.getRectangle(this.width, this.height) + ); + + // Apply camera view to these nodes: + this.camera.applyView( + undefined, + undefined, + { + nodes: a, + edges: [], + width: this.width, + height: this.height + } + ); + + o = function(key) { + return self.settings({ + prefix: self.camera.prefix + }, key); + }; + + for (i = 0, l = a.length; i < l; i++) + if (!a[i].hidden) + ( + sigma.canvas.labels[ + a[i].type || + this.settings(options, 'defaultNodeType') + ] || sigma.canvas.labels.def + )(a[i], this.contexts.labels, o); + } + + this.dispatchEvent('render'); + + return this; + }; + + + + + /** + * This method creates a DOM element of the specified type, switches its + * position to "absolute", references it to the domElements attribute, and + * finally appends it to the container. + * + * @param {string} tag The label tag. + * @param {string} id The id of the element (to store it in + * "domElements"). + * @param {?boolean} webgl Will init the WebGL context if true. + */ + sigma.renderers.webgl.prototype.initDOM = function(tag, id, webgl) { + var gl, + dom = document.createElement(tag), + self = this; + + dom.style.position = 'absolute'; + dom.setAttribute('class', 'sigma-' + id); + + this.domElements[id] = dom; + this.container.appendChild(dom); + + if (tag.toLowerCase() === 'canvas') { + this.contexts[id] = dom.getContext(webgl ? 'experimental-webgl' : '2d', { + preserveDrawingBuffer: true + }); + + // Adding webgl context loss listeners + if (webgl) { + dom.addEventListener('webglcontextlost', function(e) { + e.preventDefault(); + }, false); + + dom.addEventListener('webglcontextrestored', function(e) { + self.render(); + }, false); + } + } + }; + + /** + * This method resizes each DOM elements in the container and stores the new + * dimensions. Then, it renders the graph. + * + * @param {?number} width The new width of the container. + * @param {?number} height The new height of the container. + * @return {sigma.renderers.webgl} Returns the instance itself. + */ + sigma.renderers.webgl.prototype.resize = function(w, h) { + var k, + oldWidth = this.width, + oldHeight = this.height, + pixelRatio = sigma.utils.getPixelRatio(); + + if (w !== undefined && h !== undefined) { + this.width = w; + this.height = h; + } else { + this.width = this.container.offsetWidth; + this.height = this.container.offsetHeight; + + w = this.width; + h = this.height; + } + + if (oldWidth !== this.width || oldHeight !== this.height) { + for (k in this.domElements) { + this.domElements[k].style.width = w + 'px'; + this.domElements[k].style.height = h + 'px'; + + if (this.domElements[k].tagName.toLowerCase() === 'canvas') { + // If simple 2D canvas: + if (this.contexts[k] && this.contexts[k].scale) { + this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px'); + this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px'); + + if (pixelRatio !== 1) + this.contexts[k].scale(pixelRatio, pixelRatio); + } else { + this.domElements[k].setAttribute( + 'width', + (w * this.settings('webglOversamplingRatio')) + 'px' + ); + this.domElements[k].setAttribute( + 'height', + (h * this.settings('webglOversamplingRatio')) + 'px' + ); + } + } + } + } + + // Scale: + for (k in this.contexts) + if (this.contexts[k] && this.contexts[k].viewport) + this.contexts[k].viewport( + 0, + 0, + this.width * this.settings('webglOversamplingRatio'), + this.height * this.settings('webglOversamplingRatio') + ); + + return this; + }; + + /** + * This method clears each canvas. + * + * @return {sigma.renderers.webgl} Returns the instance itself. + */ + sigma.renderers.webgl.prototype.clear = function() { + this.contexts.labels.clearRect(0, 0, this.width, this.height); + this.contexts.nodes.clear(this.contexts.nodes.COLOR_BUFFER_BIT); + this.contexts.edges.clear(this.contexts.edges.COLOR_BUFFER_BIT); + + return this; + }; + + /** + * This method kills contexts and other attributes. + */ + sigma.renderers.webgl.prototype.kill = function() { + var k, + captor; + + // Kill captors: + while ((captor = this.captors.pop())) + captor.kill(); + delete this.captors; + + // Kill contexts: + for (k in this.domElements) { + this.domElements[k].parentNode.removeChild(this.domElements[k]); + delete this.domElements[k]; + delete this.contexts[k]; + } + delete this.domElements; + delete this.contexts; + }; + + + + + /** + * The object "sigma.webgl.nodes" contains the different WebGL node + * renderers. The default one draw nodes as discs. Here are the attributes + * any node renderer must have: + * + * {number} POINTS The number of points required to draw a node. + * {number} ATTRIBUTES The number of attributes needed to draw one point. + * {function} addNode A function that adds a node to the data stack that + * will be given to the buffer. Here is the arguments: + * > {object} node + * > {number} index The node index in the + * nodes array. + * > {Float32Array} data The stack. + * > {object} options Some options. + * {function} render The function that will effectively render the nodes + * into the buffer. + * > {WebGLRenderingContext} gl + * > {WebGLProgram} program + * > {Float32Array} data The stack to give to the + * buffer. + * > {object} params An object containing some + * options, like width, + * height, the camera ratio. + * {function} initProgram The function that will initiate the program, with + * the relevant shaders and parameters. It must return + * the newly created program. + * + * Check sigma.webgl.nodes.def or sigma.webgl.nodes.fast to see how it + * works more precisely. + */ + sigma.utils.pkg('sigma.webgl.nodes'); + + + + + /** + * The object "sigma.webgl.edges" contains the different WebGL edge + * renderers. The default one draw edges as direct lines. Here are the + * attributes any edge renderer must have: + * + * {number} POINTS The number of points required to draw an edge. + * {number} ATTRIBUTES The number of attributes needed to draw one point. + * {function} addEdge A function that adds an edge to the data stack that + * will be given to the buffer. Here is the arguments: + * > {object} edge + * > {object} source + * > {object} target + * > {Float32Array} data The stack. + * > {object} options Some options. + * {function} render The function that will effectively render the edges + * into the buffer. + * > {WebGLRenderingContext} gl + * > {WebGLProgram} program + * > {Float32Array} data The stack to give to the + * buffer. + * > {object} params An object containing some + * options, like width, + * height, the camera ratio. + * {function} initProgram The function that will initiate the program, with + * the relevant shaders and parameters. It must return + * the newly created program. + * + * Check sigma.webgl.edges.def or sigma.webgl.edges.fast to see how it + * works more precisely. + */ + sigma.utils.pkg('sigma.webgl.edges'); + + + + + /** + * The object "sigma.canvas.labels" contains the different + * label renderers for the WebGL renderer. Since displaying texts in WebGL is + * definitely painful and since there a way less labels to display than nodes + * or edges, the default renderer simply renders them in a canvas. + * + * A labels renderer is a simple function, taking as arguments the related + * node, the renderer and a settings function. + */ + sigma.utils.pkg('sigma.canvas.labels'); +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.curve.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.curve.js new file mode 100644 index 0000000..37f82e6 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.curve.js @@ -0,0 +1,84 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.svg.edges'); + + /** + * The curve edge renderer. It renders the node as a bezier curve. + */ + sigma.svg.edges.curve = { + + /** + * SVG Element creation. + * + * @param {object} edge The edge object. + * @param {object} source The source node object. + * @param {object} target The target node object. + * @param {configurable} settings The settings function. + */ + create: function(edge, source, target, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + var path = document.createElementNS(settings('xmlns'), 'path'); + + // Attributes + path.setAttributeNS(null, 'data-edge-id', edge.id); + path.setAttributeNS(null, 'class', settings('classPrefix') + '-edge'); + path.setAttributeNS(null, 'stroke', color); + + return path; + }, + + /** + * SVG Element update. + * + * @param {object} edge The edge object. + * @param {DOMElement} line The line DOM Element. + * @param {object} source The source node object. + * @param {object} target The target node object. + * @param {configurable} settings The settings function. + */ + update: function(edge, path, source, target, settings) { + var prefix = settings('prefix') || ''; + + path.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1); + + // Control point + var cx = (source[prefix + 'x'] + target[prefix + 'x']) / 2 + + (target[prefix + 'y'] - source[prefix + 'y']) / 4, + cy = (source[prefix + 'y'] + target[prefix + 'y']) / 2 + + (source[prefix + 'x'] - target[prefix + 'x']) / 4; + + // Path + var p = 'M' + source[prefix + 'x'] + ',' + source[prefix + 'y'] + ' ' + + 'Q' + cx + ',' + cy + ' ' + + target[prefix + 'x'] + ',' + target[prefix + 'y']; + + // Updating attributes + path.setAttributeNS(null, 'd', p); + path.setAttributeNS(null, 'fill', 'none'); + + // Showing + path.style.display = ''; + + return this; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.def.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.def.js new file mode 100644 index 0000000..e48d57b --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.edges.def.js @@ -0,0 +1,73 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.svg.edges'); + + /** + * The default edge renderer. It renders the node as a simple line. + */ + sigma.svg.edges.def = { + + /** + * SVG Element creation. + * + * @param {object} edge The edge object. + * @param {object} source The source node object. + * @param {object} target The target node object. + * @param {configurable} settings The settings function. + */ + create: function(edge, source, target, settings) { + var color = edge.color, + prefix = settings('prefix') || '', + edgeColor = settings('edgeColor'), + defaultNodeColor = settings('defaultNodeColor'), + defaultEdgeColor = settings('defaultEdgeColor'); + + if (!color) + switch (edgeColor) { + case 'source': + color = source.color || defaultNodeColor; + break; + case 'target': + color = target.color || defaultNodeColor; + break; + default: + color = defaultEdgeColor; + break; + } + + var line = document.createElementNS(settings('xmlns'), 'line'); + + // Attributes + line.setAttributeNS(null, 'data-edge-id', edge.id); + line.setAttributeNS(null, 'class', settings('classPrefix') + '-edge'); + line.setAttributeNS(null, 'stroke', color); + + return line; + }, + + /** + * SVG Element update. + * + * @param {object} edge The edge object. + * @param {DOMElement} line The line DOM Element. + * @param {object} source The source node object. + * @param {object} target The target node object. + * @param {configurable} settings The settings function. + */ + update: function(edge, line, source, target, settings) { + var prefix = settings('prefix') || ''; + + line.setAttributeNS(null, 'stroke-width', edge[prefix + 'size'] || 1); + line.setAttributeNS(null, 'x1', source[prefix + 'x']); + line.setAttributeNS(null, 'y1', source[prefix + 'y']); + line.setAttributeNS(null, 'x2', target[prefix + 'x']); + line.setAttributeNS(null, 'y2', target[prefix + 'y']); + + // Showing + line.style.display = ''; + + return this; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.hovers.def.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.hovers.def.js new file mode 100644 index 0000000..6525ab9 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.hovers.def.js @@ -0,0 +1,113 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.svg.hovers'); + + /** + * The default hover renderer. + */ + sigma.svg.hovers.def = { + + /** + * SVG Element creation. + * + * @param {object} node The node object. + * @param {CanvasElement} measurementCanvas A fake canvas handled by + * the svg to perform some measurements and + * passed by the renderer. + * @param {DOMElement} nodeCircle The node DOM Element. + * @param {configurable} settings The settings function. + */ + create: function(node, nodeCircle, measurementCanvas, settings) { + + // Defining visual properties + var x, + y, + w, + h, + e, + d, + fontStyle = settings('hoverFontStyle') || settings('fontStyle'), + prefix = settings('prefix') || '', + size = node[prefix + 'size'], + fontSize = (settings('labelSize') === 'fixed') ? + settings('defaultLabelSize') : + settings('labelSizeRatio') * size, + fontColor = (settings('labelHoverColor') === 'node') ? + (node.color || settings('defaultNodeColor')) : + settings('defaultLabelHoverColor'); + + // Creating elements + var group = document.createElementNS(settings('xmlns'), 'g'), + rectangle = document.createElementNS(settings('xmlns'), 'rect'), + circle = document.createElementNS(settings('xmlns'), 'circle'), + text = document.createElementNS(settings('xmlns'), 'text'); + + // Defining properties + group.setAttributeNS(null, 'class', settings('classPrefix') + '-hover'); + group.setAttributeNS(null, 'data-node-id', node.id); + + if (typeof node.label === 'string') { + + // Text + text.innerHTML = node.label; + text.textContent = node.label; + text.setAttributeNS( + null, + 'class', + settings('classPrefix') + '-hover-label'); + text.setAttributeNS(null, 'font-size', fontSize); + text.setAttributeNS(null, 'font-family', settings('font')); + text.setAttributeNS(null, 'fill', fontColor); + text.setAttributeNS(null, 'x', + Math.round(node[prefix + 'x'] + size + 3)); + text.setAttributeNS(null, 'y', + Math.round(node[prefix + 'y'] + fontSize / 3)); + + // Measures + // OPTIMIZE: Find a better way than a measurement canvas + x = Math.round(node[prefix + 'x'] - fontSize / 2 - 2); + y = Math.round(node[prefix + 'y'] - fontSize / 2 - 2); + w = Math.round( + measurementCanvas.measureText(node.label).width + + fontSize / 2 + size + 9 + ); + h = Math.round(fontSize + 4); + e = Math.round(fontSize / 2 + 2); + + // Circle + circle.setAttributeNS( + null, + 'class', + settings('classPrefix') + '-hover-area'); + circle.setAttributeNS(null, 'fill', '#fff'); + circle.setAttributeNS(null, 'cx', node[prefix + 'x']); + circle.setAttributeNS(null, 'cy', node[prefix + 'y']); + circle.setAttributeNS(null, 'r', e); + + // Rectangle + rectangle.setAttributeNS( + null, + 'class', + settings('classPrefix') + '-hover-area'); + rectangle.setAttributeNS(null, 'fill', '#fff'); + rectangle.setAttributeNS(null, 'x', node[prefix + 'x'] + e / 4); + rectangle.setAttributeNS(null, 'y', node[prefix + 'y'] - e); + rectangle.setAttributeNS(null, 'width', w); + rectangle.setAttributeNS(null, 'height', h); + } + + // Appending childs + group.appendChild(circle); + group.appendChild(rectangle); + group.appendChild(text); + group.appendChild(nodeCircle); + + return group; + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.labels.def.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.labels.def.js new file mode 100644 index 0000000..4027c83 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.labels.def.js @@ -0,0 +1,80 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.svg.labels'); + + /** + * The default label renderer. It renders the label as a simple text. + */ + sigma.svg.labels.def = { + + /** + * SVG Element creation. + * + * @param {object} node The node object. + * @param {configurable} settings The settings function. + */ + create: function(node, settings) { + var prefix = settings('prefix') || '', + size = node[prefix + 'size'], + text = document.createElementNS(settings('xmlns'), 'text'); + + var fontSize = (settings('labelSize') === 'fixed') ? + settings('defaultLabelSize') : + settings('labelSizeRatio') * size; + + var fontColor = (settings('labelColor') === 'node') ? + (node.color || settings('defaultNodeColor')) : + settings('defaultLabelColor'); + + text.setAttributeNS(null, 'data-label-target', node.id); + text.setAttributeNS(null, 'class', settings('classPrefix') + '-label'); + text.setAttributeNS(null, 'font-size', fontSize); + text.setAttributeNS(null, 'font-family', settings('font')); + text.setAttributeNS(null, 'fill', fontColor); + + text.innerHTML = node.label; + text.textContent = node.label; + + return text; + }, + + /** + * SVG Element update. + * + * @param {object} node The node object. + * @param {DOMElement} text The label DOM element. + * @param {configurable} settings The settings function. + */ + update: function(node, text, settings) { + var prefix = settings('prefix') || '', + size = node[prefix + 'size']; + + var fontSize = (settings('labelSize') === 'fixed') ? + settings('defaultLabelSize') : + settings('labelSizeRatio') * size; + + // Case when we don't want to display the label + if (!settings('forceLabels') && size < settings('labelThreshold')) + return; + + if (typeof node.label !== 'string') + return; + + // Updating + text.setAttributeNS(null, 'x', + Math.round(node[prefix + 'x'] + size + 3)); + text.setAttributeNS(null, 'y', + Math.round(node[prefix + 'y'] + fontSize / 3)); + + // Showing + text.style.display = ''; + + return this; + } + }; +}).call(this); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.nodes.def.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.nodes.def.js new file mode 100644 index 0000000..4c01b7a --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.nodes.def.js @@ -0,0 +1,58 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.svg.nodes'); + + /** + * The default node renderer. It renders the node as a simple disc. + */ + sigma.svg.nodes.def = { + + /** + * SVG Element creation. + * + * @param {object} node The node object. + * @param {configurable} settings The settings function. + */ + create: function(node, settings) { + var prefix = settings('prefix') || '', + circle = document.createElementNS(settings('xmlns'), 'circle'); + + // Defining the node's circle + circle.setAttributeNS(null, 'data-node-id', node.id); + circle.setAttributeNS(null, 'class', settings('classPrefix') + '-node'); + circle.setAttributeNS( + null, 'fill', node.color || settings('defaultNodeColor')); + + // Returning the DOM Element + return circle; + }, + + /** + * SVG Element update. + * + * @param {object} node The node object. + * @param {DOMElement} circle The node DOM element. + * @param {configurable} settings The settings function. + */ + update: function(node, circle, settings) { + var prefix = settings('prefix') || ''; + + // Applying changes + // TODO: optimize - check if necessary + circle.setAttributeNS(null, 'cx', node[prefix + 'x']); + circle.setAttributeNS(null, 'cy', node[prefix + 'y']); + circle.setAttributeNS(null, 'r', node[prefix + 'size']); + + // Updating only if not freestyle + if (!settings('freeStyle')) + circle.setAttributeNS( + null, 'fill', node.color || settings('defaultNodeColor')); + + // Showing + circle.style.display = ''; + + return this; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/svg/sigma.svg.utils.js b/blogContent/projects/steam/src/renderers/svg/sigma.svg.utils.js new file mode 100644 index 0000000..f00e2e5 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/svg/sigma.svg.utils.js @@ -0,0 +1,31 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.svg.utils'); + + /** + * Some useful functions used by sigma's SVG renderer. + */ + sigma.svg.utils = { + + /** + * SVG Element show. + * + * @param {DOMElement} element The DOM element to show. + */ + show: function(element) { + element.style.display = ''; + return this; + }, + + /** + * SVG Element hide. + * + * @param {DOMElement} element The DOM element to hide. + */ + hide: function(element) { + element.style.display = 'none'; + return this; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.arrow.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.arrow.js new file mode 100644 index 0000000..4b548ac --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.arrow.js @@ -0,0 +1,391 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This edge renderer will display edges as arrows going from the source node + * to the target node. To deal with edge thicknesses, the lines are made of + * three triangles: two forming rectangles, with the gl.TRIANGLES drawing + * mode. + * + * It is expensive, since drawing a single edge requires 9 points, each + * having a lot of attributes. + */ + sigma.webgl.edges.arrow = { + POINTS: 9, + ATTRIBUTES: 11, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var w = (edge[prefix + 'size'] || 1) / 2, + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + targetSize = target[prefix + 'size'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 0.0; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = 0.0; + data[i++] = color; + + // Arrow head: + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 1.0; + data[i++] = -1.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = targetSize; + data[i++] = 1.0; + data[i++] = 0.0; + data[i++] = 1.0; + data[i++] = 1.0; + data[i++] = color; + }, + render: function(gl, program, data, params) { + var buffer; + + // Define attributes: + var positionLocation1 = + gl.getAttribLocation(program, 'a_pos1'), + positionLocation2 = + gl.getAttribLocation(program, 'a_pos2'), + thicknessLocation = + gl.getAttribLocation(program, 'a_thickness'), + targetSizeLocation = + gl.getAttribLocation(program, 'a_tSize'), + delayLocation = + gl.getAttribLocation(program, 'a_delay'), + minusLocation = + gl.getAttribLocation(program, 'a_minus'), + headLocation = + gl.getAttribLocation(program, 'a_head'), + headPositionLocation = + gl.getAttribLocation(program, 'a_headPosition'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'), + matrixHalfPiLocation = + gl.getUniformLocation(program, 'u_matrixHalfPi'), + matrixHalfPiMinusLocation = + gl.getUniformLocation(program, 'u_matrixHalfPiMinus'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + nodeRatioLocation = + gl.getUniformLocation(program, 'u_nodeRatio'), + arrowHeadLocation = + gl.getUniformLocation(program, 'u_arrowHead'), + scaleLocation = + gl.getUniformLocation(program, 'u_scale'); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio')) + ); + gl.uniform1f( + nodeRatioLocation, + Math.pow(params.ratio, params.settings('nodesPowRatio')) / + params.ratio + ); + gl.uniform1f(arrowHeadLocation, 5.0); + gl.uniform1f(scaleLocation, params.scalingRatio); + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + gl.uniformMatrix2fv( + matrixHalfPiLocation, + false, + sigma.utils.matrices.rotation(Math.PI / 2, true) + ); + gl.uniformMatrix2fv( + matrixHalfPiMinusLocation, + false, + sigma.utils.matrices.rotation(-Math.PI / 2, true) + ); + + gl.enableVertexAttribArray(positionLocation1); + gl.enableVertexAttribArray(positionLocation2); + gl.enableVertexAttribArray(thicknessLocation); + gl.enableVertexAttribArray(targetSizeLocation); + gl.enableVertexAttribArray(delayLocation); + gl.enableVertexAttribArray(minusLocation); + gl.enableVertexAttribArray(headLocation); + gl.enableVertexAttribArray(headPositionLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer(positionLocation1, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(positionLocation2, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer(thicknessLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 16 + ); + gl.vertexAttribPointer(targetSizeLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 20 + ); + gl.vertexAttribPointer(delayLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 24 + ); + gl.vertexAttribPointer(minusLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 28 + ); + gl.vertexAttribPointer(headLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 32 + ); + gl.vertexAttribPointer(headPositionLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 36 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 40 + ); + + gl.drawArrays( + gl.TRIANGLES, + params.start || 0, + params.count || (data.length / this.ATTRIBUTES) + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_pos1;', + 'attribute vec2 a_pos2;', + 'attribute float a_thickness;', + 'attribute float a_tSize;', + 'attribute float a_delay;', + 'attribute float a_minus;', + 'attribute float a_head;', + 'attribute float a_headPosition;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform float u_nodeRatio;', + 'uniform float u_arrowHead;', + 'uniform float u_scale;', + 'uniform mat3 u_matrix;', + 'uniform mat2 u_matrixHalfPi;', + 'uniform mat2 u_matrixHalfPiMinus;', + + 'varying vec4 color;', + + 'void main() {', + // Find the good point: + 'vec2 pos = normalize(a_pos2 - a_pos1);', + + 'mat2 matrix = (1.0 - a_head) *', + '(', + 'a_minus * u_matrixHalfPiMinus +', + '(1.0 - a_minus) * u_matrixHalfPi', + ') + a_head * (', + 'a_headPosition * u_matrixHalfPiMinus * 0.6 +', + '(a_headPosition * a_headPosition - 1.0) * mat2(1.0)', + ');', + + 'pos = a_pos1 + (', + // Deal with body: + '(1.0 - a_head) * a_thickness * u_ratio * matrix * pos +', + // Deal with head: + 'a_head * u_arrowHead * a_thickness * u_ratio * matrix * pos +', + // Deal with delay: + 'a_delay * pos * (', + 'a_tSize / u_nodeRatio +', + 'u_arrowHead * a_thickness * u_ratio', + ')', + ');', + + // Scale from [[-1 1] [-1 1]] to the container: + 'gl_Position = vec4(', + '((u_matrix * vec3(pos, 1)).xy /', + 'u_resolution * 2.0 - 1.0) * vec2(1, -1),', + '0,', + '1', + ');', + + // Extract the color: + 'float c = a_color;', + 'color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;', + 'color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 color;', + + 'void main(void) {', + 'gl_FragColor = color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.def.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.def.js new file mode 100644 index 0000000..6931bb5 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.def.js @@ -0,0 +1,258 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This edge renderer will display edges as lines going from the source node + * to the target node. To deal with edge thicknesses, the lines are made of + * two triangles forming rectangles, with the gl.TRIANGLES drawing mode. + * + * It is expensive, since drawing a single edge requires 6 points, each + * having 7 attributes (source position, target position, thickness, color + * and a flag indicating which vertice of the rectangle it is). + */ + sigma.webgl.edges.def = { + POINTS: 6, + ATTRIBUTES: 7, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var w = (edge[prefix + 'size'] || 1) / 2, + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = 1.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = w; + data[i++] = 0.0; + data[i++] = color; + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = 1.0; + data[i++] = color; + + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = w; + data[i++] = 0.0; + data[i++] = color; + }, + render: function(gl, program, data, params) { + var buffer; + + // Define attributes: + var colorLocation = + gl.getAttribLocation(program, 'a_color'), + positionLocation1 = + gl.getAttribLocation(program, 'a_position1'), + positionLocation2 = + gl.getAttribLocation(program, 'a_position2'), + thicknessLocation = + gl.getAttribLocation(program, 'a_thickness'), + minusLocation = + gl.getAttribLocation(program, 'a_minus'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'), + matrixHalfPiLocation = + gl.getUniformLocation(program, 'u_matrixHalfPi'), + matrixHalfPiMinusLocation = + gl.getUniformLocation(program, 'u_matrixHalfPiMinus'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + scaleLocation = + gl.getUniformLocation(program, 'u_scale'); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio')) + ); + gl.uniform1f(scaleLocation, params.scalingRatio); + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + gl.uniformMatrix2fv( + matrixHalfPiLocation, + false, + sigma.utils.matrices.rotation(Math.PI / 2, true) + ); + gl.uniformMatrix2fv( + matrixHalfPiMinusLocation, + false, + sigma.utils.matrices.rotation(-Math.PI / 2, true) + ); + + gl.enableVertexAttribArray(colorLocation); + gl.enableVertexAttribArray(positionLocation1); + gl.enableVertexAttribArray(positionLocation2); + gl.enableVertexAttribArray(thicknessLocation); + gl.enableVertexAttribArray(minusLocation); + + gl.vertexAttribPointer(positionLocation1, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(positionLocation2, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer(thicknessLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 16 + ); + gl.vertexAttribPointer(minusLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 20 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 24 + ); + + gl.drawArrays( + gl.TRIANGLES, + params.start || 0, + params.count || (data.length / this.ATTRIBUTES) + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position1;', + 'attribute vec2 a_position2;', + 'attribute float a_thickness;', + 'attribute float a_minus;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform float u_scale;', + 'uniform mat3 u_matrix;', + 'uniform mat2 u_matrixHalfPi;', + 'uniform mat2 u_matrixHalfPiMinus;', + + 'varying vec4 color;', + + 'void main() {', + // Find the good point: + 'vec2 position = a_thickness * u_ratio *', + 'normalize(a_position2 - a_position1);', + + 'mat2 matrix = a_minus * u_matrixHalfPiMinus +', + '(1.0 - a_minus) * u_matrixHalfPi;', + + 'position = matrix * position + a_position1;', + + // Scale from [[-1 1] [-1 1]] to the container: + 'gl_Position = vec4(', + '((u_matrix * vec3(position, 1)).xy /', + 'u_resolution * 2.0 - 1.0) * vec2(1, -1),', + '0,', + '1', + ');', + + // Extract the color: + 'float c = a_color;', + 'color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;', + 'color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 color;', + + 'void main(void) {', + 'gl_FragColor = color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.fast.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.fast.js new file mode 100644 index 0000000..48f56d7 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.fast.js @@ -0,0 +1,147 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This edge renderer will display edges as lines with the gl.LINES display + * mode. Since this mode does not support well thickness, edges are all drawn + * with the same thickness (3px), independantly of the edge attributes or the + * zooming ratio. + */ + sigma.webgl.edges.fast = { + POINTS: 2, + ATTRIBUTES: 3, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var w = (edge[prefix + 'size'] || 1) / 2, + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + data[i++] = x1; + data[i++] = y1; + data[i++] = color; + + data[i++] = x2; + data[i++] = y2; + data[i++] = color; + }, + render: function(gl, program, data, params) { + var buffer; + + // Define attributes: + var colorLocation = + gl.getAttribLocation(program, 'a_color'), + positionLocation = + gl.getAttribLocation(program, 'a_position'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW); + + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + gl.enableVertexAttribArray(positionLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer(positionLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + + gl.lineWidth(3); + gl.drawArrays( + gl.LINES, + params.start || 0, + params.count || (data.length / this.ATTRIBUTES) + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform mat3 u_matrix;', + + 'varying vec4 color;', + + 'void main() {', + // Scale from [[-1 1] [-1 1]] to the container: + 'gl_Position = vec4(', + '((u_matrix * vec3(a_position, 1)).xy /', + 'u_resolution * 2.0 - 1.0) * vec2(1, -1),', + '0,', + '1', + ');', + + // Extract the color: + 'float c = a_color;', + 'color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;', + 'color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 color;', + + 'void main(void) {', + 'gl_FragColor = color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLine.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLine.js new file mode 100644 index 0000000..5f3d3bd --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLine.js @@ -0,0 +1,255 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This will render edges as thick lines using four points translated + * orthogonally from the source & target's centers by half thickness. + * + * Rendering two triangles by using only four points is made possible through + * the use of indices. + * + * This method should be faster than the 6 points / 2 triangles approach and + * should handle thickness better than with gl.LINES. + * + * This version of the shader balances geometry computation evenly between + * the CPU & GPU (normals are computed on the CPU side). + */ + sigma.webgl.edges.thickLine = { + POINTS: 4, + ATTRIBUTES: 6, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var thickness = (edge[prefix + 'size'] || 1), + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + // Computing normals: + var dx = x2 - x1, + dy = y2 - y1, + len = dx * dx + dy * dy, + normals; + + if (!len) { + normals = [0, 0]; + } + else { + len = 1 / Math.sqrt(len); + + var normals = [ + -dy * len, + dx * len + ]; + } + + // First point + data[i++] = x1; + data[i++] = y1; + data[i++] = normals[0]; + data[i++] = normals[1]; + data[i++] = thickness; + data[i++] = color; + + // First point flipped + data[i++] = x1; + data[i++] = y1; + data[i++] = -normals[0]; + data[i++] = -normals[1]; + data[i++] = thickness; + data[i++] = color; + + // Second point + data[i++] = x2; + data[i++] = y2; + data[i++] = normals[0]; + data[i++] = normals[1]; + data[i++] = thickness; + data[i++] = color; + + // Second point flipped + data[i++] = x2; + data[i++] = y2; + data[i++] = -normals[0]; + data[i++] = -normals[1]; + data[i++] = thickness; + data[i++] = color; + }, + computeIndices: function(data) { + var indices = new Uint16Array(data.length * 6), + c = 0, + i = 0, + j, + l; + + for (j = 0, l = data.length / this.ATTRIBUTES; i < l; i++) { + indices[c++] = i + 0; + indices[c++] = i + 1; + indices[c++] = i + 2; + indices[c++] = i + 2; + indices[c++] = i + 1; + indices[c++] = i + 3; + i += 3; + } + + return indices; + }, + render: function(gl, program, data, params) { + + // Define attributes: + var positionLocation = + gl.getAttribLocation(program, 'a_position'), + normalLocation = + gl.getAttribLocation(program, 'a_normal'), + thicknessLocation = + gl.getAttribLocation(program, 'a_thickness'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'); + + // Creating buffer: + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + + // Binding uniforms: + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio')) + ); + + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + // Binding attributes: + gl.enableVertexAttribArray(positionLocation); + gl.enableVertexAttribArray(normalLocation); + gl.enableVertexAttribArray(thicknessLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer(positionLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(normalLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer(thicknessLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 16 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 20 + ); + + // Creating indices buffer: + var indicesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, params.indicesData, gl.STATIC_DRAW); + + // Drawing: + gl.drawElements( + gl.TRIANGLES, + params.indicesData.length, + gl.UNSIGNED_SHORT, + params.start || 0 + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position;', + 'attribute vec2 a_normal;', + 'attribute float a_thickness;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform mat3 u_matrix;', + + 'varying vec4 v_color;', + + 'void main() {', + + // Scale from [[-1 1] [-1 1]] to the container: + 'vec2 delta = vec2(a_normal * a_thickness / 2.0);', + 'vec2 position = (u_matrix * vec3(a_position + delta, 1)).xy;', + 'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);', + + // Applying + 'gl_Position = vec4(position, 0, 1);', + 'gl_PointSize = 10.0;', + + // Extract the color: + 'float c = a_color;', + 'v_color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.r = mod(c, 256.0); c = floor(c / 256.0); v_color /= 255.0;', + 'v_color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 v_color;', + + 'void main(void) {', + 'gl_FragColor = v_color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineCPU.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineCPU.js new file mode 100644 index 0000000..0eda8bf --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineCPU.js @@ -0,0 +1,220 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This will render edges as thick lines using four points translated + * orthogonally from the source & target's centers by half thickness. + * + * Rendering two triangles by using only four points is made possible through + * the use of indices. + * + * This method should be faster than the 6 points / 2 triangles approach and + * should handle thickness better than with gl.LINES. + * + * This version of the shader computes geometry on the CPU side to make + * the handled array buffer much lighter. + */ + sigma.webgl.edges.thickLineCPU = { + POINTS: 4, + ATTRIBUTES: 3, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var thickness = (edge[prefix + 'size'] || 1) / 2, + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + // Computing normals: + var dx = x2 - x1, + dy = y2 - y1, + len = dx * dx + dy * dy, + normals; + + if (!len) { + normals = [0, 0]; + } + else { + len = 1 / Math.sqrt(len); + + var normals = [ + -dy * len * thickness, + dx * len * thickness + ]; + } + + // First point + data[i++] = x1 + normals[0]; + data[i++] = y1 + normals[1]; + data[i++] = color; + + // First point flipped + data[i++] = x1 - normals[0]; + data[i++] = y1 - normals[1]; + data[i++] = color; + + // Second point + data[i++] = x2 + normals[0]; + data[i++] = y2 + normals[1]; + data[i++] = color; + + // Second point flipped + data[i++] = x2 - normals[0]; + data[i++] = y2 - normals[1]; + data[i++] = color; + }, + computeIndices: function(data) { + var indices = new Uint16Array(data.length * 6), + c = 0, + i = 0, + j, + l; + + for (j = 0, l = data.length / this.ATTRIBUTES; i < l; i++) { + indices[c++] = i + 0; + indices[c++] = i + 1; + indices[c++] = i + 2; + indices[c++] = i + 2; + indices[c++] = i + 1; + indices[c++] = i + 3; + i += 3; + } + + return indices; + }, + render: function(gl, program, data, params) { + + // Define attributes: + var positionLocation = + gl.getAttribLocation(program, 'a_position'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'); + + // Creating buffer: + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + + // Binding uniforms: + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio')) + ); + + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + // Binding attributes: + gl.enableVertexAttribArray(positionLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer(positionLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + + // Creating indices buffer: + var indicesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, params.indicesData, gl.STATIC_DRAW); + + // Drawing: + gl.drawElements( + gl.TRIANGLES, + params.indicesData.length, + gl.UNSIGNED_SHORT, + params.start || 0 + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform mat3 u_matrix;', + + 'varying vec4 v_color;', + + 'void main() {', + + // Scale from [[-1 1] [-1 1]] to the container: + 'vec2 position = (u_matrix * vec3(a_position, 1)).xy;', + 'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);', + + // Applying + 'gl_Position = vec4(position, 0, 1);', + 'gl_PointSize = 10.0;', + + // Extract the color: + 'float c = a_color;', + 'v_color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.r = mod(c, 256.0); c = floor(c / 256.0); v_color /= 255.0;', + 'v_color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 v_color;', + + 'void main(void) {', + 'gl_FragColor = v_color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineGPU.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineGPU.js new file mode 100644 index 0000000..e34fe55 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.edges.thickLineGPU.js @@ -0,0 +1,255 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.edges'); + + /** + * This will render edges as thick lines using four points translated + * orthogonally from the source & target's centers by half thickness. + * + * Rendering two triangles by using only four points is made possible through + * the use of indices. + * + * This method should be faster than the 6 points / 2 triangles approach and + * should handle thickness better than with gl.LINES. + * + * This version of the shader computes geometry on the GPU side only, making + * the handled array buffer heavier but sparing costly computation to the + * CPU side. + */ + sigma.webgl.edges.thickLineGPU = { + POINTS: 4, + ATTRIBUTES: 7, + addEdge: function(edge, source, target, data, i, prefix, settings) { + var thickness = (edge[prefix + 'size'] || 1), + x1 = source[prefix + 'x'], + y1 = source[prefix + 'y'], + x2 = target[prefix + 'x'], + y2 = target[prefix + 'y'], + color = edge.color; + + if (!color) + switch (settings('edgeColor')) { + case 'source': + color = source.color || settings('defaultNodeColor'); + break; + case 'target': + color = target.color || settings('defaultNodeColor'); + break; + default: + color = settings('defaultEdgeColor'); + break; + } + + // Normalize color: + color = sigma.utils.floatColor(color); + + // First point + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = 1.0; + data[i++] = thickness; + data[i++] = color; + + // First point flipped + data[i++] = x1; + data[i++] = y1; + data[i++] = x2; + data[i++] = y2; + data[i++] = -1.0; + data[i++] = thickness; + data[i++] = color; + + // Second point + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = 1.0; + data[i++] = thickness; + data[i++] = color; + + // Second point flipped + data[i++] = x2; + data[i++] = y2; + data[i++] = x1; + data[i++] = y1; + data[i++] = -1.0; + data[i++] = thickness; + data[i++] = color; + }, + computeIndices: function(data) { + var indices = new Uint16Array(data.length * 6), + c = 0, + i = 0, + j, + l; + + for (j = 0, l = data.length / this.ATTRIBUTES; i < l; i++) { + indices[c++] = i + 0; + indices[c++] = i + 1; + indices[c++] = i + 2; + indices[c++] = i + 2; + indices[c++] = i + 1; + indices[c++] = i + 3; + i += 3; + } + + return indices; + }, + render: function(gl, program, data, params) { + + // Define attributes: + var position1Location = + gl.getAttribLocation(program, 'a_position1'), + position2Location = + gl.getAttribLocation(program, 'a_position2'), + directionLocation = + gl.getAttribLocation(program, 'a_direction'), + thicknessLocation = + gl.getAttribLocation(program, 'a_thickness'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'); + + // Creating buffer: + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); + + // Binding uniforms: + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + params.ratio / Math.pow(params.ratio, params.settings('edgesPowRatio')) + ); + + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + // Binding attributes: + gl.enableVertexAttribArray(position1Location); + gl.enableVertexAttribArray(position2Location); + gl.enableVertexAttribArray(directionLocation); + gl.enableVertexAttribArray(thicknessLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer(position1Location, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer(position2Location, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer(directionLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 16 + ); + gl.vertexAttribPointer(thicknessLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 20 + ); + gl.vertexAttribPointer(colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 24 + ); + + // Creating indices buffer: + var indicesBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, params.indicesData, gl.STATIC_DRAW); + + // Drawing: + gl.drawElements( + gl.TRIANGLES, + params.indicesData.length, + gl.UNSIGNED_SHORT, + params.start || 0 + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position1;', + 'attribute vec2 a_position2;', + 'attribute float a_direction;', + 'attribute float a_thickness;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform mat3 u_matrix;', + + 'varying vec4 v_color;', + + 'void main() {', + + // Scale from [[-1 1] [-1 1]] to the container: + 'vec2 translation = a_position2 - a_position1;', + 'vec2 orthogonal = vec2(translation.y, -translation.x);', + 'vec2 delta = a_thickness / 2.0 * a_direction * normalize(orthogonal);', + 'vec2 position = (u_matrix * vec3(a_position1 + delta, 1)).xy;', + 'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);', + + // Applying + 'gl_Position = vec4(position, 0, 1);', + 'gl_PointSize = 10.0;', + + // Extract the color: + 'float c = a_color;', + 'v_color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'v_color.r = mod(c, 256.0); c = floor(c / 256.0); v_color /= 255.0;', + 'v_color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER, function(error) {console.log(error);} + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 v_color;', + + 'void main(void) {', + 'gl_FragColor = v_color;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.def.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.def.js new file mode 100644 index 0000000..d169713 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.def.js @@ -0,0 +1,201 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.nodes'); + + /** + * This node renderer will display nodes as discs, shaped in triangles with + * the gl.TRIANGLES display mode. So, to be more precise, to draw one node, + * it will store three times the center of node, with the color and the size, + * and an angle indicating which "corner" of the triangle to draw. + * + * The fragment shader does not deal with anti-aliasing, so make sure that + * you deal with it somewhere else in the code (by default, the WebGL + * renderer will oversample the rendering through the webglOversamplingRatio + * value). + */ + sigma.webgl.nodes.def = { + POINTS: 3, + ATTRIBUTES: 5, + addNode: function(node, data, i, prefix, settings) { + var color = sigma.utils.floatColor( + node.color || settings('defaultNodeColor') + ); + + data[i++] = node[prefix + 'x']; + data[i++] = node[prefix + 'y']; + data[i++] = node[prefix + 'size']; + data[i++] = color; + data[i++] = 0; + + data[i++] = node[prefix + 'x']; + data[i++] = node[prefix + 'y']; + data[i++] = node[prefix + 'size']; + data[i++] = color; + data[i++] = 2 * Math.PI / 3; + + data[i++] = node[prefix + 'x']; + data[i++] = node[prefix + 'y']; + data[i++] = node[prefix + 'size']; + data[i++] = color; + data[i++] = 4 * Math.PI / 3; + }, + render: function(gl, program, data, params) { + var buffer; + + // Define attributes: + var positionLocation = + gl.getAttribLocation(program, 'a_position'), + sizeLocation = + gl.getAttribLocation(program, 'a_size'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + angleLocation = + gl.getAttribLocation(program, 'a_angle'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + scaleLocation = + gl.getUniformLocation(program, 'u_scale'); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW); + + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + 1 / Math.pow(params.ratio, params.settings('nodesPowRatio')) + ); + gl.uniform1f(scaleLocation, params.scalingRatio); + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + gl.enableVertexAttribArray(positionLocation); + gl.enableVertexAttribArray(sizeLocation); + gl.enableVertexAttribArray(colorLocation); + gl.enableVertexAttribArray(angleLocation); + + gl.vertexAttribPointer( + positionLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer( + sizeLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer( + colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 12 + ); + gl.vertexAttribPointer( + angleLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 16 + ); + + gl.drawArrays( + gl.TRIANGLES, + params.start || 0, + params.count || (data.length / this.ATTRIBUTES) + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position;', + 'attribute float a_size;', + 'attribute float a_color;', + 'attribute float a_angle;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform float u_scale;', + 'uniform mat3 u_matrix;', + + 'varying vec4 color;', + 'varying vec2 center;', + 'varying float radius;', + + 'void main() {', + // Multiply the point size twice: + 'radius = a_size * u_ratio;', + + // Scale from [[-1 1] [-1 1]] to the container: + 'vec2 position = (u_matrix * vec3(a_position, 1)).xy;', + // 'center = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);', + 'center = position * u_scale;', + 'center = vec2(center.x, u_scale * u_resolution.y - center.y);', + + 'position = position +', + '2.0 * radius * vec2(cos(a_angle), sin(a_angle));', + 'position = (position / u_resolution * 2.0 - 1.0) * vec2(1, -1);', + + 'radius = radius * u_scale;', + + 'gl_Position = vec4(position, 0, 1);', + + // Extract the color: + 'float c = a_color;', + 'color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;', + 'color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 color;', + 'varying vec2 center;', + 'varying float radius;', + + 'void main(void) {', + 'vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);', + + 'vec2 m = gl_FragCoord.xy - center;', + 'float diff = radius - sqrt(m.x * m.x + m.y * m.y);', + + // Here is how we draw a disc instead of a square: + 'if (diff > 0.0)', + 'gl_FragColor = color;', + 'else', + 'gl_FragColor = color0;', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.fast.js b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.fast.js new file mode 100644 index 0000000..2af7bb0 --- /dev/null +++ b/blogContent/projects/steam/src/renderers/webgl/sigma.webgl.nodes.fast.js @@ -0,0 +1,176 @@ +;(function() { + 'use strict'; + + sigma.utils.pkg('sigma.webgl.nodes'); + + /** + * This node renderer will display nodes in the fastest way: Nodes are basic + * squares, drawn through the gl.POINTS drawing method. The size of the nodes + * are represented with the "gl_PointSize" value in the vertex shader. + * + * It is the fastest node renderer here since the buffer just takes one line + * to draw each node (with attributes "x", "y", "size" and "color"). + * + * Nevertheless, this method has some problems, especially due to some issues + * with the gl.POINTS: + * - First, if the center of a node is outside the scene, the point will not + * be drawn, even if it should be partly on screen. + * - I tried applying a fragment shader similar to the one in the default + * node renderer to display them as discs, but it did not work fine on + * some computers settings, filling the discs with weird gradients not + * depending on the actual color. + */ + sigma.webgl.nodes.fast = { + POINTS: 1, + ATTRIBUTES: 4, + addNode: function(node, data, i, prefix, settings) { + data[i++] = node[prefix + 'x']; + data[i++] = node[prefix + 'y']; + data[i++] = node[prefix + 'size']; + data[i++] = sigma.utils.floatColor( + node.color || settings('defaultNodeColor') + ); + }, + render: function(gl, program, data, params) { + var buffer; + + // Define attributes: + var positionLocation = + gl.getAttribLocation(program, 'a_position'), + sizeLocation = + gl.getAttribLocation(program, 'a_size'), + colorLocation = + gl.getAttribLocation(program, 'a_color'), + resolutionLocation = + gl.getUniformLocation(program, 'u_resolution'), + matrixLocation = + gl.getUniformLocation(program, 'u_matrix'), + ratioLocation = + gl.getUniformLocation(program, 'u_ratio'), + scaleLocation = + gl.getUniformLocation(program, 'u_scale'); + + buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW); + + gl.uniform2f(resolutionLocation, params.width, params.height); + gl.uniform1f( + ratioLocation, + 1 / Math.pow(params.ratio, params.settings('nodesPowRatio')) + ); + gl.uniform1f(scaleLocation, params.scalingRatio); + gl.uniformMatrix3fv(matrixLocation, false, params.matrix); + + gl.enableVertexAttribArray(positionLocation); + gl.enableVertexAttribArray(sizeLocation); + gl.enableVertexAttribArray(colorLocation); + + gl.vertexAttribPointer( + positionLocation, + 2, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 0 + ); + gl.vertexAttribPointer( + sizeLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 8 + ); + gl.vertexAttribPointer( + colorLocation, + 1, + gl.FLOAT, + false, + this.ATTRIBUTES * Float32Array.BYTES_PER_ELEMENT, + 12 + ); + + gl.drawArrays( + gl.POINTS, + params.start || 0, + params.count || (data.length / this.ATTRIBUTES) + ); + }, + initProgram: function(gl) { + var vertexShader, + fragmentShader, + program; + + vertexShader = sigma.utils.loadShader( + gl, + [ + 'attribute vec2 a_position;', + 'attribute float a_size;', + 'attribute float a_color;', + + 'uniform vec2 u_resolution;', + 'uniform float u_ratio;', + 'uniform float u_scale;', + 'uniform mat3 u_matrix;', + + 'varying vec4 color;', + + 'void main() {', + // Scale from [[-1 1] [-1 1]] to the container: + 'gl_Position = vec4(', + '((u_matrix * vec3(a_position, 1)).xy /', + 'u_resolution * 2.0 - 1.0) * vec2(1, -1),', + '0,', + '1', + ');', + + // Multiply the point size twice: + // - x SCALING_RATIO to correct the canvas scaling + // - x 2 to correct the formulae + 'gl_PointSize = a_size * u_ratio * u_scale * 2.0;', + + // Extract the color: + 'float c = a_color;', + 'color.b = mod(c, 256.0); c = floor(c / 256.0);', + 'color.g = mod(c, 256.0); c = floor(c / 256.0);', + 'color.r = mod(c, 256.0); c = floor(c / 256.0); color /= 255.0;', + 'color.a = 1.0;', + '}' + ].join('\n'), + gl.VERTEX_SHADER + ); + + fragmentShader = sigma.utils.loadShader( + gl, + [ + 'precision mediump float;', + + 'varying vec4 color;', + + 'void main(void) {', + 'float border = 0.01;', + 'float radius = 0.5;', + + 'vec4 color0 = vec4(0.0, 0.0, 0.0, 0.0);', + 'vec2 m = gl_PointCoord - vec2(0.5, 0.5);', + 'float dist = radius - sqrt(m.x * m.x + m.y * m.y);', + + 'float t = 0.0;', + 'if (dist > border)', + 't = 1.0;', + 'else if (dist > 0.0)', + 't = dist / border;', + + 'gl_FragColor = mix(color0, color, t);', + '}' + ].join('\n'), + gl.FRAGMENT_SHADER + ); + + program = sigma.utils.loadProgram(gl, [vertexShader, fragmentShader]); + + return program; + } + }; +})(); diff --git a/blogContent/projects/steam/src/sigma.core.js b/blogContent/projects/steam/src/sigma.core.js new file mode 100644 index 0000000..8738372 --- /dev/null +++ b/blogContent/projects/steam/src/sigma.core.js @@ -0,0 +1,739 @@ +;(function(undefined) { + 'use strict'; + + var __instances = {}; + + /** + * This is the sigma instances constructor. One instance of sigma represent + * one graph. It is possible to represent this grapĥ with several renderers + * at the same time. By default, the default renderer (WebGL + Canvas + * polyfill) will be used as the only renderer, with the container specified + * in the configuration. + * + * @param {?*} conf The configuration of the instance. There are a lot of + * different recognized forms to instantiate sigma, check + * example files, documentation in this file and unit + * tests to know more. + * @return {sigma} The fresh new sigma instance. + * + * Instanciating sigma: + * ******************** + * If no parameter is given to the constructor, the instance will be created + * without any renderer or camera. It will just instantiate the graph, and + * other modules will have to be instantiated through the public methods, + * like "addRenderer" etc: + * + * > s0 = new sigma(); + * > s0.addRenderer({ + * > type: 'canvas', + * > container: 'my-container-id' + * > }); + * + * In most of the cases, sigma will simply be used with the default renderer. + * Then, since the only required parameter is the DOM container, there are + * some simpler way to call the constructor. The four following calls do the + * exact same things: + * + * > s1 = new sigma('my-container-id'); + * > s2 = new sigma(document.getElementById('my-container-id')); + * > s3 = new sigma({ + * > container: document.getElementById('my-container-id') + * > }); + * > s4 = new sigma({ + * > renderers: [{ + * > container: document.getElementById('my-container-id') + * > }] + * > }); + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters, when calling the + * constructor with to top level configuration object (fourth case in the + * previous examples): + * + * {?string} id The id of the instance. It will be generated + * automatically if not specified. + * {?array} renderers An array containing objects describing renderers. + * {?object} graph An object containing an array of nodes and an array + * of edges, to avoid having to add them by hand later. + * {?object} settings An object containing instance specific settings that + * will override the default ones defined in the object + * sigma.settings. + */ + var sigma = function(conf) { + // Local variables: + // **************** + var i, + l, + a, + c, + o, + id; + + sigma.classes.dispatcher.extend(this); + + // Private attributes: + // ******************* + var _self = this, + _conf = conf || {}; + + // Little shortcut: + // **************** + // The configuration is supposed to have a list of the configuration + // objects for each renderer. + // - If there are no configuration at all, then nothing is done. + // - If there are no renderer list, the given configuration object will be + // considered as describing the first and only renderer. + // - If there are no renderer list nor "container" object, it will be + // considered as the container itself (a DOM element). + // - If the argument passed to sigma() is a string, it will be considered + // as the ID of the DOM container. + if ( + typeof _conf === 'string' || + _conf instanceof HTMLElement + ) + _conf = { + renderers: [_conf] + }; + else if (Object.prototype.toString.call(_conf) === '[object Array]') + _conf = { + renderers: _conf + }; + + // Also check "renderer" and "container" keys: + o = _conf.renderers || _conf.renderer || _conf.container; + if (!_conf.renderers || _conf.renderers.length === 0) + if ( + typeof o === 'string' || + o instanceof HTMLElement || + (typeof o === 'object' && 'container' in o) + ) + _conf.renderers = [o]; + + // Recense the instance: + if (_conf.id) { + if (__instances[_conf.id]) + throw 'sigma: Instance "' + _conf.id + '" already exists.'; + Object.defineProperty(this, 'id', { + value: _conf.id + }); + } else { + id = 0; + while (__instances[id]) + id++; + Object.defineProperty(this, 'id', { + value: '' + id + }); + } + __instances[this.id] = this; + + // Initialize settings function: + this.settings = new sigma.classes.configurable( + sigma.settings, + _conf.settings || {} + ); + + // Initialize locked attributes: + Object.defineProperty(this, 'graph', { + value: new sigma.classes.graph(this.settings), + configurable: true + }); + Object.defineProperty(this, 'middlewares', { + value: [], + configurable: true + }); + Object.defineProperty(this, 'cameras', { + value: {}, + configurable: true + }); + Object.defineProperty(this, 'renderers', { + value: {}, + configurable: true + }); + Object.defineProperty(this, 'renderersPerCamera', { + value: {}, + configurable: true + }); + Object.defineProperty(this, 'cameraFrames', { + value: {}, + configurable: true + }); + Object.defineProperty(this, 'camera', { + get: function() { + return this.cameras[0]; + } + }); + Object.defineProperty(this, 'events', { + value: [ + 'click', + 'rightClick', + 'clickStage', + 'doubleClickStage', + 'rightClickStage', + 'clickNode', + 'clickNodes', + 'doubleClickNode', + 'doubleClickNodes', + 'rightClickNode', + 'rightClickNodes', + 'overNode', + 'overNodes', + 'outNode', + 'outNodes', + 'downNode', + 'downNodes', + 'upNode', + 'upNodes' + ], + configurable: true + }); + + // Add a custom handler, to redispatch events from renderers: + this._handler = (function(e) { + var k, + data = {}; + + for (k in e.data) + data[k] = e.data[k]; + + data.renderer = e.target; + this.dispatchEvent(e.type, data); + }).bind(this); + + // Initialize renderers: + a = _conf.renderers || []; + for (i = 0, l = a.length; i < l; i++) + this.addRenderer(a[i]); + + // Initialize middlewares: + a = _conf.middlewares || []; + for (i = 0, l = a.length; i < l; i++) + this.middlewares.push( + typeof a[i] === 'string' ? + sigma.middlewares[a[i]] : + a[i] + ); + + // Check if there is already a graph to fill in: + if (typeof _conf.graph === 'object' && _conf.graph) { + this.graph.read(_conf.graph); + + // If a graph is given to the to the instance, the "refresh" method is + // directly called: + this.refresh(); + } + + // Deal with resize: + window.addEventListener('resize', function() { + if (_self.settings) + _self.refresh(); + }); + }; + + + + + /** + * This methods will instantiate and reference a new camera. If no id is + * specified, then an automatic id will be generated. + * + * @param {?string} id Eventually the camera id. + * @return {sigma.classes.camera} The fresh new camera instance. + */ + sigma.prototype.addCamera = function(id) { + var self = this, + camera; + + if (!arguments.length) { + id = 0; + while (this.cameras['' + id]) + id++; + id = '' + id; + } + + if (this.cameras[id]) + throw 'sigma.addCamera: The camera "' + id + '" already exists.'; + + camera = new sigma.classes.camera(id, this.graph, this.settings); + this.cameras[id] = camera; + + // Add a quadtree to the camera: + camera.quadtree = new sigma.classes.quad(); + + // Add an edgequadtree to the camera: + if (sigma.classes.edgequad !== undefined) { + camera.edgequadtree = new sigma.classes.edgequad(); + } + + camera.bind('coordinatesUpdated', function(e) { + self.renderCamera(camera, camera.isAnimated); + }); + + this.renderersPerCamera[id] = []; + + return camera; + }; + + /** + * This method kills a camera, and every renderer attached to it. + * + * @param {string|camera} v The camera to kill or its ID. + * @return {sigma} Returns the instance. + */ + sigma.prototype.killCamera = function(v) { + v = typeof v === 'string' ? this.cameras[v] : v; + + if (!v) + throw 'sigma.killCamera: The camera is undefined.'; + + var i, + l, + a = this.renderersPerCamera[v.id]; + + for (l = a.length, i = l - 1; i >= 0; i--) + this.killRenderer(a[i]); + + delete this.renderersPerCamera[v.id]; + delete this.cameraFrames[v.id]; + delete this.cameras[v.id]; + + if (v.kill) + v.kill(); + + return this; + }; + + /** + * This methods will instantiate and reference a new renderer. The "type" + * argument can be the constructor or its name in the "sigma.renderers" + * package. If no type is specified, then "sigma.renderers.def" will be used. + * If no id is specified, then an automatic id will be generated. + * + * @param {?object} options Eventually some options to give to the renderer + * constructor. + * @return {renderer} The fresh new renderer instance. + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters in the "options" + * object: + * + * {?string} id Eventually the renderer id. + * {?(function|string)} type Eventually the renderer constructor or its + * name in the "sigma.renderers" package. + * {?(camera|string)} camera Eventually the renderer camera or its + * id. + */ + sigma.prototype.addRenderer = function(options) { + var id, + fn, + camera, + renderer, + o = options || {}; + + // Polymorphism: + if (typeof o === 'string') + o = { + container: document.getElementById(o) + }; + else if (o instanceof HTMLElement) + o = { + container: o + }; + + // If the container still is a string, we get it by id + if (typeof o.container === 'string') + o.container = document.getElementById(o.container); + + // Reference the new renderer: + if (!('id' in o)) { + id = 0; + while (this.renderers['' + id]) + id++; + id = '' + id; + } else + id = o.id; + + if (this.renderers[id]) + throw 'sigma.addRenderer: The renderer "' + id + '" already exists.'; + + // Find the good constructor: + fn = typeof o.type === 'function' ? o.type : sigma.renderers[o.type]; + fn = fn || sigma.renderers.def; + + // Find the good camera: + camera = 'camera' in o ? + ( + o.camera instanceof sigma.classes.camera ? + o.camera : + this.cameras[o.camera] || this.addCamera(o.camera) + ) : + this.addCamera(); + + if (this.cameras[camera.id] !== camera) + throw 'sigma.addRenderer: The camera is not properly referenced.'; + + // Instantiate: + renderer = new fn(this.graph, camera, this.settings, o); + this.renderers[id] = renderer; + Object.defineProperty(renderer, 'id', { + value: id + }); + + // Bind events: + if (renderer.bind) + renderer.bind( + [ + 'click', + 'rightClick', + 'clickStage', + 'doubleClickStage', + 'rightClickStage', + 'clickNode', + 'clickNodes', + 'clickEdge', + 'clickEdges', + 'doubleClickNode', + 'doubleClickNodes', + 'doubleClickEdge', + 'doubleClickEdges', + 'rightClickNode', + 'rightClickNodes', + 'rightClickEdge', + 'rightClickEdges', + 'overNode', + 'overNodes', + 'overEdge', + 'overEdges', + 'outNode', + 'outNodes', + 'outEdge', + 'outEdges', + 'downNode', + 'downNodes', + 'downEdge', + 'downEdges', + 'upNode', + 'upNodes', + 'upEdge', + 'upEdges' + ], + this._handler + ); + + // Reference the renderer by its camera: + this.renderersPerCamera[camera.id].push(renderer); + + return renderer; + }; + + /** + * This method kills a renderer. + * + * @param {string|renderer} v The renderer to kill or its ID. + * @return {sigma} Returns the instance. + */ + sigma.prototype.killRenderer = function(v) { + v = typeof v === 'string' ? this.renderers[v] : v; + + if (!v) + throw 'sigma.killRenderer: The renderer is undefined.'; + + var a = this.renderersPerCamera[v.camera.id], + i = a.indexOf(v); + + if (i >= 0) + a.splice(i, 1); + + if (v.kill) + v.kill(); + + delete this.renderers[v.id]; + + return this; + }; + + + + + /** + * This method calls the "render" method of each renderer, with the same + * arguments than the "render" method, but will also check if the renderer + * has a "process" method, and call it if it exists. + * + * It is useful for quadtrees or WebGL processing, for instance. + * + * @param {?object} options Eventually some options to give to the refresh + * method. + * @return {sigma} Returns the instance itself. + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters in the "options" + * object: + * + * {?boolean} skipIndexation A flag specifying wether or not the refresh + * function should reindex the graph in the + * quadtrees or not (default: false). + */ + sigma.prototype.refresh = function(options) { + var i, + l, + k, + a, + c, + bounds, + prefix = 0; + + options = options || {}; + + // Call each middleware: + a = this.middlewares || []; + for (i = 0, l = a.length; i < l; i++) + a[i].call( + this, + (i === 0) ? '' : 'tmp' + prefix + ':', + (i === l - 1) ? 'ready:' : ('tmp' + (++prefix) + ':') + ); + + // Then, for each camera, call the "rescale" middleware, unless the + // settings specify not to: + for (k in this.cameras) { + c = this.cameras[k]; + if ( + c.settings('autoRescale') && + this.renderersPerCamera[c.id] && + this.renderersPerCamera[c.id].length + ) + sigma.middlewares.rescale.call( + this, + a.length ? 'ready:' : '', + c.readPrefix, + { + width: this.renderersPerCamera[c.id][0].width, + height: this.renderersPerCamera[c.id][0].height + } + ); + else + sigma.middlewares.copy.call( + this, + a.length ? 'ready:' : '', + c.readPrefix + ); + + if (!options.skipIndexation) { + // Find graph boundaries: + bounds = sigma.utils.getBoundaries( + this.graph, + c.readPrefix + ); + + // Refresh quadtree: + c.quadtree.index(this.graph.nodes(), { + prefix: c.readPrefix, + bounds: { + x: bounds.minX, + y: bounds.minY, + width: bounds.maxX - bounds.minX, + height: bounds.maxY - bounds.minY + } + }); + + // Refresh edgequadtree: + if ( + c.edgequadtree !== undefined && + c.settings('drawEdges') && + c.settings('enableEdgeHovering') + ) { + c.edgequadtree.index(this.graph, { + prefix: c.readPrefix, + bounds: { + x: bounds.minX, + y: bounds.minY, + width: bounds.maxX - bounds.minX, + height: bounds.maxY - bounds.minY + } + }); + } + } + } + + // Call each renderer: + a = Object.keys(this.renderers); + for (i = 0, l = a.length; i < l; i++) + if (this.renderers[a[i]].process) { + if (this.settings('skipErrors')) + try { + this.renderers[a[i]].process(); + } catch (e) { + console.log( + 'Warning: The renderer "' + a[i] + '" crashed on ".process()"' + ); + } + else + this.renderers[a[i]].process(); + } + + this.render(); + + return this; + }; + + /** + * This method calls the "render" method of each renderer. + * + * @return {sigma} Returns the instance itself. + */ + sigma.prototype.render = function() { + var i, + l, + a, + prefix = 0; + + // Call each renderer: + a = Object.keys(this.renderers); + for (i = 0, l = a.length; i < l; i++) + if (this.settings('skipErrors')) + try { + this.renderers[a[i]].render(); + } catch (e) { + if (this.settings('verbose')) + console.log( + 'Warning: The renderer "' + a[i] + '" crashed on ".render()"' + ); + } + else + this.renderers[a[i]].render(); + + return this; + }; + + /** + * This method calls the "render" method of each renderer that is bound to + * the specified camera. To improve the performances, if this method is + * called too often, the number of effective renderings is limitated to one + * per frame, unless you are using the "force" flag. + * + * @param {sigma.classes.camera} camera The camera to render. + * @param {?boolean} force If true, will render the camera + * directly. + * @return {sigma} Returns the instance itself. + */ + sigma.prototype.renderCamera = function(camera, force) { + var i, + l, + a, + self = this; + + if (force) { + a = this.renderersPerCamera[camera.id]; + for (i = 0, l = a.length; i < l; i++) + if (this.settings('skipErrors')) + try { + a[i].render(); + } catch (e) { + if (this.settings('verbose')) + console.log( + 'Warning: The renderer "' + a[i].id + '" crashed on ".render()"' + ); + } + else + a[i].render(); + } else { + if (!this.cameraFrames[camera.id]) { + a = this.renderersPerCamera[camera.id]; + for (i = 0, l = a.length; i < l; i++) + if (this.settings('skipErrors')) + try { + a[i].render(); + } catch (e) { + if (this.settings('verbose')) + console.log( + 'Warning: The renderer "' + + a[i].id + + '" crashed on ".render()"' + ); + } + else + a[i].render(); + + this.cameraFrames[camera.id] = requestAnimationFrame(function() { + delete self.cameraFrames[camera.id]; + }); + } + } + + return this; + }; + + /** + * This method calls the "kill" method of each module and destroys any + * reference from the instance. + */ + sigma.prototype.kill = function() { + var k; + + // Dispatching event + this.dispatchEvent('kill'); + + // Kill graph: + this.graph.kill(); + + // Kill middlewares: + delete this.middlewares; + + // Kill each renderer: + for (k in this.renderers) + this.killRenderer(this.renderers[k]); + + // Kill each camera: + for (k in this.cameras) + this.killCamera(this.cameras[k]); + + delete this.renderers; + delete this.cameras; + + // Kill everything else: + for (k in this) + if (this.hasOwnProperty(k)) + delete this[k]; + + delete __instances[this.id]; + }; + + + + + /** + * Returns a clone of the instances object or a specific running instance. + * + * @param {?string} id Eventually an instance ID. + * @return {object} The related instance or a clone of the instances + * object. + */ + sigma.instances = function(id) { + return arguments.length ? + __instances[id] : + sigma.utils.extend({}, __instances); + }; + + + + /** + * The current version of sigma: + */ + sigma.version = '1.2.1'; + + + + + /** + * EXPORT: + * ******* + */ + if (typeof this.sigma !== 'undefined') + throw 'An object called sigma is already in the global scope.'; + + this.sigma = sigma; + +}).call(this); diff --git a/blogContent/projects/steam/src/sigma.export.js b/blogContent/projects/steam/src/sigma.export.js new file mode 100644 index 0000000..2d15623 --- /dev/null +++ b/blogContent/projects/steam/src/sigma.export.js @@ -0,0 +1,20 @@ +// Hardcoded export for the node.js version: +var sigma = this.sigma, + conrad = this.conrad; + +sigma.conrad = conrad; + +// Dirty polyfills to permit sigma usage in node +if (typeof HTMLElement === 'undefined') + HTMLElement = function() {}; + +if (typeof window === 'undefined') + window = { + addEventListener: function() {} + }; + +if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) + exports = module.exports = sigma; + exports.sigma = sigma; +} diff --git a/blogContent/projects/steam/src/sigma.settings.js b/blogContent/projects/steam/src/sigma.settings.js new file mode 100644 index 0000000..cee34b8 --- /dev/null +++ b/blogContent/projects/steam/src/sigma.settings.js @@ -0,0 +1,251 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Packages initialization: + sigma.utils.pkg('sigma.settings'); + + var settings = { + /** + * GRAPH SETTINGS: + * *************** + */ + // {boolean} Indicates if the data have to be cloned in methods to add + // nodes or edges. + clone: true, + // {boolean} Indicates if nodes "id" values and edges "id", "source" and + // "target" values must be set as immutable. + immutable: true, + // {boolean} Indicates if sigma can log its errors and warnings. + verbose: false, + + + /** + * RENDERERS SETTINGS: + * ******************* + */ + // {string} + classPrefix: 'sigma', + // {string} + defaultNodeType: 'def', + // {string} + defaultEdgeType: 'def', + // {string} + defaultLabelColor: '#000', + // {string} + defaultEdgeColor: '#000', + // {string} + defaultNodeColor: '#000', + // {string} + defaultLabelSize: 14, + // {string} Indicates how to choose the edges color. Available values: + // "source", "target", "default" + edgeColor: 'source', + // {number} Defines the minimal edge's arrow display size. + minArrowSize: 0, + // {string} + font: 'arial', + // {string} Example: 'bold' + fontStyle: '', + // {string} Indicates how to choose the labels color. Available values: + // "node", "default" + labelColor: 'default', + // {string} Indicates how to choose the labels size. Available values: + // "fixed", "proportional" + labelSize: 'fixed', + // {string} The ratio between the font size of the label and the node size. + labelSizeRatio: 1, + // {number} The minimum size a node must have to see its label displayed. + labelThreshold: 8, + // {number} The oversampling factor used in WebGL renderer. + webglOversamplingRatio: 2, + // {number} The size of the border of hovered nodes. + borderSize: 0, + // {number} The default hovered node border's color. + defaultNodeBorderColor: '#000', + // {number} The hovered node's label font. If not specified, will heritate + // the "font" value. + hoverFont: '', + // {boolean} If true, then only one node can be hovered at a time. + singleHover: true, + // {string} Example: 'bold' + hoverFontStyle: '', + // {string} Indicates how to choose the hovered nodes shadow color. + // Available values: "node", "default" + labelHoverShadow: 'default', + // {string} + labelHoverShadowColor: '#000', + // {string} Indicates how to choose the hovered nodes color. + // Available values: "node", "default" + nodeHoverColor: 'node', + // {string} + defaultNodeHoverColor: '#000', + // {string} Indicates how to choose the hovered nodes background color. + // Available values: "node", "default" + labelHoverBGColor: 'default', + // {string} + defaultHoverLabelBGColor: '#fff', + // {string} Indicates how to choose the hovered labels color. + // Available values: "node", "default" + labelHoverColor: 'default', + // {string} + defaultLabelHoverColor: '#000', + // {string} Indicates how to choose the edges hover color. Available values: + // "edge", "default" + edgeHoverColor: 'edge', + // {number} The size multiplicator of hovered edges. + edgeHoverSizeRatio: 1, + // {string} + defaultEdgeHoverColor: '#000', + // {boolean} Indicates if the edge extremities must be hovered when the + // edge is hovered. + edgeHoverExtremities: false, + // {booleans} The different drawing modes: + // false: Layered not displayed. + // true: Layered displayed. + drawEdges: true, + drawNodes: true, + drawLabels: true, + drawEdgeLabels: false, + // {boolean} Indicates if the edges must be drawn in several frames or in + // one frame, as the nodes and labels are drawn. + batchEdgesDrawing: false, + // {boolean} Indicates if the edges must be hidden during dragging and + // animations. + hideEdgesOnMove: false, + // {numbers} The different batch sizes, when elements are displayed in + // several frames. + canvasEdgesBatchSize: 500, + webglEdgesBatchSize: 1000, + + + + + /** + * RESCALE SETTINGS: + * ***************** + */ + // {string} Indicates of to scale the graph relatively to its container. + // Available values: "inside", "outside" + scalingMode: 'inside', + // {number} The margin to keep around the graph. + sideMargin: 0, + // {number} Determine the size of the smallest and the biggest node / edges + // on the screen. This mapping makes easier to display the graph, + // avoiding too big nodes that take half of the screen, or too + // small ones that are not readable. If the two parameters are + // equals, then the minimal display size will be 0. And if they + // are both equal to 0, then there is no mapping, and the radius + // of the nodes will be their size. + minEdgeSize: 0.5, + maxEdgeSize: 1, + minNodeSize: 1, + maxNodeSize: 8, + + + + + /** + * CAPTORS SETTINGS: + * ***************** + */ + // {boolean} + touchEnabled: true, + // {boolean} + mouseEnabled: true, + // {boolean} + mouseWheelEnabled: true, + // {boolean} + doubleClickEnabled: true, + // {boolean} Defines whether the custom events such as "clickNode" can be + // used. + eventsEnabled: true, + // {number} Defines by how much multiplicating the zooming level when the + // user zooms with the mouse-wheel. + zoomingRatio: 1.7, + // {number} Defines by how much multiplicating the zooming level when the + // user zooms by double clicking. + doubleClickZoomingRatio: 2.2, + // {number} The minimum zooming level. + zoomMin: 0.0625, + // {number} The maximum zooming level. + zoomMax: 2, + // {number} The duration of animations following a mouse scrolling. + mouseZoomDuration: 200, + // {number} The duration of animations following a mouse double click. + doubleClickZoomDuration: 200, + // {number} The duration of animations following a mouse dropping. + mouseInertiaDuration: 200, + // {number} The inertia power (mouse captor). + mouseInertiaRatio: 3, + // {number} The duration of animations following a touch dropping. + touchInertiaDuration: 200, + // {number} The inertia power (touch captor). + touchInertiaRatio: 3, + // {number} The maximum time between two clicks to make it a double click. + doubleClickTimeout: 300, + // {number} The maximum time between two taps to make it a double tap. + doubleTapTimeout: 300, + // {number} The maximum time of dragging to trigger intertia. + dragTimeout: 200, + + + + + /** + * GLOBAL SETTINGS: + * **************** + */ + // {boolean} Determines whether the instance has to refresh itself + // automatically when a "resize" event is dispatched from the + // window object. + autoResize: true, + // {boolean} Determines whether the "rescale" middleware has to be called + // automatically for each camera on refresh. + autoRescale: true, + // {boolean} If set to false, the camera method "goTo" will basically do + // nothing. + enableCamera: true, + // {boolean} If set to false, the nodes cannot be hovered. + enableHovering: true, + // {boolean} If set to true, the edges can be hovered. + enableEdgeHovering: false, + // {number} The size of the area around the edges to activate hovering. + edgeHoverPrecision: 5, + // {boolean} If set to true, the rescale middleware will ignore node sizes + // to determine the graphs boundings. + rescaleIgnoreSize: false, + // {boolean} Determines if the core has to try to catch errors on + // rendering. + skipErrors: false, + + + + + /** + * CAMERA SETTINGS: + * **************** + */ + // {number} The power degrees applied to the nodes/edges size relatively to + // the zooming level. Basically: + // > onScreenR = Math.pow(zoom, nodesPowRatio) * R + // > onScreenT = Math.pow(zoom, edgesPowRatio) * T + nodesPowRatio: 0.5, + edgesPowRatio: 0.5, + + + + + /** + * ANIMATIONS SETTINGS: + * ******************** + */ + // {number} The default animation time. + animationsTime: 200 + }; + + // Export the previously designed settings: + sigma.settings = sigma.utils.extend(sigma.settings || {}, settings); +}).call(this); diff --git a/blogContent/projects/steam/src/supervisor.js b/blogContent/projects/steam/src/supervisor.js new file mode 100644 index 0000000..57f9b94 --- /dev/null +++ b/blogContent/projects/steam/src/supervisor.js @@ -0,0 +1,340 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + /** + * Sigma ForceAtlas2.5 Supervisor + * =============================== + * + * Author: Guillaume Plique (Yomguithereal) + * Version: 0.1 + */ + var _root = this; + + /** + * Feature detection + * ------------------ + */ + var webWorkers = 'Worker' in _root; + + /** + * Supervisor Object + * ------------------ + */ + function Supervisor(sigInst, options) { + var _this = this, + workerFn = sigInst.getForceAtlas2Worker && + sigInst.getForceAtlas2Worker(); + + options = options || {}; + + // _root URL Polyfill + _root.URL = _root.URL || _root.webkitURL; + + // Properties + this.sigInst = sigInst; + this.graph = this.sigInst.graph; + this.ppn = 10; + this.ppe = 3; + this.config = {}; + this.shouldUseWorker = + options.worker === false ? false : true && webWorkers; + this.workerUrl = options.workerUrl; + + // State + this.started = false; + this.running = false; + + // Web worker or classic DOM events? + if (this.shouldUseWorker) { + if (!this.workerUrl) { + var blob = this.makeBlob(workerFn); + this.worker = new Worker(URL.createObjectURL(blob)); + } + else { + this.worker = new Worker(this.workerUrl); + } + + // Post Message Polyfill + this.worker.postMessage = + this.worker.webkitPostMessage || this.worker.postMessage; + } + else { + + eval(workerFn); + } + + // Worker message receiver + this.msgName = (this.worker) ? 'message' : 'newCoords'; + this.listener = function(e) { + + // Retrieving data + _this.nodesByteArray = new Float32Array(e.data.nodes); + + // If ForceAtlas2 is running, we act accordingly + if (_this.running) { + + // Applying layout + _this.applyLayoutChanges(); + + // Send data back to worker and loop + _this.sendByteArrayToWorker(); + + // Rendering graph + _this.sigInst.refresh(); + } + }; + + (this.worker || document).addEventListener(this.msgName, this.listener); + + // Filling byteArrays + this.graphToByteArrays(); + + // Binding on kill to properly terminate layout when parent is killed + sigInst.bind('kill', function() { + sigInst.killForceAtlas2(); + }); + } + + Supervisor.prototype.makeBlob = function(workerFn) { + var blob; + + try { + blob = new Blob([workerFn], {type: 'application/javascript'}); + } + catch (e) { + _root.BlobBuilder = _root.BlobBuilder || + _root.WebKitBlobBuilder || + _root.MozBlobBuilder; + + blob = new BlobBuilder(); + blob.append(workerFn); + blob = blob.getBlob(); + } + + return blob; + }; + + Supervisor.prototype.graphToByteArrays = function() { + var nodes = this.graph.nodes(), + edges = this.graph.edges(), + nbytes = nodes.length * this.ppn, + ebytes = edges.length * this.ppe, + nIndex = {}, + i, + j, + l; + + // Allocating Byte arrays with correct nb of bytes + this.nodesByteArray = new Float32Array(nbytes); + this.edgesByteArray = new Float32Array(ebytes); + + // Iterate through nodes + for (i = j = 0, l = nodes.length; i < l; i++) { + + // Populating index + nIndex[nodes[i].id] = j; + + // Populating byte array + this.nodesByteArray[j] = nodes[i].x; + this.nodesByteArray[j + 1] = nodes[i].y; + this.nodesByteArray[j + 2] = 0; + this.nodesByteArray[j + 3] = 0; + this.nodesByteArray[j + 4] = 0; + this.nodesByteArray[j + 5] = 0; + this.nodesByteArray[j + 6] = 1 + this.graph.degree(nodes[i].id); + this.nodesByteArray[j + 7] = 1; + this.nodesByteArray[j + 8] = nodes[i].size; + this.nodesByteArray[j + 9] = 0; + j += this.ppn; + } + + // Iterate through edges + for (i = j = 0, l = edges.length; i < l; i++) { + this.edgesByteArray[j] = nIndex[edges[i].source]; + this.edgesByteArray[j + 1] = nIndex[edges[i].target]; + this.edgesByteArray[j + 2] = edges[i].weight || 0; + j += this.ppe; + } + }; + + // TODO: make a better send function + Supervisor.prototype.applyLayoutChanges = function() { + var nodes = this.graph.nodes(), + j = 0, + realIndex; + + // Moving nodes + for (var i = 0, l = this.nodesByteArray.length; i < l; i += this.ppn) { + nodes[j].x = this.nodesByteArray[i]; + nodes[j].y = this.nodesByteArray[i + 1]; + j++; + } + }; + + Supervisor.prototype.sendByteArrayToWorker = function(action) { + var content = { + action: action || 'loop', + nodes: this.nodesByteArray.buffer + }; + + var buffers = [this.nodesByteArray.buffer]; + + if (action === 'start') { + content.config = this.config || {}; + content.edges = this.edgesByteArray.buffer; + buffers.push(this.edgesByteArray.buffer); + } + + if (this.shouldUseWorker) + this.worker.postMessage(content, buffers); + else + _root.postMessage(content, '*'); + }; + + Supervisor.prototype.start = function() { + if (this.running) + return; + + this.running = true; + + // Do not refresh edgequadtree during layout: + var k, + c; + for (k in this.sigInst.cameras) { + c = this.sigInst.cameras[k]; + c.edgequadtree._enabled = false; + } + + if (!this.started) { + + // Sending init message to worker + this.sendByteArrayToWorker('start'); + this.started = true; + } + else { + this.sendByteArrayToWorker(); + } + }; + + Supervisor.prototype.stop = function() { + if (!this.running) + return; + + // Allow to refresh edgequadtree: + var k, + c, + bounds; + for (k in this.sigInst.cameras) { + c = this.sigInst.cameras[k]; + c.edgequadtree._enabled = true; + + // Find graph boundaries: + bounds = sigma.utils.getBoundaries( + this.graph, + c.readPrefix + ); + + // Refresh edgequadtree: + if (c.settings('drawEdges') && c.settings('enableEdgeHovering')) + c.edgequadtree.index(this.sigInst.graph, { + prefix: c.readPrefix, + bounds: { + x: bounds.minX, + y: bounds.minY, + width: bounds.maxX - bounds.minX, + height: bounds.maxY - bounds.minY + } + }); + } + + this.running = false; + }; + + Supervisor.prototype.killWorker = function() { + if (this.worker) { + this.worker.terminate(); + } + else { + _root.postMessage({action: 'kill'}, '*'); + document.removeEventListener(this.msgName, this.listener); + } + }; + + Supervisor.prototype.configure = function(config) { + + // Setting configuration + this.config = config; + + if (!this.started) + return; + + var data = {action: 'config', config: this.config}; + + if (this.shouldUseWorker) + this.worker.postMessage(data); + else + _root.postMessage(data, '*'); + }; + + /** + * Interface + * ---------- + */ + sigma.prototype.startForceAtlas2 = function(config) { + + // Create supervisor if undefined + if (!this.supervisor) + this.supervisor = new Supervisor(this, config); + + // Configuration provided? + if (config) + this.supervisor.configure(config); + + // Start algorithm + this.supervisor.start(); + + return this; + }; + + sigma.prototype.stopForceAtlas2 = function() { + if (!this.supervisor) + return this; + + // Pause algorithm + this.supervisor.stop(); + + return this; + }; + + sigma.prototype.killForceAtlas2 = function() { + if (!this.supervisor) + return this; + + // Stop Algorithm + this.supervisor.stop(); + + // Kill Worker + this.supervisor.killWorker(); + + // Kill supervisor + this.supervisor = null; + + return this; + }; + + sigma.prototype.configForceAtlas2 = function(config) { + if (!this.supervisor) + this.supervisor = new Supervisor(this, config); + + this.supervisor.configure(config); + + return this; + }; + + sigma.prototype.isForceAtlas2Running = function(config) { + return !!this.supervisor && this.supervisor.running; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/utils/sigma.polyfills.js b/blogContent/projects/steam/src/utils/sigma.polyfills.js new file mode 100644 index 0000000..459b943 --- /dev/null +++ b/blogContent/projects/steam/src/utils/sigma.polyfills.js @@ -0,0 +1,77 @@ +;(function(global) { + 'use strict'; + + /** + * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + * requestAnimationFrame polyfill by Erik Möller. + * fixes from Paul Irish and Tino Zijdel + * MIT license + */ + var x, + lastTime = 0, + vendors = ['ms', 'moz', 'webkit', 'o']; + + for (x = 0; x < vendors.length && !global.requestAnimationFrame; x++) { + global.requestAnimationFrame = + global[vendors[x] + 'RequestAnimationFrame']; + global.cancelAnimationFrame = + global[vendors[x] + 'CancelAnimationFrame'] || + global[vendors[x] + 'CancelRequestAnimationFrame']; + } + + if (!global.requestAnimationFrame) + global.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(), + timeToCall = Math.max(0, 16 - (currTime - lastTime)), + id = global.setTimeout( + function() { + callback(currTime + timeToCall); + }, + timeToCall + ); + + lastTime = currTime + timeToCall; + return id; + }; + + if (!global.cancelAnimationFrame) + global.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + + /** + * Function.prototype.bind polyfill found on MDN. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility + * Public domain + */ + if (!Function.prototype.bind) + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') + // Closest thing possible to the ECMAScript 5 internal IsCallable + // function: + throw new TypeError( + 'Function.prototype.bind - what is trying to be bound is not callable' + ); + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP, + fBound; + + fNOP = function() {}; + fBound = function() { + return fToBind.apply( + this instanceof fNOP && oThis ? + this : + oThis, + aArgs.concat(Array.prototype.slice.call(arguments)) + ); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +})(this); diff --git a/blogContent/projects/steam/src/utils/sigma.utils.js b/blogContent/projects/steam/src/utils/sigma.utils.js new file mode 100644 index 0000000..98c59a4 --- /dev/null +++ b/blogContent/projects/steam/src/utils/sigma.utils.js @@ -0,0 +1,1022 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + var _root = this; + + // Initialize packages: + sigma.utils = sigma.utils || {}; + + /** + * MISC UTILS: + */ + /** + * This function takes any number of objects as arguments, copies from each + * of these objects each pair key/value into a new object, and finally + * returns this object. + * + * The arguments are parsed from the last one to the first one, such that + * when several objects have keys in common, the "earliest" object wins. + * + * Example: + * ******** + * > var o1 = { + * > a: 1, + * > b: 2, + * > c: '3' + * > }, + * > o2 = { + * > c: '4', + * > d: [ 5 ] + * > }; + * > sigma.utils.extend(o1, o2); + * > // Returns: { + * > // a: 1, + * > // b: 2, + * > // c: '3', + * > // d: [ 5 ] + * > // }; + * + * @param {object+} Any number of objects. + * @return {object} The merged object. + */ + sigma.utils.extend = function() { + var i, + k, + res = {}, + l = arguments.length; + + for (i = l - 1; i >= 0; i--) + for (k in arguments[i]) + res[k] = arguments[i][k]; + + return res; + }; + + /** + * A short "Date.now()" polyfill. + * + * @return {Number} The current time (in ms). + */ + sigma.utils.dateNow = function() { + return Date.now ? Date.now() : new Date().getTime(); + }; + + /** + * Takes a package name as parameter and checks at each lebel if it exists, + * and if it does not, creates it. + * + * Example: + * ******** + * > sigma.utils.pkg('a.b.c'); + * > a.b.c; + * > // Object {}; + * > + * > sigma.utils.pkg('a.b.d'); + * > a.b; + * > // Object { c: {}, d: {} }; + * + * @param {string} pkgName The name of the package to create/find. + * @return {object} The related package. + */ + sigma.utils.pkg = function(pkgName) { + return (pkgName || '').split('.').reduce(function(context, objName) { + return (objName in context) ? + context[objName] : + (context[objName] = {}); + }, _root); + }; + + /** + * Returns a unique incremental number ID. + * + * Example: + * ******** + * > sigma.utils.id(); + * > // 1; + * > + * > sigma.utils.id(); + * > // 2; + * > + * > sigma.utils.id(); + * > // 3; + * + * @param {string} pkgName The name of the package to create/find. + * @return {object} The related package. + */ + sigma.utils.id = (function() { + var i = 0; + return function() { + return ++i; + }; + })(); + + /** + * This function takes an hexa color (for instance "#ffcc00" or "#fc0") or a + * rgb / rgba color (like "rgb(255,255,12)" or "rgba(255,255,12,1)") and + * returns an integer equal to "r * 255 * 255 + g * 255 + b", to gain some + * memory in the data given to WebGL shaders. + * + * Note that the function actually caches its results for better performance. + * + * @param {string} val The hexa or rgba color. + * @return {number} The number value. + */ + var floatColorCache = {}; + + sigma.utils.floatColor = function(val) { + + // Is the color already computed? + if (floatColorCache[val]) + return floatColorCache[val]; + + var original = val, + r = 0, + g = 0, + b = 0; + + if (val[0] === '#') { + val = val.slice(1); + + if (val.length === 3) { + r = parseInt(val.charAt(0) + val.charAt(0), 16); + g = parseInt(val.charAt(1) + val.charAt(1), 16); + b = parseInt(val.charAt(2) + val.charAt(2), 16); + } + else { + r = parseInt(val.charAt(0) + val.charAt(1), 16); + g = parseInt(val.charAt(2) + val.charAt(3), 16); + b = parseInt(val.charAt(4) + val.charAt(5), 16); + } + } else if (val.match(/^ *rgba? *\(/)) { + val = val.match( + /^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.*)?\) *$/ + ); + r = +val[1]; + g = +val[2]; + b = +val[3]; + } + + var color = ( + r * 256 * 256 + + g * 256 + + b + ); + + // Caching the color + floatColorCache[original] = color; + + return color; + }; + + /** + * Perform a zoom into a camera, with or without animation, to the + * coordinates indicated using a specified ratio. + * + * Recognized parameters: + * ********************** + * Here is the exhaustive list of every accepted parameters in the animation + * object: + * + * {?number} duration An amount of time that means the duration of the + * animation. If this parameter doesn't exist the + * zoom will be performed without animation. + * {?function} onComplete A function to perform it after the animation. It + * will be performed even if there is no duration. + * + * @param {camera} The camera where perform the zoom. + * @param {x} The X coordiantion where the zoom goes. + * @param {y} The Y coordiantion where the zoom goes. + * @param {ratio} The ratio to apply it to the current camera ratio. + * @param {?animation} A dictionary with options for a possible animation. + */ + sigma.utils.zoomTo = function(camera, x, y, ratio, animation) { + var settings = camera.settings, + count, + newRatio, + animationSettings, + coordinates; + + // Create the newRatio dealing with min / max: + newRatio = Math.max( + settings('zoomMin'), + Math.min( + settings('zoomMax'), + camera.ratio * ratio + ) + ); + + // Check that the new ratio is different from the initial one: + if (newRatio !== camera.ratio) { + // Create the coordinates variable: + ratio = newRatio / camera.ratio; + coordinates = { + x: x * (1 - ratio) + camera.x, + y: y * (1 - ratio) + camera.y, + ratio: newRatio + }; + + if (animation && animation.duration) { + // Complete the animation setings: + count = sigma.misc.animation.killAll(camera); + animation = sigma.utils.extend( + animation, + { + easing: count ? 'quadraticOut' : 'quadraticInOut' + } + ); + + sigma.misc.animation.camera(camera, coordinates, animation); + } else { + camera.goTo(coordinates); + if (animation && animation.onComplete) + animation.onComplete(); + } + } + }; + + /** + * Return the control point coordinates for a quadratic bezier curve. + * + * @param {number} x1 The X coordinate of the start point. + * @param {number} y1 The Y coordinate of the start point. + * @param {number} x2 The X coordinate of the end point. + * @param {number} y2 The Y coordinate of the end point. + * @return {x,y} The control point coordinates. + */ + sigma.utils.getQuadraticControlPoint = function(x1, y1, x2, y2) { + return { + x: (x1 + x2) / 2 + (y2 - y1) / 4, + y: (y1 + y2) / 2 + (x1 - x2) / 4 + }; + }; + + /** + * Compute the coordinates of the point positioned + * at length t in the quadratic bezier curve. + * + * @param {number} t In [0,1] the step percentage to reach + * the point in the curve from the context point. + * @param {number} x1 The X coordinate of the context point. + * @param {number} y1 The Y coordinate of the context point. + * @param {number} x2 The X coordinate of the ending point. + * @param {number} y2 The Y coordinate of the ending point. + * @param {number} xi The X coordinate of the control point. + * @param {number} yi The Y coordinate of the control point. + * @return {object} {x,y}. + */ + sigma.utils.getPointOnQuadraticCurve = function(t, x1, y1, x2, y2, xi, yi) { + // http://stackoverflow.com/a/5634528 + return { + x: Math.pow(1 - t, 2) * x1 + 2 * (1 - t) * t * xi + Math.pow(t, 2) * x2, + y: Math.pow(1 - t, 2) * y1 + 2 * (1 - t) * t * yi + Math.pow(t, 2) * y2 + }; + }; + + /** + * Compute the coordinates of the point positioned + * at length t in the cubic bezier curve. + * + * @param {number} t In [0,1] the step percentage to reach + * the point in the curve from the context point. + * @param {number} x1 The X coordinate of the context point. + * @param {number} y1 The Y coordinate of the context point. + * @param {number} x2 The X coordinate of the end point. + * @param {number} y2 The Y coordinate of the end point. + * @param {number} cx The X coordinate of the first control point. + * @param {number} cy The Y coordinate of the first control point. + * @param {number} dx The X coordinate of the second control point. + * @param {number} dy The Y coordinate of the second control point. + * @return {object} {x,y} The point at t. + */ + sigma.utils.getPointOnBezierCurve = + function(t, x1, y1, x2, y2, cx, cy, dx, dy) { + // http://stackoverflow.com/a/15397596 + // Blending functions: + var B0_t = Math.pow(1 - t, 3), + B1_t = 3 * t * Math.pow(1 - t, 2), + B2_t = 3 * Math.pow(t, 2) * (1 - t), + B3_t = Math.pow(t, 3); + + return { + x: (B0_t * x1) + (B1_t * cx) + (B2_t * dx) + (B3_t * x2), + y: (B0_t * y1) + (B1_t * cy) + (B2_t * dy) + (B3_t * y2) + }; + }; + + /** + * Return the coordinates of the two control points for a self loop (i.e. + * where the start point is also the end point) computed as a cubic bezier + * curve. + * + * @param {number} x The X coordinate of the node. + * @param {number} y The Y coordinate of the node. + * @param {number} size The node size. + * @return {x1,y1,x2,y2} The coordinates of the two control points. + */ + sigma.utils.getSelfLoopControlPoints = function(x , y, size) { + return { + x1: x - size * 7, + y1: y, + x2: x, + y2: y + size * 7 + }; + }; + + /** + * Return the euclidian distance between two points of a plane + * with an orthonormal basis. + * + * @param {number} x1 The X coordinate of the first point. + * @param {number} y1 The Y coordinate of the first point. + * @param {number} x2 The X coordinate of the second point. + * @param {number} y2 The Y coordinate of the second point. + * @return {number} The euclidian distance. + */ + sigma.utils.getDistance = function(x0, y0, x1, y1) { + return Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)); + }; + + /** + * Return the coordinates of the intersection points of two circles. + * + * @param {number} x0 The X coordinate of center location of the first + * circle. + * @param {number} y0 The Y coordinate of center location of the first + * circle. + * @param {number} r0 The radius of the first circle. + * @param {number} x1 The X coordinate of center location of the second + * circle. + * @param {number} y1 The Y coordinate of center location of the second + * circle. + * @param {number} r1 The radius of the second circle. + * @return {xi,yi} The coordinates of the intersection points. + */ + sigma.utils.getCircleIntersection = function(x0, y0, r0, x1, y1, r1) { + // http://stackoverflow.com/a/12219802 + var a, dx, dy, d, h, rx, ry, x2, y2; + + // dx and dy are the vertical and horizontal distances between the circle + // centers: + dx = x1 - x0; + dy = y1 - y0; + + // Determine the straight-line distance between the centers: + d = Math.sqrt((dy * dy) + (dx * dx)); + + // Check for solvability: + if (d > (r0 + r1)) { + // No solution. circles do not intersect. + return false; + } + if (d < Math.abs(r0 - r1)) { + // No solution. one circle is contained in the other. + return false; + } + + //'point 2' is the point where the line through the circle intersection + // points crosses the line between the circle centers. + + // Determine the distance from point 0 to point 2: + a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d); + + // Determine the coordinates of point 2: + x2 = x0 + (dx * a / d); + y2 = y0 + (dy * a / d); + + // Determine the distance from point 2 to either of the intersection + // points: + h = Math.sqrt((r0 * r0) - (a * a)); + + // Determine the offsets of the intersection points from point 2: + rx = -dy * (h / d); + ry = dx * (h / d); + + // Determine the absolute intersection points: + var xi = x2 + rx; + var xi_prime = x2 - rx; + var yi = y2 + ry; + var yi_prime = y2 - ry; + + return {xi: xi, xi_prime: xi_prime, yi: yi, yi_prime: yi_prime}; + }; + + /** + * Check if a point is on a line segment. + * + * @param {number} x The X coordinate of the point to check. + * @param {number} y The Y coordinate of the point to check. + * @param {number} x1 The X coordinate of the line start point. + * @param {number} y1 The Y coordinate of the line start point. + * @param {number} x2 The X coordinate of the line end point. + * @param {number} y2 The Y coordinate of the line end point. + * @param {number} epsilon The precision (consider the line thickness). + * @return {boolean} True if point is "close to" the line + * segment, false otherwise. + */ + sigma.utils.isPointOnSegment = function(x, y, x1, y1, x2, y2, epsilon) { + // http://stackoverflow.com/a/328122 + var crossProduct = Math.abs((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)), + d = sigma.utils.getDistance(x1, y1, x2, y2), + nCrossProduct = crossProduct / d; // normalized cross product + + return (nCrossProduct < epsilon && + Math.min(x1, x2) <= x && x <= Math.max(x1, x2) && + Math.min(y1, y2) <= y && y <= Math.max(y1, y2)); + }; + + /** + * Check if a point is on a quadratic bezier curve segment with a thickness. + * + * @param {number} x The X coordinate of the point to check. + * @param {number} y The Y coordinate of the point to check. + * @param {number} x1 The X coordinate of the curve start point. + * @param {number} y1 The Y coordinate of the curve start point. + * @param {number} x2 The X coordinate of the curve end point. + * @param {number} y2 The Y coordinate of the curve end point. + * @param {number} cpx The X coordinate of the curve control point. + * @param {number} cpy The Y coordinate of the curve control point. + * @param {number} epsilon The precision (consider the line thickness). + * @return {boolean} True if (x,y) is on the curve segment, + * false otherwise. + */ + sigma.utils.isPointOnQuadraticCurve = + function(x, y, x1, y1, x2, y2, cpx, cpy, epsilon) { + // Fails if the point is too far from the extremities of the segment, + // preventing for more costly computation: + var dP1P2 = sigma.utils.getDistance(x1, y1, x2, y2); + if (Math.abs(x - x1) > dP1P2 || Math.abs(y - y1) > dP1P2) { + return false; + } + + var dP1 = sigma.utils.getDistance(x, y, x1, y1), + dP2 = sigma.utils.getDistance(x, y, x2, y2), + t = 0.5, + r = (dP1 < dP2) ? -0.01 : 0.01, + rThreshold = 0.001, + i = 100, + pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy), + dt = sigma.utils.getDistance(x, y, pt.x, pt.y), + old_dt; + + // This algorithm minimizes the distance from the point to the curve. It + // find the optimal t value where t=0 is the start point and t=1 is the end + // point of the curve, starting from t=0.5. + // It terminates because it runs a maximum of i interations. + while (i-- > 0 && + t >= 0 && t <= 1 && + (dt > epsilon) && + (r > rThreshold || r < -rThreshold)) { + old_dt = dt; + pt = sigma.utils.getPointOnQuadraticCurve(t, x1, y1, x2, y2, cpx, cpy); + dt = sigma.utils.getDistance(x, y, pt.x, pt.y); + + if (dt > old_dt) { + // not the right direction: + // halfstep in the opposite direction + r = -r / 2; + t += r; + } + else if (t + r < 0 || t + r > 1) { + // oops, we've gone too far: + // revert with a halfstep + r = r / 2; + dt = old_dt; + } + else { + // progress: + t += r; + } + } + + return dt < epsilon; + }; + + + /** + * Check if a point is on a cubic bezier curve segment with a thickness. + * + * @param {number} x The X coordinate of the point to check. + * @param {number} y The Y coordinate of the point to check. + * @param {number} x1 The X coordinate of the curve start point. + * @param {number} y1 The Y coordinate of the curve start point. + * @param {number} x2 The X coordinate of the curve end point. + * @param {number} y2 The Y coordinate of the curve end point. + * @param {number} cpx1 The X coordinate of the 1st curve control point. + * @param {number} cpy1 The Y coordinate of the 1st curve control point. + * @param {number} cpx2 The X coordinate of the 2nd curve control point. + * @param {number} cpy2 The Y coordinate of the 2nd curve control point. + * @param {number} epsilon The precision (consider the line thickness). + * @return {boolean} True if (x,y) is on the curve segment, + * false otherwise. + */ + sigma.utils.isPointOnBezierCurve = + function(x, y, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2, epsilon) { + // Fails if the point is too far from the extremities of the segment, + // preventing for more costly computation: + var dP1CP1 = sigma.utils.getDistance(x1, y1, cpx1, cpy1); + if (Math.abs(x - x1) > dP1CP1 || Math.abs(y - y1) > dP1CP1) { + return false; + } + + var dP1 = sigma.utils.getDistance(x, y, x1, y1), + dP2 = sigma.utils.getDistance(x, y, x2, y2), + t = 0.5, + r = (dP1 < dP2) ? -0.01 : 0.01, + rThreshold = 0.001, + i = 100, + pt = sigma.utils.getPointOnBezierCurve( + t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2), + dt = sigma.utils.getDistance(x, y, pt.x, pt.y), + old_dt; + + // This algorithm minimizes the distance from the point to the curve. It + // find the optimal t value where t=0 is the start point and t=1 is the end + // point of the curve, starting from t=0.5. + // It terminates because it runs a maximum of i interations. + while (i-- > 0 && + t >= 0 && t <= 1 && + (dt > epsilon) && + (r > rThreshold || r < -rThreshold)) { + old_dt = dt; + pt = sigma.utils.getPointOnBezierCurve( + t, x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2); + dt = sigma.utils.getDistance(x, y, pt.x, pt.y); + + if (dt > old_dt) { + // not the right direction: + // halfstep in the opposite direction + r = -r / 2; + t += r; + } + else if (t + r < 0 || t + r > 1) { + // oops, we've gone too far: + // revert with a halfstep + r = r / 2; + dt = old_dt; + } + else { + // progress: + t += r; + } + } + + return dt < epsilon; + }; + + + /** + * ************ + * EVENTS UTILS: + * ************ + */ + /** + * Here are some useful functions to unify extraction of the information we + * need with mouse events and touch events, from different browsers: + */ + + /** + * Extract the local X position from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {number} The local X value of the mouse. + */ + sigma.utils.getX = function(e) { + return ( + (e.offsetX !== undefined && e.offsetX) || + (e.layerX !== undefined && e.layerX) || + (e.clientX !== undefined && e.clientX) + ); + }; + + /** + * Extract the local Y position from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {number} The local Y value of the mouse. + */ + sigma.utils.getY = function(e) { + return ( + (e.offsetY !== undefined && e.offsetY) || + (e.layerY !== undefined && e.layerY) || + (e.clientY !== undefined && e.clientY) + ); + }; + + /** + * The pixel ratio of the screen. Taking zoom into account + * + * @return {number} Pixel ratio of the screen + */ + sigma.utils.getPixelRatio = function() { + var ratio = 1; + if (window.screen.deviceXDPI !== undefined && + window.screen.logicalXDPI !== undefined && + window.screen.deviceXDPI > window.screen.logicalXDPI) { + ratio = window.screen.systemXDPI / window.screen.logicalXDPI; + } + else if (window.devicePixelRatio !== undefined) { + ratio = window.devicePixelRatio; + } + return ratio; + }; + + /** + * Extract the width from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {number} The width of the event's target. + */ + sigma.utils.getWidth = function(e) { + var w = (!e.target.ownerSVGElement) ? + e.target.width : + e.target.ownerSVGElement.width; + + return ( + (typeof w === 'number' && w) || + (w !== undefined && w.baseVal !== undefined && w.baseVal.value) + ); + }; + + /** + * Extract the center from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {object} The center of the event's target. + */ + sigma.utils.getCenter = function(e) { + var ratio = e.target.namespaceURI.indexOf('svg') !== -1 ? 1 : + sigma.utils.getPixelRatio(); + return { + x: sigma.utils.getWidth(e) / (2 * ratio), + y: sigma.utils.getHeight(e) / (2 * ratio) + }; + }; + + /** + * Convert mouse coords to sigma coords + * + * @param {event} e A mouse or touch event. + * @param {number?} x The x coord to convert + * @param {number?} x The y coord to convert + * + * @return {object} The standardized event + */ + sigma.utils.mouseCoords = function(e, x, y) { + x = x || sigma.utils.getX(e); + y = y || sigma.utils.getY(e); + return { + x: x - sigma.utils.getCenter(e).x, + y: y - sigma.utils.getCenter(e).y, + clientX: e.clientX, + clientY: e.clientY, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + altKey: e.altKey, + shiftKey: e.shiftKey + }; + }; + + /** + * Extract the height from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {number} The height of the event's target. + */ + sigma.utils.getHeight = function(e) { + var h = (!e.target.ownerSVGElement) ? + e.target.height : + e.target.ownerSVGElement.height; + + return ( + (typeof h === 'number' && h) || + (h !== undefined && h.baseVal !== undefined && h.baseVal.value) + ); + }; + + /** + * Extract the wheel delta from a mouse or touch event. + * + * @param {event} e A mouse or touch event. + * @return {number} The wheel delta of the mouse. + */ + sigma.utils.getDelta = function(e) { + return ( + (e.wheelDelta !== undefined && e.wheelDelta) || + (e.detail !== undefined && -e.detail) + ); + }; + + /** + * Returns the offset of a DOM element. + * + * @param {DOMElement} dom The element to retrieve the position. + * @return {object} The offset of the DOM element (top, left). + */ + sigma.utils.getOffset = function(dom) { + var left = 0, + top = 0; + + while (dom) { + top = top + parseInt(dom.offsetTop); + left = left + parseInt(dom.offsetLeft); + dom = dom.offsetParent; + } + + return { + top: top, + left: left + }; + }; + + /** + * Simulates a "double click" event. + * + * @param {HTMLElement} target The event target. + * @param {string} type The event type. + * @param {function} callback The callback to execute. + */ + sigma.utils.doubleClick = function(target, type, callback) { + var clicks = 0, + self = this, + handlers; + + target._doubleClickHandler = target._doubleClickHandler || {}; + target._doubleClickHandler[type] = target._doubleClickHandler[type] || []; + handlers = target._doubleClickHandler[type]; + + handlers.push(function(e) { + clicks++; + + if (clicks === 2) { + clicks = 0; + return callback(e); + } else if (clicks === 1) { + setTimeout(function() { + clicks = 0; + }, sigma.settings.doubleClickTimeout); + } + }); + + target.addEventListener(type, handlers[handlers.length - 1], false); + }; + + /** + * Unbind simulated "double click" events. + * + * @param {HTMLElement} target The event target. + * @param {string} type The event type. + */ + sigma.utils.unbindDoubleClick = function(target, type) { + var handler, + handlers = (target._doubleClickHandler || {})[type] || []; + + while ((handler = handlers.pop())) { + target.removeEventListener(type, handler); + } + + delete (target._doubleClickHandler || {})[type]; + }; + + + + + /** + * Here are just some of the most basic easing functions, used for the + * animated camera "goTo" calls. + * + * If you need some more easings functions, don't hesitate to add them to + * sigma.utils.easings. But I will not add some more here or merge PRs + * containing, because I do not want sigma sources full of overkill and never + * used stuff... + */ + sigma.utils.easings = sigma.utils.easings || {}; + sigma.utils.easings.linearNone = function(k) { + return k; + }; + sigma.utils.easings.quadraticIn = function(k) { + return k * k; + }; + sigma.utils.easings.quadraticOut = function(k) { + return k * (2 - k); + }; + sigma.utils.easings.quadraticInOut = function(k) { + if ((k *= 2) < 1) + return 0.5 * k * k; + return - 0.5 * (--k * (k - 2) - 1); + }; + sigma.utils.easings.cubicIn = function(k) { + return k * k * k; + }; + sigma.utils.easings.cubicOut = function(k) { + return --k * k * k + 1; + }; + sigma.utils.easings.cubicInOut = function(k) { + if ((k *= 2) < 1) + return 0.5 * k * k * k; + return 0.5 * ((k -= 2) * k * k + 2); + }; + + + + + /** + * ************ + * WEBGL UTILS: + * ************ + */ + /** + * Loads a WebGL shader and returns it. + * + * @param {WebGLContext} gl The WebGLContext to use. + * @param {string} shaderSource The shader source. + * @param {number} shaderType The type of shader. + * @param {function(string): void} error Callback for errors. + * @return {WebGLShader} The created shader. + */ + sigma.utils.loadShader = function(gl, shaderSource, shaderType, error) { + var compiled, + shader = gl.createShader(shaderType); + + // Load the shader source + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check the compile status + compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + + // If something went wrong: + if (!compiled) { + if (error) { + error( + 'Error compiling shader "' + shader + '":' + + gl.getShaderInfoLog(shader) + ); + } + + gl.deleteShader(shader); + return null; + } + + return shader; + }; + + /** + * Creates a program, attaches shaders, binds attrib locations, links the + * program and calls useProgram. + * + * @param {Array.} shaders The shaders to attach. + * @param {Array.} attribs The attribs names. + * @param {Array.} locations The locations for the attribs. + * @param {function(string): void} error Callback for errors. + * @return {WebGLProgram} The created program. + */ + sigma.utils.loadProgram = function(gl, shaders, attribs, loc, error) { + var i, + linked, + program = gl.createProgram(); + + for (i = 0; i < shaders.length; ++i) + gl.attachShader(program, shaders[i]); + + if (attribs) + for (i = 0; i < attribs.length; ++i) + gl.bindAttribLocation( + program, + locations ? locations[i] : i, + opt_attribs[i] + ); + + gl.linkProgram(program); + + // Check the link status + linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + if (error) + error('Error in program linking: ' + gl.getProgramInfoLog(program)); + + gl.deleteProgram(program); + return null; + } + + return program; + }; + + + + + /** + * ********* + * MATRICES: + * ********* + * The following utils are just here to help generating the transformation + * matrices for the WebGL renderers. + */ + sigma.utils.pkg('sigma.utils.matrices'); + + /** + * The returns a 3x3 translation matrix. + * + * @param {number} dx The X translation. + * @param {number} dy The Y translation. + * @return {array} Returns the matrix. + */ + sigma.utils.matrices.translation = function(dx, dy) { + return [ + 1, 0, 0, + 0, 1, 0, + dx, dy, 1 + ]; + }; + + /** + * The returns a 3x3 or 2x2 rotation matrix. + * + * @param {number} angle The rotation angle. + * @param {boolean} m2 If true, the function will return a 2x2 matrix. + * @return {array} Returns the matrix. + */ + sigma.utils.matrices.rotation = function(angle, m2) { + var cos = Math.cos(angle), + sin = Math.sin(angle); + + return m2 ? [ + cos, -sin, + sin, cos + ] : [ + cos, -sin, 0, + sin, cos, 0, + 0, 0, 1 + ]; + }; + + /** + * The returns a 3x3 or 2x2 homothetic transformation matrix. + * + * @param {number} ratio The scaling ratio. + * @param {boolean} m2 If true, the function will return a 2x2 matrix. + * @return {array} Returns the matrix. + */ + sigma.utils.matrices.scale = function(ratio, m2) { + return m2 ? [ + ratio, 0, + 0, ratio + ] : [ + ratio, 0, 0, + 0, ratio, 0, + 0, 0, 1 + ]; + }; + + /** + * The returns a 3x3 or 2x2 homothetic transformation matrix. + * + * @param {array} a The first matrix. + * @param {array} b The second matrix. + * @param {boolean} m2 If true, the function will assume both matrices are + * 2x2. + * @return {array} Returns the matrix. + */ + sigma.utils.matrices.multiply = function(a, b, m2) { + var l = m2 ? 2 : 3, + a00 = a[0 * l + 0], + a01 = a[0 * l + 1], + a02 = a[0 * l + 2], + a10 = a[1 * l + 0], + a11 = a[1 * l + 1], + a12 = a[1 * l + 2], + a20 = a[2 * l + 0], + a21 = a[2 * l + 1], + a22 = a[2 * l + 2], + b00 = b[0 * l + 0], + b01 = b[0 * l + 1], + b02 = b[0 * l + 2], + b10 = b[1 * l + 0], + b11 = b[1 * l + 1], + b12 = b[1 * l + 2], + b20 = b[2 * l + 0], + b21 = b[2 * l + 1], + b22 = b[2 * l + 2]; + + return m2 ? [ + a00 * b00 + a01 * b10, + a00 * b01 + a01 * b11, + a10 * b00 + a11 * b10, + a10 * b01 + a11 * b11 + ] : [ + a00 * b00 + a01 * b10 + a02 * b20, + a00 * b01 + a01 * b11 + a02 * b21, + a00 * b02 + a01 * b12 + a02 * b22, + a10 * b00 + a11 * b10 + a12 * b20, + a10 * b01 + a11 * b11 + a12 * b21, + a10 * b02 + a11 * b12 + a12 * b22, + a20 * b00 + a21 * b10 + a22 * b20, + a20 * b01 + a21 * b11 + a22 * b21, + a20 * b02 + a21 * b12 + a22 * b22 + ]; + }; +}).call(this); diff --git a/blogContent/projects/steam/src/worker.js b/blogContent/projects/steam/src/worker.js new file mode 100644 index 0000000..adc0b29 --- /dev/null +++ b/blogContent/projects/steam/src/worker.js @@ -0,0 +1,1129 @@ +;(function(undefined) { + 'use strict'; + + /** + * Sigma ForceAtlas2.5 Webworker + * ============================== + * + * Author: Guillaume Plique (Yomguithereal) + * Algorithm author: Mathieu Jacomy @ Sciences Po Medialab & WebAtlas + * Version: 1.0.3 + */ + + var _root = this, + inWebWorker = !('document' in _root); + + /** + * Worker Function Wrapper + * ------------------------ + * + * The worker has to be wrapped into a single stringified function + * to be passed afterwards as a BLOB object to the supervisor. + */ + var Worker = function(undefined) { + 'use strict'; + + /** + * Worker settings and properties + */ + var W = { + + // Properties + ppn: 10, + ppe: 3, + ppr: 9, + maxForce: 10, + iterations: 0, + converged: false, + + // Possible to change through config + settings: { + linLogMode: false, + outboundAttractionDistribution: false, + adjustSizes: false, + edgeWeightInfluence: 0, + scalingRatio: 1, + strongGravityMode: false, + gravity: 1, + slowDown: 1, + barnesHutOptimize: false, + barnesHutTheta: 0.5, + startingIterations: 1, + iterationsPerRender: 1 + } + }; + + var NodeMatrix, + EdgeMatrix, + RegionMatrix; + + /** + * Helpers + */ + function extend() { + var i, + k, + res = {}, + l = arguments.length; + + for (i = l - 1; i >= 0; i--) + for (k in arguments[i]) + res[k] = arguments[i][k]; + return res; + } + + function __emptyObject(obj) { + var k; + + for (k in obj) + if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k)) + delete obj[k]; + + return obj; + } + + /** + * Matrices properties accessors + */ + var nodeProperties = { + x: 0, + y: 1, + dx: 2, + dy: 3, + old_dx: 4, + old_dy: 5, + mass: 6, + convergence: 7, + size: 8, + fixed: 9 + }; + + var edgeProperties = { + source: 0, + target: 1, + weight: 2 + }; + + var regionProperties = { + node: 0, + centerX: 1, + centerY: 2, + size: 3, + nextSibling: 4, + firstChild: 5, + mass: 6, + massCenterX: 7, + massCenterY: 8 + }; + + function np(i, p) { + + // DEBUG: safeguards + if ((i % W.ppn) !== 0) + throw 'np: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'np: non int.'; + + if (p in nodeProperties) + return i + nodeProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant node property given (' + p + ').'; + } + + function ep(i, p) { + + // DEBUG: safeguards + if ((i % W.ppe) !== 0) + throw 'ep: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'ep: non int.'; + + if (p in edgeProperties) + return i + edgeProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant edge property given (' + p + ').'; + } + + function rp(i, p) { + + // DEBUG: safeguards + if ((i % W.ppr) !== 0) + throw 'rp: non correct (' + i + ').'; + if (i !== parseInt(i)) + throw 'rp: non int.'; + + if (p in regionProperties) + return i + regionProperties[p]; + else + throw 'ForceAtlas2.Worker - ' + + 'Inexistant region property given (' + p + ').'; + } + + // DEBUG + function nan(v) { + if (isNaN(v)) + throw 'NaN alert!'; + } + + + /** + * Algorithm initialization + */ + + function init(nodes, edges, config) { + config = config || {}; + var i, l; + + // Matrices + NodeMatrix = nodes; + EdgeMatrix = edges; + + // Length + W.nodesLength = NodeMatrix.length; + W.edgesLength = EdgeMatrix.length; + + // Merging configuration + configure(config); + } + + function configure(o) { + W.settings = extend(o, W.settings); + } + + /** + * Algorithm pass + */ + + // MATH: get distances stuff and power 2 issues + function pass() { + var a, i, j, l, r, n, n1, n2, e, w, g, k, m; + + var outboundAttCompensation, + coefficient, + xDist, + yDist, + ewc, + mass, + distance, + size, + factor; + + // 1) Initializing layout data + //----------------------------- + + // Resetting positions & computing max values + for (n = 0; n < W.nodesLength; n += W.ppn) { + NodeMatrix[np(n, 'old_dx')] = NodeMatrix[np(n, 'dx')]; + NodeMatrix[np(n, 'old_dy')] = NodeMatrix[np(n, 'dy')]; + NodeMatrix[np(n, 'dx')] = 0; + NodeMatrix[np(n, 'dy')] = 0; + } + + // If outbound attraction distribution, compensate + if (W.settings.outboundAttractionDistribution) { + outboundAttCompensation = 0; + for (n = 0; n < W.nodesLength; n += W.ppn) { + outboundAttCompensation += NodeMatrix[np(n, 'mass')]; + } + + outboundAttCompensation /= W.nodesLength; + } + + + // 1.bis) Barnes-Hut computation + //------------------------------ + + if (W.settings.barnesHutOptimize) { + + var minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity, + q, q0, q1, q2, q3; + + // Setting up + // RegionMatrix = new Float32Array(W.nodesLength / W.ppn * 4 * W.ppr); + RegionMatrix = []; + + // Computing min and max values + for (n = 0; n < W.nodesLength; n += W.ppn) { + minX = Math.min(minX, NodeMatrix[np(n, 'x')]); + maxX = Math.max(maxX, NodeMatrix[np(n, 'x')]); + minY = Math.min(minY, NodeMatrix[np(n, 'y')]); + maxY = Math.max(maxY, NodeMatrix[np(n, 'y')]); + } + + // Build the Barnes Hut root region + RegionMatrix[rp(0, 'node')] = -1; + RegionMatrix[rp(0, 'centerX')] = (minX + maxX) / 2; + RegionMatrix[rp(0, 'centerY')] = (minY + maxY) / 2; + RegionMatrix[rp(0, 'size')] = Math.max(maxX - minX, maxY - minY); + RegionMatrix[rp(0, 'nextSibling')] = -1; + RegionMatrix[rp(0, 'firstChild')] = -1; + RegionMatrix[rp(0, 'mass')] = 0; + RegionMatrix[rp(0, 'massCenterX')] = 0; + RegionMatrix[rp(0, 'massCenterY')] = 0; + + // Add each node in the tree + l = 1; + for (n = 0; n < W.nodesLength; n += W.ppn) { + + // Current region, starting with root + r = 0; + + while (true) { + // Are there sub-regions? + + // We look at first child index + if (RegionMatrix[rp(r, 'firstChild')] >= 0) { + + // There are sub-regions + + // We just iterate to find a "leave" of the tree + // that is an empty region or a region with a single node + // (see next case) + + // Find the quadrant of n + if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { + + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q = RegionMatrix[rp(r, 'firstChild')]; + } + else { + + // Bottom Left quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + // Update center of mass and mass (we only do it for non-leave regions) + RegionMatrix[rp(r, 'massCenterX')] = + (RegionMatrix[rp(r, 'massCenterX')] * RegionMatrix[rp(r, 'mass')] + + NodeMatrix[np(n, 'x')] * NodeMatrix[np(n, 'mass')]) / + (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); + + RegionMatrix[rp(r, 'massCenterY')] = + (RegionMatrix[rp(r, 'massCenterY')] * RegionMatrix[rp(r, 'mass')] + + NodeMatrix[np(n, 'y')] * NodeMatrix[np(n, 'mass')]) / + (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); + + RegionMatrix[rp(r, 'mass')] += NodeMatrix[np(n, 'mass')]; + + // Iterate on the right quadrant + r = q; + continue; + } + else { + + // There are no sub-regions: we are in a "leave" + + // Is there a node in this leave? + if (RegionMatrix[rp(r, 'node')] < 0) { + + // There is no node in region: + // we record node n and go on + RegionMatrix[rp(r, 'node')] = n; + break; + } + else { + + // There is a node in this region + + // We will need to create sub-regions, stick the two + // nodes (the old one r[0] and the new one n) in two + // subregions. If they fall in the same quadrant, + // we will iterate. + + // Create sub-regions + RegionMatrix[rp(r, 'firstChild')] = l * W.ppr; + w = RegionMatrix[rp(r, 'size')] / 2; // new size (half) + + // NOTE: we use screen coordinates + // from Top Left to Bottom Right + + // Top Left sub-region + g = RegionMatrix[rp(r, 'firstChild')]; + + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Bottom Left sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Top Right sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + // Bottom Right sub-region + g += W.ppr; + RegionMatrix[rp(g, 'node')] = -1; + RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; + RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; + RegionMatrix[rp(g, 'size')] = w; + RegionMatrix[rp(g, 'nextSibling')] = RegionMatrix[rp(r, 'nextSibling')]; + RegionMatrix[rp(g, 'firstChild')] = -1; + RegionMatrix[rp(g, 'mass')] = 0; + RegionMatrix[rp(g, 'massCenterX')] = 0; + RegionMatrix[rp(g, 'massCenterY')] = 0; + + l += 4; + + // Now the goal is to find two different sub-regions + // for the two nodes: the one previously recorded (r[0]) + // and the one we want to add (n) + + // Find the quadrant of the old node + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')] < RegionMatrix[rp(r, 'centerX')]) { + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q = RegionMatrix[rp(r, 'firstChild')]; + } + else { + + // Bottom Left quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + // We remove r[0] from the region r, add its mass to r and record it in q + RegionMatrix[rp(r, 'mass')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')]; + RegionMatrix[rp(r, 'massCenterX')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; + RegionMatrix[rp(r, 'massCenterY')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; + + RegionMatrix[rp(q, 'node')] = RegionMatrix[rp(r, 'node')]; + RegionMatrix[rp(r, 'node')] = -1; + + // Find the quadrant of n + if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { + if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Left quarter + q2 = RegionMatrix[rp(r, 'firstChild')]; + } + else { + // Bottom Left quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr; + } + } + else { + if(NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { + + // Top Right quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; + } + else { + + // Bottom Right quarter + q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; + } + } + + if (q === q2) { + + // If both nodes are in the same quadrant, + // we have to try it again on this quadrant + r = q; + continue; + } + + // If both quadrants are different, we record n + // in its quadrant + RegionMatrix[rp(q2, 'node')] = n; + break; + } + } + } + } + } + + + // 2) Repulsion + //-------------- + // NOTES: adjustSizes = antiCollision & scalingRatio = coefficient + + if (W.settings.barnesHutOptimize) { + coefficient = W.settings.scalingRatio; + + // Applying repulsion through regions + for (n = 0; n < W.nodesLength; n += W.ppn) { + + // Computing leaf quad nodes iteration + + r = 0; // Starting with root region + while (true) { + + if (RegionMatrix[rp(r, 'firstChild')] >= 0) { + + // The region has sub-regions + + // We run the Barnes Hut test to see if we are at the right distance + distance = Math.sqrt( + (Math.pow(NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')], 2)) + + (Math.pow(NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')], 2)) + ); + + if (2 * RegionMatrix[rp(r, 'size')] / distance < W.settings.barnesHutTheta) { + + // We treat the region as a single body, and we repulse + + xDist = NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')]; + yDist = NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')]; + + if (W.settings.adjustSizes) { + + //-- Linear Anti-collision Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = -coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + else { + + //-- Linear Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + RegionMatrix[rp(r, 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + + // When this is done, we iterate. We have to look at the next sibling. + if (RegionMatrix[rp(r, 'nextSibling')] < 0) + break; // No next sibling: we have finished the tree + r = RegionMatrix[rp(r, 'nextSibling')]; + continue; + + } + else { + + // The region is too close and we have to look at sub-regions + r = RegionMatrix[rp(r, 'firstChild')]; + continue; + } + + } + else { + + // The region has no sub-region + // If there is a node r[0] and it is not n, then repulse + + if (RegionMatrix[rp(r, 'node')] >= 0 && RegionMatrix[rp(r, 'node')] !== n) { + xDist = NodeMatrix[np(n, 'x')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; + yDist = NodeMatrix[np(n, 'y')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; + + distance = Math.sqrt(xDist * xDist + yDist * yDist); + + if (W.settings.adjustSizes) { + + //-- Linear Anti-collision Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = -coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + else { + + //-- Linear Repulsion + if (distance > 0) { + factor = coefficient * NodeMatrix[np(n, 'mass')] * + NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; + + NodeMatrix[np(n, 'dx')] += xDist * factor; + NodeMatrix[np(n, 'dy')] += yDist * factor; + } + } + + } + + // When this is done, we iterate. We have to look at the next sibling. + if (RegionMatrix[rp(r, 'nextSibling')] < 0) + break; // No next sibling: we have finished the tree + r = RegionMatrix[rp(r, 'nextSibling')]; + continue; + } + } + } + } + else { + coefficient = W.settings.scalingRatio; + + // Square iteration + for (n1 = 0; n1 < W.nodesLength; n1 += W.ppn) { + for (n2 = 0; n2 < n1; n2 += W.ppn) { + + // Common to both methods + xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; + yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; + + if (W.settings.adjustSizes) { + + //-- Anticollision Linear Repulsion + distance = Math.sqrt(xDist * xDist + yDist * yDist) - + NodeMatrix[np(n1, 'size')] - + NodeMatrix[np(n2, 'size')]; + + if (distance > 0) { + factor = coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')] / + distance / distance; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] += xDist * factor; + NodeMatrix[np(n2, 'dy')] += yDist * factor; + } + else if (distance < 0) { + factor = 100 * coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')]; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + else { + + //-- Linear Repulsion + distance = Math.sqrt(xDist * xDist + yDist * yDist); + + if (distance > 0) { + factor = coefficient * + NodeMatrix[np(n1, 'mass')] * + NodeMatrix[np(n2, 'mass')] / + distance / distance; + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + } + } + } + + + // 3) Gravity + //------------ + g = W.settings.gravity / W.settings.scalingRatio; + coefficient = W.settings.scalingRatio; + for (n = 0; n < W.nodesLength; n += W.ppn) { + factor = 0; + + // Common to both methods + xDist = NodeMatrix[np(n, 'x')]; + yDist = NodeMatrix[np(n, 'y')]; + distance = Math.sqrt( + Math.pow(xDist, 2) + Math.pow(yDist, 2) + ); + + if (W.settings.strongGravityMode) { + + //-- Strong gravity + if (distance > 0) + factor = coefficient * NodeMatrix[np(n, 'mass')] * g; + } + else { + + //-- Linear Anti-collision Repulsion n + if (distance > 0) + factor = coefficient * NodeMatrix[np(n, 'mass')] * g / distance; + } + + // Updating node's dx and dy + NodeMatrix[np(n, 'dx')] -= xDist * factor; + NodeMatrix[np(n, 'dy')] -= yDist * factor; + } + + + + // 4) Attraction + //--------------- + coefficient = 1 * + (W.settings.outboundAttractionDistribution ? + outboundAttCompensation : + 1); + + // TODO: simplify distance + // TODO: coefficient is always used as -c --> optimize? + for (e = 0; e < W.edgesLength; e += W.ppe) { + n1 = EdgeMatrix[ep(e, 'source')]; + n2 = EdgeMatrix[ep(e, 'target')]; + w = EdgeMatrix[ep(e, 'weight')]; + + // Edge weight influence + ewc = Math.pow(w, W.settings.edgeWeightInfluence); + + // Common measures + xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; + yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; + + // Applying attraction to nodes + if (W.settings.adjustSizes) { + + distance = Math.sqrt( + (Math.pow(xDist, 2) + Math.pow(yDist, 2)) - + NodeMatrix[np(n1, 'size')] - + NodeMatrix[np(n2, 'size')] + ); + + if (W.settings.linLogMode) { + if (W.settings.outboundAttractionDistribution) { + + //-- LinLog Degree Distributed Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / + distance / + NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- LinLog Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / distance; + } + } + } + else { + if (W.settings.outboundAttractionDistribution) { + + //-- Linear Degree Distributed Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- Linear Anti-collision Attraction + if (distance > 0) { + factor = -coefficient * ewc; + } + } + } + } + else { + + distance = Math.sqrt( + Math.pow(xDist, 2) + Math.pow(yDist, 2) + ); + + if (W.settings.linLogMode) { + if (W.settings.outboundAttractionDistribution) { + + //-- LinLog Degree Distributed Attraction + if (distance > 0) { + factor = -coefficient * ewc * Math.log(1 + distance) / + distance / + NodeMatrix[np(n1, 'mass')]; + } + } + else { + + //-- LinLog Attraction + if (distance > 0) + factor = -coefficient * ewc * Math.log(1 + distance) / distance; + } + } + else { + if (W.settings.outboundAttractionDistribution) { + + //-- Linear Attraction Mass Distributed + // NOTE: Distance is set to 1 to override next condition + distance = 1; + factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; + } + else { + + //-- Linear Attraction + // NOTE: Distance is set to 1 to override next condition + distance = 1; + factor = -coefficient * ewc; + } + } + } + + // Updating nodes' dx and dy + // TODO: if condition or factor = 1? + if (distance > 0) { + + // Updating nodes' dx and dy + NodeMatrix[np(n1, 'dx')] += xDist * factor; + NodeMatrix[np(n1, 'dy')] += yDist * factor; + + NodeMatrix[np(n2, 'dx')] -= xDist * factor; + NodeMatrix[np(n2, 'dy')] -= yDist * factor; + } + } + + + // 5) Apply Forces + //----------------- + var force, + swinging, + traction, + nodespeed; + + // MATH: sqrt and square distances + if (W.settings.adjustSizes) { + + for (n = 0; n < W.nodesLength; n += W.ppn) { + if (!NodeMatrix[np(n, 'fixed')]) { + force = Math.sqrt( + Math.pow(NodeMatrix[np(n, 'dx')], 2) + + Math.pow(NodeMatrix[np(n, 'dy')], 2) + ); + + if (force > W.maxForce) { + NodeMatrix[np(n, 'dx')] = + NodeMatrix[np(n, 'dx')] * W.maxForce / force; + NodeMatrix[np(n, 'dy')] = + NodeMatrix[np(n, 'dy')] * W.maxForce / force; + } + + swinging = NodeMatrix[np(n, 'mass')] * + Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) + ); + + traction = Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) + ) / 2; + + nodespeed = + 0.1 * Math.log(1 + traction) / (1 + Math.sqrt(swinging)); + + // Updating node's positon + NodeMatrix[np(n, 'x')] = + NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * + (nodespeed / W.settings.slowDown); + NodeMatrix[np(n, 'y')] = + NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * + (nodespeed / W.settings.slowDown); + } + } + } + else { + + for (n = 0; n < W.nodesLength; n += W.ppn) { + if (!NodeMatrix[np(n, 'fixed')]) { + + swinging = NodeMatrix[np(n, 'mass')] * + Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) + ); + + traction = Math.sqrt( + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * + (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) + ) / 2; + + nodespeed = NodeMatrix[np(n, 'convergence')] * + Math.log(1 + traction) / (1 + Math.sqrt(swinging)); + + // Updating node convergence + NodeMatrix[np(n, 'convergence')] = + Math.min(1, Math.sqrt( + nodespeed * + (Math.pow(NodeMatrix[np(n, 'dx')], 2) + + Math.pow(NodeMatrix[np(n, 'dy')], 2)) / + (1 + Math.sqrt(swinging)) + )); + + // Updating node's positon + NodeMatrix[np(n, 'x')] = + NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * + (nodespeed / W.settings.slowDown); + NodeMatrix[np(n, 'y')] = + NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * + (nodespeed / W.settings.slowDown); + } + } + } + + // Counting one more iteration + W.iterations++; + } + + /** + * Message reception & sending + */ + + // Sending data back to the supervisor + var sendNewCoords; + + if (typeof window !== 'undefined' && window.document) { + + // From same document as sigma + sendNewCoords = function() { + var e; + + if (document.createEvent) { + e = document.createEvent('Event'); + e.initEvent('newCoords', true, false); + } + else { + e = document.createEventObject(); + e.eventType = 'newCoords'; + } + + e.eventName = 'newCoords'; + e.data = { + nodes: NodeMatrix.buffer + }; + requestAnimationFrame(function() { + document.dispatchEvent(e); + }); + }; + } + else { + + // From a WebWorker + sendNewCoords = function() { + self.postMessage( + {nodes: NodeMatrix.buffer}, + [NodeMatrix.buffer] + ); + }; + } + + // Algorithm run + function run(n) { + for (var i = 0; i < n; i++) + pass(); + sendNewCoords(); + } + + // On supervisor message + var listener = function(e) { + switch (e.data.action) { + case 'start': + init( + new Float32Array(e.data.nodes), + new Float32Array(e.data.edges), + e.data.config + ); + + // First iteration(s) + run(W.settings.startingIterations); + break; + + case 'loop': + NodeMatrix = new Float32Array(e.data.nodes); + run(W.settings.iterationsPerRender); + break; + + case 'config': + + // Merging new settings + configure(e.data.config); + break; + + case 'kill': + + // Deleting context for garbage collection + __emptyObject(W); + NodeMatrix = null; + EdgeMatrix = null; + RegionMatrix = null; + self.removeEventListener('message', listener); + break; + + default: + } + }; + + // Adding event listener + self.addEventListener('message', listener); + }; + + + /** + * Exporting + * ---------- + * + * Crush the worker function and make it accessible by sigma's instances so + * the supervisor can call it. + */ + function crush(fnString) { + var pattern, + i, + l; + + var np = [ + 'x', + 'y', + 'dx', + 'dy', + 'old_dx', + 'old_dy', + 'mass', + 'convergence', + 'size', + 'fixed' + ]; + + var ep = [ + 'source', + 'target', + 'weight' + ]; + + var rp = [ + 'node', + 'centerX', + 'centerY', + 'size', + 'nextSibling', + 'firstChild', + 'mass', + 'massCenterX', + 'massCenterY' + ]; + + // rp + // NOTE: Must go first + for (i = 0, l = rp.length; i < l; i++) { + pattern = new RegExp('rp\\(([^,]*), \'' + rp[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + // np + for (i = 0, l = np.length; i < l; i++) { + pattern = new RegExp('np\\(([^,]*), \'' + np[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + // ep + for (i = 0, l = ep.length; i < l; i++) { + pattern = new RegExp('ep\\(([^,]*), \'' + ep[i] + '\'\\)', 'g'); + fnString = fnString.replace( + pattern, + (i === 0) ? '$1' : '$1 + ' + i + ); + } + + return fnString; + } + + // Exporting + function getWorkerFn() { + var fnString = crush ? crush(Worker.toString()) : Worker.toString(); + return ';(' + fnString + ').call(this);'; + } + + if (inWebWorker) { + + // We are in a webworker, so we launch the Worker function + eval(getWorkerFn()); + } + else { + + // We are requesting the worker from sigma, we retrieve it therefore + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + sigma.prototype.getForceAtlas2Worker = getWorkerFn; + } +}).call(this); diff --git a/docs/projectsSites.svg b/docs/projectsSites.svg new file mode 100644 index 0000000..b7a3f05 --- /dev/null +++ b/docs/projectsSites.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/includes/projects.js b/includes/projects.js new file mode 100644 index 0000000..22c545e --- /dev/null +++ b/includes/projects.js @@ -0,0 +1,64 @@ +//file io +const utils = require('../utils/utils.js'); + +//used to parse the request URL +const url = require('url'); + + +/** + * @author Jeffery Russell 10-30-18 + * + * @type {{main: (function(*=, *): Promise)}} + */ +module.exports= + { + /** + * Calls posts and sidebar modules to render blog contents in order + * + * @param requestURL + * @returns {Promise|*} + */ + main: function(request, result, baseURL) + { + //const filename = url.parse(request.url, true).pathname + + var filename = url.parse(request.url, true).pathname; + + if(filename.includes(".svg") || filename.includes(".svg")) + { + result.writeHead(200, {'Content-Type': 'image/svg+xml'}); + } + else if(filename.includes("/img/") || filename.includes(".jpg") || + filename.includes(".png") || filename.includes(".ico")) + { + result.writeHead(200, {'Content-Type': 'image/png'}); + } + else if(filename.includes("/css/") || filename.includes(".woff2") || + filename.includes(".txt")) + { + result.writeHead(200, {'Content-Type': 'text/css'}); + } + else if(filename.includes("/js/") || filename.includes(".js")) + { + result.writeHead(200, {'Content-Type': 'application/javascript'}); + } + else + { + result.writeHead(200, {'Content-Type': 'text/html'}); + } + + if(filename == baseURL || filename == baseURL.substring(0, baseURL.length - 1)) + { + filename = baseURL + "index.html"; + } + + utils.include("./blogContent/projects" + filename).then(function(content) + { + result.write(content); + result.end(); + }).catch(function(error) + { + console.log(error); + }); + } + }; \ No newline at end of file diff --git a/server.js b/server.js index c4941e8..b2cbd6d 100644 --- a/server.js +++ b/server.js @@ -37,6 +37,7 @@ map.main(); //port for the server to run on const port = 8000; +const projects = ["/steam/"]; /** * Parses the request url and calls correct JS files @@ -49,9 +50,26 @@ app.use(function(request, result) { const filename = url.parse(request.url, true).pathname; + var project = false; + projects.forEach(function(projectName) + { + if(filename.includes(projectName)) + { + require("./includes/projects.js").main(request, result, projectName); + project = true; + } + }); + + + if(project) + { + //don't do blog stuff + } + //handles image requests - if(filename.includes("/img/") || filename.includes(".jpg") || - filename.includes(".png") || filename.includes(".ico")) + else if(filename.includes("/img/") || filename.includes(".jpg") || + filename.includes(".png") || filename.includes(".ico") + || filename.includes(".svg")) { includes.sendImage(result, filename, cache); }