'CLASS: NestedSet Datenbankfehler', 'NODEERROR' => 'CLASS: NestedSet Angegebener Knoten existiert nicht', 'ROOTERROR' => 'CLASS: NestedSet Angegebener Rootknoten existiert nicht', 'RESOURCEERROR' => 'CLASS: NestedSet Parameter ist keine Resource-Kennung', 'DBFIELDERROR' => 'CLASS: NestedSet Falscher Schlssel erlaubt sind: id, root_id, left_id, right_id, sequence, payload' ); /** * @var ARRAY dbfields Alle Namen fr die 'default' DB-Tabelle * @access private */ var $dbfields = array( 'id' => 'id', 'root_id' => 'root_id', 'left_id' => 'left_id', 'right_id' => 'right_id', 'sequence' => 'sequence', 'payload' => 'payload' ); /** * Konstruktor * * Optional kann ein anderer Tabellenname beim Instanziieren bergeben werden * * Parameter * * @param STRING Tabellenname * @param ARRAY Mit den Defaultnamen der DB-Tabelle als Schlssel und den angepassten Namen als Value * @access public * */ function NestedSet($tablename = "nested_set", $dbfields = false) { $this->tablename = $tablename; if ($dbfields && is_array($dbfields)) { foreach ($dbfields as $key => $value) { $this->set_db_column($key, $value); } } } /** * Factory-Methode * * Instanziiert automatisch ein passendes Objekt und liefert es per Referenz zurck * Wird als Modulname "QUERY" angegeben, dann wird ein Objekt der Klasse NestedSet zurckgegeben. Damit stehen dem * Aufrufer alle Funktionen zur Verfgung, um einen Nested-Set Baum abzufragen. * Wird als Modulname "MANIPULATION" angegeben, wird zur Laufzeit die abgeleitete Klasse 'NestedSetManipulation' * inkludiert und instanziiert. Somit stehen dann auch alle Funktionen zur Verfgung, um einen Baum zu erstellen * oder zu verndern. Da auf verschachtelte Baumstrukturen zumeist nur lesend zugegriffen wird, sollte * in solchen Fllen nur das Standard-Modul instanziiert werden, um dem PHP-Interpreter das berflssige * Parsen und Bereitstellen von nicht bentigtem Code zu ersparen * * Wird eines der defaultmig vorgegebenen Feldnamen 'id', 'root_id', 'left_id', 'right_id', 'sequence' und 'payload' * in der DB umbenannt, muss als dritter Parameter ein Assoziatives-Array mit den Defaultwerten als Schlssel und * den genderten Spaltenamen als Value mitbergeben werden, um das NestedSet Objekt an der genderten Datenbankstruktur * anzupassen * * Beispiel: Wurden die Default-Felder 'id' und 'payload' in der Datenbank in 'kategorie_id' und 'kategoriename' * umbenannt, muss die Factory-Methode mit drei Parametern folgendermaen aufgerufen werden: * $dbfields = array('id' => 'kategorie_id', 'payload' => 'kategoriename'); * $nested_set2 = &NestedSet::factory("MANIPULATION", "nested_set2", $dbfields); * * Parameter * * @param STRING Modulname "QUERY" oder "MANIPULATION" * @param STRING optional Name der Datenbanktabelle * @param ARRAY optional Array mit den genderten Datenbankfeldern * * @return OBJECT NestedSet oder NestedSetManipulation * @access public * */ function &factory($modul, $table = false, $dbfields = false) { if ($modul == "QUERY") { return new NestedSet($table, $dbfields); } else if ($modul == "MANIPULATION") { require_once("nestedset.manipulation.class.php"); return new NestedSetManipulation($table, $dbfields); } } /** * ndert die Spaltennamen der Datenbank, die bei Sql-Statements verwendet werden * * Wird eines der defaultmig vorgegebenen Feldnamen in der DB gendert, muss dies der Klasse mitgeteilt werden * Somit ist es z.B. mglich, in der Datenbank das Feld 'id' in 'kategorie_id' umzubenennen * bergeben werden mssen der Defaultname und der genderte Name des jeweiligen Feldes * Fr den ersten Parameter sind folgende Werte erlaubt: 'id', 'root_id', 'left_id', 'right_id', 'sequence', 'payload' * Diese Methode wird automatisch vom Konstruktor aufgerufen, falls ein dritter Parameter bergeben wurde. * * Parameter * * @param STRING Name des defaultmig verwendeten Spaltennames(DB), der gendert werden soll * @param STRING Neuer Name des genderten Spaltennames * * @return BOOL 'true' bei Erfolg * @access private * */ function set_db_column($dbfield, $value) { // Workaround fr alte PHP Versionen, die kein 'array_key_exists' kennen! if (!isset($this->dbfields["$dbfield"])) { $this->lasterror = $this->errors['DBFIELDERROR']; return false; } $this->dbfields["$dbfield"] = $value; return true; } /** * Erstellt aus einem indexierten Array Feldnamen, die dynamisch in ein Sql-Statement eingefgt werden * * Erstellt dynamisch aus einem indexierten Array Feldnamen, die gegebenfalls den Sql-Statements hinzugefgt * werden knnen. Als zweiter Parameter kann optional ein Alias-Name bergeben werden, der allen im Array * bergebenen Spaltennamen hinzugefgt wird (Joins) * * Parameter * * @param ARRAY Indexiertes Array mit zustzlichen Tabellenspalten * @param STRING Optional Alias-Name, der allen zustzlichen Tabellennamen vorangestellt wird * * @return BOOL 'true' bei Erfolg * @access private * */ function create_db_fields($fields, $alias = false) { if (!is_array($fields)) { return ""; } if ($alias) { $sqlfields = ", $alias." . implode(", $alias.", $fields) . " "; } else { $sqlfields = ", " . implode(", ", $fields) . " "; } return $sqlfields; } /** * Liefert Daten eines Knotens * * bergeben werden muss die 'id' des Knotens * Bei Erfolg wird folgendes Array zurckgegeben: array('root_id, left_id, right_id, sequence, payload') * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param INT 'id' des Knotens * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * * @return mixed 'false' bei einem Fehler oder array mit Ergebnis bei Erfolg * @access public * */ function get_node($id, $fields = false) { // Gegebenenfalls bergebene Zusatzfelder generieren $addition = $this->create_db_fields($fields); $col = $this->dbfields; // Selektiere den Knoten $sql = "SELECT {$col['root_id']}, {$col['left_id']}, {$col['right_id']}, {$col['sequence']}, {$col['payload']}, "; $sql.= "round((({$col['right_id']} - {$col['left_id']} - 1) / 2), 0) AS children $addition"; $sql.= "FROM {$this->tablename} "; $sql.= "WHERE {$col['id']} = '$id'"; if (!$result = $this->db_query($sql)) { return false; //Fehler } $row = $this->db_fetch($result); // berprfe, ob Knoten existiert if (!$row[$col['root_id']] > 0) { $this->lasterror = $this->errors['NODEERROR']; //Fehler $this->db_query("UNLOCK TABLES"); return false; } return $row; } /** * Liefert einen einzelnen Baum einer root_id (Thread, Hauptkategorie) * * bergeben werden muss die root_id des Knotens, dessen gesamte Baumstruktur (Thread) zurckgeliefert werden soll * Wird als zweiter Parameter optional die 'id' eines Knotens bergeben, wird der gesamte Teilbaum * zurckgegeben, zu dem der betreffende Knoten gehrt. Das heit, es wird die root_id des bergebenen Knotens benutzt * Bei Erfolg wird eine Sql-Resource-Kennung zurckgegeben, sonst 'false' * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param INT root_id des Knotens * @param INT optional 'id' des Knotens * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_root_tree($root_id, $id = false, $fields = false) { // Gegebenenfalls bergebene Zusatzfelder generieren $addition = $this->create_db_fields($fields, "a"); $root_id = (int)$root_id; // Wurde die 'id' anstatt der root_id bergeben if ($id) { // Ja: dann ermittel die root_id, zu dem der Knoten gehrt $id = (int)$id; //root_id des Knotens ermitteln if (!$row = $this->get_node($id)) { return false; } unset($root_id);// Eventuell mitbergebene root_id lschen list($root_id) = $row; } $col = $this->dbfields; // Selektiere den gesamten Baum $sql = "SELECT a.{$col['id']}, a.{$col['root_id']}, a.{$col['left_id']}, a.{$col['right_id']}, a.{$col['sequence']}, a.{$col['payload']}, "; $sql.= "round(((a.{$col['right_id']} - a.{$col['left_id']} - 1) / 2), 0) AS children, COUNT(a.{$col['left_id']}) AS level $addition"; $sql.= "FROM {$this->tablename} AS a INNER JOIN {$this->tablename} AS b USING ({$col['root_id']}) "; $sql.= "WHERE a.{$col['left_id']} BETWEEN b.{$col['left_id']} AND b.{$col['right_id']} AND a.{$col['root_id']} = '$root_id' "; $sql.= "GROUP BY a.{$col['root_id']}, a.{$col['left_id']} "; $sql.= "ORDER BY a.{$col['root_id']}, a.{$col['left_id']}"; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Liefert alle oder mehrere Teilbume der gesamten Baumstruktur * * Wird die Funktion ohne Parameter aufgerufen, liefert sie die gesamte Baumstruktur. * Optional kann als erster Parameter die root_id des ersten anzuzeigenden Baumes und gegebenenfalls * als zweiter die maximale Anzahl der anzuzeigenden Bume (Threads) bergeben werden. * Dies dient z.B. dazu, eine Bltter-Funktion fr die Threads (root_id's) eines Forums zu generieren. * Bei Erfolg wird die RESOURCE_ID des Sql-Statements zurckgeliefert * * @todo :TODO: Wird ein Teilbaum (Thread) gelscht, liefert der manuell berechnete Offset einen falschen Wert * Der Offset sollte mit einem Select auf die 'root_id' mit limit = ($roor_id, $offset) ermittelt werden * * Parameter * * @param INT Optional root_id des ersten anzuzeigenden Baumes * @param INT Optional Anzahl der maximal anzuzeigenden Bume * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_tree_range($firstroot = false, $offset = false, $fields = false) { $col = $this->dbfields; // Prfe, ob und welche Parameter bergeben wurden if ($firstroot) { // Fge dem Statement gegebenenfalls erstes und maximales anzuzeigendes Root-Element hinzu // Dient z.B. dazu, eine Bltter-Funktion fr die Threads (root_id's) eines Forums zu generieren. $firstroot = (int)$firstroot; $and_sql = "AND a.{$col['root_id']} >= '$firstroot'"; // Wurde ein zweiter Parameter bergeben if ($offset) { $offset = (int)$offset; $lastroot = $firstroot + $offset; $and_sql.= " AND a.{$col['root_id']} < '$lastroot'"; } } // Gegebenenfalls bergebene Zusatzfelder generieren $addition = $this->create_db_fields($fields, "a"); $and_sql = (isset($and_sql)) ? "$and_sql " : ""; // Selektiere den Baum $sql = "SELECT a.{$col['id']}, a.{$col['root_id']}, a.{$col['left_id']}, a.{$col['right_id']}, a.{$col['sequence']}, a.{$col['payload']}, "; $sql.= "round(((a.{$col['right_id']} - a.{$col['left_id']} - 1) / 2), 0) AS children, COUNT(a.{$col['left_id']}) AS level $addition"; $sql.= "FROM {$this->tablename} AS a INNER JOIN {$this->tablename} AS b USING ({$col['root_id']}) "; $sql.= "WHERE a.{$col['left_id']} BETWEEN b.{$col['left_id']} AND b.{$col['right_id']} $and_sql"; $sql.= "GROUP BY a.{$col['root_id']}, a.{$col['left_id']} "; $sql.= "ORDER BY a.{$col['root_id']}, a.{$col['left_id']} "; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Liefert alle Teilbume der gesamten Baumstruktur * * Dient als Wrapper-Funktion fr 'get_tree_range', um die API einfach zu halten * Wird die Funktion ohne Parameter aufgerufen, liefert sie die gesamte Baumstruktur. * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_all_trees($fields = false) { if (!$result = $this->get_tree_range(false, false, $fields)) { return false; } return $result; } /** * Liefert den direkten Pfad zum angegebenen Element * * Liefert den direkten Pfad zum angegebenen Element, also alle direkten bergeordneten Knotenpunkte * bergeben werden muss die 'id' des Knotenpunktes, wobei bei Erfolg ein Array mit allen bergeordneten * Vaterelementen zurckgegeben wird. Dies ist z.B. ntzlich, um bei verschachtelten Kategorien dem User * den momentanen Aufenthaltsort anzuzeigen: 'PHP:Scripte:Shops:Addons' * * Parameter * * @param INT 'id' des Knotens * * @return ARRAY mit allen bergeordeten Elementen, ausgehend vom jeweiligen Wurzelknoten, sonst 'false' * @access public * */ function get_path($id) { $id = (int)$id; //left und right des Knotens ermitteln if (!$row = $this->get_node($id)) { return false; } list($root_id, $left_id, $right_id) = $row; // Gegebenenfalls bergebene Zusatzfelder generieren $col = $this->dbfields; // Den direkten Pfad des Elementes ermitteln $sql = "SELECT {$col['id']}, {$col['payload']} FROM {$this->tablename} "; $sql.= "WHERE {$col['left_id']} <= '$left_id' AND {$col['right_id']} >= '$right_id' AND {$col['root_id']} = '$root_id'"; $sql.= "ORDER BY {$col['left_id']}"; if (!$result = $this->db_query($sql)) { return false; //Fehler } // In ein array berfhren $path = array(); while ($row = $this->db_fetch($result)) { //$path[] = $row["{$col['payload']}"]; $path[$row["{$col['id']}"]] = $row["{$col['payload']}"]; } return $path; } /** * Liefert den gesamten Baum, der sich unterhalb des bergebenen Knotens befindet * * Liefert alle Kindelemente inklusive dem bergebenen Knoten, wobei die Einrcktiefe (Level) * defaulmig absolut vom Wurzelknoten errechnet wird. Setzt man den zweiten optionalen Parameter auf 'true', * dann wird die Einrckung der untergeordeten Elemente relativ zum bergebenen Knotenpunkt berechnet. * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param INT 'id' des Knotens * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * @param BOOL Optional Gibt an, ob die Einrcktiefe relativ (true) oder absolut berechnet werden soll (false) * * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst false * @access public * */ function get_sub_tree($id, $fields = false, $relative = false) { $id = (int)$id; $relative = (bool)$relative; // wird unten im Statement benutzt, um die Einrckung absolut oder relativ zu berechnen $level = ($relative) ? "b" : "a"; // Tabellen ALIAS //root_id,left_id und right_id des Knotens ermitteln if (!$row = $this->get_node($id)) { return false; } list($root_id, $left_id, $right_id) = $row; $addition = $this->create_db_fields($fields, "a"); $col = $this->dbfields; // Selektiere den gesamten Baum ab dem bergebenen Knotenpunkt $sql = "SELECT a.{$col['id']}, a.{$col['root_id']}, a.{$col['left_id']}, a.{$col['right_id']}, a.{$col['sequence']}, a.{$col['payload']}, "; $sql.= "round(((a.{$col['right_id']} - a.{$col['left_id']} - 1) / 2), 0) AS children, COUNT(a.{$col['left_id']}) AS level $addition"; $sql.= "FROM {$this->tablename} AS a INNER JOIN {$this->tablename} AS b USING ({$col['root_id']}) "; $sql.= "WHERE a.{$col['left_id']} BETWEEN b.{$col['left_id']} AND b.{$col['right_id']} AND a.{$col['root_id']} = '$root_id' "; $sql.= "AND $level.{$col['left_id']} BETWEEN '$left_id' AND '$right_id' "; $sql.= "GROUP BY a.{$col['root_id']}, a.{$col['left_id']} "; $sql.= "ORDER BY a.{$col['root_id']}, a.{$col['left_id']} "; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Liefert alle direkten Kinder eines Knotens * * Liefert alle Kindelemente, die sich unterhalb des bergebenen Knotens befinden, wobei NICHT der komplette * Teilbaum geliefert wird, sondern nur die Kindelemente, die sich eine Stufe unterhalb des Knotens befinden * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param INT 'id' des Knotens * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_childs($id, $fields = false) { // Teilbaum $id = (int)$id; //root_id,left_id und right_id des Knotens ermitteln if (!$row = $this->get_node($id)) { return false; } list($root_id, $left_id, $right_id) = $row; $addition = $this->create_db_fields($fields, "a"); $col = $this->dbfields; // Selektiere alle Kindelemente eines Knotens, die eine Einrcktiefe von zwei besitzen. (Kindelemente) $sql = "SELECT a.{$col['id']}, a.{$col['root_id']}, a.{$col['left_id']}, a.{$col['right_id']}, a.{$col['sequence']}, a.{$col['payload']}, "; $sql.= "round(((a.{$col['right_id']} - a.{$col['left_id']} - 1) / 2), 0) AS children, COUNT(a.{$col['left_id']}) AS level $addition"; $sql.= "FROM {$this->tablename} AS a INNER JOIN {$this->tablename} AS b USING ({$col['root_id']}) "; $sql.= "WHERE a.{$col['left_id']} BETWEEN b.{$col['left_id']} AND b.{$col['right_id']} AND b.{$col['left_id']} BETWEEN '$left_id' AND '$right_id' "; $sql.= "AND a.{$col['root_id']} = '$root_id' GROUP BY a.{$col['id']} HAVING level = '2' "; $sql.= "ORDER BY a.{$col['sequence']} "; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Liefert den direkten Vater des bergebenen Knotens * Optional kann ein Array mit allen zustzlich zu selektierenden DB-Feldern bergeben werden * * Parameter * * @param INT 'id' des Knotens * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_father($id, $fields = false) { $id = (int)$id; //left und right des Knotens ermitteln if (!$row = $this->get_node($id)) { return false; } list($root_id, $left_id, $right_id) = $row; $addition = $this->create_db_fields($fields, "a"); $col = $this->dbfields; // Selektiere den bergeordneten Knoten des Elementes $sql = "SELECT a.{$col['id']}, a.{$col['root_id']}, a.{$col['left_id']}, a.{$col['right_id']}, a.{$col['sequence']}, a.{$col['payload']}, "; $sql.= "round(((a.{$col['right_id']} - a.{$col['left_id']} - 1) / 2), 0) AS children, COUNT(a.{$col['left_id']}) AS level $addition"; $sql.= "FROM {$this->tablename} AS a INNER JOIN {$this->tablename} AS b USING ({$col['root_id']}) "; $sql.= "WHERE a.{$col['left_id']} BETWEEN b.{$col['left_id']} AND b.{$col['right_id']} AND a.{$col['root_id']} = '$root_id' "; $sql.= "AND a.{$col['left_id']} < '$left_id' AND a.{$col['right_id']} > '$right_id' "; $sql.= "GROUP BY a.{$col['id']} "; $sql.= "ORDER BY a.{$col['left_id']} DESC LIMIT 1"; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Fetcht ein bergebenes Sql-Resultset und erstellt automatisch die 'options' fr 'Select'-Felder * * Der erste Parameter muss eine Resource-Kennung sein, optional kann der zweite Parameter die 'id' * des Knotens enthalten, der vorselektiert werden soll. * Beispiel: $options = $nested_set->create_options($nested_set->get_all_trees(), 4) * * Parameter * * @param RESOURCE_ID Resource Kennung der Datenbankabfrage * @param INT 'id' des vorzuselektierenden Knotens * * @return STRING alle 'Option'-Felder mit der 'id' des Knotens als 'value' * @access public * */ function create_options($resource_id, $select = false) { $col = $this->dbfields; if (!is_resource($resource_id)) { $this->lasterror = $this->errors['RESOURCEERROR']; return false; } $options = ""; if ($select) $select = (int)$select; while ($row = $this->db_fetch($resource_id)) { // Einrcktiefe generieren $level = ""; for ($i=1; $i< $row['level']; $i++) { $level.= "-"; } $selected = ($select == $row["{$col['id']}"]) ? "selected" : ""; $options.= "\n\r"; } return $options; } /** * Liefert alle Wurzelknoten OHNE Kinder * * Parameter * * @param ARRAY Optional Array mit allen DB-Feldern, die zustzlich selektiert werden sollen * @return RESOURCE_ID Ergebniskennung des Sql-Statements, sonst 'false' * @access public * */ function get_root_nodes($fields = false){ $col = $this->dbfields; $addition = $this->create_db_fields($fields); $sql = "SELECT {$col['id']}, {$col['root_id']}, {$col['left_id']}, {$col['right_id']}, {$col['sequence']}, {$col['payload']} $addition"; $sql.= "FROM {$this->tablename} "; $sql.= "WHERE {$col['left_id']} = '1' ORDER BY {$col['sequence']}"; if (!$result = $this->db_query($sql)) { return false; //Fehler } return $result; } /** * Liefert den zuletzt aufgetretenen Fehler * * Parameter * * @return ARRAY Fehlermeldung * @access public * */ function get_last_error() { return $this->lasterror; } /** * Wrapper-Funktionen fr Datenbankfunktionen * * Diese Funktionen dienen dazu, diese Klasse auch mit anderen Datenbanken (Klassen) * verwenden zu knnen. Bei einem eventuellen Wechsel der Datenbank brauchen nur diese * Funktionen angepasst werden * * Parameter * * @param STRING Sql-Statement * * @return RESOURCE_ID resource_id des Resultsets * @access private * */ function db_query($sql) { global $db; $result = $db->query($sql); return $result; } /** * Wrapper-Funktionen fr Datenbankfunktionen * * Parameter * * @param RESOURCE_ID resource_id des vorangegangenen Sql-Statements * * @return ARRAY Zeile des Sql-Resulsets * @access private * */ function db_fetch($resource) { global $db; return $db->fetch_array($resource); } /** * Wrapper-Funktion fr Datenbankfunktionen * * Parameter * * @return INT Auto-Id des letzten Insert-Statements * @access private * */ function db_lastid() { global $db; return $db->insert_id(); } /** * Wrapper-Funktion fr Datenbankfunktionen * * Parameter * * @param PHP resource_id * * @return INT Anzahl der betroffenen Datenstze * @access private * */ function db_num_rows($resource_id) { global $db; return $db->num_rows($resource_id); } } ?> xYms۸|7%J~MdI'$IZ7%$x dMYHVW;x}5{z)v-L)u>&qDDyy7prxr<] f߾_.`ű˟s a뫷o+RZ* Kbf9i ޅ7DkBC:9JlcYrCf:`4@Y0"N'&x?VZ%/Yg,6\8e\d4 ,[ ůky=܊n ?+nlH8vC3B-nFEZ<-e2&qz4$TDg'J*jJ,cGǤ=ϊ[ n⴦쟾Zu#5/c# ;-ZY+u8>|r.n imYZYţQ=UYRG\(6v8D:ePfʾLX+y2kKhwTSfy3~1ශ'̧鵄?y&m\^d7DpؔAa@db#?˼+}]qluw_w,^HXwޖ1vM=Aj $P7m{z2Zwp sc#hm?.OQ'\g/G^!wb7BX?aZɄ}WAp#2s]qukVgw,*K.zQՊ$2eW:R2WBa5bwGU3X"e u#ӱEJo0o Co7RT6P즓R =]4 yq[kTD(7SfMXw%.Aoxm~m*I[{J7#iWz)$5{TP,f)הb8Fd A,f2[h2eSYeG\y0W>_YȫkF+'b5tzz^qiھ+YTV ao_@+xP^@Ub4bu_]jUTzE+yP;/:~U4P 0[vQ~ ZR t.P:r>zDmJ3`oѴK8}#{Z-t/* ~#?׆÷F M7 u؋YdpȽjQ'|Zѿ/uPݍmdp~J(m-vd-1\։MO̲TA cH%s*Mt ]εTA$:/L0aQ.5&7r.J0\b4=+#J9]I4o&rf<^k!)k@MtP; adд&`90st |9'LPF욣&lveAs?`Ƹ K/pt?*Lk/VE{\a=qOIb ƕ:D3B쇘RloPl:N,\mV FruNX,|6> (^@]n׷nQ<𭕶 Kw>4aU*ħb~+ڌwre So(Zj?H&@:?Q/ȿC *_t[%\ݼz-mZW/I#a-d[<[3M2 H9gdhWS5  GFC߷{EnASɃ۸g@⿥