TimeTrex/classes/modules/core/DependencyTree.class.php

533 lines
13 KiB
PHP

<?php
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
/**
* @package Core
*/
class DependencyTree {
/*
Take a look at PEAR: Structures_Graph
*/
var $raw_data = null;
var $raw_data_order = [];
protected $provide_id_raw_data = [];
protected $require_id_raw_data = [];
protected $provide_ids = [];
protected $tree = null;
// set this flag to true to enable tree ordering, eg, the final output will have whole trees in contiguous array slice.
protected $tree_ordering = false; // faster without tree ordering.
/**
* @return bool
*/
function getTreeOrdering() {
return $this->tree_ordering;
}
/**
* @param $bool
*/
function setTreeOrdering( $bool ) {
$this->tree_ordering = $bool;
}
/**
* @param string $id ID of node
* @param string|int|array $requires array of IDs this node requires
* @param string|int|array $provides array of IDs this node provides
* @param int $order integer to help resolve circular dependencies, lower order comes first.
* @return bool
*/
function addNode( $id, $requires, $provides, $order = 0 ) {
if ( $id == '' ) {
return false;
}
if ( isset( $this->raw_data[$id] ) ) {
//ID already exists.
return false;
}
$dtn = new DependencyTreeNode();
$dtn->setId( $id );
$dtn->setRequires( $requires );
$dtn->setProvides( $provides );
$dtn->setOrder( $order );
$dtn->removeCircularDependency();
$this->addProvideIDs( $dtn->getProvides() );
$this->addObjectByProvideIDs( $dtn->getProvides(), $dtn );
$this->addObjectByRequireIDs( $dtn->getRequires(), $dtn );
$this->raw_data[$id] = $dtn;
if ( $this->tree_ordering ) {
array_push( $this->raw_data_order, $dtn );
}
unset( $dtn );
return true;
}
/**
* @param string $provide_ids UUID
* @param object $obj
* @return bool
*/
private function addObjectByProvideIDs( $provide_ids, $obj ) {
if ( is_array( $provide_ids ) ) {
foreach ( $provide_ids as $provide_id ) {
$this->provide_id_raw_data[$provide_id][] = $obj;
}
}
return true;
}
/**
* @param string $requires_ids UUID
* @param object $obj
* @return bool
*/
private function addObjectByRequireIDs( $requires_ids, $obj ) {
if ( is_array( $requires_ids ) ) {
foreach ( $requires_ids as $require_id ) {
$this->require_id_raw_data[$require_id][] = $obj;
}
}
return true;
}
/**
* @return array|bool|null
*/
private function getProvideIDs() {
if ( isset( $this->provide_ids ) ) {
return $this->provide_ids;
}
return false;
}
/**
* @param $provide_arr
* @return bool
*/
private function addProvideIDs( $provide_arr ) {
if ( is_array( $provide_arr ) ) {
$this->provide_ids = array_merge( $this->provide_ids, $provide_arr );
//foreach ( $provide_arr as $provide_id ) {
// $this->provide_ids[] = $provide_id;
//}
$this->provide_ids = array_unique( $this->provide_ids );
}
return true;
}
/**
* @return bool
*/
private function deleteOrphanRequireIDs() {
if ( is_array( $this->raw_data ) ) {
foreach ( $this->raw_data as $obj ) {
if ( is_array( $obj->getRequires() ) ) {
$valid_require_ids = array_intersect( $obj->getRequires(), (array)$this->getProvideIDs() );
//$valid_require_ids = [];
//foreach ( $obj->getRequires() as $require_id ) {
// if ( in_array( $require_id, (array)$this->getProvideIDs() ) ) {
// $valid_require_ids[] = $require_id;
// }
//}
$obj->setRequires( $valid_require_ids );
}
}
}
//Debug::Arr($this->raw_data, 'With Valid Require Ids', __FILE__, __LINE__, __METHOD__, 10);
return true;
}
/**
* 02-Nov-2006: changing the sort functionality to depth-based
* @param $a
* @param $b
* @return int
*/
private function sort( $a, $b ) {
//Debug::Arr($a, 'A: ', __FILE__, __LINE__, __METHOD__, 10);
//Debug::Arr($b, 'B: ', __FILE__, __LINE__, __METHOD__, 10);
// first compare if nodes are in the same tree
if ( $this->tree_ordering ) {
if ( $a->getTreeNumber() < $b->getTreeNumber() ) {
return -1;
} else if ( $a->getTreeNumber() > $b->getTreeNumber() ) {
return 1;
}
}
// sort by depth first
if ( $a->getDepth() < $b->getDepth() ) {
return -1;
}
if ( $a->getDepth() > $b->getDepth() ) {
return 1;
}
// if depth is the same, then they are either: different graphs, same graph but in a circular reference loop (or just another branch.)
// sort by order, if ==, then sort by id.
$order_cmp = strnatcasecmp( $a->getOrder(), $b->getOrder() );
if ( $order_cmp !== 0 ) {
return $order_cmp;
}
// nothing left, sort by id, but use strnatcasecmp to handle UUIDs.
return strnatcasecmp( $a->getId(), $b->getId() );
// should probably never reach here, but if the ids are the same, they might as well be equal.
//return 0;
}
/**
* Traverse a tree starting with a node.
* @param $node
* @param $tree_number
* @param array $marked_edges
*/
function markTreeNumber( $node, $tree_number, $marked_edges = [] ) {
// mark the node. but should we check to see if it was marked under another tree number?
if ( $node->getTreeNumber() !== null ) {
return;
}
$node->setTreeNumber( $tree_number );
// first look to see if any other node gives what this node requires
if ( is_array( $node->getRequires() ) ) {
foreach ( $node->getRequires() as $require_id ) {
if ( isset( $this->provide_id_raw_data[$require_id] ) ) {
foreach ( $this->provide_id_raw_data[$require_id] as $obj ) { // (we already know obj provides this req id...)
if ( $node->getId() != $obj->getId() ) {
if ( !isset( $marked_edges[$node->getId()][$obj->getId()] ) ) {
$marked_edges[$node->getId()][$obj->getId()] = true;
$marked_edges[$obj->getId()][$node->getId()] = true;
$this->markTreeNumber( $obj, $tree_number, $marked_edges );
}
}
}
}
}
}
// now vice versa
if ( is_array( $node->getProvides() ) ) {
foreach ( $node->getProvides() as $provide_id ) {
if ( isset( $this->require_id_raw_data[$provide_id] ) ) {
foreach ( $this->require_id_raw_data[$provide_id] as $obj ) { // (we already know obj provides this req id...)
if ( $node->getId() != $obj->getId() ) {
if ( !isset( $marked_edges[$node->getId()][$obj->getId()] ) ) {
$marked_edges[$node->getId()][$obj->getId()] = true;
$marked_edges[$obj->getId()][$node->getId()] = true;
$this->markTreeNumber( $obj, $tree_number, $marked_edges );
}
}
}
}
}
}
// we're done if after all the recursion we end up here.
}
/**
* Get an object's depth by traversing all its parents (recursively) until there are no edges left. The count of edges is the 'depth'.
* @param object $obj
* @param array $marked_edges
* @param int $depth
* @return int
*/
function _findDepth( $obj, &$marked_edges = [], $depth = 0 ) {
if ( is_array( $obj->getRequires() ) ) {
foreach ( $obj->getRequires() as $req_id ) {
if ( isset( $this->provide_id_raw_data[$req_id] ) ) {
foreach ( $this->provide_id_raw_data[$req_id] as $node ) { // (we already know obj provides this req id...)
if ( !isset( $marked_edges[$node->getId()][$obj->getId()] ) ) {
$marked_edges[$node->getId()][$obj->getId()] = true;
$this->_findDepth( $node, $marked_edges, ( $depth + 1 ) );
}
}
}
}
}
if ( $depth == 0 ) {
return count( $marked_edges );
}
return false;
}
/**
* @return array|bool
*/
function _buildTree() {
if ( !is_array( $this->raw_data ) ) {
return false;
}
$this->deleteOrphanRequireIDs();
if ( $this->tree_ordering ) {
// now number the trees so that the algorithm knows how to sort them properly
// eg the list of nodes might have 5 in one tree, and another unconnected tree with 3 nodes.
// this needs to be handled properly.
$treenumber = 0;
foreach ( $this->raw_data_order as $obj ) {
if ( $obj->getTreeNumber() === null ) {
$this->markTreeNumber( $obj, $treenumber++ );
}
}
}
//Debug::Arr($this, 'Before - Raw Data: ', __FILE__, __LINE__, __METHOD__, 10);
// mark all depths first.
foreach ( $this->raw_data as $obj ) {
$obj->setDepth( $this->_findDepth( $obj ) );
}
usort( $this->raw_data, [ $this, 'sort' ] );
//Debug::Arr($this->provide_id_raw_data, 'provides, raw', __FILE__, __LINE__, __METHOD__, 10);
//Debug::Arr($this, 'After - Raw Data: ', __FILE__, __LINE__, __METHOD__, 10);
$retarr = [];
foreach ( $this->raw_data as $obj ) {
$retarr[] = $obj->getId();
}
//Debug::Arr($retarr, 'Dependency Tree Final Result!!', __FILE__, __LINE__, __METHOD__, 10);
return $retarr;
}
/**
* @return array
*/
function getAllNodesInOrder() {
return $this->_buildTree();
}
}
/**
* @package Core
*/
class DependencyTreeNode {
protected $data;
/**
* @param string $id UUID
* @return bool
*/
function setId( $id ) {
if ( $id != '' ) {
$this->data['id'] = $id;
}
return false;
}
/**
* @return bool
*/
function getId() {
if ( isset( $this->data['id'] ) ) {
return $this->data['id'];
}
return false;
}
/**
* @param $arg
* @return bool
*/
function setDepth( $arg ) {
$this->data['depth'] = (int)$arg;
return true;
}
/**
* @return null
*/
function getDepth() {
if ( isset( $this->data['depth'] ) ) {
return $this->data['depth'];
}
return null;
}
/**
* @param $arr
* @return bool
*/
function setRequires( $arr ) {
if ( $arr != '' ) {
if ( !is_array( $arr ) ) {
$arr = [ $arr ];
}
$this->data['requires'] = array_unique( $arr );
}
return false;
}
/**
* @return bool|array
*/
function getRequires() {
if ( isset( $this->data['requires'] ) ) {
return $this->data['requires'];
}
return false;
}
/**
* @param $arr
* @return bool
*/
function setProvides( $arr ) {
if ( $arr != '' ) {
if ( !is_array( $arr ) ) {
$arr = [ $arr ];
}
$this->data['provides'] = array_unique( $arr );
}
return false;
}
/**
* Removes circular dependencies within the same node. Must be run after both requires and provides are defined.
* @return bool
*/
function removeCircularDependency() {
//Check to see if any item in $requires also appears in $provides, if so strip them out as it creates a circular dependency within the same node.
if ( is_array( $this->getRequires() ) && is_array( $this->getProvides() ) ) {
$this->data['requires'] = array_diff( $this->getRequires(), $this->getProvides() );
}
return true;
}
/**
* @return bool|array
*/
function getProvides() {
if ( isset( $this->data['provides'] ) ) {
return $this->data['provides'];
}
return false;
}
/**
* @param $treenumber
* @return bool
*/
function setTreeNumber( $treenumber ) {
$this->data['treenumber'] = (int)$treenumber;
return true;
}
/**
* @return null
*/
function getTreeNumber() {
if ( isset( $this->data['treenumber'] ) ) {
return $this->data['treenumber'];
}
return null;
}
/**
* @param $order
* @return bool
*/
function setOrder( $order ) {
$this->data['order'] = $order; //Allow int/strings.
return true;
}
/**
* @return int
*/
function getOrder() {
if ( isset( $this->data['order'] ) ) {
return $this->data['order'];
}
return 0;
}
}
?>