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

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();
}
}

Related

How to find Level of element from array

I have an array which has id and parent_id like this
$element=new array(
[0]=>array(1,0), --------level 1
[1]=>array(2,0), -------level 1
[2]=>array(3,1), ------level 2
[3]=>array(4,1), ------level 2
[4]=>array(5,1), ------level 2
[5]=>array(6,2), ------level 2
[6]=>array(7,3), ------level 3
[7]=>array(8,2), ------level 2
[8]=>array(9,3), ------level 3
[9]=>array(10,6), ------level 3
[10]=>array(11,6), ------level 3
);
this is my array, in inner array first element is id of the array and second element is id of parent element.
now i want to find level of each element from root.
assume zero (0) is root element.
You can use a recursive approach. I'm assuming that the item at index N in the outer array always has id N+1. If not, you'll first have to search for the item with the matching id, but otherwise the rest of the logic should be the same.
<?php
function findLevel($id) {
$item = $element[$id-1]; //If my assumption (above) is incorrect,
// you'll need to replace this with an appropriate
// search function, which could be as simple as
// a loop through the array.
$parent = $item[1];
if ($parent == 0) {
//Parent is root. Assuming root is level 0, then
// this item is level 1.
return 1;
}
return 1 + findLevel($parent);
?>
$parent_id = n;
$level = 0;
while($parent_id != 0){
$inner_array = $element[$parent_id];
$parent_id = $inner_array[1];
$level ++;
}
Let's try this, you initially set $parent_id to the index of the $element array you want to know the level.
Make sure each level can be calculated

Building a specific array from a table of data

I have a table of data as such:
id | item | parent_id
1 item 1 0
2 item 2 0
3 item 3 2
4 item 4 3
5 item 5 1
...
The id is autoincrementing, and the parent_id reflects the id on the left. You may have come accross a database table design like this before.
The parent_id is not sequential as you can see.
I need to get this table data into an array in the format where all parents become a potential heading with their children underneath.
So I am looking at a structure like this:
Item 1
Item 5
Item 2
Item 3
Item 4
etc
In PHP I need an array structure that can display the above. But I am having a serious brain fart!
Can anyone help me with the array structure?
you may write somethin like this:
$a = Array(item1 => Array(item5), item2 => Array(item3 => Array(item4)))
or
$a = Array(item1 => parentid, item2 => parentid2 ....)
in the first example one item is the key for all ist children stored in the Array, in the other example all items are stored using an item key and an int value. this int value is the key for the parent item. which method you use depends on what you want to do with this Array. maybe both of my ideas are not good enough for your Needs. if thats the case, tell me what you need.
First of all, i will suggest you to read this, it's very useful for hierarchical structured data and there are available queries which will help you to get parents, children, etc ... and so and so.
Now to answer your question, try this :
$result = array();
while($data = mysql_fetch_assoc($query)) {
$id = $data['id'];
$parent = $data['parent_id'];
$keys = array_keys($result);
if(in_array($parent, $keys)) {
$result[$parent] = array_merge($result[$parent], array($id => $data['item']));
} else {
$result = array_merge($result, array($id => $data['item']));
}
}

Creating a hierarchy from simple strings (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

Linear Array with nodes randomly linked to other nodes in array, shortest path

INFO:
I have an Array of 100 nodes, [ 0 .. 99 ]. Each node can have an arbitrary number of linked nodes:
eg1, 0 links to 5, 10, 15, 20.
eg2, 1 links to 30, 40, 50.
eg3, etc..
All 100 nodes have at least one linked node, nodes do not know who links to them.
QUESTION:
How can I find the shortest link-path if provided with START and END.
eg. START=5, END=80, Link Path (example) : [5]->10->24->36->[80]?
I'm using Pascal and/or PHP, but understanding how is what I'm looking for [code helps too].
Plenty of reading/algorithms:
Shortest path problem. You effectively just have every edge ("link", as you called it) with an equal weight.
Do a Breadth First Traversal starting with the Start node and quit as soon as you find the end node.
Does this have cycles? i.e. is it a DAG?
If there aren't cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
//If this is the node you are looking for...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the end node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
List<Node> bestPath = null;
foreach(Node child in startNode.Children)
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
bestPath.Insert(0, startNode);
return bestPath;
}
[Edit: Added an example for cycles]
If there can be cycles:
List<Node> GetShortestPath(Node startNode, Node endNode)
{
List<Node> nodesToExclude = new List<Node>();
return GetShortestPath(startNode, endNOde, nodesToExclude);
}
List<Node> GetShortestPath(Node startNode, Node endNode, List<Node> nodesToExclude)
{
nodesToExclude.Add(startNode);
List<Node> bestPath = null;
//If this is end node...
if (startNode.ReferenceEquals(endNode))
{
//return a list with just the child node
List<Nodes> result = new List<Nodes>();
result.Add(endNode);
return result;
}
foreach(Node child in startNode.Children)
{
if (!nodesToExclude.Contains(child))
{
//get the shortest path from this child
List<Node> childPath = GetShortestPath(child, endNode);
if (childPath != null &&
( bestPath == null || childPath.Count < bestPath.Count))
{
bestPath = childPath;
}
}
}
nodesToExclude.Remove(startNode);
bestPath.Insert(0, child);
return bestPath;
}
Two structures: a set and a list.
In the set, you store nodes you have already visited. This prevents you from following cycles.
The list is of objects containing: (1) a node, and (2) a pointer back to the node that found it.
Starting at the start node, add it to the set, add it to the list with a null back reference, and then add all the nodes it can reach to the list with back references to the index 0 in the list (the start node).
Then for each element in the list thereafter, up until you reach the end, do the following:
if it is in the set already skip it (you have already visited it) and move to the next item in the list.
otherwise, add it to the set, and add all nodes it can reach to the list with back references to the index you are 'looking at' to the end of the list. Then go to the next index in the list and repeat.
If at any point you reach the end node (optimally as you are adding it to the list - as opposed to visiting it in the list), track back through the back references to the start node and invert the path.
Example:
Given nodes 0 through 3, where
node0 --> node1
node0 --> node2
node1 --> node2
node2 --> node3
and node0 is START and node3 is END
SET = {}
LIST = []
Step 1 - add START:
SET = {node0}
LIST = [[node0, null]]
Step 2 - at index 0 of the list - add reachable nodes:
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 3 - at index 1 of the LIST - add reachable nodes:
node2 is already in the SET. skip adding reachable nodes to LIST.
SET = {node0, node1, node2}
LIST = [[node0, null], [node1, 0], [node2, 0]]
Step 4 - at index 2 of the LIST - add reachable nodes:
SET = {node0, node1, node2, node3}
LIST = [[node0, null], [node1, 0], [node2, 0], [node3, 2]]
Step 5 - reached END, now backtrack:
The END node (node3) inserted in the LIST has a back reference to index 2 in the list, which is node2. This has a back reference to index 0 in the list, which is node0 (START). Invert this and you get a shortest path of node0 --> node2 --> node3.
Is this a tree/graph or a forest? If it is a forest, the path may not be defined always. In case this is a tree/graph, try using Breadth-First-Search.
Think of it this way: say, you are out on a stealth mission to find cute chicks in your neighbourhood. You start at your own house and mark it as the START. You'd next go to knock on your closest neighbours, right? So, we'll do just that -- push all nodes connected to the start in a queue. Now, repeat the neighbour search for all the nodes in this queue. And keep doing this till you get your girl, err, the END.

Create SQL query that orders results by which condition they meet

This is for MySQL and PHP
I have a table that contains the following columns:
navigation_id (unsigned int primary key)
navigation_category (unsigned int)
navigation_path (varchar (256))
navigation_is_active (bool)
navigation_store_id (unsigned int index)
Data will be filled like:
1, 32, "4/32/", 1, 32
2, 33, "4/32/33/", 1, 32
3, 34, "4/32/33/34/", 1, 32
4, 35, "4/32/33/35/", 1, 32
5, 36, "4/32/33/36/", 1, 32
6, 37, "4/37/", 1, 32
... another group that is under the "4/37" node
... and so on
So this will represent a tree like structure. My goal is to write a SQL query that, given the store ID of 32 and category ID of 33, will return
First, a group of elements that are the parents of the category 33 (in this case 4 and 32)
Then, a group of elements that are a child of category 33 (in this case 34, 35, and 36)
Then the rest of the "root" categories under category 4 (in this case 37).
So the following query will return the correct results:
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
My problem is that I want to order the "groups" of categories in the order listed above (parents of 33 first, children of 33 second, and parents of the root node last). So if they meet the first condition, order them first, if they meet the second condition order them second and if they meet the third (and fourth) condition order them last.
You can see an example of how the category structure works at this site:
www.eanacortes.net
You may notice that it's fairly slow. The current way I am doing this I am using magento's original category table and executing three particularly slow queries on it; then putting the results together in PHP. Using this new table I am solving another issue that I have with magento but would also like to improve my performance at the same time. The best way I see this being accomplished is putting all three queries together and having PHP work less by having the results sorted properly.
Thanks
EDIT
Alright, it works great now. Cut it down from 4 seconds down to 500 MS. Great speed now :)
Here is my code in the Colleciton class:
function addCategoryFilter($cat)
{
$path = $cat->getPath();
$select = $this->getSelect();
$id = $cat->getId();
$root = Mage::app()->getStore()->getRootCategoryId();
$commaPath = implode(", ", explode("/", $path));
$where = new Zend_Db_Expr(
"(navigation_category IN ({$commaPath})
OR navigation_parent = {$id}
OR (navigation_parent = {$root}
AND navigation_category <> {$cat->getId()}))");
$order = new Zend_Db_Expr("
CASE
WHEN navigation_category IN ({$commaPath}) THEN 1
WHEN navigation_parent = {$id} THEN 2
ELSE 3
END, LENGTH(navigation_path), navigation_name");
$select->where($where)->order($order);
return $this;
}
Then I consume it with the following code found in my Category block:
// get our data
$navigation = Mage::getModel("navigation/navigation")->getCollection();
$navigation->
addStoreFilter(Mage::app()->getStore()->getId())->
addCategoryFilter($currentCat);
// put it in an array
$node = &$tree;
$navArray = array();
foreach ($navigation as $cat)
{
$navArray[] = $cat;
}
$navCount = count($navArray);
$i = 0;
// skip passed the root category
for (; $i < $navCount; $i++)
{
if ($navArray[$i]->getNavigationCategory() == $root)
{
$i++;
break;
}
}
// add the parents of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
$node = &$node[0]["children"];
if ($cat->getNavigationCategory() == $currentCat->getId())
{
$i++;
break;
}
}
// add the children of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$path = explode("/", $cat->getNavigationPath());
if ($path[count($path) - 3] != $currentCat->getId())
{
break;
}
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
// add the children of the root category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$tree[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
return $tree;
If I could accept two answers I would accept the first and last one, and if I could accept an answer as "interesting/useful" I would do that with the second.
:)
A CASE expression should do the trick.
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
ORDER BY
CASE
WHEN navigation_category IN (4, 32) THEN 1
WHEN navigation_path LIKE "4/32/33/%/" THEN 2
ELSE 3
END, navigation_path
Try an additional derived column like "weight":
(untested)
(IF(criteriaA,1,0)) + (IF(criteriaB,1,0)) ... AS weight
....
ORDER BY weight
Each criteria increases the "weight" of the sort.
You could also set the weights distinctly by nesting IFs and giving the groups a particular integer to sort by like:
IF(criteriaA,0, IF(criteriaB,1, IF ... )) AS weight
Does MySQL have the UNION SQL keyword for combining queries? Your three queries have mainly non-overlapping criteria, so I suspect it's best to leave them as essentially separate queries, but combine them using UNION or UNION ALL. This will save 2 DB round-trips, and possibly make it easier for MySQL's query planner to "see" the best way to find each set of rows is.
By the way, your strategy of representing the tree by storing paths from root to tip is easy to follow but rather inefficient whenever you need to use a WHERE clause of the form navigation_path like '%XYZ' -- on all DBs I've seen, LIKE conditions must start with a non-wildcard to enable use of an index on that column. (In your example code snippet, you would need such a clause if you didn't already know that the root category was 4 (How did you know that by the way? From a separate, earlier query?))
How often do your categories change? If they don't change often, you can represent your tree using the "nested sets" method, described here, which enables much faster queries on things like "What categories are descendants/ancestors of a given category".

Categories