Creating a hierarchy from simple strings (PHP) - php

I have some strings in a DB: x, y, z, x1, x2, x12, x22, x23, y1, y2, y3, z1, z2 (don't take them literally).
From those strings, and having some rules of classification (strings x1, x2 and x3 "belong" to string x), I want to build some kind of OOP-ish that incorporates those rules of classification.
x
x1
x12
x2
x22
x23
y
y1
y2
y3
z
z1
z2
Basically, I want to make a menu from plain strings, knowing their relation.
My question is: what is the best method to store their relation to each other, so that I can pin-point their exact "location" within this hierarchy? Is there a design pattern that suits this problem? I though of entities having some property "depth" or "type".

You could store menu items within a table having columns: id, title, parent_id
In PHP, you would then be able to have objects:
class MenuItem {
$id = 0;
$parent_id = 0;
$title = "";
$children = array();
}
You'll store children in the array using their ID as a key:
function add_child($menu_item) {
$this->children[$menu_item->id] = $menu_item;
}
When loading from the DB, you'll build your menu by inserting menu items in it. Simply have an insert function within the MenuItem object:
function insert($menu_item) {
if ($menu_item->parent_id==$this->id) {
$this->add_child($menu_item);
return true;
}
foreach ($children as $child) {
if ($child->insert($menu_item))
return true;
}
// return false if we could not insert it in any of our children
return false;
}

0:(x,null)
1:(x1,0)
2:(x12,1)
3:(x2,0)
4:(x22,3)
5:(x23,3)
[...]
'5:(x23,3)' means node 5 is a child of node 3 and contains 'x23'.
'0:(x,null)' means that node 0 is a top-node with no parent node.
this data-structure could be modeled by an numerical array that contains an array with two elements.
you have to parse that tree recursively.

One way to do this is by adding a parent_id column in the table. For each record, there will be a parent with value 0 or the primary id of some other record. Once you have this, you can write a recursive function to parse the tree.

you can organize these values in a table like a nested set tree (http://www.edutech.ch/contribution/nstrees/index.php), and then is fairly easy to find all children, or a parent

Related

Changing the position of nodes with CakePHP 2.x Tree and jstree

I have an application using CakePHP 2.x and jstree 3.2.1. I'm trying to figure out how it's possible to specify the position of a node when writing it to the database with CakePHP. The position itself comes from jstree...
When I drag and drop items with jstree the request URL gives me:
id - The ID of the node being dragged.
parent_id - The (parent) ID which the node has been dropped under.
position - this is an integer which starts at 0 and represents the position at which id has been dropped under parent_id. For example, a position of 2 means it should appear in 3rd position (3rd, not 2nd, because they start at 0).
CakePHP has methods in it's Tree Behaviour that allow you to move nodes in its Tree up and down. These methods are called moveUp() and moveDown() respectively.
I don't understand how it's possible to use the data provided from jstree with Cake's Tree behaviour such that you could update/save positions correctly.
Passing position to moveUp() or moveDown() would produce the wrong result. Why?
If jstree provides position = 2 and you were moving the 10th item in a list calling moveUp(2) through CakePHP means it would go into the 8th position, not the 2nd as intended. Similarly, moveDown(2) would move it to the 12th, which is not the desired outcome.
The schema that Cake has for it's Tree Behavior does not have a "position" field. Rather it uses lft and rght fields. The schema has:
id - ID of each individual tree node (auto increment)
parent_id - parent ID of the node. NULL if top level (no parent).
lft, rght - for MPTT logic. Cake generates these values automatically. They can be used to determine the order using ORDER BY lft ASC. But they are not the same values as position provided by jstree, and are unique for every row
name - text name of the node.
For example, consider the following tree:
D (id = 149)
1 (id = 150, parent_id = 149)
2 (id = 153, parent_id = 149)
3 (id = 154, parent_id = 149)
4 (id = 155, parent_id = 149)
5 (id = 156, parent_id = 149)
In the database Cake stores this as follows:
If I was to use jstree to drag and drop "2" so it appears between "3" and "4" it would make a request containing the following GET variables:
id = 153. This is the ID of "2"
parent_id = 149. This is the ID of "D" which is the parent node of "2".
position = 2. This means the 3rd position (3rd because positions start from 0).
But I cannot regenerate the lft and rght values from this data? And moveUp()/moveDown() are not helpful here because position cannot be passed in a way that would make this work.
The tree should be in the following order. id and parent_id should not change, but lft and rght must, because everything under "D" has effectively been re-ordered:
D
1
3
2 (moved)
4
5
Can anyone help with this?
I never used jsTree, but if it gives you the new parent ID and the sort position, then you should be able to use a combination of saving the new parent:
$Model->save(array('parent_id' => $parentId));
afterwards obtaining its child list, which will include the new child:
$children = $Model->children($parentId, true, array('id'));
and using the sort position to determine the delta to move the modified child:
$childIds = Hash::extract($children, '{n}.Model.id');
$positionMap = array_flip($childIds);
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$Model->moveUp($nodeId, abs($delta));
} else {
$Model->moveDown($nodeId, abs($delta));
}
}
This is just some rough example that should illustrate the idea of how this could work, it assumes $parentId to be the ID of the new parent, $nodeId to be the ID of the row being moved, and $position the (zero based) position the child was moved to. You'd have to account for other situations too, like for example when the parent ID doesn't change, ie only the sort position changes, and this should all be done in a transaction!
Here's a complete snippet based on the Cookbook example, it will move the Gwendolyn node to the second position in the Work node:
$nodeId = 8;
$parentId = 9;
$position = 1;
$dataSource = $this->Category->getDataSource();
$dataSource->begin();
$this->Category->id = $nodeId;
if (!$this->Category->save(array('parent_id' => $parentId))) {
$dataSource->rollback();
} else {
$children = $this->Category->children($parentId, true, array('id'));
$childIds = Hash::extract($children, '{n}.Category.id');
$positionMap = array_flip($childIds);
$result = true;
if (isset($positionMap[$nodeId])) {
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$result = $this->Category->moveUp($nodeId, abs($delta));
} else {
$result = $this->Category->moveDown($nodeId, abs($delta));
}
}
}
if ($result) {
$dataSource->commit();
} else {
$dataSource->rollback();
}
}

