1<?php
2
3namespace CpChart\Chart;
4
5use CpChart\Image;
6
7/**
8 *  Spring - class to draw spring graphs
9 *
10 *  Version     : 2.1.4
11 *  Made by     : Jean-Damien POGOLOTTI
12 *  Last Update : 19/01/2014
13 *
14 *  This file can be distributed under the license you can find at :
15 *
16 *  http://www.pchart.net/license
17 *
18 *  You can find the whole class documentation on the pChart web site.
19 */
20class Spring
21{
22    /**
23     * @var array
24     */
25    public $History = [];
26
27    /**
28     * @var array
29     */
30    public $Data = [];
31
32    /**
33     * @var array
34     */
35    public $Default = [];
36
37    /**
38     * @var array
39     */
40    public $Labels = [];
41
42    /**
43     * @var array
44     */
45    public $Links = [];
46
47    /**
48     * @var type
49     */
50    public $pChartObject;
51
52    /**
53     * @var int
54     */
55    public $X1;
56
57    /**
58     * @var int
59     */
60    public $Y1;
61
62    /**
63     * @var int
64     */
65    public $X2;
66
67    /**
68     * @var int
69     */
70    public $Y2;
71
72    /**
73     * @var boolean
74     */
75    public $AutoComputeFreeZone = false;
76
77    public function __construct()
78    {
79        /* Set nodes defaults */
80        $this->Default["R"] = 255;
81        $this->Default["G"] = 255;
82        $this->Default["B"] = 255;
83        $this->Default["Alpha"] = 100;
84        $this->Default["BorderR"] = 0;
85        $this->Default["BorderG"] = 0;
86        $this->Default["BorderB"] = 0;
87        $this->Default["BorderAlpha"] = 100;
88        $this->Default["Surrounding"] = null;
89        $this->Default["BackgroundR"] = 255;
90        $this->Default["BackgroundG"] = 255;
91        $this->Default["BackgroundB"] = 255;
92        $this->Default["BackgroundAlpha"] = 0;
93        $this->Default["Force"] = 1;
94        $this->Default["NodeType"] = NODE_TYPE_FREE;
95        $this->Default["Size"] = 5;
96        $this->Default["Shape"] = NODE_SHAPE_CIRCLE;
97        $this->Default["FreeZone"] = 40;
98        $this->Default["LinkR"] = 0;
99        $this->Default["LinkG"] = 0;
100        $this->Default["LinkB"] = 0;
101        $this->Default["LinkAlpha"] = 0;
102
103        $this->Labels["Type"] = LABEL_CLASSIC;
104        $this->Labels["R"] = 0;
105        $this->Labels["G"] = 0;
106        $this->Labels["B"] = 0;
107        $this->Labels["Alpha"] = 100;
108    }
109
110    /**
111     * Set default links options
112     * @param array $Settings
113     */
114    public function setLinkDefaults(array $Settings = [])
115    {
116        if (isset($Settings["R"])) {
117            $this->Default["LinkR"] = $Settings["R"];
118        }
119        if (isset($Settings["G"])) {
120            $this->Default["LinkG"] = $Settings["G"];
121        }
122        if (isset($Settings["B"])) {
123            $this->Default["LinkB"] = $Settings["B"];
124        }
125        if (isset($Settings["Alpha"])) {
126            $this->Default["LinkAlpha"] = $Settings["Alpha"];
127        }
128    }
129
130    /**
131     * Set default links options
132     * @param array $Settings
133     */
134    public function setLabelsSettings(array $Settings = [])
135    {
136        if (isset($Settings["Type"])) {
137            $this->Labels["Type"] = $Settings["Type"];
138        }
139        if (isset($Settings["R"])) {
140            $this->Labels["R"] = $Settings["R"];
141        }
142        if (isset($Settings["G"])) {
143            $this->Labels["G"] = $Settings["G"];
144        }
145        if (isset($Settings["B"])) {
146            $this->Labels["B"] = $Settings["B"];
147        }
148        if (isset($Settings["Alpha"])) {
149            $this->Labels["Alpha"] = $Settings["Alpha"];
150        }
151    }
152
153    /**
154     * Auto compute the FreeZone size based on the number of connections
155     */
156    public function autoFreeZone()
157    {
158        /* Check connections reciprocity */
159        foreach ($this->Data as $Key => $Settings) {
160            if (isset($Settings["Connections"])) {
161                $this->Data[$Key]["FreeZone"] = count($Settings["Connections"]) * 10 + 20;
162            } else {
163                $this->Data[$Key]["FreeZone"] = 20;
164            }
165        }
166    }
167
168    /**
169     * Set link properties
170     * @param int $FromNode
171     * @param int $ToNode
172     * @param array $Settings
173     * @return null|int
174     */
175    public function linkProperties($FromNode, $ToNode, array $Settings)
176    {
177        if (!isset($this->Data[$FromNode])) {
178            return 0;
179        }
180        if (!isset($this->Data[$ToNode])) {
181            return 0;
182        }
183
184        $R = isset($Settings["R"]) ? $Settings["R"] : 0;
185        $G = isset($Settings["G"]) ? $Settings["G"] : 0;
186        $B = isset($Settings["B"]) ? $Settings["B"] : 0;
187        $Alpha = isset($Settings["Alpha"]) ? $Settings["Alpha"] : 100;
188        $Name = isset($Settings["Name"]) ? $Settings["Name"] : null;
189        $Ticks = isset($Settings["Ticks"]) ? $Settings["Ticks"] : null;
190
191        $this->Links[$FromNode][$ToNode]["R"] = $R;
192        $this->Links[$ToNode][$FromNode]["R"] = $R;
193        $this->Links[$FromNode][$ToNode]["G"] = $G;
194        $this->Links[$ToNode][$FromNode]["G"] = $G;
195        $this->Links[$FromNode][$ToNode]["B"] = $B;
196        $this->Links[$ToNode][$FromNode]["B"] = $B;
197        $this->Links[$FromNode][$ToNode]["Alpha"] = $Alpha;
198        $this->Links[$ToNode][$FromNode]["Alpha"] = $Alpha;
199        $this->Links[$FromNode][$ToNode]["Name"] = $Name;
200        $this->Links[$ToNode][$FromNode]["Name"] = $Name;
201        $this->Links[$FromNode][$ToNode]["Ticks"] = $Ticks;
202        $this->Links[$ToNode][$FromNode]["Ticks"] = $Ticks;
203    }
204
205    /**
206     * @param array $Settings
207     */
208    public function setNodeDefaults(array $Settings = [])
209    {
210        if (isset($Settings["R"])) {
211            $this->Default["R"] = $Settings["R"];
212        }
213        if (isset($Settings["G"])) {
214            $this->Default["G"] = $Settings["G"];
215        }
216        if (isset($Settings["B"])) {
217            $this->Default["B"] = $Settings["B"];
218        }
219        if (isset($Settings["Alpha"])) {
220            $this->Default["Alpha"] = $Settings["Alpha"];
221        }
222        if (isset($Settings["BorderR"])) {
223            $this->Default["BorderR"] = $Settings["BorderR"];
224        }
225        if (isset($Settings["BorderG"])) {
226            $this->Default["BorderG"] = $Settings["BorderG"];
227        }
228        if (isset($Settings["BorderB"])) {
229            $this->Default["BorderB"] = $Settings["BorderB"];
230        }
231        if (isset($Settings["BorderAlpha"])) {
232            $this->Default["BorderAlpha"] = $Settings["BorderAlpha"];
233        }
234        if (isset($Settings["Surrounding"])) {
235            $this->Default["Surrounding"] = $Settings["Surrounding"];
236        }
237        if (isset($Settings["BackgroundR"])) {
238            $this->Default["BackgroundR"] = $Settings["BackgroundR"];
239        }
240        if (isset($Settings["BackgroundG"])) {
241            $this->Default["BackgroundG"] = $Settings["BackgroundG"];
242        }
243        if (isset($Settings["BackgroundB"])) {
244            $this->Default["BackgroundB"] = $Settings["BackgroundB"];
245        }
246        if (isset($Settings["BackgroundAlpha"])) {
247            $this->Default["BackgroundAlpha"] = $Settings["BackgroundAlpha"];
248        }
249        if (isset($Settings["NodeType"])) {
250            $this->Default["NodeType"] = $Settings["NodeType"];
251        }
252        if (isset($Settings["Size"])) {
253            $this->Default["Size"] = $Settings["Size"];
254        }
255        if (isset($Settings["Shape"])) {
256            $this->Default["Shape"] = $Settings["Shape"];
257        }
258        if (isset($Settings["FreeZone"])) {
259            $this->Default["FreeZone"] = $Settings["FreeZone"];
260        }
261    }
262
263    /**
264     * Add a node
265     * @param int $NodeID
266     * @param array $Settings
267     * @return null|int
268     */
269    public function addNode($NodeID, array $Settings = [])
270    {
271        /* if the node already exists, ignore */
272        if (isset($this->Data[$NodeID])) {
273            return 0;
274        }
275
276        $Name = isset($Settings["Name"]) ? $Settings["Name"] : "Node " . $NodeID;
277        $Connections = isset($Settings["Connections"]) ? $Settings["Connections"] : null;
278
279        $R = isset($Settings["R"]) ? $Settings["R"] : $this->Default["R"];
280        $G = isset($Settings["G"]) ? $Settings["G"] : $this->Default["G"];
281        $B = isset($Settings["B"]) ? $Settings["B"] : $this->Default["B"];
282        $Alpha = isset($Settings["Alpha"]) ? $Settings["Alpha"] : $this->Default["Alpha"];
283        $BorderR = isset($Settings["BorderR"]) ? $Settings["BorderR"] : $this->Default["BorderR"];
284        $BorderG = isset($Settings["BorderG"]) ? $Settings["BorderG"] : $this->Default["BorderG"];
285        $BorderB = isset($Settings["BorderB"]) ? $Settings["BorderB"] : $this->Default["BorderB"];
286        $BorderAlpha = isset($Settings["BorderAlpha"]) ? $Settings["BorderAlpha"] : $this->Default["BorderAlpha"];
287        $Surrounding = isset($Settings["Surrounding"]) ? $Settings["Surrounding"] : $this->Default["Surrounding"];
288        $BackgroundR = isset($Settings["BackgroundR"]) ? $Settings["BackgroundR"] : $this->Default["BackgroundR"];
289        $BackgroundG = isset($Settings["BackgroundG"]) ? $Settings["BackgroundG"] : $this->Default["BackgroundG"];
290        $BackgroundB = isset($Settings["BackgroundB"]) ? $Settings["BackgroundB"] : $this->Default["BackgroundB"];
291        $BackgroundAlpha = isset($Settings["BackgroundAlpha"])
292            ? $Settings["BackgroundAlpha"] : $this->Default["BackgroundAlpha"]
293        ;
294        $Force = isset($Settings["Force"]) ? $Settings["Force"] : $this->Default["Force"];
295        $NodeType = isset($Settings["NodeType"]) ? $Settings["NodeType"] : $this->Default["NodeType"];
296        $Size = isset($Settings["Size"]) ? $Settings["Size"] : $this->Default["Size"];
297        $Shape = isset($Settings["Shape"]) ? $Settings["Shape"] : $this->Default["Shape"];
298        $FreeZone = isset($Settings["FreeZone"]) ? $Settings["FreeZone"] : $this->Default["FreeZone"];
299
300        if ($Surrounding != null) {
301            $BorderR = $R + $Surrounding;
302            $BorderG = $G + $Surrounding;
303            $BorderB = $B + $Surrounding;
304        }
305
306        $this->Data[$NodeID]["R"] = $R;
307        $this->Data[$NodeID]["G"] = $G;
308        $this->Data[$NodeID]["B"] = $B;
309        $this->Data[$NodeID]["Alpha"] = $Alpha;
310        $this->Data[$NodeID]["BorderR"] = $BorderR;
311        $this->Data[$NodeID]["BorderG"] = $BorderG;
312        $this->Data[$NodeID]["BorderB"] = $BorderB;
313        $this->Data[$NodeID]["BorderAlpha"] = $BorderAlpha;
314        $this->Data[$NodeID]["BackgroundR"] = $BackgroundR;
315        $this->Data[$NodeID]["BackgroundG"] = $BackgroundG;
316        $this->Data[$NodeID]["BackgroundB"] = $BackgroundB;
317        $this->Data[$NodeID]["BackgroundAlpha"] = $BackgroundAlpha;
318        $this->Data[$NodeID]["Name"] = $Name;
319        $this->Data[$NodeID]["Force"] = $Force;
320        $this->Data[$NodeID]["Type"] = $NodeType;
321        $this->Data[$NodeID]["Size"] = $Size;
322        $this->Data[$NodeID]["Shape"] = $Shape;
323        $this->Data[$NodeID]["FreeZone"] = $FreeZone;
324        if ($Connections != null) {
325            if (is_array($Connections)) {
326                foreach ($Connections as $Key => $Value) {
327                    $this->Data[$NodeID]["Connections"][] = $Value;
328                }
329            } else {
330                $this->Data[$NodeID]["Connections"][] = $Connections;
331            }
332        }
333    }
334
335    /**
336     * Set color attribute for a list of nodes
337     * @param array|string $Nodes
338     * @param array $Settings
339     */
340    public function setNodesColor($Nodes, array $Settings = [])
341    {
342        if (is_array($Nodes)) {
343            foreach ($Nodes as $Key => $NodeID) {
344                if (isset($this->Data[$NodeID])) {
345                    if (isset($Settings["R"])) {
346                        $this->Data[$NodeID]["R"] = $Settings["R"];
347                    }
348                    if (isset($Settings["G"])) {
349                        $this->Data[$NodeID]["G"] = $Settings["G"];
350                    }
351                    if (isset($Settings["B"])) {
352                        $this->Data[$NodeID]["B"] = $Settings["B"];
353                    }
354                    if (isset($Settings["Alpha"])) {
355                        $this->Data[$NodeID]["Alpha"] = $Settings["Alpha"];
356                    }
357                    if (isset($Settings["BorderR"])) {
358                        $this->Data[$NodeID]["BorderR"] = $Settings["BorderR"];
359                    }
360                    if (isset($Settings["BorderG"])) {
361                        $this->Data[$NodeID]["BorderG"] = $Settings["BorderG"];
362                    }
363                    if (isset($Settings["BorderB"])) {
364                        $this->Data[$NodeID]["BorderB"] = $Settings["BorderB"];
365                    }
366                    if (isset($Settings["BorderAlpha"])) {
367                        $this->Data[$NodeID]["BorderAlpha"] = $Settings["BorderAlpha"];
368                    }
369                    if (isset($Settings["Surrounding"])) {
370                        $this->Data[$NodeID]["BorderR"] = $this->Data[$NodeID]["R"] + $Settings["Surrounding"];
371                        $this->Data[$NodeID]["BorderG"] = $this->Data[$NodeID]["G"] + $Settings["Surrounding"];
372                        $this->Data[$NodeID]["BorderB"] = $this->Data[$NodeID]["B"] + $Settings["Surrounding"];
373                    }
374                }
375            }
376        } else {
377            if (isset($Settings["R"])) {
378                $this->Data[$Nodes]["R"] = $Settings["R"];
379            }
380            if (isset($Settings["G"])) {
381                $this->Data[$Nodes]["G"] = $Settings["G"];
382            }
383            if (isset($Settings["B"])) {
384                $this->Data[$Nodes]["B"] = $Settings["B"];
385            }
386            if (isset($Settings["Alpha"])) {
387                $this->Data[$Nodes]["Alpha"] = $Settings["Alpha"];
388            }
389            if (isset($Settings["BorderR"])) {
390                $this->Data[$Nodes]["BorderR"] = $Settings["BorderR"];
391            }
392            if (isset($Settings["BorderG"])) {
393                $this->Data[$Nodes]["BorderG"] = $Settings["BorderG"];
394            }
395            if (isset($Settings["BorderB"])) {
396                $this->Data[$Nodes]["BorderB"] = $Settings["BorderB"];
397            }
398            if (isset($Settings["BorderAlpha"])) {
399                $this->Data[$Nodes]["BorderAlpha"] = $Settings["BorderAlpha"];
400            }
401            if (isset($Settings["Surrounding"])) {
402                $this->Data[$Nodes]["BorderR"] = $this->Data[$NodeID]["R"] + $Settings["Surrounding"];
403                $this->Data[$NodeID]["BorderG"] = $this->Data[$NodeID]["G"] + $Settings["Surrounding"];
404                $this->Data[$NodeID]["BorderB"] = $this->Data[$NodeID]["B"] + $Settings["Surrounding"];
405            }
406        }
407    }
408
409    /**
410     * Returns all the nodes details
411     * @return array
412     */
413    public function dumpNodes()
414    {
415        return $this->Data;
416    }
417
418    /**
419     * Check if a connection exists and create it if required
420     * @param string|int $SourceID
421     * @param string|int $TargetID
422     * @return boolean|null
423     */
424    public function checkConnection($SourceID, $TargetID)
425    {
426        if (isset($this->Data[$SourceID]["Connections"])) {
427            foreach ($this->Data[$SourceID]["Connections"] as $ConnectionID) {
428                if ($TargetID == $ConnectionID) {
429                    return true;
430                }
431            }
432        }
433        $this->Data[$SourceID]["Connections"][] = $TargetID;
434    }
435
436    /**
437     * Get the median linked nodes position
438     * @param string $Key
439     * @param int $X
440     * @param int $Y
441     * @return array
442     */
443    public function getMedianOffset($Key, $X, $Y)
444    {
445        $Cpt = 1;
446        if (isset($this->Data[$Key]["Connections"])) {
447            foreach ($this->Data[$Key]["Connections"] as $NodeID) {
448                if (isset($this->Data[$NodeID]["X"])
449                    && isset($this->Data[$NodeID]["Y"])
450                ) {
451                    $X = $X + $this->Data[$NodeID]["X"];
452                    $Y = $Y + $this->Data[$NodeID]["Y"];
453                    $Cpt++;
454                }
455            }
456        }
457        return ["X" => $X / $Cpt, "Y" => $Y / $Cpt];
458    }
459
460    /**
461     * Return the ID of the attached partner with the biggest weight
462     * @param string $Key
463     * @return string
464     */
465    public function getBiggestPartner($Key)
466    {
467        if (!isset($this->Data[$Key]["Connections"])) {
468            return "";
469        }
470
471        $MaxWeight = 0;
472        $Result = "";
473        foreach ($this->Data[$Key]["Connections"] as $Key => $PeerID) {
474            if ($this->Data[$PeerID]["Weight"] > $MaxWeight) {
475                $MaxWeight = $this->Data[$PeerID]["Weight"];
476                $Result = $PeerID;
477            }
478        }
479        return $Result;
480    }
481
482    /**
483     * Do the initial node positions computing pass
484     * @param int $Algorithm
485     */
486    public function firstPass($Algorithm)
487    {
488        $CenterX = ($this->X2 - $this->X1) / 2 + $this->X1;
489        $CenterY = ($this->Y2 - $this->Y1) / 2 + $this->Y1;
490
491        /* Check connections reciprocity */
492        foreach ($this->Data as $Key => $Settings) {
493            if (isset($Settings["Connections"])) {
494                foreach ($Settings["Connections"] as $ID => $ConnectionID) {
495                    $this->checkConnection($ConnectionID, $Key);
496                }
497            }
498        }
499
500        if ($this->AutoComputeFreeZone) {
501            $this->autoFreeZone();
502        }
503
504        /* Get the max number of connections */
505        $MaxConnections = 0;
506        foreach ($this->Data as $Key => $Settings) {
507            if (isset($Settings["Connections"])) {
508                if ($MaxConnections < count($Settings["Connections"])) {
509                    $MaxConnections = count($Settings["Connections"]);
510                }
511            }
512        }
513
514        if ($Algorithm == ALGORITHM_WEIGHTED) {
515            foreach ($this->Data as $Key => $Settings) {
516                if ($Settings["Type"] == NODE_TYPE_CENTRAL) {
517                    $this->Data[$Key]["X"] = $CenterX;
518                    $this->Data[$Key]["Y"] = $CenterY;
519                }
520                if ($Settings["Type"] == NODE_TYPE_FREE) {
521                    if (isset($Settings["Connections"])) {
522                        $Connections = count($Settings["Connections"]);
523                    } else {
524                        $Connections = 0;
525                    }
526
527                    $Ring = $MaxConnections - $Connections;
528                    $Angle = rand(0, 360);
529
530                    $this->Data[$Key]["X"] = cos(deg2rad($Angle)) * ($Ring * $this->RingSize) + $CenterX;
531                    $this->Data[$Key]["Y"] = sin(deg2rad($Angle)) * ($Ring * $this->RingSize) + $CenterY;
532                }
533            }
534        } elseif ($Algorithm == ALGORITHM_CENTRAL) {
535            /* Put a weight on each nodes */
536            foreach ($this->Data as $Key => $Settings) {
537                if (isset($Settings["Connections"])) {
538                    $this->Data[$Key]["Weight"] = count($Settings["Connections"]);
539                } else {
540                    $this->Data[$Key]["Weight"] = 0;
541                }
542            }
543
544            $MaxConnections = $MaxConnections + 1;
545            for ($i = $MaxConnections; $i >= 0; $i--) {
546                foreach ($this->Data as $Key => $Settings) {
547                    if ($Settings["Type"] == NODE_TYPE_CENTRAL) {
548                        $this->Data[$Key]["X"] = $CenterX;
549                        $this->Data[$Key]["Y"] = $CenterY;
550                    }
551                    if ($Settings["Type"] == NODE_TYPE_FREE) {
552                        if (isset($Settings["Connections"])) {
553                            $Connections = count($Settings["Connections"]);
554                        } else {
555                            $Connections = 0;
556                        }
557
558                        if ($Connections == $i) {
559                            $BiggestPartner = $this->getBiggestPartner($Key);
560                            if ($BiggestPartner != "") {
561                                $Ring = $this->Data[$BiggestPartner]["FreeZone"];
562                                $Weight = $this->Data[$BiggestPartner]["Weight"];
563                                $AngleDivision = 360 / $this->Data[$BiggestPartner]["Weight"];
564                                $Done = false;
565                                $Tries = 0;
566                                while (!$Done && $Tries <= $Weight * 2) {
567                                    $Tries++;
568                                    $Angle = floor(rand(0, $Weight) * $AngleDivision);
569                                    if (!isset($this->Data[$BiggestPartner]["Angular"][$Angle])
570                                        || !isset($this->Data[$BiggestPartner]["Angular"])
571                                    ) {
572                                        $this->Data[$BiggestPartner]["Angular"][$Angle] = $Angle;
573                                        $Done = true;
574                                    }
575                                }
576                                if (!$Done) {
577                                    $Angle = rand(0, 360);
578                                    $this->Data[$BiggestPartner]["Angular"][$Angle] = $Angle;
579                                }
580
581                                $X = cos(deg2rad($Angle)) * ($Ring) + $this->Data[$BiggestPartner]["X"];
582                                $Y = sin(deg2rad($Angle)) * ($Ring) + $this->Data[$BiggestPartner]["Y"];
583
584                                $this->Data[$Key]["X"] = $X;
585                                $this->Data[$Key]["Y"] = $Y;
586                            }
587                        }
588                    }
589                }
590            }
591        } elseif ($Algorithm == ALGORITHM_CIRCULAR) {
592            $MaxConnections = $MaxConnections + 1;
593            for ($i = $MaxConnections; $i >= 0; $i--) {
594                foreach ($this->Data as $Key => $Settings) {
595                    if ($Settings["Type"] == NODE_TYPE_CENTRAL) {
596                        $this->Data[$Key]["X"] = $CenterX;
597                        $this->Data[$Key]["Y"] = $CenterY;
598                    }
599                    if ($Settings["Type"] == NODE_TYPE_FREE) {
600                        if (isset($Settings["Connections"])) {
601                            $Connections = count($Settings["Connections"]);
602                        } else {
603                            $Connections = 0;
604                        }
605
606                        if ($Connections == $i) {
607                            $Ring = $MaxConnections - $Connections;
608                            $Angle = rand(0, 360);
609
610                            $X = cos(deg2rad($Angle)) * ($Ring * $this->RingSize) + $CenterX;
611                            $Y = sin(deg2rad($Angle)) * ($Ring * $this->RingSize) + $CenterY;
612
613                            $MedianOffset = $this->getMedianOffset($Key, $X, $Y);
614
615                            $this->Data[$Key]["X"] = $MedianOffset["X"];
616                            $this->Data[$Key]["Y"] = $MedianOffset["Y"];
617                        }
618                    }
619                }
620            }
621        } elseif ($Algorithm == ALGORITHM_RANDOM) {
622            foreach ($this->Data as $Key => $Settings) {
623                if ($Settings["Type"] == NODE_TYPE_FREE) {
624                    $this->Data[$Key]["X"] = $CenterX + rand(-20, 20);
625                    $this->Data[$Key]["Y"] = $CenterY + rand(-20, 20);
626                }
627                if ($Settings["Type"] == NODE_TYPE_CENTRAL) {
628                    $this->Data[$Key]["X"] = $CenterX;
629                    $this->Data[$Key]["Y"] = $CenterY;
630                }
631            }
632        }
633    }
634
635    /**
636     * Compute one pass
637     */
638    public function doPass()
639    {
640        /* Compute vectors */
641        foreach ($this->Data as $Key => $Settings) {
642            if ($Settings["Type"] != NODE_TYPE_CENTRAL) {
643                unset($this->Data[$Key]["Vectors"]);
644
645                $X1 = $Settings["X"];
646                $Y1 = $Settings["Y"];
647
648                /* Repulsion vectors */
649                foreach ($this->Data as $Key2 => $Settings2) {
650                    if ($Key != $Key2) {
651                        $X2 = $this->Data[$Key2]["X"];
652                        $Y2 = $this->Data[$Key2]["Y"];
653                        $FreeZone = $this->Data[$Key2]["FreeZone"];
654
655                        $Distance = $this->getDistance($X1, $Y1, $X2, $Y2);
656                        $Angle = $this->getAngle($X1, $Y1, $X2, $Y2) + 180;
657
658                        /* Nodes too close, repulsion occurs */
659                        if ($Distance < $FreeZone) {
660                            $Force = log(pow(2, $FreeZone - $Distance));
661                            if ($Force > 1) {
662                                $this->Data[$Key]["Vectors"][] = [
663                                    "Type"  => "R",
664                                    "Angle" => $Angle % 360,
665                                    "Force" => $Force
666                                ];
667                            }
668                        }
669                    }
670                }
671
672                /* Attraction vectors */
673                if (isset($Settings["Connections"])) {
674                    foreach ($Settings["Connections"] as $ID => $NodeID) {
675                        if (isset($this->Data[$NodeID])) {
676                            $X2 = $this->Data[$NodeID]["X"];
677                            $Y2 = $this->Data[$NodeID]["Y"];
678                            $FreeZone = $this->Data[$Key2]["FreeZone"];
679
680                            $Distance = $this->getDistance($X1, $Y1, $X2, $Y2);
681                            $Angle = $this->getAngle($X1, $Y1, $X2, $Y2);
682
683                            if ($Distance > $FreeZone) {
684                                $Force = log(($Distance - $FreeZone) + 1);
685                            } else {
686                                $Force = log(($FreeZone - $Distance) + 1);
687                                ($Angle = $Angle + 180);
688                            }
689
690                            if ($Force > 1) {
691                                $this->Data[$Key]["Vectors"][] = [
692                                    "Type"  => "A",
693                                    "Angle" => $Angle % 360,
694                                    "Force" => $Force
695                                ];
696                            }
697                        }
698                    }
699                }
700            }
701        }
702
703        /* Move the nodes accoding to the vectors */
704        foreach ($this->Data as $Key => $Settings) {
705            $X = $Settings["X"];
706            $Y = $Settings["Y"];
707
708            if (isset($Settings["Vectors"]) && $Settings["Type"] != NODE_TYPE_CENTRAL) {
709                foreach ($Settings["Vectors"] as $ID => $Vector) {
710                    $Type = $Vector["Type"];
711                    $Force = $Vector["Force"];
712                    $Angle = $Vector["Angle"];
713                    $Factor = $Type == "A" ? $this->MagneticForceA : $this->MagneticForceR;
714
715                    $X = cos(deg2rad($Angle)) * $Force * $Factor + $X;
716                    $Y = sin(deg2rad($Angle)) * $Force * $Factor + $Y;
717                }
718            }
719
720            $this->Data[$Key]["X"] = $X;
721            $this->Data[$Key]["Y"] = $Y;
722        }
723    }
724
725    /**
726     * @return int|float
727     */
728    public function lastPass()
729    {
730        /* Put everything inside the graph area */
731        foreach ($this->Data as $Key => $Settings) {
732            $X = $Settings["X"];
733            $Y = $Settings["Y"];
734
735            if ($X < $this->X1) {
736                $X = $this->X1;
737            }
738            if ($X > $this->X2) {
739                $X = $this->X2;
740            }
741            if ($Y < $this->Y1) {
742                $Y = $this->Y1;
743            }
744            if ($Y > $this->Y2) {
745                $Y = $this->Y2;
746            }
747
748            $this->Data[$Key]["X"] = $X;
749            $this->Data[$Key]["Y"] = $Y;
750        }
751
752        /* Dump all links */
753        $Links = [];
754        foreach ($this->Data as $Key => $Settings) {
755            $X1 = $Settings["X"];
756            $Y1 = $Settings["Y"];
757
758            if (isset($Settings["Connections"])) {
759                foreach ($Settings["Connections"] as $ID => $NodeID) {
760                    if (isset($this->Data[$NodeID])) {
761                        $X2 = $this->Data[$NodeID]["X"];
762                        $Y2 = $this->Data[$NodeID]["Y"];
763
764                        $Links[] = [
765                            "X1" => $X1,
766                            "Y1" => $Y1,
767                            "X2" => $X2,
768                            "Y2" => $Y2,
769                            "Source" => $Settings["Name"],
770                            "Destination" => $this->Data[$NodeID]["Name"]
771                        ];
772                    }
773                }
774            }
775        }
776
777        /* Check collisions */
778        $Conflicts = 0;
779        foreach ($this->Data as $Key => $Settings) {
780            $X1 = $Settings["X"];
781            $Y1 = $Settings["Y"];
782
783            if (isset($Settings["Connections"])) {
784                foreach ($Settings["Connections"] as $ID => $NodeID) {
785                    if (isset($this->Data[$NodeID])) {
786                        $X2 = $this->Data[$NodeID]["X"];
787                        $Y2 = $this->Data[$NodeID]["Y"];
788
789                        foreach ($Links as $IDLinks => $Link) {
790                            $X3 = $Link["X1"];
791                            $Y3 = $Link["Y1"];
792                            $X4 = $Link["X2"];
793                            $Y4 = $Link["Y2"];
794
795                            if (!($X1 == $X3 && $X2 == $X4 && $Y1 == $Y3 && $Y2 == $Y4)) {
796                                if ($this->intersect($X1, $Y1, $X2, $Y2, $X3, $Y3, $X4, $Y4)) {
797                                    if ($Link["Source"] != $Settings["Name"]
798                                        && $Link["Source"] != $this->Data[$NodeID]["Name"]
799                                        && $Link["Destination"] != $Settings["Name"]
800                                        && $Link["Destination"] != $this->Data[$NodeID]["Name"]
801                                    ) {
802                                        $Conflicts++;
803                                    }
804                                }
805                            }
806                        }
807                    }
808                }
809            }
810        }
811        return $Conflicts / 2;
812    }
813
814    /**
815     * Center the graph
816     */
817    public function center()
818    {
819        /* Determine the real center */
820        $TargetCenterX = ($this->X2 - $this->X1) / 2 + $this->X1;
821        $TargetCenterY = ($this->Y2 - $this->Y1) / 2 + $this->Y1;
822
823        /* Get current boundaries */
824        $XMin = $this->X2;
825        $XMax = $this->X1;
826        $YMin = $this->Y2;
827        $YMax = $this->Y1;
828        foreach ($this->Data as $Key => $Settings) {
829            $X = $Settings["X"];
830            $Y = $Settings["Y"];
831
832            if ($X < $XMin) {
833                $XMin = $X;
834            }
835            if ($X > $XMax) {
836                $XMax = $X;
837            }
838            if ($Y < $YMin) {
839                $YMin = $Y;
840            }
841            if ($Y > $YMax) {
842                $YMax = $Y;
843            }
844        }
845        $CurrentCenterX = ($XMax - $XMin) / 2 + $XMin;
846        $CurrentCenterY = ($YMax - $YMin) / 2 + $YMin;
847
848        /* Compute the offset to apply */
849        $XOffset = $TargetCenterX - $CurrentCenterX;
850        $YOffset = $TargetCenterY - $CurrentCenterY;
851
852        /* Correct the points position */
853        foreach ($this->Data as $Key => $Settings) {
854            $this->Data[$Key]["X"] = $Settings["X"] + $XOffset;
855            $this->Data[$Key]["Y"] = $Settings["Y"] + $YOffset;
856        }
857    }
858
859    /**
860     * Create the encoded string
861     * @param Image $Object
862     * @param string $Settings
863     * @return array
864     */
865    public function drawSpring(Image $Object, array $Settings = [])
866    {
867        $this->pChartObject = $Object;
868
869        $Pass = isset($Settings["Pass"]) ? $Settings["Pass"] : 50;
870        $Retries = isset($Settings["Retry"]) ? $Settings["Retry"] : 10;
871        $this->MagneticForceA = isset($Settings["MagneticForceA"]) ? $Settings["MagneticForceA"] : 1.5;
872        $this->MagneticForceR = isset($Settings["MagneticForceR"]) ? $Settings["MagneticForceR"] : 2;
873        $this->RingSize = isset($Settings["RingSize"]) ? $Settings["RingSize"] : 40;
874        $DrawVectors = isset($Settings["DrawVectors"]) ? $Settings["DrawVectors"] : false;
875        $DrawQuietZone = isset($Settings["DrawQuietZone"]) ? $Settings["DrawQuietZone"] : false;
876        $CenterGraph = isset($Settings["CenterGraph"]) ? $Settings["CenterGraph"] : true;
877        $TextPadding = isset($Settings["TextPadding"]) ? $Settings["TextPadding"] : 4;
878        $Algorithm = isset($Settings["Algorithm"]) ? $Settings["Algorithm"] : ALGORITHM_WEIGHTED;
879
880        $this->X1 = $Object->GraphAreaX1;
881        $this->Y1 = $Object->GraphAreaY1;
882        $this->X2 = $Object->GraphAreaX2;
883        $this->Y2 = $Object->GraphAreaY2;
884
885        $Conflicts = 1;
886        $Jobs = 0;
887        $this->History["MinimumConflicts"] = -1;
888        while ($Conflicts != 0 && $Jobs < $Retries) {
889            $Jobs++;
890
891            /* Compute the initial settings */
892            $this->firstPass($Algorithm);
893
894            /* Apply the vectors */
895            if ($Pass > 0) {
896                for ($i = 0; $i <= $Pass; $i++) {
897                    $this->doPass();
898                }
899            }
900
901            $Conflicts = $this->lastPass();
902            if ($this->History["MinimumConflicts"] == -1
903                || $Conflicts < $this->History["MinimumConflicts"]
904            ) {
905                $this->History["MinimumConflicts"] = $Conflicts;
906                $this->History["Result"] = $this->Data;
907            }
908        }
909
910        $Conflicts = $this->History["MinimumConflicts"];
911        $this->Data = $this->History["Result"];
912
913        if ($CenterGraph) {
914            $this->center();
915        }
916
917        /* Draw the connections */
918        $Drawn = [];
919        foreach ($this->Data as $Key => $Settings) {
920            $X = $Settings["X"];
921            $Y = $Settings["Y"];
922
923            if (isset($Settings["Connections"])) {
924                foreach ($Settings["Connections"] as $ID => $NodeID) {
925                    if (!isset($Drawn[$Key])) {
926                        $Drawn[$Key] = "";
927                    }
928                    if (!isset($Drawn[$NodeID])) {
929                        $Drawn[$NodeID] = "";
930                    }
931
932                    if (isset($this->Data[$NodeID])
933                        && !isset($Drawn[$Key][$NodeID])
934                        && !isset($Drawn[$NodeID][$Key])
935                    ) {
936                        $Color = [
937                            "R" => $this->Default["LinkR"],
938                            "G" => $this->Default["LinkG"],
939                            "B" => $this->Default["LinkB"],
940                            "Alpha" => $this->Default["Alpha"]
941                        ];
942
943                        if (count($this->Links)) {
944                            if (isset($this->Links[$Key][$NodeID]["R"])) {
945                                $Color = [
946                                    "R" => $this->Links[$Key][$NodeID]["R"],
947                                    "G" => $this->Links[$Key][$NodeID]["G"],
948                                    "B" => $this->Links[$Key][$NodeID]["B"],
949                                    "Alpha" => $this->Links[$Key][$NodeID]["Alpha"]
950                                ];
951                            }
952
953                            if (isset($this->Links[$Key][$NodeID]["Ticks"])) {
954                                $Color["Ticks"] = $this->Links[$Key][$NodeID]["Ticks"];
955                            }
956                        }
957
958                        $X2 = $this->Data[$NodeID]["X"];
959                        $Y2 = $this->Data[$NodeID]["Y"];
960                        $this->pChartObject->drawLine($X, $Y, $X2, $Y2, $Color);
961                        $Drawn[$Key][$NodeID] = true;
962
963                        if (isset($this->Links) && count($this->Links)) {
964                            if (isset($this->Links[$Key][$NodeID]["Name"])
965                                || isset($this->Links[$NodeID][$Key]["Name"])
966                            ) {
967                                $Name = isset($this->Links[$Key][$NodeID]["Name"])
968                                    ? $this->Links[$Key][$NodeID]["Name"]
969                                    : $this->Links[$NodeID][$Key]["Name"]
970                                ;
971                                $TxtX = ($X2 - $X) / 2 + $X;
972                                $TxtY = ($Y2 - $Y) / 2 + $Y;
973
974                                if ($X <= $X2) {
975                                    $Angle = (360 - $this->getAngle($X, $Y, $X2, $Y2)) % 360;
976                                } else {
977                                    $Angle = (360 - $this->getAngle($X2, $Y2, $X, $Y)) % 360;
978                                }
979                                $Settings = $Color;
980                                $Settings["Angle"] = $Angle;
981                                $Settings["Align"] = TEXT_ALIGN_BOTTOMMIDDLE;
982                                $this->pChartObject->drawText($TxtX, $TxtY, $Name, $Settings);
983                            }
984                        }
985                    }
986                }
987            }
988        }
989
990        /* Draw the quiet zones */
991        if ($DrawQuietZone) {
992            foreach ($this->Data as $Key => $Settings) {
993                $X = $Settings["X"];
994                $Y = $Settings["Y"];
995                $FreeZone = $Settings["FreeZone"];
996
997                $this->pChartObject->drawFilledCircle(
998                    $X,
999                    $Y,
1000                    $FreeZone,
1001                    ["R" => 0, "G" => 0, "B" => 0, "Alpha" => 2]
1002                );
1003            }
1004        }
1005
1006
1007        /* Draw the nodes */
1008        foreach ($this->Data as $Key => $Settings) {
1009            $X = $Settings["X"];
1010            $Y = $Settings["Y"];
1011            $Name = $Settings["Name"];
1012            $FreeZone = $Settings["FreeZone"];
1013            $Shape = $Settings["Shape"];
1014            $Size = $Settings["Size"];
1015
1016            $Color = [
1017                "R" => $Settings["R"],
1018                "G" => $Settings["G"],
1019                "B" => $Settings["B"],
1020                "Alpha" => $Settings["Alpha"],
1021                "BorderR" => $Settings["BorderR"],
1022                "BorderG" => $Settings["BorderG"],
1023                "BorderB" => $Settings["BorderB"],
1024                "BorderApha" => $Settings["BorderAlpha"]
1025            ];
1026
1027            if ($Shape == NODE_SHAPE_CIRCLE) {
1028                $this->pChartObject->drawFilledCircle($X, $Y, $Size, $Color);
1029            } elseif ($Shape == NODE_SHAPE_TRIANGLE) {
1030                $Points = [];
1031                $Points[] = cos(deg2rad(270)) * $Size + $X;
1032                $Points[] = sin(deg2rad(270)) * $Size + $Y;
1033                $Points[] = cos(deg2rad(45)) * $Size + $X;
1034                $Points[] = sin(deg2rad(45)) * $Size + $Y;
1035                $Points[] = cos(deg2rad(135)) * $Size + $X;
1036                $Points[] = sin(deg2rad(135)) * $Size + $Y;
1037                $this->pChartObject->drawPolygon($Points, $Color);
1038            } elseif ($Shape == NODE_SHAPE_SQUARE) {
1039                $Offset = $Size / 2;
1040                $Size = $Size / 2;
1041                $this->pChartObject->drawFilledRectangle(
1042                    $X - $Offset,
1043                    $Y - $Offset,
1044                    $X + $Offset,
1045                    $Y + $Offset,
1046                    $Color
1047                );
1048            }
1049
1050            if ($Name != "") {
1051                $LabelOptions = [
1052                    "R" => $this->Labels["R"],
1053                    "G" => $this->Labels["G"],
1054                    "B" => $this->Labels["B"],
1055                    "Alpha" => $this->Labels["Alpha"]
1056                ];
1057
1058                if ($this->Labels["Type"] == LABEL_LIGHT) {
1059                    $LabelOptions["Align"] = TEXT_ALIGN_BOTTOMLEFT;
1060                    $this->pChartObject->drawText($X, $Y, $Name, $LabelOptions);
1061                } elseif ($this->Labels["Type"] == LABEL_CLASSIC) {
1062                    $LabelOptions["Align"] = TEXT_ALIGN_TOPMIDDLE;
1063                    $LabelOptions["DrawBox"] = true;
1064                    $LabelOptions["BoxAlpha"] = 50;
1065                    $LabelOptions["BorderOffset"] = 4;
1066                    $LabelOptions["RoundedRadius"] = 3;
1067                    $LabelOptions["BoxRounded"] = true;
1068                    $LabelOptions["NoShadow"] = true;
1069
1070                    $this->pChartObject->drawText($X, $Y + $Size + $TextPadding, $Name, $LabelOptions);
1071                }
1072            }
1073        }
1074
1075        /* Draw the vectors */
1076        if ($DrawVectors) {
1077            foreach ($this->Data as $Key => $Settings) {
1078                $X1 = $Settings["X"];
1079                $Y1 = $Settings["Y"];
1080
1081                if (isset($Settings["Vectors"]) && $Settings["Type"] != NODE_TYPE_CENTRAL) {
1082                    foreach ($Settings["Vectors"] as $ID => $Vector) {
1083                        $Type = $Vector["Type"];
1084                        $Force = $Vector["Force"];
1085                        $Angle = $Vector["Angle"];
1086                        $Factor = $Type == "A" ? $this->MagneticForceA : $this->MagneticForceR;
1087                        $Color = $Type == "A"
1088                            ? ["FillR" => 255, "FillG" => 0, "FillB" => 0]
1089                            : ["FillR" => 0, "FillG" => 255, "FillB" => 0]
1090                        ;
1091
1092                        $X2 = cos(deg2rad($Angle)) * $Force * $Factor + $X1;
1093                        $Y2 = sin(deg2rad($Angle)) * $Force * $Factor + $Y1;
1094
1095                        $this->pChartObject->drawArrow($X1, $Y1, $X2, $Y2, $Color);
1096                    }
1097                }
1098            }
1099        }
1100
1101        return ["Pass" => $Jobs, "Conflicts" => $Conflicts];
1102    }
1103
1104    /**
1105     * Return the distance between two points
1106     * @param int $X1
1107     * @param int $Y1
1108     * @param int $X2
1109     * @param int $Y2
1110     * @return int|float
1111     */
1112    public function getDistance($X1, $Y1, $X2, $Y2)
1113    {
1114        return sqrt(($X2 - $X1) * ($X2 - $X1) + ($Y2 - $Y1) * ($Y2 - $Y1));
1115    }
1116
1117    /**
1118     * Return the angle made by a line and the X axis
1119     * @param int $X1
1120     * @param int $Y1
1121     * @param int $X2
1122     * @param int $Y2
1123     * @return int|float
1124     */
1125    public function getAngle($X1, $Y1, $X2, $Y2)
1126    {
1127        $Opposite = $Y2 - $Y1;
1128        $Adjacent = $X2 - $X1;
1129        $Angle = rad2deg(atan2($Opposite, $Adjacent));
1130
1131        return $Angle > 0 ? $Angle : 360 - abs($Angle);
1132    }
1133
1134    /**
1135     * @param int $X1
1136     * @param int $Y1
1137     * @param int $X2
1138     * @param int $Y2
1139     * @param int $X3
1140     * @param int $Y3
1141     * @param int $X4
1142     * @param int $Y4
1143     * @return boolean
1144     */
1145    public function intersect($X1, $Y1, $X2, $Y2, $X3, $Y3, $X4, $Y4)
1146    {
1147        $A = (($X3 * $Y4 - $X4 * $Y3) * ($X1 - $X2) - ($X1 * $Y2 - $X2 * $Y1) * ($X3 - $X4));
1148        $B = (($Y1 - $Y2) * ($X3 - $X4) - ($Y3 - $Y4) * ($X1 - $X2));
1149
1150        if ($B == 0) {
1151            return false;
1152        }
1153        $Xi = $A / $B;
1154
1155        $C = ($X1 - $X2);
1156        if ($C == 0) {
1157            return false;
1158        }
1159        $Yi = $Xi * (($Y1 - $Y2) / $C) + (($X1 * $Y2 - $X2 * $Y1) / $C);
1160
1161        if ($Xi >= min($X1, $X2)
1162            && $Xi >= min($X3, $X4)
1163            && $Xi <= max($X1, $X2)
1164            && $Xi <= max($X3, $X4)
1165            && $Yi >= min($Y1, $Y2)
1166            && $Yi >= min($Y3, $Y4)
1167            && $Yi <= max($Y1, $Y2)
1168            && $Yi <= max($Y3, $Y4)
1169        ) {
1170            return true;
1171        }
1172
1173        return false;
1174    }
1175}
1176