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