*/ public static function getTree(int ...$startLocationId): array { static $treeCache = null; if ($treeCache === null) { $treeCache = self::buildTree(self::queryLocations()); } if (empty($startLocationId) || in_array(0, $startLocationId)) return $treeCache; // Subtree, search return self::filterTreeByLocations($treeCache, $startLocationId); } /** * Return a filtered tree that only contains the locations present in $idList, plus their respective * child locations in the 'children' array key, if applicable. Top level elements will only be those * contained in $idList, but not every location given in $idList will be a top level element, as they * might already be present in one of the nested 'children' arrays. * @param array $locationTree part of tree to search in * @param array $idList list of wanted ids * @return array filtered tree */ private static function filterTreeByLocations(array $locationTree, array $idList): array { $ret = []; foreach ($locationTree as $location) { if (in_array($location['locationid'], $idList)) { $ret[] = $location; } elseif (!empty($location['children'])) { $ret = array_merge($ret, self::filterTreeByLocations($location['children'], $idList)); } } return $ret; } /** * @return array */ private static function buildTree(array $elements, int $parentId = 0): array { $branch = array(); foreach ($elements as $lid => $element) { $lid = (int)$lid; if ($lid === 0 || $lid === $parentId) continue; if ($element['parentlocationid'] === $parentId) { $children = self::buildTree($elements, $lid); if (!empty($children)) { $element['children'] = $children; } $element['locationid'] = $lid; $branch[] = $element; } } ArrayUtil::sortByColumn($branch, 'locationname'); return $branch; } private static function queryLocations(): array { static $locationDbCache = null; if ($locationDbCache === null) { $locationDbCache = Database::queryIndexedList("SELECT locationid, parentlocationid, locationname FROM location"); foreach ($locationDbCache as &$loc) { $loc['parentlocationid'] = (int)$loc['parentlocationid']; } } return $locationDbCache; } /** * Get all location ids starting from the specified location id. * If $startId is 0, it will return all available location ids. * If $withStart is true, the starting location id will be included in the result. * * @param int $startId The location id to start from (default is 0 to get all location ids). * @param bool $withStart True to include the starting location id in the result (default is false). * @return int[] An array of location ids, including the starting location id if requested. */ public static function getAllLocationIds(int $startId = 0, bool $withStart = false): array { if ($startId === 0) { $locs = array_keys(self::queryLocations()); } else { $locs = self::getLocationsAssoc(); if (!isset($locs[$startId])) return []; $locs = $locs[$startId]['children'] ?? []; } if ($withStart) { $locs[] = $startId; } return $locs; } /** * Return row from location table for $locationId. * @return array|bool row from DB, false if not found */ public static function get(int $locationId) { return Database::queryFirst("SELECT * FROM location WHERE locationid = :locationId", compact('locationId')); } /** * Get name of location * @param int $locationId id of location to get name for * @return string|false Name of location, false if locationId doesn't exist */ public static function getName(int $locationId) { $locs = self::queryLocations(); return $locs[$locationId]['locationname'] ?? false; } /** * Get all the names of the given location and its parents, up * to the root element. Array keys will be locationids, value the names. * @return ?array locations, from furthest to nearest or false if locationId doesn't exist */ public static function getNameChain(int $locationId): ?array { $locs = self::queryLocations(); $ret = []; while (isset($locs[$locationId])) { $ret[$locationId] = $locs[$locationId]['locationname']; $locationId = $locs[$locationId]['parentlocationid']; } if (empty($ret)) return null; return array_reverse($ret, true); } /** * Get associative array of locations. Mostly used for working with locations, in contrast to ::getLocations() * @return array All the locations */ public static function getLocationsAssoc(): array { if (self::$assocLocationCache === null) { self::$assocLocationCache = self::flattenTreeAssoc(self::getTree()); } return self::$assocLocationCache; } /** * @return array All the locations */ private static function flattenTreeAssoc(array $tree, array $parents = [], int $depth = 0): array { $output = []; foreach ($tree as $node) { $cc = empty($node['children']) ? [] : array_column($node['children'], 'locationid'); $output[$node['locationid']] = [ 'locationid' => $node['locationid'], 'parentlocationid' => $node['parentlocationid'], 'parents' => $parents, 'children' => $cc, 'directchildren' => $cc, 'locationname' => $node['locationname'], 'depth' => $depth, 'isleaf' => empty($cc), ]; if (!empty($node['children'])) { $childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, [$node['locationid']]), $depth + 1); $output[$node['locationid']]['children'] = array_merge($output[$node['locationid']]['children'], array_reduce($childNodes, function ($carry, $item) { return array_merge($carry, $item['children']); }, [])); $output += $childNodes; } } return $output; } private static function buildFlatTree(): void { if (self::$flatLocationCache === null) { self::$flatLocationCache = self::flattenTree(self::getTree()); } } /** * get locations as flat array - mostly used for rendering a