Recursive tree traversal with PHP - layout issue

I am creating a questionnaire for a client that requires the questions to be organized by 3 layers of levels, with a unknown amount of questions and categories. I've successfully created the U.I. and the code to pull the information from the database however I've been trying to find out how to get everything loading in the right place. The database is organized like so by the client so I have no control over it:
id description parentId
1 Level 1 0
2 Level 2 0
3 Level 1a 1
4 Level 1b 1
5 Level 1a1 3
and the code I'm using right now is this:
function printChildQuestions($parentid) {
$sql="SELECT * FROM pB_test WHERE parentID=$parentid";
$result=mysql_query($sql);
$i=0;
while (true) {
$row=mysql_fetch_array($result);
if (!$row) break;
if ($row['parentId'] == 0) {
echo '<div class="tabs">';
echo '<span class="level1">'.$row['description'].'</span>';
echo '<ul id="main-nav"><h2 align="center">Categories</h2></ul>';
echo '<div>'.$row['id'].' '.$row['description'].' '.$row['parentId'].'</div>';
if ($row['parentId'] == 1) {
}
echo '</div>';
} else {
echo '<div>'.$row['id'].' '.$row['description'].' '.$row['parentId'].'</div>';
}
printChildQuestions($row['id']);
}
}
printChildQuestions(0);
and the html that needs to be generated for is here: http://jsfiddle.net/Cyb2N/
The issue is every idea I've come up with needs me to hardcode the levels in and breaks when level 2 is introduced. Could someone shove me in the right direction? Thanks!
If I understand your question correctly, you're looking for a way to resolve the levels implied by the parentId references in the client's data? Like so:
Level 1
Level 1a
Level 1a1
Level 1b
Level 2
How about creating a Level class with a parent property which references the parent Level? Then you can iterate through the client's data, resolving the Level instances into a nice tree, and do anything you want with it from there.
Alternatively, you could give the Level class a children property, which would be a collection of the levels that reference that instance as a parent... anyway the point is that you can transform your client's flat data into a useful tree model with a fairly simple class. By counting parents (or children), you can tell how far up/down the tree a given Level is.
printChildQuestions is a recursive function. It sounds like you don't know how to determine which level of recursion you're at. Here's how:
printChildQuestions($parent_id);
function printChildQuestions($parent_id, $level = 1){
if( $need_to_recurse ){
printChildQuestions($new_parent, $level + 1);
}
}

Create a Dynamic Nav on the Fly

