├── src ├── check.sh ├── go.mod ├── common_utils.go ├── go.sum ├── html_generation.go ├── linkitall_assets │ ├── template.html │ ├── style.css │ └── main.js ├── gdf_loading.go ├── main.go ├── node_computation.go └── linkitall_vendor │ └── leader-line │ └── leader-line-v1.1.5.min.js ├── aux ├── start_server.bat └── RELEASE_README.md ├── docs └── algo-config │ ├── images │ ├── node-sorting.png │ └── level-strategy.png │ └── README.md ├── examples └── simple │ ├── resources │ ├── microorganisms.pdf │ ├── pexels-daria-klet-8479585.jpg │ └── main.html │ └── graph.yaml ├── LICENSE └── README.md /src/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | go fmt && go vet && staticcheck 3 | -------------------------------------------------------------------------------- /aux/start_server.bat: -------------------------------------------------------------------------------- 1 | CALL linkitall.exe --overwrite -i "?" --release --serve --listen ":8101" 2 | PAUSE 3 | -------------------------------------------------------------------------------- /docs/algo-config/images/node-sorting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charstorm/linkitall/HEAD/docs/algo-config/images/node-sorting.png -------------------------------------------------------------------------------- /docs/algo-config/images/level-strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charstorm/linkitall/HEAD/docs/algo-config/images/level-strategy.png -------------------------------------------------------------------------------- /examples/simple/resources/microorganisms.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charstorm/linkitall/HEAD/examples/simple/resources/microorganisms.pdf -------------------------------------------------------------------------------- /examples/simple/resources/pexels-daria-klet-8479585.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charstorm/linkitall/HEAD/examples/simple/resources/pexels-daria-klet-8479585.jpg -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/charstorm/linkitall/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.4.3 // indirect 7 | github.com/alexflint/go-scalar v1.1.0 // indirect 8 | github.com/otiai10/copy v1.12.0 // indirect 9 | golang.org/x/sys v0.5.0 // indirect 10 | golang.org/x/text v0.12.0 // indirect 11 | gopkg.in/yaml.v2 v2.4.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /src/common_utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func pushBack[T any](array *[]T, object T) { 4 | *array = append(*array, object) 5 | } 6 | 7 | // func assertEqual[T comparable](expected, actual T) { 8 | // if expected != actual { 9 | // panic(fmt.Sprintf("Expected %v, got %v", expected, actual)) 10 | // } 11 | // } 12 | 13 | func getUnique[T comparable](array []T) []T { 14 | seen := make(map[T]bool) 15 | // input -> map 16 | for _, item := range array { 17 | seen[item] = true 18 | } 19 | 20 | uniqueItems := make([]T, 0, len(seen)) 21 | // map -> output 22 | for item := range seen { 23 | uniqueItems = append(uniqueItems, item) 24 | } 25 | 26 | return uniqueItems 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023- Vinay Krishnan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /aux/RELEASE_README.md: -------------------------------------------------------------------------------- 1 | # Linkitall 2 | 3 | This package contains the Linkitall release files (amd64) for Windows and Linux operating systems. 4 | There is no installation required. 5 | 6 | Steps for running the tool are given below. 7 | 8 | ## Windows 9 | 10 | 1. Extract the archive to a suitable location 11 | 2. Execute the batch script `start_server.bat` found inside the extracted directory. 12 | Just double clicking the script file should be enough in most cases. 13 | 3. Provide the path to the directory with graph file when asked (example is provided in the GitHub repository) 14 | 4. Allow permission for networking if asked 15 | 5. Go to `http://127.0.0.1:8101` in a browser to see the graph generated 16 | 17 | The tool will be running in server mode. It follows a simple `update-generate-refresh` cycle. 18 | 19 | 1. Edit/Update the graph file 20 | 2. Press enter in the script window to generate the new HTML for the graph 21 | 3. Refresh the web page 22 | 4. When development is complete, press `q` to quit the tool! 23 | 24 | ## Linux 25 | 26 | Use the following command to run the tool: 27 | 28 | ```bash 29 | linkitall --overwrite --release --serve --listen ":8101" -i path/to/graph/dir 30 | ``` 31 | 32 | See the main `README.md` of the linitall repo to understand the meaning of these flags. 33 | For the usage see the `update-generate-refresh` part explained in the Windows section above. 34 | 35 | ## License 36 | 37 | [The license for the project](https://github.com/charstorm/linkitall/blob/main/LICENSE) 38 | 39 | External packages: 40 | 41 | * You can find the list of external Go packages [here](https://github.com/charstorm/linkitall/blob/main/src/go.mod). 42 | Their licenses can be found in their respective repositories. 43 | * The project uses the Javascript package `leader-line` to draw all the connections. 44 | License for it can be found [here](https://anseki.github.io/leader-line/). 45 | 46 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= 2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= 3 | github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= 4 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= 8 | github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 14 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 16 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 18 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 21 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /src/html_generation.go: -------------------------------------------------------------------------------- 1 | // This file deals with the generation of the HTML document based on the processed data 2 | package main 3 | 4 | import ( 5 | "html/template" 6 | "os" 7 | ) 8 | 9 | // Info about the board (outer board used for holding all the nodes) 10 | type BoardConfigFields struct { 11 | Width int 12 | Height int 13 | } 14 | 15 | type ControlConfigFields struct { 16 | // In release mode, we use CDN for all the vendor files 17 | Release bool 18 | } 19 | 20 | // All the data required for generating HTML page from template is stored in this struct 21 | type TemplateData struct { 22 | // Input GDF Data 23 | GdfData *GdfDataStruct 24 | // With computed fields 25 | Nodes []NodeData 26 | // Board configuration 27 | BoardConfig BoardConfigFields 28 | // Controlling template generation 29 | ControlConfig ControlConfigFields 30 | } 31 | 32 | func computeBoardConfig(gdfData *GdfDataStruct, nodes []NodeData) BoardConfigFields { 33 | extraWidth := 10 34 | nodeBoxWidthPx := gdfData.DisplayConfig.NodeBoxWidthPx 35 | // Not configurable for now (but it shouldn't matter much anyway) 36 | nodeBoxHeightPx := 150 37 | maxWidth := 0 38 | maxHeight := 0 39 | // Initially compute max of left and top. Then add node width and height. 40 | for _, node := range nodes { 41 | if node.ElemFields.LeftPx > maxWidth { 42 | maxWidth = node.ElemFields.LeftPx 43 | } 44 | if node.ElemFields.TopPx > maxHeight { 45 | maxHeight = node.ElemFields.TopPx 46 | } 47 | } 48 | 49 | return BoardConfigFields{maxWidth + nodeBoxWidthPx + extraWidth, maxHeight + nodeBoxHeightPx} 50 | } 51 | 52 | // Constructor for TemplateData. There are some fields like BoardConfig that needs to be 53 | // calculated 54 | func newTemplateData(gdfData *GdfDataStruct, 55 | nodes []NodeData, controlConfig ControlConfigFields) TemplateData { 56 | boardConfig := computeBoardConfig(gdfData, nodes) 57 | return TemplateData{gdfData, nodes, boardConfig, controlConfig} 58 | } 59 | 60 | // The function responsible for generating the final HTML from template 61 | func fillTemplateWriteOutput(templateFile string, data TemplateData, outputFile string) error { 62 | tmpl, err := template.ParseFiles(templateFile) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | writer, err := os.Create(outputFile) 68 | if err != nil { 69 | return err 70 | } 71 | defer writer.Close() 72 | 73 | err = tmpl.Execute(writer, data) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /docs/algo-config/README.md: -------------------------------------------------------------------------------- 1 | ## algo-config 2 | 3 | These fields control the behavior of the graph generation algorithm related to 4 | the placements of nodes. 5 | 6 | It is difficult to explain these without an example graph. Consider the following 7 | set of nodes. The images below are based on these nodes. 8 | 9 | ```yaml 10 | - name: A 11 | - name: B 12 | - name: C 13 | depends-on: 14 | - A 15 | - name: D 16 | depends-on: 17 | - A 18 | - C 19 | - name: E 20 | depends-on: 21 | - B 22 | - D 23 | - name: F 24 | depends-on: 25 | - D 26 | - B 27 | ``` 28 | 29 | ### node-sorting 30 | 31 | By default, the graph generation places the most fundamental nodes at the bottom 32 | (Node A and B in the graph above). 33 | The default behavior resembles the construction of a building. 34 | Complex ideas are built on the TOP of fundamental ideas. 35 | 36 | However, the common flow of ideas in a book or a webpage is from top to bottom. 37 | For that, we can use `node-sorting: descend`. Here the most fundamental nodes will be 38 | placed at the top. Compare these two graphs: 39 | 40 | ![Node-Sorting Comparison](images/node-sorting.png) 41 | 42 | Node positions get flipped along the vertical axis. 43 | Also, notice that the arrows still keep a top to bottom flow. 44 | 45 | Note: This behavior of arrows may change in the future. 46 | 47 | ### arrow-direction 48 | 49 | This controls the direction of the arrows. There are two options: 50 | * child2parent (default) 51 | * parent2child 52 | 53 | ### level-strategy 54 | 55 | This controls how the levels are assigned to each node. There are two options: 56 | * bottom2top (default) 57 | * top2bottom 58 | 59 | With `bottom2top`, all the nodes without dependencies get the bottom row. 60 | Other nodes are progressively arranged on the top. The level of a node is 1 61 | more than the max level of all its dependencies. This is usually enough for 62 | most of the use-cases. 63 | 64 | However in some cases, we want to assign levels from the other end. 65 | All the nodes that are not used by any other node as a dependency is assigned 66 | the top row. In other words, all the nodes at the top will have 0 users. 67 | Every other node gets a level 1 below the minimum level of all its users. 68 | This is the strategy used for `top2bottom`. 69 | 70 | ![Level-Strategy Comparison](images/level-strategy.png) 71 | 72 | It is highly encouraged to try the various strategies on a small graph before 73 | trying anything big. 74 | -------------------------------------------------------------------------------- /src/linkitall_assets/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{.GdfData.HeadConfig.Title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 |
23 | 28 |
29 | {{range .Nodes}} 30 |
31 | 32 | 41 | 42 |
43 |
44 |
45 | {{if eq (len .ElemFields.Link) 0}} 46 | {{.InputFields.Title}} 47 | {{else}} 48 | 51 | {{.InputFields.Title}} 52 | 53 | {{end}} 54 |
55 | {{if ne (len .InputFields.Subtitle) 0}} 56 |
{{.InputFields.Subtitle}}
57 | {{end}} 58 |
59 |
60 | 61 | 70 | 71 |
72 | {{end}} 73 |
74 |
75 | 80 | {{if .ControlConfig.Release}} 81 | 82 | {{else}} 83 | 84 | 85 | {{end}} 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/linkitall_assets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background-attachment: fixed; 9 | background-repeat: no-repeat; 10 | color: #ddd; 11 | font-family: Sans; 12 | background-color: hsl(205, 0%, 17%); 13 | padding: 20px; 14 | } 15 | 16 | .board { 17 | position: relative; 18 | margin: 10px; 19 | margin-left: auto; 20 | margin-right: auto; 21 | margin-top: 80px; 22 | } 23 | 24 | .node { 25 | position: absolute; 26 | z-index: 1; 27 | } 28 | 29 | a, a:link, a:visited, a:hover, a:active { 30 | text-decoration: none; 31 | color: inherit; 32 | } 33 | 34 | .node-content { 35 | position: relative; 36 | min-height: 100px; 37 | background-color: hsl(205, 0%, 13%); 38 | text-align: center; 39 | border: 2px solid hsl(50, 0%, 20%); 40 | border-radius: 10px; 41 | } 42 | 43 | .node-content-inner { 44 | position: absolute; 45 | margin: auto; 46 | top: 50%; 47 | transform: translateY(-50%); 48 | width: 100%; 49 | } 50 | 51 | .node .title { 52 | font-size: 1.2em; 53 | margin: 5px; 54 | color: hsl(50, 0%, 50%); 55 | } 56 | 57 | .node .title a { 58 | color: #eee; 59 | } 60 | 61 | .node .title a:hover { 62 | text-decoration: underline; 63 | color: #2af; 64 | } 65 | 66 | .node .subtitle { 67 | font-size: 1em; 68 | margin: 5px; 69 | color: hsl(50, 0%, 50%); 70 | } 71 | 72 | .link-panel { 73 | display: flex; 74 | width: 100%; 75 | position: static; 76 | min-height: 18px; 77 | } 78 | 79 | .dot-outer { 80 | flex-grow: 1; 81 | } 82 | 83 | .dot { 84 | width: 20px; 85 | margin-left: auto; 86 | margin-right: auto; 87 | text-align: center; 88 | background-color: #444; 89 | color: #eee; 90 | border-radius: 10px; 91 | } 92 | 93 | .link-source { 94 | /* its empty */ 95 | } 96 | 97 | .highlighted-node { 98 | border: 2px solid #ccc; 99 | box-shadow: 0px 0px 20px #ccc; 100 | } 101 | 102 | #link-view-panel { 103 | z-index: 2; 104 | display: none; 105 | position: fixed; 106 | top: 0px; 107 | left: 0; 108 | right: 0; 109 | margin-left: auto; 110 | margin-right: auto; 111 | width: 0px; 112 | } 113 | 114 | #link-view-panel .close-button { 115 | margin-top: 10px; 116 | margin-bottom: 5px; 117 | min-width: 100px; 118 | font-size: 1em; 119 | transform: translateX(-50%); 120 | } 121 | 122 | #link-view-panel iframe { 123 | overflow: scroll; 124 | transform: translateX(-50%); 125 | background-color: #111; 126 | box-shadow: 0px 0px 40px #000; 127 | } 128 | 129 | #link-view-panel .div-frame { 130 | transform: translateX(-50%); 131 | background-color: #111e; 132 | box-shadow: 0px 0px 40px #000; 133 | overflow: auto; 134 | justify-content: center; 135 | } 136 | 137 | .div-frame img { 138 | display: block; 139 | margin-left: auto; 140 | margin-right: auto; 141 | } 142 | 143 | @keyframes zoomIn { 144 | 0% { 145 | transform: scale(0.8); 146 | } 147 | 100% { 148 | transform: scale(1); 149 | } 150 | } 151 | 152 | .display-load-effect { 153 | animation: zoomIn 0.5s ease; 154 | } 155 | 156 | ::-webkit-scrollbar { 157 | height: 12px; 158 | width: 12px; 159 | background-color: #111; 160 | } 161 | 162 | ::-webkit-scrollbar-thumb { 163 | background-color: #555; 164 | box-shadow: inset 0 0 8px #000; 165 | border-radius: 12px; 166 | } 167 | 168 | ::-webkit-scrollbar-corner { 169 | background: #111; 170 | } 171 | 172 | button { 173 | cursor: pointer; 174 | outline: 0; 175 | display: inline-block; 176 | text-align: center; 177 | background-color: #444; 178 | border: 1px solid transparent; 179 | padding: 5px; 180 | transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; 181 | color: #BBB; 182 | border-color: #555; 183 | } 184 | 185 | button:hover { 186 | color: #CCC; 187 | background-color: #222; 188 | } 189 | -------------------------------------------------------------------------------- /examples/simple/graph.yaml: -------------------------------------------------------------------------------- 1 | # THIS IS A SIMPLE EXAMPLE CREATED PURELY TO SHOWCASE THE FEATURES OF LINKITALL. 2 | # INFORMATION PROVIDED HERE IS NOT MEANT TO BE RELIABLE. 3 | 4 | # Build this using the following command: 5 | # -> cd path/to/linkitall/repo 6 | # -> linkitall -i examples/simple 7 | 8 | # These will be added to the section of the page. 9 | head-config: 10 | title: Tap Water 11 | description: What is in tap water? 12 | author: Vinay Krishnan 13 | 14 | algo-config: 15 | # Supported: bottom2top (default), top2bottom 16 | level-strategy: top2bottom 17 | # Supported: child2parent (default), parent2child 18 | arrow-direction: child2parent 19 | # Supported: ascend (default), descend 20 | node-sorting: ascend 21 | 22 | resources: 23 | # Page stored in resources locally. Nodes can 'linkto' this page. 24 | # Make sure to copy the resources dir along with the html file if you take it somewhere else. 25 | # Optionally, one can target a specific element id in the page using the target field. 26 | main: resources/main.html 27 | # It can be an external webpage as well 28 | disinfectants_wiki: https://en.wikipedia.org/wiki/Disinfectant 29 | # Local or web images work too 30 | algae_image: resources/pexels-daria-klet-8479585.jpg 31 | # Local or web pdf references also work. As target, use page= (eg: page=10) 32 | # (Note: #view=fit is not part of the filename. It is added to resize the pdf view) 33 | microorganisms_pdf: resources/microorganisms.pdf#view=fit 34 | 35 | 36 | nodes: 37 | - name: tap_water 38 | title: Tap Water 39 | linkto: 40 | # resource defined in section `resources` 41 | resource: main 42 | # the id of the target section 43 | target: tap-water 44 | depends-on: 45 | - pure_water 46 | - impurities 47 | 48 | - name: pure_water 49 | # If title is not provided, it will be guessed based on name 50 | linkto: 51 | resource: main 52 | target: pure-water 53 | 54 | - name: impurities 55 | subtitle: Chemicals, Gases, Organisms 56 | linkto: 57 | resource: main 58 | target: impurities 59 | depends-on: 60 | - dissolved_gases 61 | - disinfectants 62 | - natural_minerals 63 | - microorganisms 64 | - chemicals 65 | 66 | - name: disinfectants 67 | subtitle: Most commonly Chlorine 68 | linkto: 69 | resource: disinfectants_wiki 70 | depends-on: 71 | - chlorine 72 | 73 | - name: dissolved_gases 74 | linkto: 75 | resource: main 76 | target: dissolved-gases 77 | 78 | - name: natural_minerals 79 | linkto: 80 | resource: main 81 | target: natural-minerals 82 | 83 | - name: chlorine 84 | title: Chlorine 85 | subtitle: Most common chemical in water treatment 86 | linkto: 87 | resource: main 88 | target: chlorine 89 | 90 | - name: microorganisms 91 | title: Micro-organisms 92 | subtitle: Tiny, yet sometimes harmful 93 | linkto: 94 | resource: main 95 | target: microorganisms 96 | depends-on: 97 | - algae 98 | - bacteria 99 | - fungi 100 | - protozoa 101 | - viruses 102 | 103 | - name: chemicals 104 | title: Chemicals 105 | subtitle: From various sources 106 | linkto: 107 | resource: main 108 | target: chemicals 109 | 110 | - name: algae 111 | title: Algae 112 | linkto: 113 | # Using the image resource 114 | resource: algae_image 115 | 116 | - name: bacteria 117 | title: Bacteria 118 | linkto: 119 | resource: microorganisms_pdf 120 | target: page=2 121 | 122 | - name: fungi 123 | title: Fungi 124 | subtitle: Has 'fun' in name 125 | linkto: 126 | resource: microorganisms_pdf 127 | target: page=3 128 | 129 | - name: protozoa 130 | title: Protozoa 131 | subtitle: No idea what this is 132 | linkto: 133 | resource: microorganisms_pdf 134 | target: page=4 135 | 136 | - name: viruses 137 | title: Viruses 138 | linkto: 139 | resource: microorganisms_pdf 140 | target: page=5 141 | -------------------------------------------------------------------------------- /src/linkitall_assets/main.js: -------------------------------------------------------------------------------- 1 | // This script is used by the generated HTML page containing the graph. 2 | // 3 | // Thanks to: 4 | // - https://anseki.github.io/leader-line/ 5 | // Used for making connections. 6 | // Note: These entities are not associated with the project. 7 | 8 | let links = [] 9 | let imgWidth = "60%" 10 | let _buildConfig = null 11 | 12 | // just renaming the function to a simpler one 13 | function id2el(idstr) { 14 | return document.getElementById(idstr) 15 | } 16 | 17 | function getBuildConfig() { 18 | if (_buildConfig != null) { 19 | return _buildConfig 20 | } 21 | const elem = id2el("buildconfig") 22 | _buildConfig = JSON.parse(elem.innerHTML) 23 | return _buildConfig 24 | } 25 | 26 | function getBaseUrl(url) { 27 | return url.split(/[?#]/)[0] 28 | } 29 | 30 | function removeAndAddClass(elem, className) { 31 | elem.classList.remove(className) 32 | elem.classList.add(className) 33 | } 34 | 35 | function isImageFile(filename) { 36 | let checkExt = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg'] 37 | let lowerFilename = filename.toLowerCase() 38 | return checkExt.some(ext => lowerFilename.endsWith(ext)) 39 | } 40 | 41 | function getLinkOptions(source, target, color) { 42 | let sourceTop = source.getBoundingClientRect().top 43 | let targetTop = target.getBoundingClientRect().top 44 | 45 | let startSocket = 'bottom' 46 | let endSocket = 'top' 47 | if (targetTop < sourceTop) { 48 | startSocket = 'top' 49 | endSocket = 'bottom' 50 | } 51 | 52 | let options = { 53 | startSocket, 54 | endSocket, 55 | color, 56 | size: 2 57 | } 58 | 59 | return options 60 | } 61 | 62 | // Find all the link-source dots and connect them to their target dot. 63 | function connectDots() { 64 | // Template used to color links and their dots 65 | const colorTemplate = "hsl({hue}, 40%, 50%)" 66 | // we will cycle over different values of hue 67 | let hue = 0 68 | const linkSources = document.getElementsByClassName("link-source") 69 | 70 | for (let idx=0; idx < linkSources.length; idx++) { 71 | let source = linkSources[idx] 72 | const sourceId = source.id 73 | if (!sourceId.startsWith("D_")) { 74 | console.log(`ERROR: source ${source} does not start with D_`) 75 | continue 76 | } 77 | 78 | const targetId = sourceId.replace(/^D_/, "U_") 79 | let target = id2el(targetId) 80 | if (target == null) { 81 | console.log(`ERROR: no target dot with id ${targetId}`) 82 | continue 83 | } 84 | 85 | // Color for this link 86 | let color = colorTemplate.replace("{hue}", hue.toString()) 87 | hue = (hue + 67) % 360 88 | source.style.backgroundColor = color 89 | target.style.backgroundColor = color 90 | 91 | let buildConfig = getBuildConfig() 92 | if (buildConfig.ArrowDirection == "parent2child") { 93 | // Swap source and target in this case 94 | let temp = source 95 | source = target 96 | target = temp 97 | } 98 | 99 | let link = new LeaderLine(source, target) 100 | link.setOptions(getLinkOptions(source, target, color)) 101 | links.push(link) 102 | } 103 | } 104 | 105 | function main() { 106 | connectDots() 107 | } 108 | 109 | document.addEventListener("DOMContentLoaded", main) 110 | 111 | // When clicking the link box, focus and show the target node. 112 | function showNode(nodeId) { 113 | const elem = id2el(nodeId) 114 | elem.scrollIntoView({behavior: "smooth", block: "center", inline: "center"}) 115 | elem.classList.add("highlighted-node") 116 | 117 | // TODO: this looks so wrong. We need a *better* way to highlight a node. 118 | setTimeout(() => { 119 | elem.classList.remove("highlighted-node") 120 | }, 1500) 121 | } 122 | 123 | // Use state=true to enable link-view-panel and state=false to hide it. 124 | function setLinkViewPatelState(state) { 125 | let linkViewOuterElem = id2el("link-view-panel") 126 | if (state) { 127 | linkViewOuterElem.style.zIndex = "3" 128 | linkViewOuterElem.style.display = "block" 129 | document.body.style.overflow = 'hidden' 130 | } else if (id2el("link-view-panel").style.display != "none") { 131 | linkViewOuterElem.style.zIndex = "-1" 132 | linkViewOuterElem.style.display = "none" 133 | document.body.style.overflow = 'visible' 134 | } 135 | } 136 | 137 | var currentLinkViewUrl = "" 138 | 139 | function getWidthAndHeightForFrame() { 140 | let height = window.innerHeight - 100 141 | let width = Math.floor(window.innerWidth * 0.8) 142 | if (width < 800) { 143 | width = 800 144 | } 145 | return [width, height] 146 | } 147 | 148 | function withpx(val) { 149 | return `${val}px` 150 | } 151 | 152 | function openInIframe(url, targetParent) { 153 | let [width, height] = getWidthAndHeightForFrame() 154 | 155 | // Open link in an iframe and insert it into the inner div 156 | let iframe = document.createElement("iframe") 157 | iframe.src = url 158 | iframe.width = withpx(width) 159 | iframe.height = withpx(height) 160 | iframe.frameBorder="0" 161 | iframe.onload = () => { 162 | setLinkViewPatelState(true) 163 | iframe.focus() 164 | } 165 | targetParent.appendChild(iframe) 166 | } 167 | 168 | function openInDiv(url, targetParent) { 169 | let [width, height] = getWidthAndHeightForFrame() 170 | 171 | targetParent.innerHTML = ` 172 |
173 | 174 |
175 | ` 176 | let divFrame = id2el("div-frame") 177 | divFrame.style.width = withpx(width) 178 | divFrame.style.height = withpx(height) 179 | 180 | setLinkViewPatelState(true) 181 | removeAndAddClass(targetParent, "display-load-effect") 182 | } 183 | 184 | // Open panel for viewing the target url. 185 | // If we open the same link again, reuse the iframe. 186 | function openNodeLink(evt, url, aux) { 187 | evt.preventDefault() 188 | 189 | // In case of middle-click or ctrl-click, open link in a new tab 190 | if (aux || (evt.ctrlKey == true)) { 191 | window.open(url, "newTab") 192 | return 193 | } 194 | 195 | if (url == currentLinkViewUrl) { 196 | setLinkViewPatelState(true) 197 | return 198 | } 199 | 200 | // clear contents of the link-view-inner 201 | let inner = id2el("link-view-inner") 202 | inner.textContent = "" 203 | 204 | let baseUrl = getBaseUrl(url) 205 | if (isImageFile(baseUrl)) { 206 | // Images are open witout an iframe, with a simple div 207 | openInDiv(url, inner) 208 | } else { 209 | // Everything else opened with an iframe 210 | openInIframe(url, inner) 211 | } 212 | currentLinkViewUrl = url 213 | } 214 | 215 | function closeLinkViewPanel() { 216 | setLinkViewPatelState(false) 217 | } 218 | 219 | 220 | function zoomFrameImg(key) { 221 | let minWidth = 100 222 | let maxWidth = 4000 223 | let elem = id2el("frame-img") 224 | if (elem == null) { 225 | return 226 | } 227 | let width = elem.width 228 | if ((key === "[") && (width > minWidth)) { 229 | imgWidth = width * 0.9 230 | elem.width = imgWidth 231 | } 232 | if ((key === "]") && (width < maxWidth)) { 233 | imgWidth = width * 1 / 0.9 234 | elem.width = imgWidth 235 | } 236 | } 237 | 238 | 239 | // Handle escape keypress. 240 | // Close the link view panel when pressing escape. 241 | document.onkeydown = function(evt) { 242 | if(evt.key === "Escape") { 243 | closeLinkViewPanel() 244 | } 245 | 246 | if((evt.key === "[") || (evt.key === "]")) { 247 | zoomFrameImg(evt.key) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/gdf_loading.go: -------------------------------------------------------------------------------- 1 | // This file handles the loading of Graph Definition File (GDF) 2 | // Contains loading of data to struct, checking input values, and filling optional fields. 3 | // Some of the checks may be redundant as similar checks may be present in modules that actually 4 | // use the data. 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os" 11 | "regexp" 12 | "strings" 13 | 14 | "golang.org/x/text/cases" 15 | "golang.org/x/text/language" 16 | "gopkg.in/yaml.v2" 17 | ) 18 | 19 | var name_pattern = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) 20 | var importance_pattern = regexp.MustCompile(`^(lowest|lower|low|normal|high|higher|highest)$`) 21 | 22 | // Used in the of the final HTML 23 | type HeadConfigFields struct { 24 | Title string 25 | Description string 26 | Author string 27 | } 28 | 29 | // Configuration related to positioning 30 | type DisplayConfigFields struct { 31 | // Size of horizontal grid step 32 | HorizontalStepPx int `yaml:"horizontal-step-px,omitempty"` 33 | // Size of vertical grid step 34 | VerticalStepPx int `yaml:"vertical-step-px,omitempty"` 35 | // Width of the node box 36 | NodeBoxWidthPx int `yaml:"node-box-width-px,omitempty"` 37 | } 38 | 39 | type LinkToFields struct { 40 | // Resource name to be linked to 41 | ResourceName string `yaml:"resource"` 42 | // A target for the final resource (page/section/div-id) etc. 43 | Target string `yaml:"target"` 44 | } 45 | 46 | type ResourceConfigMap map[string]string 47 | 48 | // Defines the node definition by the user in the 49 | type NodeInputFields struct { 50 | // A unique name for the node (no spaces, all small letters) 51 | Name string 52 | // Title of the node (shown in big font) 53 | Title string 54 | // Subtitle (shown in smaller font or sometimes omitted) 55 | Subtitle string `yaml:"subtitle,omitempty"` 56 | // Importance to be assigned to this node. It is a 7 point scale: 57 | // lowest, lower, low, normal, high, higher, highest 58 | Importance string `yaml:"importance,omitempty"` 59 | // List of node names (current node depends on these nodes) 60 | DependsOn []string `yaml:"depends-on,omitempty"` 61 | // Link to the resource 62 | LinkTo LinkToFields `yaml:"linkto,omitempty"` 63 | } 64 | 65 | type AlgoConfigFields struct { 66 | LevelStrategy string `yaml:"level-strategy,omitempty"` 67 | ArrowDirection string `yaml:"arrow-direction,omitempty"` 68 | NodeSorting string `yaml:"node-sorting,omitempty"` 69 | } 70 | 71 | type GdfDataStruct struct { 72 | Nodes []NodeInputFields `yaml:"nodes"` 73 | HeadConfig HeadConfigFields `yaml:"head-config"` 74 | DisplayConfig DisplayConfigFields `yaml:"display-config,omitempty"` 75 | ResourceConfig ResourceConfigMap `yaml:"resources"` 76 | AlgoConfig AlgoConfigFields `yaml:"algo-config,omitempty"` 77 | } 78 | 79 | func validateAndUpdateAlgoConfig(algoConfig *AlgoConfigFields) error { 80 | if len(algoConfig.LevelStrategy) == 0 { 81 | algoConfig.LevelStrategy = "bottom2top" 82 | } 83 | if algoConfig.LevelStrategy != "bottom2top" && algoConfig.LevelStrategy != "top2bottom" { 84 | return fmt.Errorf("invalid level strategy: '%v'", algoConfig.LevelStrategy) 85 | } 86 | 87 | if len(algoConfig.ArrowDirection) == 0 { 88 | algoConfig.ArrowDirection = "child2parent" 89 | } 90 | if algoConfig.ArrowDirection != "child2parent" && algoConfig.ArrowDirection != "parent2child" { 91 | return fmt.Errorf("invalid arrow direction: '%v'", algoConfig.ArrowDirection) 92 | } 93 | 94 | if len(algoConfig.NodeSorting) == 0 { 95 | algoConfig.NodeSorting = "ascend" 96 | } 97 | if algoConfig.NodeSorting != "ascend" && algoConfig.NodeSorting != "descend" { 98 | return fmt.Errorf("invalid growth strategy: '%v'", algoConfig.NodeSorting) 99 | } 100 | return nil 101 | } 102 | 103 | func validateAndUpdateDisplayConfig(displayConfig *DisplayConfigFields) error { 104 | if displayConfig.HorizontalStepPx == 0 { 105 | displayConfig.HorizontalStepPx = 400 106 | } 107 | if displayConfig.VerticalStepPx == 0 { 108 | displayConfig.VerticalStepPx = 300 109 | } 110 | if displayConfig.NodeBoxWidthPx == 0 { 111 | displayConfig.NodeBoxWidthPx = 300 112 | } 113 | return nil 114 | } 115 | 116 | // Replace underscores with spaces and capitalize first letter of every word. 117 | func convertNameToTitle(name string) string { 118 | spacedStr := strings.ReplaceAll(name, "_", " ") 119 | // We have to do some extra-steps to do since strings.Title is deprecated. :eyeroll: 120 | caser := cases.Title(language.English) 121 | result := caser.String(spacedStr) 122 | return result 123 | } 124 | 125 | // Validate data related to nodes in GDF 126 | // This function changes blank ("") value for node.Importance to "normal". 127 | func validateAndUpdateNodes(nodes []NodeInputFields) error { 128 | // Number of nodes without any dependencies (level 0 nodes) 129 | numLevel0Nodes := 0 130 | // Unique nodes (names) 131 | uniqueNames := map[string]bool{} 132 | for idx := range nodes { 133 | node := &nodes[idx] 134 | 135 | // CHECK: node name must be [a-zA-Z0-9_] 136 | if !name_pattern.MatchString(node.Name) { 137 | return fmt.Errorf("invalid node name (only letters, numbers, _) '%v'", node.Name) 138 | } 139 | 140 | // CHECK: node name must be unique 141 | if _, ok := uniqueNames[node.Name]; ok { 142 | return fmt.Errorf("node name repeated '%v'", node.Name) 143 | } 144 | uniqueNames[node.Name] = true 145 | 146 | if node.Importance == "" { 147 | node.Importance = "normal" 148 | } 149 | // CHECK: importance must be one of the 7 options 150 | if !importance_pattern.MatchString(node.Importance) { 151 | return fmt.Errorf("unknown importance pattern for node '%v': '%v'", 152 | node.Name, node.Importance) 153 | } 154 | 155 | if len(node.DependsOn) == 0 { 156 | // These nodes do not depend on any nodes 157 | numLevel0Nodes += 1 158 | } 159 | 160 | // If the title is not given, fill it using the name. 161 | if len(node.Title) == 0 { 162 | node.Title = convertNameToTitle(node.Name) 163 | } 164 | } 165 | 166 | if numLevel0Nodes == 0 { 167 | return fmt.Errorf("there must be atleast 1 node without any dependency") 168 | } 169 | 170 | for _, node := range nodes { 171 | for _, dep := range node.DependsOn { 172 | // CHECK: dependency must be one of the node names 173 | if _, ok := uniqueNames[dep]; !ok { 174 | return fmt.Errorf("unknown dependency for node '%v': '%v'", node.Name, dep) 175 | } 176 | } 177 | } 178 | 179 | return nil 180 | } 181 | 182 | // Goes over each source in resources and makes sure the input is proper. 183 | // Also iterates over the nodes and makes sure all the resources are available. 184 | // TODO: check if the specified resource file actually exists! 185 | func validateAndUpdateResources(resources ResourceConfigMap, nodes []NodeInputFields) error { 186 | // Check all nodes are using resources actually present in the GDF 187 | for idx := range nodes { 188 | node := &nodes[idx] 189 | if len(node.LinkTo.ResourceName) == 0 { 190 | continue 191 | } 192 | _, ok := resources[node.LinkTo.ResourceName] 193 | if !ok { 194 | return fmt.Errorf("error in node %s: linkto resource %s not found", 195 | node.Name, node.LinkTo.ResourceName) 196 | } 197 | } 198 | 199 | return nil 200 | } 201 | 202 | // Validate graph data loaded from YAML 203 | // Input must not be nil. 204 | func validateAndUpdateGraphData(data *GdfDataStruct) error { 205 | err := validateAndUpdateNodes(data.Nodes) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | err = validateAndUpdateDisplayConfig(&data.DisplayConfig) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | err = validateAndUpdateAlgoConfig(&data.AlgoConfig) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | err = validateAndUpdateResources(data.ResourceConfig, data.Nodes) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | return nil 226 | } 227 | 228 | // Load Graph Definition File 229 | // 230 | // Inputs: 231 | // filename - input filename (YAML file for GDF) 232 | // 233 | // Returns: (data, readable, error) 234 | // data - loaded data (if everything goes fine) 235 | // readable - true if file is readable 236 | // error - error if any 237 | func loadGdf(filename string) (*GdfDataStruct, bool, error) { 238 | file, err := os.Open(filename) 239 | if err != nil { 240 | return nil, false, err 241 | } 242 | defer file.Close() 243 | 244 | fileData, err := io.ReadAll(file) 245 | if err != nil { 246 | return nil, false, err 247 | } 248 | 249 | var data GdfDataStruct 250 | err = yaml.UnmarshalStrict(fileData, &data) 251 | if err != nil { 252 | return nil, true, err 253 | } 254 | 255 | err = validateAndUpdateGraphData(&data) 256 | if err != nil { 257 | return nil, true, err 258 | } 259 | 260 | return &data, true, nil 261 | } 262 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | // This file handles the conversion of Graph in YAML format to HTML 2 | 3 | // Important uncommon shortforms used: 4 | // GDF - Graph Definition File (usually in YAML) 5 | 6 | // TODO: Add an output directory option for this tool. As of now, input directory is the target 7 | // directory where we will keep all the files. When used in serve mode, we serve files from the 8 | // input directory. 9 | 10 | package main 11 | 12 | import ( 13 | // "html/template" 14 | "bufio" 15 | "fmt" 16 | "log" 17 | "net/http" 18 | "os" 19 | "path/filepath" 20 | "strings" 21 | "time" 22 | 23 | argparse "github.com/alexflint/go-arg" 24 | copylib "github.com/otiai10/copy" 25 | ) 26 | 27 | // Some fields (GraphFile, OutFile) are basepaths (just the filename without dir). 28 | // For these, full path is attached by the getInputsForProcessing() function. 29 | type CliArgs struct { 30 | ServerMode bool `arg:"-s,--serve" help:"run in edit-update-serve mode"` 31 | Release bool `arg:"-r,--release" help:"run in release mode"` 32 | ServerAddr string `arg:"-l,--listen" default:":8101" help:"listen address in serve mode"` 33 | InputDir string `arg:"-i,--indir,required" help:"path to the input directory"` 34 | GraphFile string `arg:"-g,--graph" default:"graph.yaml" help:"input graph base filename"` 35 | OutFile string `arg:"-o,--out" default:"index.html" help:"output html base filename"` 36 | Overwrite bool `arg:"--overwrite" help:"overwrite asset files"` 37 | } 38 | 39 | var bufferedStdin *bufio.Reader = bufio.NewReader(os.Stdin) 40 | 41 | // Return path to the assets directory inside indir 42 | func getPathToAssetDir(indir string) string { 43 | return filepath.Join(indir, "linkitall_assets") 44 | } 45 | 46 | // Similar to the above, but for vendor directory 47 | func getPathToVendorDir(indir string) string { 48 | return filepath.Join(indir, "linkitall_vendor") 49 | } 50 | 51 | // Check if the give `path` is accessible. 52 | // kind can be "file" or "dir" 53 | func isPathAccessible(path string, kind string) bool { 54 | stat, err := os.Stat(path) 55 | if err != nil { 56 | return false 57 | } 58 | 59 | result := false 60 | if kind == "dir" { 61 | result = stat.IsDir() 62 | } else if kind == "file" { 63 | file, err := os.Open(path) 64 | if err == nil { 65 | result = true 66 | file.Close() 67 | } 68 | } 69 | return result 70 | } 71 | 72 | // Check if file can be written 73 | func canFileWrite(path string) bool { 74 | file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0644) 75 | if err != nil { 76 | return false 77 | } 78 | file.Close() 79 | return true 80 | } 81 | 82 | // Parse arguments and perform steps to prepare input for processing. 83 | // If --indir is specified "?", get the input path from the user via stdin. 84 | // Final InputDir path is converted to absolute path. 85 | // Check for existence of indir and graph file. 86 | func getInputsForProcessing() (CliArgs, error) { 87 | var args CliArgs 88 | if len(os.Args) == 1 { 89 | fmt.Printf("No args. Use --help\n") 90 | os.Exit(1) 91 | } 92 | argparse.MustParse(&args) 93 | 94 | if args.InputDir == "?" { 95 | fmt.Printf("Enter input directory => ") 96 | line, err := bufferedStdin.ReadString('\n') 97 | if err != nil { 98 | return args, err 99 | } 100 | line = strings.TrimSpace(line) 101 | args.InputDir = line 102 | } 103 | 104 | if !isPathAccessible(args.InputDir, "dir") { 105 | return args, fmt.Errorf("input dir not accessible: %s", args.InputDir) 106 | } 107 | 108 | absInputDir, err := filepath.Abs(args.InputDir) 109 | if err != nil { 110 | return args, err 111 | } 112 | 113 | args.InputDir = absInputDir 114 | args.GraphFile = filepath.Join(args.InputDir, args.GraphFile) 115 | if !isPathAccessible(args.GraphFile, "file") { 116 | return args, fmt.Errorf("unable to find graph file: %s", args.GraphFile) 117 | } 118 | // Fill full path to input and output 119 | args.OutFile = filepath.Join(args.InputDir, args.OutFile) 120 | if !canFileWrite(args.OutFile) { 121 | return args, fmt.Errorf("unable to open file for writing: %s", args.OutFile) 122 | } 123 | 124 | return args, nil 125 | } 126 | 127 | // Return path to the parent dir where the executable is. 128 | func getExecutableDir() (string, error) { 129 | execFile, err := os.Executable() 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | execDir := filepath.Dir(execFile) 135 | absExecDir, err := filepath.Abs(execDir) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | return absExecDir, nil 141 | } 142 | 143 | // Copy all the assets files to the target directory where the output will be generated. 144 | // The asset files (source) are located in the same directory of the executable. 145 | func copyAssetsAndVendorFilesToDir(targetDir string, overwrite bool, release bool) error { 146 | execDir, err := getExecutableDir() 147 | if err != nil { 148 | return err 149 | } 150 | 151 | assetPath := getPathToAssetDir(execDir) 152 | targetAssetPath := getPathToAssetDir(targetDir) 153 | if !overwrite && isPathAccessible(targetAssetPath, "dir") { 154 | log.Printf("Asset dir %s already exists. Skipping copying assets\n", targetAssetPath) 155 | } else { 156 | log.Printf("Copy %s -> %s\n", assetPath, targetAssetPath) 157 | copylib.Copy(assetPath, targetAssetPath) 158 | } 159 | 160 | vendorPath := getPathToVendorDir(execDir) 161 | targetVendorPath := getPathToVendorDir(targetDir) 162 | if release { 163 | log.Printf("In release mode. Not copying vendor dir") 164 | } else if !overwrite && isPathAccessible(targetVendorPath, "dir") { 165 | log.Printf("Vendor dir %s already exists. Skipping copying vendor\n", targetVendorPath) 166 | } else { 167 | log.Printf("Copy %s -> %s\n", vendorPath, targetVendorPath) 168 | copylib.Copy(vendorPath, targetVendorPath) 169 | } 170 | 171 | return nil 172 | } 173 | 174 | // ** This is the core function which does all the processing ** 175 | // Process Graph Data File (GDF) and writes the HTML output. 176 | // The `indir` is also the target dir. Output is generated at the same location. 177 | // Copy the required asset dir to the `indir` before calling this function. 178 | func processGraphWriteOutput(args *CliArgs) error { 179 | log.Printf("Reading graph: %s\n", args.GraphFile) 180 | gdfData, readable, err := loadGdf(args.GraphFile) 181 | if !readable { 182 | log.Fatalf("graph file %s not readable: %s\n", args.GraphFile, err) 183 | } 184 | 185 | if err != nil { 186 | return err 187 | } 188 | 189 | log.Printf("Preparing nodes\n") 190 | nodes, err := createComputeAndFillNodeDataList(gdfData) 191 | if err != nil { 192 | return err 193 | } 194 | log.Printf("Number of nodes: %d\n", len(nodes)) 195 | 196 | log.Printf("Generating template data\n") 197 | controlConfig := ControlConfigFields{ 198 | Release: args.Release, 199 | } 200 | templateData := newTemplateData(gdfData, nodes, controlConfig) 201 | 202 | targetAssetDir := getPathToAssetDir(args.InputDir) 203 | templateFile := filepath.Join(targetAssetDir, "template.html") 204 | 205 | log.Printf("Filling template and writing output\n") 206 | err = fillTemplateWriteOutput(templateFile, templateData, args.OutFile) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | log.Printf("Done\n") 212 | return nil 213 | } 214 | 215 | // Process the graph file. Print error if any. 216 | func processAndLogError(args *CliArgs) { 217 | err := processGraphWriteOutput(args) 218 | 219 | if err != nil { 220 | log.Printf("Error: %s", err) 221 | } 222 | } 223 | 224 | // In server mode, we run a http server on the target directory. 225 | // We also run a read-update cycle to update the output file. 226 | func runInServerMode(args *CliArgs) { 227 | // Run processing once before starting server 228 | processAndLogError(args) 229 | 230 | // Start server on the target dir 231 | fileServer := http.FileServer(http.Dir(args.InputDir)) 232 | go func() { 233 | log.Printf("Starting server for dir %s. Listening at %s\n", 234 | args.InputDir, args.ServerAddr) 235 | http.ListenAndServe(args.ServerAddr, fileServer) 236 | }() 237 | 238 | time.Sleep(time.Second) 239 | 240 | // Run read-update cycle 241 | for { 242 | fmt.Printf("\nq: quit, enter: update output => ") 243 | line, err := bufferedStdin.ReadString('\n') 244 | if err != nil { 245 | log.Fatalf("unable to read from stdin. %s", err) 246 | } 247 | 248 | line = strings.TrimSpace(line) 249 | if line == "q" { 250 | break 251 | } else if len(line) > 0 { 252 | log.Printf("Warning: Ignoring input: '%s'", line) 253 | continue 254 | } 255 | processAndLogError(args) 256 | } 257 | } 258 | 259 | func main() { 260 | args, err := getInputsForProcessing() 261 | if err != nil { 262 | log.Fatalf("unable to read args. %s", err) 263 | } 264 | 265 | // Check if the graph file exists. It is the input file. User only specifies the dir. 266 | 267 | // It doesn't matter whether we are running in server mode or not. We always copy the asset 268 | // files to the target dir (input dir in this case). 269 | err = copyAssetsAndVendorFilesToDir(args.InputDir, args.Overwrite, args.Release) 270 | if err != nil { 271 | log.Fatalf("unable to copy asset files to %s", args.InputDir) 272 | } 273 | 274 | if args.ServerMode { 275 | runInServerMode(&args) 276 | } else { 277 | err = processGraphWriteOutput(&args) 278 | } 279 | 280 | if err != nil { 281 | log.Fatalf("error while processing %s", err) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Linkitall lets you create a clear, visual map showing how different ideas are related. This map, structured as a dependency graph, shows the hierarchy and connections between concepts. To use it, you simply create a YAML file describing the graph structure, and Linkitall then generates an HTML file with clickable nodes and links. It's especially useful for making educational or reference content, offering a graphical view of topics. 4 | 5 | Linkitall also helps deepen understanding by exploring dependencies. As we ask "why" or "how," we follow these links, uncovering complex relationships that build our knowledge. This process improves both understanding and critical thinking. 6 | 7 | An example: [Trigonometric Relations](https://charstorm.github.io/class-11-12-india/class11/maths/trigonometry/relations/) 8 | 9 | ## Usage 10 | 11 | Download the latest release zipfile and follow the instructions in the README.md inside. 12 | 13 | If the release files fail due to some reason, please build the tool from the source as explained below. 14 | 15 | ## Build 16 | 17 | The steps for building the tool from the source are given here. 18 | 19 | Clone the source files locally: 20 | 21 | ```bash 22 | git clone https://github.com/charstorm/linkitall.git 23 | ``` 24 | 25 | The source files are located in `linkitall/src` directory. The project is written in Golang and is tested on version `go1.20.7`. 26 | 27 | CD to the `src` directory, build the tool, and check the result: 28 | 29 | ```bash 30 | cd linkitall/src 31 | go build 32 | ./linkitall --help 33 | ``` 34 | 35 | To make the tool available globally, it is required to add the directory path to the PATH environment variable. 36 | 37 | ```bash 38 | export PATH="$PATH:/path/to/linkitall/src/" 39 | ``` 40 | 41 | ## Graph Generation 42 | 43 | The tool takes the path to a directory containing the graph file `graph.yaml` and its dependent resources as the input. See `examples/simple` for a quick reference. 44 | More details about the graph file are explained in the section "Graph File", 45 | 46 | To run the graph generation, execute the following command (assuming `targetdir` is the directory of graph file and its resources): 47 | 48 | ```bash 49 | linkitall -i targetdir 50 | ``` 51 | 52 | If executed successfully, it will generate an `index.html` file at `targetdir`. Additionally, asset files (CSS, JS, etc) required for the generated HTML will be copied to the `targetdir` with name `linkitall_assets`. These are initially located in the same directory of the `linkitall` tool. However, it is a one-time action. Subsequent invocation of the tool will skip this copy-assets step. Same is true for the vendor files (3rd party libraries) used by the project. They are stored in `linkitall_vendor` directory. 53 | 54 | One can open the generated HTML file in a browser and see the result. 55 | 56 | ### CLI 57 | 58 | ``` 59 | Usage: linkitall [--serve] [--release] [--listen LISTEN] --indir INDIR [--graph GRAPH] [--out OUT] [--overwrite] 60 | 61 | Options: 62 | --serve, -s run in edit-update-serve mode 63 | --release, -r run in release mode 64 | --listen LISTEN, -l LISTEN 65 | listen address in serve mode [default: :8101] 66 | --indir INDIR, -i INDIR 67 | path to the input directory 68 | --graph GRAPH, -g GRAPH 69 | input graph base filename [default: graph.yaml] 70 | --out OUT, -o OUT output html base filename [default: index.html] 71 | --overwrite overwrite asset files 72 | --help, -h display this help and exit 73 | ``` 74 | 75 | 1. `serve` - to run in server mode. See below. 76 | 2. `release` - run in release mode. This includes: 77 | - use CDN for links, instead of local vendor files. 78 | 3. `listen` - the address to listen to (eg: ":8101") in the server mode. 79 | 4. `indir` - input (or target) directory containing the graph file. 80 | 5. `graph` - base-name of the graph file (eg: "main.yaml") inside `indir`. 81 | 6. `out` - base-name of the output file to be created inside `indir`. 82 | 83 | 84 | ### Server Mode 85 | 86 | The default behavior of the tool is to run the generation process only once. This is not ideal for development. For that, we have added a server mode, which can be enabled by the -s flag. 87 | 88 | ```bash 89 | linkitall -s -i targetdir 90 | ``` 91 | 92 | This will start an HTTP development server at default port 8101. One can see the results by visiting http://127.0.0.1:8101 . 93 | 94 | The tool will wait for user input to update the generated graph. Enter will trigger a graph generation, q will quit the tool. 95 | 96 | With this development process for the graph will be as follows: 97 | 98 | 1. Make changes to the graph file 99 | 100 | 2. Press Enter to trigger a graph generation 101 | 102 | 1. If there are errors, fix them and continue 103 | 104 | 3. Refresh the webpage to see the updated graph 105 | 106 | ## Graph File 107 | 108 | The Graph Definition File (GDF) is a YAML file with different sections. 109 | The default filename expected for the file is `graph.yaml`. 110 | Different sections of the graph file are explained below. 111 | 112 | ### head-config 113 | These fields will be forwarded to the `head` section of the output html file. 114 | Example: 115 | ```yaml 116 | head-config: 117 | title: Awesome Graph 118 | description: A long description 119 | author: Someone 120 | ``` 121 | 122 | ### display-config 123 | These fields control the size and spacing of nodes in the graph. 124 | Example: 125 | ```yaml 126 | display-config: 127 | # Horizontal spacing 128 | horizontal-step-px: 400 129 | # Vertical spacing 130 | vertical-step-px: 300 131 | # Width of each node 132 | node-box-width-px: 300 133 | # There is no configuration for the node height from the graph file. 134 | ``` 135 | The numerical values shown in the configuration above are the default values. 136 | 137 | ### resources 138 | These are the resources used in the graph. 139 | Resources can be images, pdf-files, html pages, etc. 140 | When clicked on that node, the corresponding link will be opened. 141 | Each node can "linkto" a resource. 142 | Multiple nodes can share the same resource. 143 | Example: 144 | ```yaml 145 | # The keys will be used in the "linkto" field of each node 146 | resources: 147 | # An internal link 148 | main: resources/main.html 149 | # An external link 150 | disinfectants_wiki: https://en.wikipedia.org/wiki/Disinfectant 151 | # Local or web images work too 152 | algae_image: resources/pexels-daria-klet-8479585.jpg 153 | # Local or web pdf references also work. 154 | # (Note: #view=fit is not part of the filename. It is added to resize the pdf view) 155 | microorganisms_pdf: resources/microorganisms.pdf#view=fit 156 | ``` 157 | An explanation of using these references will be provided below. 158 | 159 | ### nodes 160 | This is a list of data dictionaries for each node in the graph. 161 | Example: 162 | ```yaml 163 | nodes: 164 | # Expects lowercase, without space, must be unique 165 | - name: tap_water 166 | # This the the text that will be shown on the node 167 | title: Tap Water 168 | # Text that will be shown below title 169 | subtitle: Node about tap water 170 | # Resource information for this node 171 | linkto: 172 | # Resource defined in section resources 173 | resource: main 174 | # The id of the target section in the resource page (optional) 175 | target: tap-water 176 | depends-on: 177 | # List of dependencies (their name, not title) 178 | - pure_water 179 | - impurities 180 | 181 | - name: pure_water 182 | # If title is not provided, it will be guessed based on name 183 | linkto: 184 | resource: main 185 | target: pure-water 186 | ``` 187 | 188 | ### algo-config 189 | These fields control the node placement, direction, etc of the graph generation 190 | algorithm. Example: 191 | ```yaml 192 | algo-config: 193 | # Supported: bottom2top (default), top2bottom 194 | level-strategy: top2bottom 195 | # Supported: child2parent (default), parent2child 196 | arrow-direction: child2parent 197 | # Supported: ascend (default), descend 198 | node-sorting: ascend 199 | ``` 200 | 201 | [More details on algo-config](docs/algo-config/README.md) 202 | 203 | ## User Interface 204 | 205 | The graph generated is a basic HTML web-page. But we have added a few features to make it 206 | easy to use. 207 | 208 | 1. A resource connected to a node (if available) can be accessed by left-clicking the 209 | node's title. Middle-click or Ctrl-click will open the same resource in a new tab. 210 | 2. Clicking on the connection box (those small circles with a +) will move the view to 211 | the target node. The target node will be highlighted in this case. 212 | 3. When an image resource is viewed in the same page of the graph, keys "[" and "]" 213 | can be used to control the size/zoom of the image. 214 | 215 | ## External Examples 216 | 217 | We are currently building graphs for topics covered in class 11 and 12 (plus-one and plus-two). 218 | You will find the example graphs in this repository: 219 | [class-11-12-india](https://github.com/charstorm/class-11-12-india) (See readme.md). 220 | 221 | ## Status 222 | 223 | ❗The project is still in beta phase. 224 | 225 | ## License 226 | 227 | All files in this repo (except those in `src/linkitall_vendor/`) follow the MIT License. 228 | See LICENSE for details. The files in the vendor directory have their own licenses. 229 | -------------------------------------------------------------------------------- /examples/simple/resources/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tap Water 8 | 34 | 35 | 36 |

