Taxonomy Tree Problems

I have been using jquery-option-tree (http://code.google.com/p/jquery-option-tree/) for a form on a clients website in order for them to choose a google category for their product search feed.
You can see an example of what it looks like here: http://www.creativenaturemedia.co.uk/demo/jq-option-tree-demo.php.
The only problem is it displays some categories in the wrong section, if the parent section ends with the same wording. For example, in my demo, try clicking on Office Supplies, then General Office Supplies - you will notice that Adhesives comes under both - it should only appear under Office Supplies > General Office Supplies > Adhesives.
I’ve been through the code several times and can’t locate the problem - though I don’t understand a lot of it to be fair as I’m a bit of a nube when it comes to javascript.

Demo page:

<script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="jquery.optionTree.js"></script>
<label for="gcategory" id="goocattitle">Edit Google Product Category:</label>
<p>You can search the taxonomy here: <a class="external" href="http://support.google.com/merchants/bin/answer.py?hl=en-GB&answer=1705911">Taxonomy Search</a></p>
	<div>
	<input type="text" name="demo7" />
	</div>
	<div class="results" id="demo7-result"></div>
	<script type="text/javascript">
	$(function() {
	
	    var options = {
		    empty_value: 'null',
		    indexed: true,  // the data in tree is indexed by values (ids), not by labels
		    on_each_change: 'get-subtree.php', // this file will be called with 'id' parameter, JSON data must be returned
		    choose: function(level) {
			return 'Choose level ' + level;
		    },
		    loading_image: 'ajax-load.gif',
		    show_multiple: 21, // if true - will set the size to show all options
		    choose: '', // no choose item
		    set_value_on: 'each'
		};
	
		var displayParents = function() {
		    var labels = []; // initialize array
		    $(this).siblings('select') // find all select
				   .find(':selected') // and their current options
				     .each(function() { labels.push($(this).text()); }); // and add option text to array
				     $('#demo7-result').empty();
		    $('<textarea id="goocategory" name="goocategory" cols="100">').text(labels.join(' > ')).appendTo('#demo7-result'); // and display the labels
		    }
	
	    $.getJSON('get-subtree.php', function(tree) { // initialize the tree by loading the file first
		$('input[name=demo7]').optionTree(tree, options).change(displayParents);
	    });
	});
	</script>
</div>

get-sub-tree.php:

<?php
require_once 'LazyTaxonomyReader.php';

$reader = new LazyTaxonomyReader('taxonomy.txt');

$line_no = (isset($_GET['id']) && is_numeric($_GET['id']) ? (int) $_GET['id'] : null);
echo json_encode($reader->getDirectDescendants($line_no));
?>

LazyTaxonomyReader.php:

class LazyTaxonomyReader {

    private $base = null;
    private $separator = ' > ';
    protected $lines;

    public function __construct($file = 'taxonomy.txt') {
        $this->lines = file($file, FILE_IGNORE_NEW_LINES);
    }

    public function setBaseNode($line_no) {
        if (is_null($line_no)) {
            $this->base = null;
            return;
        }

        if (!array_key_exists($line_no, $this->lines)) {
            throw new IllegalArgumentException("Invalid line number.");
        }
        $this->base = $this->lines[$line_no];
    }

    public function getDirectDescendants($line_no = null) {
        $this->setBaseNode($line_no);
        // select only lines that are directly below current base node
        $direct = array_filter($this->lines, array($this, 'isDirectlyBelowBase'));
        // return only last part of their names
        return array_map(array($this, 'getLastNode'), $direct);
    }

    protected function getLastNode($line) {
        if (strpos($line, $this->separator) === false) {
            // no separator present
            return $line;
        }
        // strip up to and including last separator
        return substr($line, strrpos($line, $this->separator) + strlen($this->separator));
    }

    protected function isDirectlyBelowBase($line) {

        // starting text that must be present
        if (is_null($this->base)) {
            $start = '';
        } else {
            $start = $this->base . $this->separator;
        }

        if ($start !== '') {
            $starts_at_base = (strpos($line, $start) === 0);

            if (!$starts_at_base) { // starts with something different
                return false;
            }

            // remove start text AND the following separator
            $line = str_replace($start, '', $line);
        }

        // we're direct descendants if we have no separators left on the line
        if (strpos($line, $this->separator) !== false)
            return false;

        return true;
    }
}

jquery.optionTree.js:

(function($){
$.fn.optionTree = function(tree, options) {

    options = $.extend({
        choose: 'Choose...', // string with text or function that will be passed current level and returns a string
        show_multiple: false, // show multiple values (if true takes number of items as size, or number (eg. 12) to show fixed size)
        preselect: {},
        loading_image: '', // show an ajax loading graphics (animated gif) while loading ajax (eg. /ajax-loader.gif)
        select_class: '',
        leaf_class: 'final',
        empty_value: '', // what value to set the input to if no valid option was selected
        on_each_change: false, // URL to lazy load (JSON, 'id' parameter will be added) or function. See default_lazy_load
        set_value_on: 'leaf', // leaf - sets input value only when choosing leaf node. 'each' - sets value on each level change.
                              // makes sense only then indexed=true
        indexed: true,
        preselect_only_once: false // if true, once preselected items will be chosen, the preselect list is cleared. This is to allow
                                    // changing the higher level options without automatically changing lower levels when a whole subtree is in preselect list
    }, options || {});

    var cleanName = function (name) {
        return name.replace(/_*$/, '');
    };

    var removeNested = function (name) {
        $("select[name^='"+ name + "']").remove();
    };

    var setValue = function(name, value) {
        $("input[name='" + cleanName(name) + "']").val(value).change();
    };

    // default lazy loading function
    var default_lazy_load = function(value) {
        var input = this;
        if ( options.loading_image !== '' ) {
          // show loading animation
          $("<img>")
            .attr('src', options.loading_image)
            .attr('class', 'optionTree-loader')
            .insertAfter(input);
        }

        $.getJSON(options.lazy_load, {id: value}, function(tree) {
            $('.optionTree-loader').remove();
            var prop;
            for (prop in tree) {
                if (tree.hasOwnProperty(prop)) { // tree not empty
                    $(input).optionTree(tree, options);
                    return;
                }
            }
            // tree empty, call value switch
            $(input).optionTree(value, options);
        });
    };

    if (typeof options.on_each_change === 'string') { // URL given as an onchange
        options.lazy_load = options.on_each_change;
        options.on_each_change = default_lazy_load;
    }

    var isPreselectedFor = function(clean, v) {
      if (!options.preselect || !options.preselect[clean]) {
        return false;
      }

      if ($.isArray(options.preselect[clean])) {
        return $.inArray(v, options.preselect[clean]) !== -1;
      }

      return (options.preselect[clean] === v);
    };

    return this.each(function() {
        var name = $(this).attr('name') + "_";

        // remove all dynamic options of lower levels
        removeNested(name);

        if (typeof tree === "object") { // many options exists for current nesting level

            // create select element with all the options
            // and bind onchange event to recursively call this function

            var $select = $("<select>").attr('name',name)
            .change(function() {
                if (this.options[this.selectedIndex].value !== '') {
                    if ($.isFunction(options.on_each_change)) {
                      removeNested(name + '_');
                        options.on_each_change.apply(this, [this.options[this.selectedIndex].value, tree]);
                    } else {
                      // call with value as a first parameter
                        $(this).optionTree(tree[this.options[this.selectedIndex].value], options);
                    }
                    if (options.set_value_on === 'each') {
                      setValue(name, this.options[this.selectedIndex].value);
                    }
                } else {
                  removeNested(name + '_');
                    setValue(name, options.empty_value);
                }
            });

            var text_to_choose = '';

            if (jQuery.isFunction(options.choose)) {
                var level = $(this).siblings().andSelf().filter('select').length;
                text_to_choose = options.choose.apply(this, [level]);
            } else if ( options.choose !== '' ) {
                text_to_choose = options.choose;
            }

            // if show multiple -> show open select
            var count_tree_objects = 0;
            if ( text_to_choose !== '' ) {
              // we have a default value
              count_tree_objects++;
            }
            if (options.show_multiple > 1) {
                count_tree_objects = options.show_multiple;
            } else if (options.show_multiple === true) {
              $.each(tree, function() {
                 count_tree_objects++;
              });
            }
            if ( count_tree_objects > 1 ){
              $select.attr('size', count_tree_objects);
            }

            if ($(this).is('input')) {
                $select.insertBefore(this);
            } else {
                $select.insertAfter(this);
            }

            if (options.select_class) {
                $select.addClass(options.select_class);
            }

            if ( text_to_choose !== '' ) {
              $("<option>").html(text_to_choose).val('').appendTo($select);
            }

            var foundPreselect = false;
            $.each(tree, function(k, v) {
                var label, value;
                if (options.indexed) {
                    label = v;
                    value = k;
                } else {
                    label = value = k;
                }
                var o = $("<option>").html(label)
                    .attr('value', value);
                var clean = cleanName(name);
                    if (options.leaf_class && typeof value !== 'object') { // this option is a leaf node
                        o.addClass(options.leaf_class);
                    }

                    o.appendTo($select);
                    if (isPreselectedFor(clean, value)) {
                      o.get(0).selected = true;
                      foundPreselect = true;
                    }
            });

            if (foundPreselect) {
              $select.change();
            }

            if (!foundPreselect && options.preselect_only_once) { // clear preselect on first not-found level
                options.preselect[cleanName(name)] = null;
            }

        } else if (options.set_value_on === 'leaf') { // single option is selected by the user (function called via onchange event())
            if (options.indexed) {
                setValue(name, this.options[this.selectedIndex].value);
            } else {
                setValue(name, tree);
            }
        }
    });

};
}(jQuery));

There are a couple of issues that when fixed up, result in working code.

[list][]The code in LazyTaxonomyReader.php needs PHP tags to surround the code
[
]The demo page says ‘get-subtree.php’ whereas you list the file as being called get-sub-tree.php
[*]The taxonomy needs to exist in a file called taxonomy.txt[/list]

Hi,
The first 2 issues were typos in the forum post not the actual code. I assumed that the fact I had listed taxonomy.txt it was a given that it actually exists - it can be viewed here: http://www.creativenaturemedia.co.uk/demo/taxonomy.txt.
Also I think you may have misunderstood the question - the code works in sense, just not the way it should. Please see working demo at: http://www.creativenaturemedia.co.uk/demo/jq-option-tree-demo.php

This this problem is purely due to how LaxyTaxonomyReader.php fetches the data from taxonomy.txt, I’m moving this over to the PHP thread where you can receive the correct assistance with this.

Thanks Paul

Anyone any ideas on this?

I looked at your txt file and I don’t get your point.

If you have established beyond doubt that it is a PHP problem, then you should focus down on that and set up a small test data rig and prove what works and what does not.

If you are still stumped you should post that code here and someone will be happy to look at it.

What happens, is that with the following data in the taxonomy:

Office Supplies
Office Supplies > General Office Supplies
Office Supplies > General Office Supplies > Adhesives
Office Supplies > General Office Supplies > Adhesives > Office Tape

That ends up being incorrectly interpreted as two different sets of data.

Office Supplies > Adhesives > Office Tape
Office Supplies > General Office Supplies > Adhesives > Office Tape

The cause of that seems to be due to how “Office Supplies” is repeated in both the main category, and in the secondary category.

The OP is looking for a way to fix that, which from what I see, seems to be caused somewhere in LazyTaxonomyReader.php

Thanks for clarifying the issue Paul - spot on.

Right, I see it now, “Software” suffers from the same problem…

Something to do with this line I think:


            // remove start text AND the following separator 
            $line = str_replace($start, '', $line);

Or possibly this line?

// starting text that must be present
if (is_null($this->base)) {
  $start = '';
} else {
  $start = $this->base . $this->separator;
}

I contacted the developer and he gave me the solution - for those who are interested, please see this page for the alterations needed to LazyTaxonomyReader.php:
https://code.google.com/p/jquery-option-tree/source/detail?r=15#