I'm looking for the best way to create a complex navigation element on the fly. I have all of the elements in a database (title, id, parentId) and I want to efficiently take them out of the DB and display them correctly. I also want to collapse all of the navigation elements that aren't active. So if I was browsing through "Sofas" I wouldn't see "Chandeliers" or any of the categories under lighting but I would see "Lighting".
This is what I want the final product to look like:
Furniture
Living Room
Sofas
Chairs
Ottomans
Bedroom
Beds
Nightstands
Lighting
Chandeliers
Floor Lamps
Sconces
Rugs & Textiles
Contemporary
Vintage
My current method is
write one SQL query that pulls down all of the category names, ids, and parent ids
Iterate through the categories and put into a sorted multi-dimensional array with child categories stored under their parents.
Iterate through the new array and add another entry to mark the appropriate categories as open (all categories are closed by default)
iterate through the array and write HTML
I'm trying to to this with as few interations as possible and I'm sure the code I have right now is inefficient. Especially step 2 I iterate through the array several times. There has to be a general solution to this (common?) problem.
Consider adding a new field to your database table: level.
Main-categories will have level 0.
Sub-categories will have level 1.
Sub-sub-categories will have level 2.
etc.
This trick will help you to know which sub-categories to disable without 2nd iteration of the array.
I believe this is the perfect place to generate your html code using recursion.
I used this function a while ago. It is working with a multi-dimensional array (tree)
function buildMenu($menu_array, $is_sub=FALSE) {
$attr = (!$is_sub) ? 'id="menu"' : 'class="submenu"';
$menu = "<ul $attr>\n";
foreach($menu_array as $id => $elements) {
foreach($elements as $key => $val) {
if(is_array($val)) {
$sub = buildMenu($val, TRUE);
}
else {
$sub = NULL;
$$key = $val;
}
}
if(!isset($url)) {
$url = $id;
}
$menu .= "<li>$display$sub</li>\n";
unset($url, $display, $sub);
}
return $menu . "</ul>\n";
}
echo buildMenu($menu_array);
This adds css properties too. If you wish to mark the currently active page you can use the strpos() function to find your current url. If you need some more functionality you can easily add them to buildMenu()
Using level as mentioned in the answer above will help too. If you were using the nested set model in your database I could also help you with my query which is a single select returning the whole menu data.

Recursing Properly Through Related Entities