Tap Water

37 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

38 |

Pure Water

39 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

40 |

Impurities

41 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

42 |

Dissolved gases

43 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

44 |

Disinfectants

45 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

46 |

Microorganisms

47 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

48 |

Chemicals

49 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

50 |

Natural Minerals

51 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

52 |

Agricultural runoff

53 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

54 |

Pharmaceuticals

55 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

56 |

Other impurities

57 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

58 |

Organic Compounds

59 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

60 |

Sediments

61 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

62 |

Chlorine

63 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

64 |

Methane

65 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

66 |

Carbon Dioxide

67 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

68 |

Hydrogen Sulfide

69 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

70 |

Bacteria

71 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

72 |

Protozoa

73 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

74 |

Viruses

75 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

76 |

Fungi

77 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

78 |

Algae

79 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

80 | 81 | 82 | -------------------------------------------------------------------------------- /src/node_computation.go: -------------------------------------------------------------------------------- 1 | // This file handles the computation of various fields related to nodes. 2 | // There are some more computations required for the final HTML generation. Those will be handled 3 | // by html_generation.go. 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "math" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | const defaultCapacity = 5 14 | const defaultInvalidLevel = -1 15 | 16 | // All the fields related to defining IDs for node and connections 17 | type NodeIntIdFields struct { 18 | // Assign unique integer ID to every node. This is just the index 19 | Uid int 20 | // UIDs of all dependencies that this node depends on 21 | DependsOnIds []int 22 | // UIDs of all nodes that depend on this node 23 | UsedByIds []int 24 | } 25 | 26 | // Fields related to position (level, shift) 27 | type NodePositionFields struct { 28 | // Level is for the vertical position. Level 0 is at the bottom (for axioms and such) 29 | Level int 30 | // Shift is for the horizontal position 31 | Shift int 32 | } 33 | 34 | // In the generated HTML file, every node has a set of connection dots. 35 | // Each connection dot (a div in html) has an element ID of its own. 36 | // Also we should keep track of the ID of the other node which is connected to this node. 37 | type DotElemFields struct { 38 | // HTML ID of the Dot element 39 | DotElemId string 40 | // HTML ID of the partner node (not the node holding the dot) 41 | PartnerNodeId string 42 | // only used for sorting 43 | LinkAngle float64 44 | } 45 | 46 | // Used to allow sorting of DotElemFields using to untangle links. 47 | // Len, Swap, and Less are defined below with the type. 48 | type LinkDots []DotElemFields 49 | 50 | func (x LinkDots) Len() int { 51 | return len(x) 52 | } 53 | 54 | func (x LinkDots) Swap(i int, j int) { 55 | x[i], x[j] = x[j], x[i] 56 | } 57 | 58 | func (x LinkDots) Less(i int, j int) bool { 59 | return x[i].LinkAngle < x[j].LinkAngle 60 | } 61 | 62 | // For every node, we keep track of all the HTML data required. 63 | // IDs in this struct are strings which will be mapped to HTML element IDs. 64 | type NodeElemFields struct { 65 | // Element ID of the node 66 | NodeElemId string 67 | // Dots used for "depends-on" connections. 68 | // By default, these appear at the bottom of every node as a node depends on other nodes that 69 | // are of lower level (more fundamental). 70 | DependsOnDots []DotElemFields 71 | // Dots for "used-by" connections. 72 | // By default, these appear at the top of every node. 73 | UsedByDots []DotElemFields 74 | // Classes used by the node (HTML). This will be used to handle parameters like Importance. 75 | Classes string 76 | // Left edge position (px) 77 | LeftPx int 78 | // Top edge position (px) 79 | TopPx int 80 | // Link to the associated resource 81 | Link string 82 | } 83 | 84 | // All the data corresponding to a node 85 | type NodeData struct { 86 | // Fields coming from input 87 | InputFields NodeInputFields 88 | // Unique number based IDs (computed) 89 | IntIdFields NodeIntIdFields 90 | // Position related fields (computed). Does not handle HTML related positions. 91 | Position NodePositionFields 92 | // HTML related fields (computed). Also handles positions on the HTML page. 93 | ElemFields NodeElemFields 94 | } 95 | 96 | // Create a list of NodeData based on GDF data 97 | // Not handling the error. 98 | func createNodeDataList(gdfData *GdfDataStruct) []NodeData { 99 | // This will contain the final result 100 | nodeDataSeq := make([]NodeData, 0, len(gdfData.Nodes)) 101 | for _, node := range gdfData.Nodes { 102 | var nodeData NodeData 103 | nodeData.InputFields = node 104 | pushBack(&nodeDataSeq, nodeData) 105 | } 106 | return nodeDataSeq 107 | } 108 | 109 | // Fill all the fields related to integer IDs. 110 | func fillIntIdFields(nodeDataSeq []NodeData) error { 111 | // A mapping of name -> integer unique ID 112 | nodeName2Id := map[string]int{} 113 | 114 | // Fill Uid. Also report if node names are repeated. This may not be really required 115 | // since there is already some error checking when reading the input data. 116 | for idx := range nodeDataSeq { 117 | node := &nodeDataSeq[idx] 118 | if _, found := nodeName2Id[node.InputFields.Name]; found { 119 | return fmt.Errorf("node name repeated '%v'", node.InputFields.Name) 120 | } 121 | nodeName2Id[node.InputFields.Name] = idx 122 | node.IntIdFields.Uid = idx 123 | // Initialize the arrays inside the struct 124 | node.IntIdFields.DependsOnIds = make([]int, 0, defaultCapacity) 125 | node.IntIdFields.UsedByIds = make([]int, 0, defaultCapacity) 126 | } 127 | 128 | // Fill DependsOnIds and UsedByIds using nodeName2Id 129 | for idx := range nodeDataSeq { 130 | node := &nodeDataSeq[idx] 131 | for _, depNodeName := range node.InputFields.DependsOn { 132 | depNodeId, ok := nodeName2Id[depNodeName] 133 | if !ok { 134 | return fmt.Errorf("dependency not found '%v'", depNodeName) 135 | } 136 | pushBack(&node.IntIdFields.DependsOnIds, depNodeId) 137 | depNode := &nodeDataSeq[depNodeId] 138 | pushBack(&depNode.IntIdFields.UsedByIds, idx) 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // Initialization step of computeLevels algorithm. 146 | // Assign level 0 to all nodes without the specified linked nodes. 147 | // Everyone else gets an invalid level. 148 | // Finally, keep the level 0 nodes in nextLevelNodeIds. 149 | // What linked nodes are considered depends on the strategy. 150 | // 151 | // For bottom2top -> DependsOnIds 152 | // For top2bottom -> UsedByIds 153 | func initializeForComputeLevels(strategy string, nodes []NodeData, level0NodeIds *[]int) { 154 | for idx := range nodes { 155 | node := &nodes[idx] 156 | // We set an invalid value here. This will be useful when checking if all nodes received 157 | // a valid value. 158 | node.Position.Level = defaultInvalidLevel 159 | // count to be used to decide level 0 nodes 160 | level0DecisionCount := defaultInvalidLevel 161 | switch strategy { 162 | case "bottom2top": 163 | level0DecisionCount = len(node.IntIdFields.DependsOnIds) 164 | case "top2bottom": 165 | level0DecisionCount = len(node.IntIdFields.UsedByIds) 166 | default: 167 | panic("unknown strategy for level initialization") 168 | } 169 | if level0DecisionCount == 0 { 170 | // No dependencies -> level 0 (absolute bottom) 171 | node.Position.Level = 0 172 | pushBack(level0NodeIds, idx) 173 | } 174 | } 175 | } 176 | 177 | // Process a single node in the computeLevels function. 178 | func processNodeComputeLevels(strategy string, node *NodeData, nodes []NodeData, 179 | nextLevelNodeIds *[]int) { 180 | nextLevel := node.Position.Level + 1 181 | 182 | var linkedNodeIds []int 183 | switch strategy { 184 | case "bottom2top": 185 | linkedNodeIds = node.IntIdFields.UsedByIds 186 | case "top2bottom": 187 | linkedNodeIds = node.IntIdFields.DependsOnIds 188 | default: 189 | panic("unknown strategy for level initialization") 190 | } 191 | 192 | for _, linkedNodeId := range linkedNodeIds { 193 | linkedNode := &nodes[linkedNodeId] 194 | linkedNode.Position.Level = nextLevel 195 | pushBack(nextLevelNodeIds, linkedNodeId) 196 | } 197 | } 198 | 199 | // Go over all the current nodes and process them 200 | func processCurrentNodesComputeLevels(strategy string, nodes []NodeData, currentLevelNodeIds []int, 201 | nextLevelNodeIds *[]int) { 202 | for _, nodeId := range currentLevelNodeIds { 203 | processNodeComputeLevels(strategy, &nodes[nodeId], nodes, nextLevelNodeIds) 204 | } 205 | } 206 | 207 | // Validate the result of computeLevels 208 | func validateComputeLevels(strategy string, nodes []NodeData) error { 209 | // Compute max-level 210 | maxLevel := 0 211 | for _, node := range nodes { 212 | if node.Position.Level > maxLevel { 213 | maxLevel = node.Position.Level 214 | } 215 | } 216 | 217 | for _, node := range nodes { 218 | // Every node must have a valid level 219 | if node.Position.Level == defaultInvalidLevel { 220 | return fmt.Errorf("unreachable node '%v'", node.InputFields.Name) 221 | } 222 | 223 | // Every child must be at least 1 level above the current node 224 | expectedMinLevelForChildren := node.Position.Level + 1 225 | for _, childNodeId := range node.IntIdFields.UsedByIds { 226 | childNode := &nodes[childNodeId] 227 | if childNode.Position.Level < expectedMinLevelForChildren { 228 | // Shows a bug in the code 229 | panic(fmt.Sprintf("Child node level %v < expected level %v (child %v, parent %v)", 230 | childNode.Position.Level, expectedMinLevelForChildren, 231 | childNode.InputFields.Name, node.InputFields.Name)) 232 | } 233 | } 234 | 235 | // Every parent should be at least 1 level below the current node 236 | expectedMaxLevelForParent := node.Position.Level - 1 237 | for _, parentNodeId := range node.IntIdFields.DependsOnIds { 238 | parentNode := &nodes[parentNodeId] 239 | if parentNode.Position.Level > expectedMaxLevelForParent { 240 | // Shows a bug in the code 241 | panic(fmt.Sprintf("Parent node level %v > expected level %v (child %v, parent %v)", 242 | parentNode.Position.Level, expectedMaxLevelForParent, 243 | node.InputFields.Name, parentNode.InputFields.Name)) 244 | } 245 | } 246 | 247 | if strategy == "bottom2top" { 248 | // A nodes level = max(level of all parents) + 1 249 | maxParentLevel := -1 250 | for _, parentNodeId := range node.IntIdFields.DependsOnIds { 251 | parentNode := &nodes[parentNodeId] 252 | if parentNode.Position.Level > maxParentLevel { 253 | maxParentLevel = parentNode.Position.Level 254 | } 255 | } 256 | if node.Position.Level != maxParentLevel+1 { 257 | // Shows a bug in the code 258 | panic(fmt.Sprintf("For node %v, mismatch in level. Got %v, expected %v", 259 | node.InputFields.Name, node.Position.Level, maxParentLevel+1)) 260 | } 261 | } else if strategy == "top2bottom" { 262 | // A nodes level = min(level of all children) - 1 263 | minChildLevel := maxLevel + 1 264 | for _, childNodeId := range node.IntIdFields.UsedByIds { 265 | childNode := &nodes[childNodeId] 266 | if childNode.Position.Level < minChildLevel { 267 | minChildLevel = childNode.Position.Level 268 | } 269 | } 270 | if node.Position.Level != minChildLevel-1 { 271 | // Shows a bug in the code 272 | panic(fmt.Sprintf("For node %v, mismatch in level. Got %v, expected %v", 273 | node.InputFields.Name, node.Position.Level, minChildLevel-1)) 274 | } 275 | } 276 | } 277 | 278 | return nil 279 | } 280 | 281 | // Reverse node levels. Needed when using top2bottom strategy since the algorithm assigns 282 | // level 0 to the top most nodes. 283 | func reverseNodeLevels(nodes []NodeData) { 284 | maxNodeLevel := 0 285 | // get highest level 286 | for idx := range nodes { 287 | node := &nodes[idx] 288 | if node.Position.Level > maxNodeLevel { 289 | maxNodeLevel = node.Position.Level 290 | } 291 | } 292 | 293 | // reduce node level from highest level to reverse levels 294 | for idx := range nodes { 295 | node := &nodes[idx] 296 | node.Position.Level = maxNodeLevel - node.Position.Level 297 | } 298 | } 299 | 300 | // Compute level for all the nodes. 301 | // This is a tricky algorithm. Basic idea is as follows: 302 | // Find all nodes that do not have any dependencies and assign level 0 303 | // Now, iteratively run for level = 0, 1, ... so on: 304 | // 305 | // for all nodes in current level, 306 | // find nodes that use them 307 | // assign them current level + 1 308 | // keep a list of these and use as the starting point of next iteration 309 | // 310 | // Maximum number of iterations = number of nodes. 311 | // At the end, perform sanity checks on the code (and coder) 312 | func computeLevels(algoConfig *AlgoConfigFields, nodes []NodeData) error { 313 | strategy := algoConfig.LevelStrategy 314 | var currentLevelNodeIds []int 315 | nextLevelNodeIds := make([]int, 0, defaultCapacity) 316 | 317 | initializeForComputeLevels(strategy, nodes, &nextLevelNodeIds) 318 | if len(nextLevelNodeIds) == 0 { 319 | return fmt.Errorf("found no level 0 nodes") 320 | } 321 | 322 | // In the worse case, every node get a unique level. 323 | // We already assigned 0 for the initialization for at least 1 node. 324 | maxIterationCount := len(nodes) - 1 325 | // Heart of the algorithm 326 | for step := 0; step < maxIterationCount; step++ { 327 | // Check if there is any node to process 328 | if len(nextLevelNodeIds) == 0 { 329 | break 330 | } 331 | currentLevelNodeIds = nextLevelNodeIds 332 | nextLevelNodeIds = make([]int, 0, defaultCapacity) 333 | 334 | processCurrentNodesComputeLevels(strategy, nodes, currentLevelNodeIds, &nextLevelNodeIds) 335 | nextLevelNodeIds = getUnique(nextLevelNodeIds) 336 | } 337 | 338 | if strategy == "top2bottom" { 339 | // In this strategy, we get the node levels reversed. We have to reverse the levels. 340 | reverseNodeLevels(nodes) 341 | } 342 | 343 | err := validateComputeLevels(strategy, nodes) 344 | if err != nil { 345 | return err 346 | } 347 | 348 | if algoConfig.NodeSorting == "descend" { 349 | reverseNodeLevels(nodes) 350 | } 351 | 352 | return nil 353 | } 354 | 355 | // Compute shifts - This is straightforward. For every level, we go from left to right. 356 | // We can also compute levelMap with this function. 357 | func computeShiftsAndGetLevelMap(nodes []NodeData) [][]int { 358 | levelMap := make([][]int, 0) 359 | if len(nodes) == 0 { 360 | return levelMap 361 | } 362 | 363 | // find maxLevel 364 | maxLevel := -1 365 | for _, node := range nodes { 366 | if node.Position.Level > maxLevel { 367 | maxLevel = node.Position.Level 368 | } 369 | } 370 | 371 | if maxLevel < 0 { 372 | // shows a bug in code 373 | panic("Unable to find maxLevel") 374 | } 375 | 376 | // Initialize levelMap for each level 377 | levelMap = make([][]int, maxLevel+1) 378 | for level := 0; level <= maxLevel; level++ { 379 | levelMap[level] = make([]int, 0) 380 | } 381 | 382 | // Fill levelMap and shift based on each node level 383 | for idx := range nodes { 384 | node := &nodes[idx] 385 | level := node.Position.Level 386 | node.Position.Shift = len(levelMap[level]) 387 | levelMap[level] = append(levelMap[level], idx) 388 | } 389 | 390 | // Sanity check: 391 | for level := 0; level <= maxLevel; level++ { 392 | if len(levelMap[level]) == 0 { 393 | panic(fmt.Sprintf("Level %v has 0 nodes", level)) 394 | } 395 | } 396 | 397 | return levelMap 398 | } 399 | 400 | // Used to convert numeric IDs to string IDs used by HTML elements 401 | func formatIntId(id int) string { 402 | // TODO: do we need this many digits?! 403 | return fmt.Sprintf("%05d", id) 404 | } 405 | 406 | // Build HTML field IDs used by the connection dots 407 | // D connections: D_OWNER_PARTNER 408 | // U connections: U_PARTNER_OWNER 409 | func buildDotElemFields(prefix string, ownerId int, partnerId int) DotElemFields { 410 | ownerIdStr := formatIntId(ownerId) 411 | partnerIdStr := formatIntId(partnerId) 412 | first := ownerIdStr 413 | second := partnerIdStr 414 | if prefix == "U" { 415 | first = partnerIdStr 416 | second = ownerIdStr 417 | } 418 | dotElemId := fmt.Sprintf("%s_%s_%s", prefix, first, second) 419 | return DotElemFields{dotElemId, partnerIdStr, 0} 420 | } 421 | 422 | // Fill HTML element IDs in string form 423 | // The ids are formatted as follows (examples): 424 | // Node => N00001 (N prefix) 425 | // DependsOn Connection Dot: D_N00008_N00005 (dot is carried by the first node N00008. 426 | // It depends on the second node N00005) 427 | // UsedBy Connection Dot: U_N00003_N00002 (dot is carried by the first node N00002. 428 | // It is used by the second node N00003). Note that the order of nodes is reversed for 429 | // the UsedBy connection dot. This makes it easy to map one dot to another when making 430 | // connections. It is always the node on the top that comes first. 431 | func fillElemIds(node *NodeData) { 432 | nodeElemId := formatIntId(node.IntIdFields.Uid) 433 | node.ElemFields.NodeElemId = nodeElemId 434 | 435 | node.ElemFields.DependsOnDots = make([]DotElemFields, 0) 436 | for _, dependsOnId := range node.IntIdFields.DependsOnIds { 437 | dotElemFields := buildDotElemFields("D", node.IntIdFields.Uid, dependsOnId) 438 | pushBack(&node.ElemFields.DependsOnDots, dotElemFields) 439 | } 440 | 441 | node.ElemFields.UsedByDots = make([]DotElemFields, 0) 442 | for _, usedById := range node.IntIdFields.UsedByIds { 443 | dotElemFields := buildDotElemFields("U", node.IntIdFields.Uid, usedById) 444 | pushBack(&node.ElemFields.UsedByDots, dotElemFields) 445 | } 446 | } 447 | 448 | // Fill HTML element IDs for all the nodes 449 | func fillElemIdsForAllNodes(nodes []NodeData) { 450 | for idx := range nodes { 451 | fillElemIds(&nodes[idx]) 452 | } 453 | } 454 | 455 | // Each node gets a position, which will be set based on inline CSS. 456 | // It is a bit tricky since we want to center the alignment. 457 | func computeNodePositionsAndUpdate(displayConfig *DisplayConfigFields, 458 | levelMap [][]int, nodes []NodeData) { 459 | 460 | // To be used to calculate max shift and center aligning 461 | maxNodesPerLevel := 0 462 | for _, nodeIdsForLevel := range levelMap { 463 | if len(nodeIdsForLevel) > maxNodesPerLevel { 464 | maxNodesPerLevel = len(nodeIdsForLevel) 465 | } 466 | } 467 | 468 | if maxNodesPerLevel == 0 { 469 | return 470 | } 471 | 472 | maxLevel := len(levelMap) - 1 473 | horScale := displayConfig.HorizontalStepPx 474 | levelHorScale := horScale 475 | centering := 0 476 | // We can use levelMap to initialize the positions of nodes 477 | for level, nodeIdsForLevel := range levelMap { 478 | if len(nodeIdsForLevel) == maxNodesPerLevel { 479 | levelHorScale = horScale 480 | centering = 0 481 | } else { 482 | ratio := float64(maxNodesPerLevel-1) / float64(len(nodeIdsForLevel)) 483 | levelHorScaleF64 := ratio * float64(horScale) 484 | levelHorScale = int(levelHorScaleF64) 485 | centering = int(levelHorScaleF64 / 2) 486 | } 487 | for shift, nodeId := range nodeIdsForLevel { 488 | node := &nodes[nodeId] 489 | // Horizontal centering shift 490 | node.ElemFields.LeftPx = shift*levelHorScale + centering 491 | node.ElemFields.TopPx = (maxLevel - level) * displayConfig.VerticalStepPx 492 | } 493 | } 494 | } 495 | 496 | // Compute angle of the link given two nodes 497 | func computeLinkAngle(node1 *NodeData, node2 *NodeData) float64 { 498 | hdiff := node2.ElemFields.LeftPx - node1.ElemFields.LeftPx 499 | vdiff := node2.ElemFields.TopPx - node1.ElemFields.TopPx 500 | angle := math.Atan2(float64(vdiff), float64(hdiff)) 501 | return angle 502 | } 503 | 504 | // We want to adjust the order of connection dots to minimize the amount of link crossings. 505 | // We do this by sorting them according to their angle one way or another. 506 | // Only to be called after computing the position! 507 | func sortDotsToUntangleLinks(nodes []NodeData) { 508 | // Fill the LinkAngle for all nodes (dependson and usedby) 509 | for idx := range nodes { 510 | node1 := &nodes[idx] 511 | 512 | for ii, id2 := range node1.IntIdFields.DependsOnIds { 513 | node2 := &nodes[id2] 514 | node1.ElemFields.DependsOnDots[ii].LinkAngle = -computeLinkAngle(node1, node2) 515 | } 516 | sort.Sort(LinkDots(node1.ElemFields.DependsOnDots)) 517 | 518 | for ii, id2 := range node1.IntIdFields.UsedByIds { 519 | node2 := &nodes[id2] 520 | node1.ElemFields.UsedByDots[ii].LinkAngle = computeLinkAngle(node1, node2) 521 | } 522 | sort.Sort(LinkDots(node1.ElemFields.UsedByDots)) 523 | } 524 | } 525 | 526 | // Fill the fields related to link to resource. 527 | // The gdf nodes initially only contain reference to the resource name. 528 | // We have to map them to real reference files/links. 529 | func computeResourceLinkFields(gdfData *GdfDataStruct, nodes []NodeData) error { 530 | resourceConfig := gdfData.ResourceConfig 531 | for idx := range nodes { 532 | node := &nodes[idx] 533 | if len(node.InputFields.LinkTo.ResourceName) == 0 { 534 | continue 535 | } 536 | link, ok := resourceConfig[node.InputFields.LinkTo.ResourceName] 537 | if !ok { 538 | return fmt.Errorf("error in node %s: linkto resource %s not found", 539 | node.InputFields.Name, node.InputFields.LinkTo.ResourceName) 540 | } 541 | updatedLink := link 542 | if len(node.InputFields.LinkTo.Target) > 0 { 543 | updatedLink = fmt.Sprintf("%s#%s", link, node.InputFields.LinkTo.Target) 544 | // HACK! for pdf document, we allow the link to have additional fields 545 | // For example: ..some_doc.pdf#view=fit 546 | // In this case, we have to add target as &target at the end. 547 | if strings.Contains(link, ".pdf#") { 548 | updatedLink = fmt.Sprintf("%s&%s", link, node.InputFields.LinkTo.Target) 549 | } 550 | } 551 | node.ElemFields.Link = updatedLink 552 | } 553 | 554 | return nil 555 | } 556 | 557 | // In case of NodeSorting == descend, we have to switch the positions of depends-on dots 558 | // and used-by dots. 559 | func handleNodeSorting(algoConfig *AlgoConfigFields, nodes []NodeData) { 560 | if algoConfig.NodeSorting == "ascend" { 561 | // default behavior - nothing to do 562 | return 563 | } 564 | for idx := range nodes { 565 | node := &nodes[idx] 566 | 567 | // we also have to flip the IntIdFields 568 | tempIds := node.IntIdFields.UsedByIds 569 | node.IntIdFields.UsedByIds = node.IntIdFields.DependsOnIds 570 | node.IntIdFields.DependsOnIds = tempIds 571 | } 572 | } 573 | 574 | // Do all the steps related to creating list of NodeData and filling all the fields. 575 | // This is the top level function which handles everything. 576 | func createComputeAndFillNodeDataList(gdfData *GdfDataStruct) ([]NodeData, error) { 577 | nodeDataSeq := createNodeDataList(gdfData) 578 | 579 | err := fillIntIdFields(nodeDataSeq) 580 | if err != nil { 581 | return nodeDataSeq, err 582 | } 583 | 584 | err = computeLevels(&gdfData.AlgoConfig, nodeDataSeq) 585 | if err != nil { 586 | return nodeDataSeq, err 587 | } 588 | 589 | levelMap := computeShiftsAndGetLevelMap(nodeDataSeq) 590 | handleNodeSorting(&gdfData.AlgoConfig, nodeDataSeq) 591 | fillElemIdsForAllNodes(nodeDataSeq) 592 | computeNodePositionsAndUpdate(&gdfData.DisplayConfig, levelMap, nodeDataSeq) 593 | sortDotsToUntangleLinks(nodeDataSeq) 594 | 595 | err = computeResourceLinkFields(gdfData, nodeDataSeq) 596 | if err != nil { 597 | return nodeDataSeq, err 598 | } 599 | 600 | return nodeDataSeq, nil 601 | } 602 | -------------------------------------------------------------------------------- /src/linkitall_vendor/leader-line/leader-line-v1.1.5.min.js: -------------------------------------------------------------------------------- 1 | /*! LeaderLine v1.1.5 (c) anseki https://anseki.github.io/leader-line/ */ 2 | var LeaderLine=function(){"use strict";var te,M,I,C,L,o,t,h,f,p,n,a,e,x,b,l,r,i,k,w,s,u,c,A="leader-line",V=1,P=2,N=3,T=4,W={top:V,right:P,bottom:N,left:T},B=1,R=2,F=3,G=4,D=5,z={straight:B,arc:R,fluid:F,magnet:G,grid:D},ne="behind",d=A+"-defs",y='',ae={disc:{elmId:"leader-line-disc",noRotate:!0,bBox:{left:-5,top:-5,width:10,height:10,right:5,bottom:5},widthR:2.5,heightR:2.5,bCircle:5,sideLen:5,backLen:5,overhead:0,outlineBase:1,outlineMax:4},square:{elmId:"leader-line-square",noRotate:!0,bBox:{left:-5,top:-5,width:10,height:10,right:5,bottom:5},widthR:2.5,heightR:2.5,bCircle:5,sideLen:5,backLen:5,overhead:0,outlineBase:1,outlineMax:4},arrow1:{elmId:"leader-line-arrow1",bBox:{left:-8,top:-8,width:16,height:16,right:8,bottom:8},widthR:4,heightR:4,bCircle:8,sideLen:8,backLen:8,overhead:8,outlineBase:2,outlineMax:1.5},arrow2:{elmId:"leader-line-arrow2",bBox:{left:-7,top:-8,width:11,height:16,right:4,bottom:8},widthR:2.75,heightR:4,bCircle:8,sideLen:8,backLen:7,overhead:4,outlineBase:1,outlineMax:1.75},arrow3:{elmId:"leader-line-arrow3",bBox:{left:-4,top:-5,width:12,height:10,right:8,bottom:5},widthR:3,heightR:2.5,bCircle:8,sideLen:5,backLen:4,overhead:8,outlineBase:1,outlineMax:2.5},hand:{elmId:"leader-line-hand",bBox:{left:-3,top:-12,width:40,height:24,right:37,bottom:12},widthR:10,heightR:6,bCircle:37,sideLen:12,backLen:3,overhead:37},crosshair:{elmId:"leader-line-crosshair",noRotate:!0,bBox:{left:-96,top:-96,width:192,height:192,right:96,bottom:96},widthR:48,heightR:48,bCircle:96,sideLen:96,backLen:96,overhead:0}},j={behind:ne,disc:"disc",square:"square",arrow1:"arrow1",arrow2:"arrow2",arrow3:"arrow3",hand:"hand",crosshair:"crosshair"},ie={disc:"disc",square:"square",arrow1:"arrow1",arrow2:"arrow2",arrow3:"arrow3",hand:"hand",crosshair:"crosshair"},H=[V,P,N,T],U="auto",oe={x:"left",y:"top",width:"width",height:"height"},Z=80,Y=4,X=5,q=120,Q=8,K=3.75,J=10,$=30,ee=.5522847,le=.25*Math.PI,m=/^\s*(\-?[\d\.]+)\s*(\%)?\s*$/,re="http://www.w3.org/2000/svg",S="-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style&&!window.navigator.msPointerEnabled,se=!S&&!!document.uniqueID,ue="MozAppearance"in document.documentElement.style,he=!(S||ue||!window.chrome||!window.CSS),pe=!S&&!se&&!ue&&!he&&!window.chrome&&"WebkitAppearance"in document.documentElement.style,ce=se||S?.2:.1,de={path:F,lineColor:"coral",lineSize:4,plugSE:[ne,"arrow1"],plugSizeSE:[1,1],lineOutlineEnabled:!1,lineOutlineColor:"indianred",lineOutlineSize:.25,plugOutlineEnabledSE:[!1,!1],plugOutlineSizeSE:[1,1]},fe=(s={}.toString,u={}.hasOwnProperty.toString,c=u.call(Object),function(e){var t,n;return e&&"[object Object]"===s.call(e)&&(!(t=Object.getPrototypeOf(e))||(n=t.hasOwnProperty("constructor")&&t.constructor)&&"function"==typeof n&&u.call(n)===c)}),ye=Number.isFinite||function(e){return"number"==typeof e&&window.isFinite(e)},g=(x={ease:[.25,.1,.25,1],linear:[0,0,1,1],"ease-in":[.42,0,1,1],"ease-out":[0,0,.58,1],"ease-in-out":[.42,0,.58,1]},b=1e3/60/2,l=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout(e,b)},r=window.cancelAnimationFrame||window.mozCancelAnimationFrame||window.webkitCancelAnimationFrame||window.msCancelAnimationFrame||function(e){clearTimeout(e)},i=Number.isFinite||function(e){return"number"==typeof e&&window.isFinite(e)},k=[],w=0,{add:function(n,e,t,a,i,o,l){var r,s,u,h,p,c,d,f,y,m,S,g,_,v=++w;function E(e,t){return{value:n(t),timeRatio:e,outputRatio:t}}if("string"==typeof i&&(i=x[i]),n=n||function(){},t=this._endIndex||this._string[this._currentIndex]<"0"||"9"=this._endIndex||this._string[this._currentIndex]<"0"||"9"=this._endIndex)return null;var e=null,t=this._string[this._currentIndex];if(this._currentIndex+=1,"0"===t)e=0;else{if("1"!==t)return null;e=1}return this._skipOptionalSpacesOrDelimiter(),e}};function a(e){if(!e||0===e.length)return[];var t=new i(e),n=[];if(t.initialCommandIsMoveTo())for(;t.hasMoreData();){var a=t.parseSegment();if(null===a)break;n.push(a)}return n}function r(e){return e.map(function(e){return{type:e.type,values:Array.prototype.slice.call(e.values)}})}function d(e){var m=[],S=null,g=null,_=null,v=null,E=null,x=null,b=null;return e.forEach(function(e){var t,n,a,i,o,l,r,s,u,h,p,c,d,f,y;"M"===e.type?(f=e.values[0],y=e.values[1],m.push({type:"M",values:[f,y]}),v=x=f,E=b=y):"C"===e.type?(a=e.values[0],i=e.values[1],t=e.values[2],n=e.values[3],f=e.values[4],y=e.values[5],m.push({type:"C",values:[a,i,t,n,f,y]}),g=t,_=n,v=f,E=y):"L"===e.type?(f=e.values[0],y=e.values[1],m.push({type:"L",values:[f,y]}),v=f,E=y):"H"===e.type?(f=e.values[0],m.push({type:"L",values:[f,E]}),v=f):"V"===e.type?(y=e.values[0],m.push({type:"L",values:[v,y]}),E=y):"S"===e.type?(t=e.values[0],n=e.values[1],f=e.values[2],y=e.values[3],l="C"===S||"S"===S?(o=v+(v-g),E+(E-_)):(o=v,E),m.push({type:"C",values:[o,l,t,n,f,y]}),g=t,_=n,v=f,E=y):"T"===e.type?(f=e.values[0],y=e.values[1],i="Q"===S||"T"===S?(a=v+(v-g),E+(E-_)):(a=v,E),o=v+2*(a-v)/3,l=E+2*(i-E)/3,r=f+2*(a-f)/3,s=y+2*(i-y)/3,m.push({type:"C",values:[o,l,r,s,f,y]}),g=a,_=i,v=f,E=y):"Q"===e.type?(a=e.values[0],i=e.values[1],f=e.values[2],y=e.values[3],o=v+2*(a-v)/3,l=E+2*(i-E)/3,r=f+2*(a-f)/3,s=y+2*(i-y)/3,m.push({type:"C",values:[o,l,r,s,f,y]}),g=a,_=i,v=f,E=y):"A"===e.type?(u=e.values[0],h=e.values[1],p=e.values[2],c=e.values[3],d=e.values[4],f=e.values[5],y=e.values[6],0===u||0===h?(m.push({type:"C",values:[v,E,f,y,f,y]}),v=f,E=y):v===f&&E===y||U(v,E,f,y,u,h,p,c,d).forEach(function(e){m.push({type:"C",values:e}),v=f,E=y})):"Z"===e.type&&(m.push(e),v=x,E=b),S=e.type}),m}var n=e.SVGPathElement.prototype.setAttribute,s=e.SVGPathElement.prototype.removeAttribute,f=e.Symbol?e.Symbol():"__cachedPathData",y=e.Symbol?e.Symbol():"__cachedNormalizedPathData",U=function(e,t,n,a,i,o,l,r,s,u){function h(e,t,n){return{x:e*Math.cos(n)-t*Math.sin(n),y:e*Math.sin(n)+t*Math.cos(n)}}var p,c,d,f,y,m,S,g,_,v,E,x,b,k,w,O=(p=l,Math.PI*p/180),M=[];u?(k=u[0],w=u[1],x=u[2],b=u[3]):(e=(c=h(e,t,-O)).x,t=c.y,1<(m=(f=(e-(n=(d=h(n,a,-O)).x))/2)*f/(i*i)+(y=(t-(a=d.y))/2)*y/(o*o))&&(i*=m=Math.sqrt(m),o*=m),_=(S=i*i)*(g=o*o)-S*y*y-g*f*f,v=S*y*y+g*f*f,x=(E=(r===s?-1:1)*Math.sqrt(Math.abs(_/v)))*i*y/o+(e+n)/2,b=E*-o*f/i+(t+a)/2,k=Math.asin(parseFloat(((t-b)/o).toFixed(9))),w=Math.asin(parseFloat(((a-b)/o).toFixed(9))),e120*Math.PI/180&&(I=w,C=n,L=a,w=s&&k=e.duration&&e.count&&e.loopsLeft<=1)return a=e.frames[e.lastFrame=e.reverse?0:e.frames.length-1],e.frameCallback(a.value,!0,a.timeRatio,a.outputRatio),void(e.framesStart=null);if(t>e.duration){if(n=Math.floor(t/e.duration),e.count){if(n>=e.loopsLeft)return a=e.frames[e.lastFrame=e.reverse?0:e.frames.length-1],e.frameCallback(a.value,!0,a.timeRatio,a.outputRatio),void(e.framesStart=null);e.loopsLeft-=n}e.framesStart+=e.duration*n,t=i-e.framesStart}e.reverse&&(t=e.duration-t),a=e.frames[e.lastFrame=Math.round(t/b)],!1!==e.frameCallback(a.value,!1,a.timeRatio,a.outputRatio)?o=!0:e.framesStart=null}}),o&&(e=l.call(window,be))}function ke(e,t){e.framesStart=Date.now(),null!=t&&(e.framesStart-=e.duration*(e.reverse?1-t:t)),e.loopsLeft=e.count,e.lastFrame=null,be()}function we(t,n){var e,a;return typeof t!=typeof n||(e=fe(t)?"obj":Array.isArray(t)?"array":"")!=(fe(n)?"obj":Array.isArray(n)?"array":"")||("obj"===e?we(a=Object.keys(t).sort(),Object.keys(n).sort())||a.some(function(e){return we(t[e],n[e])}):"array"===e?t.length!==n.length||t.some(function(e,t){return we(e,n[t])}):t!==n)}function Oe(n){return n?fe(n)?Object.keys(n).reduce(function(e,t){return e[t]=Oe(n[t]),e},{}):Array.isArray(n)?n.map(Oe):n:n}function Me(e){var t,n,a,i=1,o=e=(e+"").trim();function l(e){var t=1,n=m.exec(e);return n&&(t=parseFloat(n[1]),n[2]?t=0<=t&&t<=100?t/100:1:(t<0||1=Math.abs(n)?0<=t?P:T:0<=n?N:V))})),E.position_path!==x.position_path||E.position_lineStrokeWidth!==x.position_lineStrokeWidth||[0,1].some(function(e){return E.position_plugOverheadSE[e]!==x.position_plugOverheadSE[e]||(i=b[e],o=x.position_socketXYSE[e],i.x!==o.x||i.y!==o.y||i.socketId!==o.socketId)||(t=_[e],n=x.position_socketGravitySE[e],(a=null==t?"auto":Array.isArray(t)?"array":"number")!=(null==n?"auto":Array.isArray(n)?"array":"number")||("array"==a?t[0]!==n[0]||t[1]!==n[1]:t!==n));var t,n,a,i,o})){switch(u.pathList.baseVal=v=[],u.pathList.animVal=null,E.position_path){case B:v.push([O(b[0]),O(b[1])]);break;case R:t="number"==typeof _[0]&&0<_[0]||"number"==typeof _[1]&&0<_[1],o=le*(t?-1:1),l=Math.atan2(b[1].y-b[0].y,b[1].x-b[0].x),r=o-l,c=Math.PI-l-o,d=Ve(b[0],b[1])/Math.sqrt(2)*ee,m={x:b[0].x+Math.cos(r)*d,y:b[0].y+Math.sin(r)*d*-1},S={x:b[1].x+Math.cos(c)*d,y:b[1].y+Math.sin(c)*d*-1},v.push([O(b[0]),m,S,O(b[1])]);break;case F:case G:s=[_[0],E.position_path===G?0:_[1]],h=[],p=[],b.forEach(function(e,t){var n,a,i,o,l=s[t],r=Array.isArray(l)?{x:l[0],y:l[1]}:"number"==typeof l?e.socketId===V?{x:0,y:-l}:e.socketId===P?{x:l,y:0}:e.socketId===N?{x:0,y:l}:{x:-l,y:0}:(n=b[t?0:1],i=0<(a=E.position_plugOverheadSE[t])?q+(QY?(E.position_lineStrokeWidth-Y)*X:0),e.socketId===V?((o=(e.y-n.y)/2)=t.x:t.dirId===r?e.y>=t.y:e.x<=t.x}function y(e,t){return t.dirId===o||t.dirId===r?e.x===t.x:e.y===t.y}function m(e){return e[0]?{contain:0,notContain:1}:{contain:1,notContain:0}}function S(e,t,n){return Math.abs(t[n]-e[n])}function g(e,t,n){return"x"===n?e.x=$?g(h[t.notContain],h[t.contain],o[t.contain]):h[t.contain].dirId)):(i=[{x:h[0].x,y:h[0].y},{x:h[1].x,y:h[1].y}],u.forEach(function(e,t){var n=0===t?1:0,a=S(i[t],i[n],o[t]);a<$&&(h[t]=d(h[t],$-a)),e.push(h[t]),h[t]=d(h[t],$,g(h[t],h[n],o[n]))}))}return 1}(););u[1].reverse(),u[0].concat(u[1]).forEach(function(e,t){var n={x:e.x,y:e.y};0J&&(y[a]-eJ&&(y[a]-ea.outlineMax&&(t=a.outlineMax),t*=2*a.outlineBase,v=qe(S,_.plugOutline_strokeWidthSE,e,t)||v,v=qe(S,_.plugOutline_inStrokeWidthSE,e,_.plugOutline_colorTraSE[e]?t-ce/(_.line_strokeWidth/de.lineSize)/g.plugSizeSE[e]*2:t/2)||v)}),v)),(t.faces||ee.line||ee.plug||ee.lineOutline||ee.plugOutline)&&(ee.faces=(b=(E=e).curStats,k=E.aplStats,w=E.events,O=!1,!b.line_altColor&&qe(E,k,"line_color",x=b.line_color,w.apl_line_color)&&(E.lineFace.style.stroke=x,O=!0),qe(E,k,"line_strokeWidth",x=b.line_strokeWidth,w.apl_line_strokeWidth)&&(E.lineShape.style.strokeWidth=x+"px",O=!0,(ue||se)&&(He(E,E.lineShape),se&&(He(E,E.lineFace),He(E,E.lineMaskCaps)))),qe(E,k,"lineOutline_enabled",x=b.lineOutline_enabled,w.apl_lineOutline_enabled)&&(E.lineOutlineFace.style.display=x?"inline":"none",O=!0),b.lineOutline_enabled&&(qe(E,k,"lineOutline_color",x=b.lineOutline_color,w.apl_lineOutline_color)&&(E.lineOutlineFace.style.stroke=x,O=!0),qe(E,k,"lineOutline_strokeWidth",x=b.lineOutline_strokeWidth,w.apl_lineOutline_strokeWidth)&&(E.lineOutlineMaskShape.style.strokeWidth=x+"px",O=!0,se&&(He(E,E.lineOutlineMaskCaps),He(E,E.lineOutlineFace))),qe(E,k,"lineOutline_inStrokeWidth",x=b.lineOutline_inStrokeWidth,w.apl_lineOutline_inStrokeWidth)&&(E.lineMaskShape.style.strokeWidth=x+"px",O=!0,se&&(He(E,E.lineOutlineMaskCaps),He(E,E.lineOutlineFace)))),qe(E,k,"plug_enabled",x=b.plug_enabled,w.apl_plug_enabled)&&(E.plugsFace.style.display=x?"inline":"none",O=!0),b.plug_enabled&&[0,1].forEach(function(n){var e=b.plug_plugSE[n],t=e!==ne?ae[ie[e]]:null,a=Ye(n,t);qe(E,k.plug_enabledSE,n,x=b.plug_enabledSE[n],w.apl_plug_enabledSE)&&(E.plugsFace.style[a.prop]=x?"url(#"+E.plugMarkerIdSE[n]+")":"none",O=!0),b.plug_enabledSE[n]&&(qe(E,k.plug_plugSE,n,e,w.apl_plug_plugSE)&&(E.plugFaceSE[n].href.baseVal="#"+t.elmId,Ze(E,E.plugMarkerSE[n],a.orient,t.bBox,E.svg,E.plugMarkerShapeSE[n],E.plugsFace),O=!0,ue&&He(E,E.plugsFace)),qe(E,k.plug_colorSE,n,x=b.plug_colorSE[n],w.apl_plug_colorSE)&&(E.plugFaceSE[n].style.fill=x,O=!0,(he||pe||se)&&!b.line_colorTra&&He(E,se?E.lineMaskCaps:E.capsMaskLine)),["markerWidth","markerHeight"].forEach(function(e){var t="plug_"+e+"SE";qe(E,k[t],n,x=b[t][n],w["apl_"+t])&&(E.plugMarkerSE[n][e].baseVal.value=x,O=!0)}),qe(E,k.plugOutline_enabledSE,n,x=b.plugOutline_enabledSE[n],w.apl_plugOutline_enabledSE)&&(x?(E.plugFaceSE[n].style.mask="url(#"+E.plugMaskIdSE[n]+")",E.plugOutlineFaceSE[n].style.display="inline"):(E.plugFaceSE[n].style.mask="none",E.plugOutlineFaceSE[n].style.display="none"),O=!0),b.plugOutline_enabledSE[n]&&(qe(E,k.plugOutline_plugSE,n,e,w.apl_plugOutline_plugSE)&&(E.plugOutlineFaceSE[n].href.baseVal=E.plugMaskShapeSE[n].href.baseVal=E.plugOutlineMaskShapeSE[n].href.baseVal="#"+t.elmId,[E.plugMaskSE[n],E.plugOutlineMaskSE[n]].forEach(function(e){e.x.baseVal.value=t.bBox.left,e.y.baseVal.value=t.bBox.top,e.width.baseVal.value=t.bBox.width,e.height.baseVal.value=t.bBox.height}),O=!0),qe(E,k.plugOutline_colorSE,n,x=b.plugOutline_colorSE[n],w.apl_plugOutline_colorSE)&&(E.plugOutlineFaceSE[n].style.fill=x,O=!0,se&&(He(E,E.lineMaskCaps),He(E,E.lineOutlineMaskCaps))),qe(E,k.plugOutline_strokeWidthSE,n,x=b.plugOutline_strokeWidthSE[n],w.apl_plugOutline_strokeWidthSE)&&(E.plugOutlineMaskShapeSE[n].style.strokeWidth=x+"px",O=!0),qe(E,k.plugOutline_inStrokeWidthSE,n,x=b.plugOutline_inStrokeWidthSE[n],w.apl_plugOutline_inStrokeWidthSE)&&(E.plugMaskShapeSE[n].style.strokeWidth=x+"px",O=!0)))}),O)),(t.position||ee.line||ee.plug)&&(ee.position=Je(e)),(t.path||ee.position)&&(ee.path=(C=(M=e).curStats,L=M.aplStats,A=M.pathList.animVal||M.pathList.baseVal,V=C.path_edge,P=!1,A&&(V.x1=V.x2=A[0][0].x,V.y1=V.y2=A[0][0].y,C.path_pathData=I=Re(A,function(e){e.xV.x2&&(V.x2=e.x),e.y>V.y2&&(V.y2=e.y)}),Ge(I,L.path_pathData)&&(M.linePath.setPathData(I),L.path_pathData=I,P=!0,se?(He(M,M.plugsFace),He(M,M.lineMaskCaps)):ue&&He(M,M.linePath),M.events.apl_path&&M.events.apl_path.forEach(function(e){e(M,I)}))),P)),ee.viewBox=(T=(N=e).curStats,W=N.aplStats,B=T.path_edge,R=T.viewBox_bBox,F=W.viewBox_bBox,G=N.svg.viewBox.baseVal,D=N.svg.style,z=!1,j=Math.max(T.line_strokeWidth/2,T.viewBox_plugBCircleSE[0]||0,T.viewBox_plugBCircleSE[1]||0),H={x1:B.x1-j,y1:B.y1-j,x2:B.x2+j,y2:B.y2+j},N.events.new_edge4viewBox&&N.events.new_edge4viewBox.forEach(function(e){e(N,H)}),R.x=T.lineMask_x=T.lineOutlineMask_x=T.maskBGRect_x=H.x1,R.y=T.lineMask_y=T.lineOutlineMask_y=T.maskBGRect_y=H.y1,R.width=H.x2-H.x1,R.height=H.y2-H.y1,["x","y","width","height"].forEach(function(e){var t;(t=R[e])!==F[e]&&(G[e]=F[e]=t,D[oe[e]]=t+("x"===e||"y"===e?N.bodyOffset[e]:0)+"px",z=!0)}),z),ee.mask=(Y=(U=e).curStats,X=U.aplStats,q=!1,Y.plug_enabled?[0,1].forEach(function(e){Y.capsMaskMarker_enabledSE[e]=Y.plug_enabledSE[e]&&Y.plug_colorTraSE[e]||Y.plugOutline_enabledSE[e]&&Y.plugOutline_colorTraSE[e]}):Y.capsMaskMarker_enabledSE[0]=Y.capsMaskMarker_enabledSE[1]=!1,Y.capsMaskMarker_enabled=Y.capsMaskMarker_enabledSE[0]||Y.capsMaskMarker_enabledSE[1],Y.lineMask_outlineMode=Y.lineOutline_enabled,Y.caps_enabled=Y.capsMaskMarker_enabled||Y.capsMaskAnchor_enabledSE[0]||Y.capsMaskAnchor_enabledSE[1],Y.lineMask_enabled=Y.caps_enabled||Y.lineMask_outlineMode,(Y.lineMask_enabled&&!Y.lineMask_outlineMode||Y.lineOutline_enabled)&&["x","y"].forEach(function(e){var t="maskBGRect_"+e;qe(U,X,t,Z=Y[t])&&(U.maskBGRect[e].baseVal.value=Z,q=!0)}),qe(U,X,"lineMask_enabled",Z=Y.lineMask_enabled)&&(U.lineFace.style.mask=Z?"url(#"+U.lineMaskId+")":"none",q=!0,pe&&He(U,U.lineMask)),Y.lineMask_enabled&&(qe(U,X,"lineMask_outlineMode",Z=Y.lineMask_outlineMode)&&(Z?(U.lineMaskBG.style.display="none",U.lineMaskShape.style.display="inline"):(U.lineMaskBG.style.display="inline",U.lineMaskShape.style.display="none"),q=!0),["x","y"].forEach(function(e){var t="lineMask_"+e;qe(U,X,t,Z=Y[t])&&(U.lineMask[e].baseVal.value=Z,q=!0)}),qe(U,X,"caps_enabled",Z=Y.caps_enabled)&&(U.lineMaskCaps.style.display=U.lineOutlineMaskCaps.style.display=Z?"inline":"none",q=!0,pe&&He(U,U.capsMaskLine)),Y.caps_enabled&&([0,1].forEach(function(e){var t;qe(U,X.capsMaskAnchor_enabledSE,e,Z=Y.capsMaskAnchor_enabledSE[e])&&(U.capsMaskAnchorSE[e].style.display=Z?"inline":"none",q=!0,pe&&He(U,U.lineMask)),Y.capsMaskAnchor_enabledSE[e]&&(Ge(t=Y.capsMaskAnchor_pathDataSE[e],X.capsMaskAnchor_pathDataSE[e])&&(U.capsMaskAnchorSE[e].setPathData(t),X.capsMaskAnchor_pathDataSE[e]=t,q=!0),qe(U,X.capsMaskAnchor_strokeWidthSE,e,Z=Y.capsMaskAnchor_strokeWidthSE[e])&&(U.capsMaskAnchorSE[e].style.strokeWidth=Z+"px",q=!0))}),qe(U,X,"capsMaskMarker_enabled",Z=Y.capsMaskMarker_enabled)&&(U.capsMaskLine.style.display=Z?"inline":"none",q=!0),Y.capsMaskMarker_enabled&&[0,1].forEach(function(n){var e=Y.capsMaskMarker_plugSE[n],t=e!==ne?ae[ie[e]]:null,a=Ye(n,t);qe(U,X.capsMaskMarker_enabledSE,n,Z=Y.capsMaskMarker_enabledSE[n])&&(U.capsMaskLine.style[a.prop]=Z?"url(#"+U.lineMaskMarkerIdSE[n]+")":"none",q=!0),Y.capsMaskMarker_enabledSE[n]&&(qe(U,X.capsMaskMarker_plugSE,n,e)&&(U.capsMaskMarkerShapeSE[n].href.baseVal="#"+t.elmId,Ze(U,U.capsMaskMarkerSE[n],a.orient,t.bBox,U.svg,U.capsMaskMarkerShapeSE[n],U.capsMaskLine),q=!0,ue&&(He(U,U.capsMaskLine),He(U,U.lineFace))),["markerWidth","markerHeight"].forEach(function(e){var t="capsMaskMarker_"+e+"SE";qe(U,X[t],n,Z=Y[t][n])&&(U.capsMaskMarkerSE[n][e].baseVal.value=Z,q=!0)}))}))),Y.lineOutline_enabled&&["x","y"].forEach(function(e){var t="lineOutlineMask_"+e;qe(U,X,t,Z=Y[t])&&(U.lineOutlineMask[e].baseVal.value=Z,q=!0)}),q),t.effect&&(J=(Q=e).curStats,$=Q.aplStats,Object.keys(te).forEach(function(e){var t=te[e],n=e+"_enabled",a=e+"_options",i=J[a];qe(Q,$,n,K=J[n])?(K&&($[a]=Oe(i)),t[K?"init":"remove"](Q)):K&&we(i,$[a])&&(t.remove(Q),$[n]=!0,$[a]=Oe(i),t.init(Q))})),(he||pe)&&ee.line&&!ee.path&&He(e,e.lineShape),he&&ee.plug&&!ee.line&&He(e,e.plugsFace),Ue(e)}function tt(e,t){return{duration:ye(e.duration)&&0i.x2&&(i.x2=t.x2),t.y2>i.y2&&(i.y2=t.y2),["x","y"].forEach(function(e){var t,n="dropShadow_"+e;o[n]=t=i[e+"1"],qe(a,l,n,t)&&(a.efc_dropShadow_elmFilter[e].baseVal.value=t)}))}}},Object.keys(te).forEach(function(e){var t=te[e],n=t.stats;n[e+"_enabled"]={iniValue:!1},n[e+"_options"]={hasProps:!0},t.anim&&(n[e+"_animOptions"]={},n[e+"_animId"]={})}),M={none:{defaultAnimOptions:{},init:function(e,t){var n=e.curStats;n.show_animId&&(g.remove(n.show_animId),n.show_animId=null),M.none.start(e,t)},start:function(e,t){M.none.stop(e,!0)},stop:function(e,t,n){var a=e.curStats;return n=null!=n?n:e.aplStats.show_on,a.show_inAnim=!1,t&&$e(e,n),n?1:0}},fade:{defaultAnimOptions:{duration:300,timing:"linear"},init:function(n,e){var t=n.curStats,a=n.aplStats;t.show_animId&&g.remove(t.show_animId),t.show_animId=g.add(function(e){return e},function(e,t){t?M.fade.stop(n,!0):(n.svg.style.opacity=e+"",se&&(He(n,n.svg),Ue(n)))},a.show_animOptions.duration,1,a.show_animOptions.timing,null,!1),M.fade.start(n,e)},start:function(e,t){var n,a=e.curStats;a.show_inAnim&&(n=g.stop(a.show_animId)),$e(e,1),a.show_inAnim=!0,g.start(a.show_animId,!e.aplStats.show_on,null!=t?t:n)},stop:function(e,t,n){var a,i=e.curStats;return n=null!=n?n:e.aplStats.show_on,a=i.show_inAnim?g.stop(i.show_animId):n?1:0,i.show_inAnim=!1,t&&(e.svg.style.opacity=n?"":"0",$e(e,n)),a}},draw:{defaultAnimOptions:{duration:500,timing:[.58,0,.42,1]},init:function(n,e){var t=n.curStats,a=n.aplStats,l=n.pathList.baseVal,i=Fe(l),r=i.segsLen,s=i.lenAll;t.show_animId&&g.remove(t.show_animId),t.show_animId=g.add(function(e){var t,n,a,i,o=-1;if(0===e)n=[[l[0][0],l[0][0]]];else if(1===e)n=l;else{for(t=s*e,n=[];t>=r[++o];)n.push(l[o]),t-=r[o];t&&(2===(a=l[o]).length?n.push([a[0],Pe(a[0],a[1],t/r[o])]):(i=Te(a[0],a[1],a[2],a[3],Be(a[0],a[1],a[2],a[3],t)),n.push([a[0],i.fromP1,i.fromP2,i])))}return n},function(e,t){t?M.draw.stop(n,!0):(n.pathList.animVal=e,et(n,{path:!0}))},a.show_animOptions.duration,1,a.show_animOptions.timing,null,!1),M.draw.start(n,e)},start:function(e,t){var n,a=e.curStats;a.show_inAnim&&(n=g.stop(a.show_animId)),$e(e,1),a.show_inAnim=!0,De(e,"apl_position",M.draw.update),g.start(a.show_animId,!e.aplStats.show_on,null!=t?t:n)},stop:function(e,t,n){var a,i=e.curStats;return n=null!=n?n:e.aplStats.show_on,a=i.show_inAnim?g.stop(i.show_animId):n?1:0,i.show_inAnim=!1,t&&(e.pathList.animVal=n?null:[[e.pathList.baseVal[0][0],e.pathList.baseVal[0][0]]],et(e,{path:!0}),$e(e,n)),a},update:function(e){ze(e,"apl_position",M.draw.update),e.curStats.show_inAnim?M.draw.init(e,M.draw.stop(e)):e.aplStats.show_animOptions={}}}},[["start","anchorSE",0],["end","anchorSE",1],["color","lineColor"],["size","lineSize"],["startSocketGravity","socketGravitySE",0],["endSocketGravity","socketGravitySE",1],["startPlugColor","plugColorSE",0],["endPlugColor","plugColorSE",1],["startPlugSize","plugSizeSE",0],["endPlugSize","plugSizeSE",1],["outline","lineOutlineEnabled"],["outlineColor","lineOutlineColor"],["outlineSize","lineOutlineSize"],["startPlugOutline","plugOutlineEnabledSE",0],["endPlugOutline","plugOutlineEnabledSE",1],["startPlugOutlineColor","plugOutlineColorSE",0],["endPlugOutlineColor","plugOutlineColorSE",1],["startPlugOutlineSize","plugOutlineSizeSE",0],["endPlugOutlineSize","plugOutlineSizeSE",1]].forEach(function(e){var t=e[0],n=e[1],a=e[2];Object.defineProperty(lt.prototype,t,{get:function(){var e=null!=a?ge[this._id].options[n][a]:n?ge[this._id].options[n]:ge[this._id].options[t];return null==e?U:Oe(e)},set:rt(t),enumerable:!0})}),[["path",z],["startSocket",W,"socketSE",0],["endSocket",W,"socketSE",1],["startPlug",j,"plugSE",0],["endPlug",j,"plugSE",1]].forEach(function(e){var a=e[0],i=e[1],o=e[2],l=e[3];Object.defineProperty(lt.prototype,a,{get:function(){var t,n=null!=l?ge[this._id].options[o][l]:o?ge[this._id].options[o]:ge[this._id].options[a];return n?Object.keys(i).some(function(e){return i[e]===n&&(t=e,!0)})?t:new Error("It's broken"):U},set:rt(a),enumerable:!0})}),Object.keys(te).forEach(function(n){var a=te[n];Object.defineProperty(lt.prototype,n,{get:function(){var u,e,t=ge[this._id].options[n];return fe(t)?(u=t,e=a.optionsConf.reduce(function(e,t){var n,a=t[0],i=t[1],o=t[2],l=t[3],r=t[4],s=null!=r?u[l][r]:l?u[l]:u[i];return e[i]="id"===a?s?Object.keys(o).some(function(e){return o[e]===s&&(n=e,!0)})?n:new Error("It's broken"):U:null==s?U:Oe(s),e},{}),a.anim&&(e.animation=Oe(u.animation)),e):t},set:rt(n),enumerable:!0})}),["startLabel","endLabel","middleLabel"].forEach(function(e,n){Object.defineProperty(lt.prototype,e,{get:function(){var e=ge[this._id],t=e.options;return t.labelSEM[n]&&!e.optionIsAttach.labelSEM[n]?ve[t.labelSEM[n]._id].text:t.labelSEM[n]||""},set:rt(e),enumerable:!0})}),lt.prototype.setOptions=function(e){return ot(ge[this._id],e),this},lt.prototype.position=function(){return et(ge[this._id],{position:!0}),this},lt.prototype.remove=function(){var t=ge[this._id],n=t.curStats;Object.keys(te).forEach(function(e){var t=e+"_animId";n[t]&&g.remove(n[t])}),n.show_animId&&g.remove(n.show_animId),t.attachments.slice().forEach(function(e){it(t,e)}),t.baseWindow&&t.svg&&t.baseWindow.document.body.removeChild(t.svg),delete ge[this._id]},lt.prototype.show=function(e,t){return nt(ge[this._id],!0,e,t),this},lt.prototype.hide=function(e,t){return nt(ge[this._id],!1,e,t),this},o=function(t){t&&ve[t._id]&&(t.boundTargets.slice().forEach(function(e){it(e.props,t,!0)}),t.conf.remove&&t.conf.remove(t),delete ve[t._id])},st.prototype.remove=function(){var t=this,n=ve[t._id];n&&(n.boundTargets.slice().forEach(function(e){n.conf.removeOption(n,e)}),je(function(){var e=ve[t._id];e&&(console.error("LeaderLineAttachment was not removed by removeOption"),o(e))}))},C=st,window.LeaderLineAttachment=C,L=function(e,t){return e instanceof C&&(!(e.isRemoved||t&&ve[e._id].conf.type!==t)||null)},I={pointAnchor:{type:"anchor",argOptions:[{optionName:"element",type:Ie}],init:function(e,t){return e.element=I.pointAnchor.checkElement(t.element),e.x=I.pointAnchor.parsePercent(t.x,!0)||[.5,!0],e.y=I.pointAnchor.parsePercent(t.y,!0)||[.5,!0],!0},removeOption:function(e,t){var n=t.props,a={},i=e.element,o=n.options.anchorSE["start"===t.optionName?1:0];i===o&&(i=o===document.body?new C(I.pointAnchor,[i]):document.body),a[t.optionName]=i,ot(n,a)},getBBoxNest:function(e,t){var n=Ae(e.element,t.baseWindow),a=n.width,i=n.height;return n.width=n.height=0,n.left=n.right=n.left+e.x[0]*(e.x[1]?a:1),n.top=n.bottom=n.top+e.y[0]*(e.y[1]?i:1),n},parsePercent:function(e,t){var n,a,i=!1;return ye(e)?a=e:"string"==typeof e&&(n=m.exec(e))&&n[2]&&(i=0!==(a=parseFloat(n[1])/100)),null!=a&&(t||0<=a)?[a,i]:null},checkElement:function(e){if(null==e)e=document.body;else if(!Ie(e))throw new Error("`element` must be Element");return e}},areaAnchor:{type:"anchor",argOptions:[{optionName:"element",type:Ie},{optionName:"shape",type:"string"}],stats:{color:{},strokeWidth:{},elementWidth:{},elementHeight:{},elementLeft:{},elementTop:{},pathListRel:{},bBoxRel:{},pathData:{},viewBoxBBox:{hasProps:!0},dashLen:{},dashGap:{}},init:function(i,e){var t,n,a,o=[];return i.element=I.pointAnchor.checkElement(e.element),"string"==typeof e.color&&(i.color=e.color.trim()),"string"==typeof e.fillColor&&(i.fill=e.fillColor.trim()),ye(e.size)&&0<=e.size&&(i.size=e.size),e.dash&&(i.dash=!0,ye(e.dash.len)&&0i.right&&(i.right=t),ni.bottom&&(i.bottom=n)):i={left:t,right:t,top:n,bottom:n},o?P.pathListRel.push([o,{x:t,y:n}]):P.pathListRel=[],o={x:t,y:n}}),P.pathListRel.push([]),e=P.strokeWidth/2,l=[{x:i.left-e,y:i.top-e},{x:i.right+e,y:i.bottom+e}],P.bBoxRel={left:l[0].x,top:l[0].y,right:l[1].x,bottom:l[1].y,width:l[1].x-l[0].x,height:l[1].y-l[0].y}}W.pathListRel=W.bBoxRel=!0}return(W.pathListRel||W.elementLeft||W.elementTop)&&(P.pathData=Re(P.pathListRel,function(e){e.x+=a.left,e.y+=a.top})),qe(t,N,"strokeWidth",n=P.strokeWidth)&&(t.path.style.strokeWidth=n+"px"),Ge(n=P.pathData,N.pathData)&&(t.path.setPathData(n),N.pathData=n,W.pathData=!0),t.dash&&(!W.pathData&&(!W.strokeWidth||t.dashLen&&t.dashGap)||(P.dashLen=t.dashLen||2*P.strokeWidth,P.dashGap=t.dashGap||P.strokeWidth),W.dash=qe(t,N,"dashLen",P.dashLen)||W.dash,W.dash=qe(t,N,"dashGap",P.dashGap)||W.dash,W.dash&&(t.path.style.strokeDasharray=N.dashLen+","+N.dashGap)),C=P.viewBoxBBox,L=N.viewBoxBBox,A=t.svg.viewBox.baseVal,V=t.svg.style,C.x=P.bBoxRel.left+a.left,C.y=P.bBoxRel.top+a.top,C.width=P.bBoxRel.width,C.height=P.bBoxRel.height,["x","y","width","height"].forEach(function(e){(n=C[e])!==L[e]&&(A[e]=L[e]=n,V[oe[e]]=n+("x"===e||"y"===e?t.bodyOffset[e]:0)+"px")}),W.strokeWidth||W.pathListRel||W.bBoxRel}},mouseHoverAnchor:{type:"anchor",argOptions:[{optionName:"element",type:Ie},{optionName:"showEffectName",type:"string"}],style:{backgroundImage:"url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cG9seWdvbiBwb2ludHM9IjI0LDAgMCw4IDgsMTEgMCwxOSA1LDI0IDEzLDE2IDE2LDI0IiBmaWxsPSJjb3JhbCIvPjwvc3ZnPg==')",backgroundSize:"",backgroundRepeat:"no-repeat",backgroundColor:"#f8f881",cursor:"default"},hoverStyle:{backgroundImage:"none",backgroundColor:"#fadf8f"},padding:{top:1,right:15,bottom:1,left:2},minHeight:15,backgroundPosition:{right:2,top:2},backgroundSize:{width:12,height:12},dirKeys:[["top","Top"],["right","Right"],["bottom","Bottom"],["left","Left"]],init:function(a,i){var o,t,e,n,l,r,s,u,h,p,c,d=I.mouseHoverAnchor,f={};if(a.element=I.pointAnchor.checkElement(i.element),u=a.element,!((p=u.ownerDocument)&&(h=p.defaultView)&&h.HTMLElement&&u instanceof h.HTMLElement))throw new Error("`element` must be HTML element");return d.style.backgroundSize=d.backgroundSize.width+"px "+d.backgroundSize.height+"px",["style","hoverStyle"].forEach(function(e){var n=d[e];a[e]=Object.keys(n).reduce(function(e,t){return e[t]=n[t],e},{})}),"inline"===(o=a.element.ownerDocument.defaultView.getComputedStyle(a.element,"")).display?a.style.display="inline-block":"none"===o.display&&(a.style.display="block"),I.mouseHoverAnchor.dirKeys.forEach(function(e){var t=e[0],n="padding"+e[1];parseFloat(o[n])e.x2&&(e.x2=a.x2),a.y2>e.y2&&(e.y2=a.y2)},newText:function(e,t,n,a,i){var o,l,r,s,u,h=t.createElementNS(re,"text");return h.textContent=e,[h.x,h.y].forEach(function(e){var t=n.createSVGLength();t.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX,0),e.baseVal.initialize(t)}),"boolean"!=typeof f&&(f="paintOrder"in h.style),i&&!f?(l=t.createElementNS(re,"defs"),h.id=a,l.appendChild(h),(s=(o=t.createElementNS(re,"g")).appendChild(t.createElementNS(re,"use"))).href.baseVal="#"+a,(r=o.appendChild(t.createElementNS(re,"use"))).href.baseVal="#"+a,(u=s.style).strokeLinejoin="round",{elmPosition:h,styleText:h.style,styleFill:r.style,styleStroke:u,styleShow:o.style,elmsAppend:[l,o]}):(u=h.style,i&&(u.strokeLinejoin="round",u.paintOrder="stroke"),{elmPosition:h,styleText:u,styleFill:u,styleStroke:i?u:null,styleShow:u,elmsAppend:[h]})},getMidPoint:function(e,t){var n,a,i=Fe(e),o=i.segsLen,l=i.lenAll,r=-1,s=l/2+(t||0);if(s<=0)return 2===(n=e[0]).length?Pe(n[0],n[1],0):Te(n[0],n[1],n[2],n[3],0);if(l<=s)return 2===(n=e[e.length-1]).length?Pe(n[0],n[1],1):Te(n[0],n[1],n[2],n[3],1);for(a=[];s>o[++r];)a.push(e[r]),s-=o[r];return 2===(n=e[r]).length?Pe(n[0],n[1],s/o[r]):Te(n[0],n[1],n[2],n[3],Be(n[0],n[1],n[2],n[3],s))},initSvg:function(t,n){var e,a,i=I.captionLabel.newText(t.text,n.baseWindow.document,n.svg,A+"-captionLabel-"+t._id,t.outlineColor);["elmPosition","styleFill","styleShow","elmsAppend"].forEach(function(e){t[e]=i[e]}),t.isShown=!1,t.styleShow.visibility="hidden",I.captionLabel.textStyleProps.forEach(function(e){null!=t[e]&&(i.styleText[e]=t[e])}),i.elmsAppend.forEach(function(e){n.svg.appendChild(e)}),e=i.elmPosition.getBBox(),t.width=e.width,t.height=e.height,t.outlineColor&&(a=10<(a=e.height/9)?10:a<2?2:a,i.styleStroke.strokeWidth=a+"px",i.styleStroke.stroke=t.outlineColor),t.strokeWidth=a||0,Xe(t.aplStats,I.captionLabel.stats),t.updateColor(n),t.refSocketXY?t.updateSocketXY(n):t.updatePath(n),pe&&et(n,{}),t.updateShow(n)},bind:function(e,t){var n=t.props;return e.color||De(n,"cur_line_color",e.updateColor),(e.refSocketXY="startLabel"===t.optionName||"endLabel"===t.optionName)?(e.socketIndex="startLabel"===t.optionName?0:1,De(n,"apl_position",e.updateSocketXY),e.offset||(De(n,"cur_attach_plugSideLenSE",e.updateSocketXY),De(n,"cur_line_strokeWidth",e.updateSocketXY))):De(n,"apl_path",e.updatePath),De(n,"svgShow",e.updateShow),pe&&De(n,"new_edge4viewBox",e.adjustEdge),I.captionLabel.initSvg(e,n),!0},unbind:function(e,t){var n=t.props;e.elmsAppend&&(e.elmsAppend.forEach(function(e){n.svg.removeChild(e)}),e.elmPosition=e.styleFill=e.styleShow=e.elmsAppend=null),Xe(e.curStats,I.captionLabel.stats),Xe(e.aplStats,I.captionLabel.stats),e.color||ze(n,"cur_line_color",e.updateColor),e.refSocketXY?(ze(n,"apl_position",e.updateSocketXY),e.offset||(ze(n,"cur_attach_plugSideLenSE",e.updateSocketXY),ze(n,"cur_line_strokeWidth",e.updateSocketXY))):ze(n,"apl_path",e.updatePath),ze(n,"svgShow",e.updateShow),pe&&(ze(n,"new_edge4viewBox",e.adjustEdge),et(n,{}))},removeOption:function(e,t){var n=t.props,a={};a[t.optionName]="",ot(n,a)},remove:function(t){t.boundTargets.length&&(console.error("LeaderLineAttachment was not unbound by remove"),t.boundTargets.forEach(function(e){I.captionLabel.unbind(t,e)}))}},pathLabel:{type:"label",argOptions:[{optionName:"text",type:"string"}],stats:{color:{},startOffset:{},pathData:{}},init:function(s,t){return"string"==typeof t.text&&(s.text=t.text.trim()),!!s.text&&("string"==typeof t.color&&(s.color=t.color.trim()),s.outlineColor="string"==typeof t.outlineColor?t.outlineColor.trim():"#fff",ye(t.lineOffset)&&(s.lineOffset=t.lineOffset),I.captionLabel.textStyleProps.forEach(function(e){null!=t[e]&&(s[e]=t[e])}),s.updateColor=function(e){I.captionLabel.updateColor(s,e)},s.updatePath=function(e){var t,n=s.curStats,a=s.aplStats,i=e.curStats,o=e.pathList.animVal||e.pathList.baseVal;o&&(n.pathData=t=I.pathLabel.getOffsetPathData(o,i.line_strokeWidth/2+s.strokeWidth/2+s.height/4,1.25*s.height),Ge(t,a.pathData)&&(s.elmPath.setPathData(t),a.pathData=t,s.bBox=s.elmPosition.getBBox(),s.updateStartOffset(e)))},s.updateStartOffset=function(e){var t,i,n,a,o=s.curStats,l=s.aplStats,r=e.curStats;o.pathData&&(2===s.semIndex&&!s.lineOffset||(n=o.pathData.reduce(function(e,t){var n,a=t.values;switch(t.type){case"M":i={x:a[0],y:a[1]};break;case"L":n={x:a[0],y:a[1]},i&&(e+=Ve(i,n)),i=n;break;case"C":n={x:a[4],y:a[5]},i&&(e+=We(i,{x:a[0],y:a[1]},{x:a[2],y:a[3]},n)),i=n}return e},0),a=0===s.semIndex?0:1===s.semIndex?n:n/2,2!==s.semIndex&&(t=Math.max(r.attach_plugBackLenSE[s.semIndex]||0,r.line_strokeWidth/2)+s.strokeWidth/2+s.height/4,a=(a+=0===s.semIndex?t:-t)<0?0:nx?((t=b.points)[1]=Ne(t[0],t[1],-x),b.len=Ve(t[0],t[1])):(b.points=null,b.len=0),e.len>x+n?((t=e.points)[0]=Ne(t[1],t[0],-(x+n)),e.len=Ve(t[0],t[1])):(e.points=null,e.len=0)),e):null}),k.reduce(function(t,e){var n=e.points;return n&&(a&&w(n[0],a)||t.push({type:"M",values:[n[0].x,n[0].y]}),"line"===e.type?t.push({type:"L",values:[n[1].x,n[1].y]}):(n.shift(),n.forEach(function(e){t.push({type:"L",values:[e.x,e.y]})})),a=n[n.length-1]),t},[])},newText:function(e,t,n,a){var i,o,l,r,s,u,h,p,c=t.createElementNS(re,"defs"),d=c.appendChild(t.createElementNS(re,"path"));return d.id=i=n+"-path",(r=(l=t.createElementNS(re,"text")).appendChild(t.createElementNS(re,"textPath"))).href.baseVal="#"+i,r.startOffset.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX,0),r.textContent=e,"boolean"!=typeof f&&(f="paintOrder"in l.style),a&&!f?(l.id=o=n+"-text",c.appendChild(l),(h=(s=t.createElementNS(re,"g")).appendChild(t.createElementNS(re,"use"))).href.baseVal="#"+o,(u=s.appendChild(t.createElementNS(re,"use"))).href.baseVal="#"+o,(p=h.style).strokeLinejoin="round",{elmPosition:l,elmPath:d,elmOffset:r,styleText:l.style,styleFill:u.style,styleStroke:p,styleShow:s.style,elmsAppend:[c,s]}):(p=l.style,a&&(p.strokeLinejoin="round",p.paintOrder="stroke"),{elmPosition:l,elmPath:d,elmOffset:r,styleText:p,styleFill:p,styleStroke:a?p:null,styleShow:p,elmsAppend:[c,l]})},initSvg:function(t,n){var e,a,i=I.pathLabel.newText(t.text,n.baseWindow.document,A+"-pathLabel-"+t._id,t.outlineColor);["elmPosition","elmPath","elmOffset","styleFill","styleShow","elmsAppend"].forEach(function(e){t[e]=i[e]}),t.isShown=!1,t.styleShow.visibility="hidden",I.captionLabel.textStyleProps.forEach(function(e){null!=t[e]&&(i.styleText[e]=t[e])}),i.elmsAppend.forEach(function(e){n.svg.appendChild(e)}),i.elmPath.setPathData([{type:"M",values:[0,100]},{type:"h",values:[100]}]),e=i.elmPosition.getBBox(),i.styleText.textAnchor=["start","end","middle"][t.semIndex],2!==t.semIndex||t.lineOffset||i.elmOffset.startOffset.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PERCENTAGE,50),t.height=e.height,t.outlineColor&&(a=10<(a=e.height/9)?10:a<2?2:a,i.styleStroke.strokeWidth=a+"px",i.styleStroke.stroke=t.outlineColor),t.strokeWidth=a||0,Xe(t.aplStats,I.pathLabel.stats),t.updateColor(n),t.updatePath(n),t.updateStartOffset(n),pe&&et(n,{}),t.updateShow(n)},bind:function(e,t){var n=t.props;return e.color||De(n,"cur_line_color",e.updateColor),De(n,"cur_line_strokeWidth",e.updatePath),De(n,"apl_path",e.updatePath),e.semIndex="startLabel"===t.optionName?0:"endLabel"===t.optionName?1:2,2===e.semIndex&&!e.lineOffset||De(n,"cur_attach_plugBackLenSE",e.updateStartOffset),De(n,"svgShow",e.updateShow),pe&&De(n,"new_edge4viewBox",e.adjustEdge),I.pathLabel.initSvg(e,n),!0},unbind:function(e,t){var n=t.props;e.elmsAppend&&(e.elmsAppend.forEach(function(e){n.svg.removeChild(e)}),e.elmPosition=e.elmPath=e.elmOffset=e.styleFill=e.styleShow=e.elmsAppend=null),Xe(e.curStats,I.pathLabel.stats),Xe(e.aplStats,I.pathLabel.stats),e.color||ze(n,"cur_line_color",e.updateColor),ze(n,"cur_line_strokeWidth",e.updatePath),ze(n,"apl_path",e.updatePath),2===e.semIndex&&!e.lineOffset||ze(n,"cur_attach_plugBackLenSE",e.updateStartOffset),ze(n,"svgShow",e.updateShow),pe&&(ze(n,"new_edge4viewBox",e.adjustEdge),et(n,{}))},removeOption:function(e,t){var n=t.props,a={};a[t.optionName]="",ot(n,a)},remove:function(t){t.boundTargets.length&&(console.error("LeaderLineAttachment was not unbound by remove"),t.boundTargets.forEach(function(e){I.pathLabel.unbind(t,e)}))}}},Object.keys(I).forEach(function(e){lt[e]=function(){return new C(I[e],Array.prototype.slice.call(arguments))}}),lt.positionByWindowResize=!0,window.addEventListener("resize",v.add(function(){lt.positionByWindowResize&&Object.keys(ge).forEach(function(e){et(ge[e],{position:!0})})}),!1),lt}();!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e["leader-line"]=t()}(this,function(){return LeaderLine}); --------------------------------------------------------------------------------