I have a set of Organizations and their Board Members.
All organizations have board members and many board members are on the board of more than one organization.
I am using JIT Hypertree to illustrate their relationships. The JIT Hypertree schema requires that one item be the parent of all and is drawn based on a single JSON array.
I would love to have the re-centering event query and re-populate the graph based on the change. Then 2 levels would be fine but I have not been able to work out how to do that.
The code I have at present recurses manually for three levels from the starting organization but what I want is to re-curse through all related records.
So it would start with an Org and add Org's array of children (board members). Then fetch all of the boards (other than current Org) for each board member and add those as children of the board member.
This would continue until each trail dead ends - presumably at a board member who only belongs to one board.
Anyone have advice on how to create this array and avoid duplicates?
$board = $center->board();
$top['id'] = $center->ID;
$top['name'] = $center->Org;
$top['children'] = array();
if ($board) {
foreach ($board as $b) {
$child['id'] = $b->ID;
$child['name'] = (strlen(trim($b->Last)) > 0) ? $b->First . ' ' . $b->Last : 'Unknown';
$child['data']['orgname'] = $center->Org;
$child['data']['relation'] = $b->Role;
$child['data']['occupation'] = $b->Occupation;
$child['children'] = array();
$childboards = $b->boards();
if ($childboards) { foreach ($childboards as $cb) {
$gchild['id'] = $cb->ID;
$gchild['name'] = $cb->Org;
$gchild['data']['orgname'] = (strlen(trim($b->Last)) > 0) ? $b->First . ' ' . $b->Last : 'Unknown';
$gchild['children'] = array();
$childboardmembers = $cb->board();
if ($childboardmembers) { foreach ($childboardmembers as $cbm) {
$ggchild['id'] = $cbm->ID;
$ggchild['name'] = (strlen(trim($cbm->Last)) > 0) ? $cbm->First . ' ' . $cbm->Last : 'Unknown';
$ggchild['data']['orgname'] = $cb->Org;
$ggchild['data']['relation'] = $cbm->Role;
$ggchild['data']['occupation'] = $cbm->Occupation;
$ggchild['children'] = array();
$gchild['children'][]= $ggchild;
}}
$child['children'][]= $gchild;
}}
$top['children'][] = $child;
}
}
$top['data'] = array();
$top['data']['description'] = $center->Desc;
echo json_encode($top);
// Edit 2011.10.24 In Re hakre response
My data structure is a table of Organizations with unique IDs, a table of People with Unique IDs, and then a bridging table for the two specifying Organization (Entity) and Person and the Role the Person is playing in the Entity. A typical many-to-many. No sub-boards at all. I made an image of it which now seems kind of pointless but I'll add it at the bottom.
The JIT library data structure is a little nuts (to me) in that it goes like this in their band example:
Top: Nine Inch Nails
Child: Jerome Dillon
Child: Howlin Maggie (another band)
{all the bands' members and then all of their bands...}
So the organization (band) is treated as though it is a Person even though it is comprised of a number of Persons. And when I recurse using the code above I get (I think) terrible bloat but the JSON it makes works correctly despite bloat.
Example JSON and Example Visualization
// End Edit
Your question is hard to answer in the sense that your data-structure is mainly unknown.
For the graphical represenation you only need to provide simple relationships if I understand that correctly:
*- Parent
+- Child
+- Child
...
`- Child
Your data structure has a different format, I don't know specifically but it's something like:
Org <-- 1:n --> Board
Board <-- n:n --> Board # If you have sub-boards
Board <-- n:n --> Member
Whichever your data is represented, to map or transpose your data onto the required structure for the graphical representation, you need some functions that take care of that.
To do that you need to share classification/type between both and specific keys, so that you can look-up the needed data from the event to return the data. For example:
if (request_parent_is_org())
{
$id = request_parent_id();
$parent = data_get_board($id);
$parent->addChildrent(data_get_board_children($id));
}
else
{
... # All the other decisions you need to take based on request type
}
view_response_to_json($parent);
What you have with your many-to-many data model is a graph. JIT is designed for trees.
To put it another way, JIT will not correctly show the crossing lines that are represented in the data whenever a single person is connected to multiple organizations.
I'd recommend a proper network graph visualization - D3.js has a great implementation for modern browsers.
The JSON data format it uses is actually easier to implement given your table structure - for all the organizations and people, you define objects:
{
"name": "Mme.Hucheloup",
"group": 1
},
{
"name": "Afton School Board",
"group": 2
}
And for each association in your association table you define connection objects that wire them together:
{
"source": 1,
"target": 2
},
The fancy coding in D3 takes care of the rest. Good luck!
You can use function below:
function get_orgs_and_childs ($child_id, $found = array())
{
array_push ($found, $child['name']);
if($child['children'])){
$found[] = get_parents($child['id'], $found);
}
return $found;
}
Call it using:
$orgs = get_orgs_and_childs($child['id']);

How to store big binary tree

How to store big binary trees (about thousands in depth).
We tried to store in database, in table with rows: elem, parent, left_child, right_child
but it's very slow to handle this tree (to calculate, draw a part of it).
Which way you recommend? (calculating may be done in php). May be to store it in XML or json (text file) or in some matrices?
You could use an array or other linear store (e.g. relation id->node value) in the way that each node x has the children 2*x and 2*x+1. The problem might be that there are unused ids if the tree is unbalanced.
Sample:
2---4---8
/ \ \9
1-- 5---10
\ \11
3---6---12
\ \13
7---14
\15
Seems like writing to something like XML as you suggest would be best. Converting to XML and then back when you want to reload it would be pretty straight forward.
I use a structure like following
id parent tree slug name path children
1 0 1 root Root || |5|
2 7 1 child-1 Child 1 |1-5-8-7| |3|
It is super easy to query huge depth trees, because the query is linear.
There is a bit more data in each line, but that makes it quicker.
For example you can get a whole subtree with a simple query like:
$sql = 'SELECT * FROM `'.$this->_table_name.'` WHERE
`path` LIKE "%-:parent-%"
OR `path` LIKE "%|:parent-%"
OR `path` LIKE "%-:parent|%"
OR `path` LIKE "%|:parent|%"';
$result = $this->_db->custom_query($sql, array('parent' => $root->id));
And then build the depth array by a simple function:
private function _build_tree(Node $root, $data)
{
$mapped_node = array(
'node' => $root,
'subnodes' => array()
);
if ( !empty($root->children) )
{
foreach( $root->children as $child )
{
if ( array_key_exists($child, $data) )
{
$mapped_node['subnodes'][] = $this->_build_tree($data[$child], $data);
}
}
}
return $mapped_node;
}
NOTE! When I get back the children and path columns, I split them into arrays for PHP to be able to better work with them.

Categories