Обновление клиента

This commit is contained in:
root
2026-03-05 13:40:40 +00:00
parent 34bcd34979
commit b8905de237
4147 changed files with 748711 additions and 7 deletions
@@ -0,0 +1,8 @@
<?php
namespace Hexogen\KDTree\Exception;
class ValidationException extends \Exception
{
}
+185
View File
@@ -0,0 +1,185 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemFactoryInterface;
use Hexogen\KDTree\Interfaces\KDTreeInterface;
use Hexogen\KDTree\Interfaces\NodeInterface;
class FSKDTree implements KDTreeInterface
{
/**
* @const int integer size in bytes
*/
const INT_LENGTH = 4;
/**
* @const int float size in bytes
*/
const FLOAT_LENGTH = 8;
/**
* @var NodeInterface
*/
private $root;
/**
* @var array
*/
private $maxBoundary;
/**
* @var array
*/
private $minBoundary;
/**
* @var int number of items in the tree
*/
private $length;
/**
* @var int
*/
private $dimensions;
/**
* @var
*/
private $handler;
/**
* @var ItemFactoryInterface
*/
private $factory;
/**
* FSKDTree constructor.
* @param $path
* @param ItemFactoryInterface $factory
*/
public function __construct($path, ItemFactoryInterface $factory)
{
$this->factory = $factory;
$this->handler = fopen($path, 'rb');
$this->readInitData();
}
/**
* FSKDTree destructor
*/
public function __destruct()
{
fclose($this->handler);
}
/**
* @return int
*/
public function getItemCount(): int
{
return $this->length;
}
/**
* @return NodeInterface
*/
public function getRoot(): ?NodeInterface
{
return $this->root;
}
/**
* @return array
*/
public function getMinBoundary(): array
{
return $this->minBoundary;
}
/**
* @return array
*/
public function getMaxBoundary(): array
{
return $this->maxBoundary;
}
/**
* @return int
*/
public function getDimensionCount(): int
{
return $this->dimensions;
}
/**
* Read binary data and convert it to an object
*/
private function readInitData()
{
$this->readDimensionsCount();
$this->readItemsCount();
$this->readUpperBound();
$this->readLowerBound();
$this->setRoot();
}
/**
* read num of dimensions in array
*/
private function readDimensionsCount()
{
$binData = fread($this->handler, FSKDTree::INT_LENGTH);
$this->dimensions = unpack('V', $binData)[1];
}
/**
* read number of items in the tree
*/
private function readItemsCount()
{
$binData = fread($this->handler, FSKDTree::INT_LENGTH);
$this->length = unpack('V', $binData)[1];
}
/**
* read upper boundary point
*/
private function readUpperBound()
{
$this->maxBoundary = $this->readPoint();
}
/**
* read lower boundary point
*/
private function readLowerBound()
{
$this->minBoundary = $this->readPoint();
}
/**
* set tree root
*/
private function setRoot()
{
if ($this->length == 0) {
return;
}
$position = ftell($this->handler);
$this->root = new FSNode($this->factory, $this->handler, $position, $this->dimensions);
}
/**
* Read point
* @return array
*/
private function readPoint(): array
{
$dataLength = FSKDTree::FLOAT_LENGTH * $this->dimensions;
$binData = fread($this->handler, $dataLength);
$dValues = unpack('d' . $this->dimensions, $binData);
return array_values($dValues);
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemFactoryInterface;
use Hexogen\KDTree\Interfaces\ItemInterface;
use Hexogen\KDTree\Interfaces\NodeInterface;
class FSNode implements NodeInterface
{
/**
* @var ItemInterface item that belongs to the node
*/
private $item;
/**
* @var NodeInterface|null link to the left node
*/
private $left;
/**
* @var int left node offset in file
*/
private $leftPosition;
/**
* @var NodeInterface|null right node link
*/
private $right;
/**
* @var int right node offset in the file
*/
private $rightPosition;
/**
* @var resource file handler
*/
private $handler;
/**
* @var int node start position in the file
*/
private $position;
/**
* @var ItemFactoryInterface item factory
*/
private $factory;
/**
* @var int num of dimensions it item
*/
private $dimensions;
/**
* FSNode constructor.
* @param ItemFactoryInterface $factory
* @param resource $handler file handler
* @param int $position node start position in the file
* @param int $dimensions number of dimensions in item
*/
public function __construct(ItemFactoryInterface $factory, $handler, int $position, int $dimensions)
{
$this->item = null;
$this->left = null;
$this->right = null;
$this->handler = $handler;
$this->position = $position;
$this->factory = $factory;
$this->dimensions = $dimensions;
}
/**
* @return ItemInterface get item from the node
*/
public function getItem() : ItemInterface
{
if ($this->item == null) {
$this->readNode();
}
return $this->item;
}
/**
* @param NodeInterface $node set right node
*/
public function setRight(NodeInterface $node): void
{
$this->right = $node;
}
/**
* @param NodeInterface $node set left node
*/
public function setLeft(NodeInterface $node): void
{
$this->left = $node;
}
/**
* Returns right node if it exists, null otherwise
* @return NodeInterface|null get right node
*/
public function getRight(): ?NodeInterface
{
if ($this->rightPosition === null) {
$this->readNode();
}
if ($this->right === null && $this->rightPosition !== 0) {
$rightNode = new FSNode($this->factory, $this->handler, $this->rightPosition, $this->dimensions);
$this->setRight($rightNode);
}
return $this->right;
}
/**
* Returns left node if it exists, null otherwise
* @return NodeInterface|null left node
*/
public function getLeft(): ?NodeInterface
{
if ($this->leftPosition === null) {
$this->readNode();
}
if ($this->left === null && $this->leftPosition !== 0) {
$leftNode = new FSNode($this->factory, $this->handler, $this->leftPosition, $this->dimensions);
$this->setLeft($leftNode);
}
return $this->left;
}
/**
* Read node data from the file
*/
private function readNode()
{
fseek($this->handler, $this->position);
$dataLength = FSKDTree::FLOAT_LENGTH * $this->dimensions;
$binData = fread($this->handler, FSKDTree::INT_LENGTH);
$itemId = unpack('V', $binData)[1];
$binData = fread($this->handler, FSKDTree::INT_LENGTH);
$this->leftPosition = unpack('V', $binData)[1];
$binData = fread($this->handler, FSKDTree::INT_LENGTH);
$this->rightPosition = unpack('V', $binData)[1];
$binData = fread($this->handler, $dataLength);
$dValues = unpack('d'.$this->dimensions, $binData);
$dValues = array_values($dValues);
$this->item = $this->factory->make($itemId, $dValues);
}
}
@@ -0,0 +1,183 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemInterface;
use Hexogen\KDTree\Interfaces\KDTreeInterface;
use Hexogen\KDTree\Interfaces\NodeInterface;
use Hexogen\KDTree\Interfaces\TreePersisterInterface;
class FSTreePersister implements TreePersisterInterface
{
/**
* @var string path to the file
*/
private $path;
/**
* @var resource file handler
*/
private $handler;
/**
* @var int
*/
private $dimensions;
/**
* @var int
*/
private $nodeMemorySize;
public function __construct(string $path)
{
$this->path = $path;
}
/**
* @api
* @param KDTreeInterface $tree
* @param string $identifier that identifies persisted tree(may be a filename, database name etc.)
* @return mixed
*/
public function convert(KDTreeInterface $tree, string $identifier)
{
$this->initTree($identifier);
$this->dimensions = $tree->getDimensionCount();
$this->calculateNodeSize();
$this->specifyNumberOfDimensions();
$this->specifyNumberOfItems($tree);
$upperBound = $tree->getMaxBoundary();
$this->writeCoordinate($upperBound);
$lowerBound = $tree->getMinBoundary();
$this->writeCoordinate($lowerBound);
$root = $tree->getRoot();
if ($root) {
$this->writeNode($root);
}
fclose($this->handler);
}
/**
* @param NodeInterface $node
*/
private function writeNode(NodeInterface $node)
{
$position = ftell($this->handler);
$item = $node->getItem();
$this->writeItemId($item);
$dataChunk = pack('V', 0); // left position currently unknown so it equal 0/null
fwrite($this->handler, $dataChunk);
$rightNode = $node->getRight();
$rightPosition = 0;
if ($rightNode) {
$rightPosition = $position + $this->nodeMemorySize;
}
$dataChunk = pack('V', $rightPosition);
fwrite($this->handler, $dataChunk);
$this->saveItemCoordinate($item);
if ($rightNode) {
$this->writeNode($rightNode);
}
$leftNode = $node->getLeft();
if ($leftNode == null) {
return;
}
$this->persistLeftLink($position);
$this->writeNode($leftNode);
}
/**
* @param array $coordinate
*/
private function writeCoordinate(array $coordinate)
{
$dataChunk = pack('d'.$this->dimensions, ...$coordinate);
fwrite($this->handler, $dataChunk);
}
/**
* @param string $identifier
*/
private function initTree(string $identifier)
{
$this->handler = fopen($this->path . '/' . $identifier, 'wb');
}
/**
* Calculate memory size in file needed for single node
*/
private function calculateNodeSize()
{
$this->nodeMemorySize = 3 * FSKDTree::INT_LENGTH + $this->dimensions * FSKDTree::FLOAT_LENGTH;
}
/**
* Specify number of dimensions according to file format
*/
private function specifyNumberOfDimensions()
{
$dataChunk = pack('V', $this->dimensions);
fwrite($this->handler, $dataChunk);
}
/**
* @param KDTreeInterface $tree
*/
private function specifyNumberOfItems(KDTreeInterface $tree)
{
$itemCount = $tree->getItemCount();
$dataChunk = pack('V', $itemCount);
fwrite($this->handler, $dataChunk);
}
/**
* @param $item
*/
private function saveItemCoordinate(ItemInterface $item)
{
$coordinate = [];
for ($i = 0; $i < $this->dimensions; $i++) {
$coordinate[] = $item->getNthDimension($i);
}
$this->writeCoordinate($coordinate);
}
/**
* Persist current position before writing left node
* @param int $position
*/
private function persistLeftLink(int $position)
{
$leftPosition = ftell($this->handler);
fseek($this->handler, $position + FSKDTree::INT_LENGTH);
$dataChunk = pack('V', $leftPosition);
fwrite($this->handler, $dataChunk);
fseek($this->handler, $leftPosition);
}
/**
* @param $item
*/
private function writeItemId(ItemInterface $item)
{
$itemId = $item->getId();
$dataChunk = pack('V', $itemId);
fwrite($this->handler, $dataChunk);
}
}
@@ -0,0 +1,16 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface ItemFactoryInterface
{
/**
* Create an instance of ItemInterface
* @api
* @param int $id
* @param array $dValues
* @return ItemInterface
*/
public function make(int $id, array $dValues) : ItemInterface;
}
@@ -0,0 +1,14 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface ItemInterface extends PointInterface
{
/**
* get item id
*
* @api
* @return int item id
*/
public function getId() : int;
}
@@ -0,0 +1,23 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface ItemListInterface
{
/**
* Add item to the list
* @api
* @param ItemInterface $item
*/
public function addItem(ItemInterface $item);
/**
* @return ItemInterface[] list of all items in the list
*/
public function getItems(): array;
/**
* @return int number of dimensions in items(points)
*/
public function getDimensionCount(): int;
}
@@ -0,0 +1,33 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface KDTreeInterface
{
/**
* @return int number of items in the set
*/
public function getItemCount() : int;
/**
* @return int number of point dimensions
*/
public function getDimensionCount() : int;
/**
* @return NodeInterface|null root node of the tree
*/
public function getRoot() : ?NodeInterface;
/**
* @return array lower corner coordinate of the virtual multidimensional
* orthogon that fits all points of the kd tree
*/
public function getMinBoundary() : array;
/**
* @return array upper corner coordinate of the virtual multidimensional
* orthogon that fits all points of the kd tree
*/
public function getMaxBoundary() : array;
}
@@ -0,0 +1,31 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface NodeInterface
{
/**
* @return ItemInterface
*/
public function getItem(): ItemInterface;
/**
* @param NodeInterface $node
*/
public function setRight(NodeInterface $node): void;
/**
* @param NodeInterface $node
*/
public function setLeft(NodeInterface $node): void;
/**
* @return NodeInterface|null
*/
public function getRight(): ?NodeInterface;
/**
* @return NodeInterface|null
*/
public function getLeft(): ?NodeInterface;
}
@@ -0,0 +1,21 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface PointInterface
{
/**
* get nth dimension value from vector
*
* @api
* @param int $d dimension of the point
* @return float
*/
public function getNthDimension(int $d): float;
/**
* @api
* @return int number of dimensions in array
*/
public function getDimensionsCount(): int;
}
@@ -0,0 +1,36 @@
<?php
namespace Hexogen\KDTree\Interfaces;
abstract class SearchAbstract
{
/**
* @var KDTreeInterface
*/
protected $tree;
/**
* @var int
*/
protected $dimensions;
/**
* SearchAbstract constructor.
* @param KDTreeInterface $tree
*/
public function __construct(KDTreeInterface $tree)
{
$this->tree = $tree;
$this->dimensions = $tree->getDimensionCount();
}
/**
* Search items it the tree by given algorithm
*
* @api
* @param PointInterface $point
* @param int $resultLength
* @return array
*/
abstract public function search(PointInterface $point, int $resultLength = 1) : array;
}
@@ -0,0 +1,14 @@
<?php
namespace Hexogen\KDTree\Interfaces;
interface TreePersisterInterface
{
/**
* @api
* @param KDTreeInterface $tree
* @param string $identifier that identifies persisted tree(may be a filename, database name etc.)
* @return mixed
*/
public function convert(KDTreeInterface $tree, string $identifier);
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemInterface;
class Item extends Point implements ItemInterface
{
private $id;
/**
* Item constructor.
* @param int $id
* @param array $dValues
*/
public function __construct(int $id, array $dValues)
{
parent::__construct($dValues);
$this->id = $id;
}
/**
* get item id
*
* @api
* @return int item id
*/
public function getId() : int
{
return $this->id;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemFactoryInterface;
use Hexogen\KDTree\Interfaces\ItemInterface;
class ItemFactory implements ItemFactoryInterface
{
/**
* @api
* @param int $id
* @param array $dValues
* @return ItemInterface
*/
public function make(int $id, array $dValues) : ItemInterface
{
return new Item($id, $dValues);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Exception\ValidationException;
use Hexogen\KDTree\Interfaces\ItemInterface;
use Hexogen\KDTree\Interfaces\ItemListInterface;
class ItemList implements ItemListInterface
{
private $dimensions;
private $items;
private $ids;
private $lastPosition;
/**
* ItemList constructor.
* @param int $dimensions
* @throws ValidationException
*/
public function __construct(int $dimensions)
{
if ($dimensions <= 0) {
throw new ValidationException('$dimensions should be bigger than 0');
}
$this->lastPosition = 0;
$this->dimensions = $dimensions;
$this->items = [];
$this->ids = [];
}
/**
* Add or replace an item in the item list
*
* @api
* @param ItemInterface $item
*/
public function addItem(ItemInterface $item)
{
$this->validateItem($item);
$id = $item->getId();
if (isset($this->ids[$id])) {
$index = $this->ids[$id];
$this->items[$index] = $item;
} else {
$this->items[] = $item;
$this->ids[$id] = $this->lastPosition++;
}
}
/**
* Get all items in the list
* @return ItemInterface[]
*/
public function getItems(): array
{
return $this->items;
}
/**
* @return int number of dimensions in item
*/
public function getDimensionCount(): int
{
return $this->dimensions;
}
/**
* @param ItemInterface $item
* @throws ValidationException
*/
private function validateItem(ItemInterface $item)
{
if ($item->getDimensionsCount() !== $this->dimensions) {
throw new ValidationException('$dValues number dimensions should be equal to ' . $this->dimensions);
}
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\ItemInterface;
use Hexogen\KDTree\Interfaces\ItemListInterface;
use Hexogen\KDTree\Interfaces\KDTreeInterface;
use Hexogen\KDTree\Interfaces\NodeInterface;
class KDTree implements KDTreeInterface
{
/**
* @var NodeInterface
*/
private $root;
/**
* @var ItemInterface[]|null array of items or null after tree has been built
*/
private $items;
/**
* @var array
*/
private $maxBoundary;
/**
* @var array
*/
private $minBoundary;
/**
* @var int number of items in the tree
*/
private $length;
/**
* @var int
*/
private $dimensions;
/**
* KDTree constructor.
* @param ItemListInterface $itemList
*/
public function __construct(ItemListInterface $itemList)
{
$this->dimensions = $itemList->getDimensionCount();
$this->items = $itemList->getItems();
$this->length = count($this->items);
$this->setBoundaries($this->items);
$this->buildTree();
unset($this->items);
}
/**
* Get number of items in the tree
* @return int
*/
public function getItemCount(): int
{
return $this->length;
}
/**
* Get root node
* @return NodeInterface|null return node or null if there is no nodes in the tree
*/
public function getRoot(): ?NodeInterface
{
return $this->root;
}
/**
* Get lower boundary coordinate
* @return array
*/
public function getMinBoundary(): array
{
return $this->minBoundary;
}
/**
* Get upper boundary coordinate
* @return array
*/
public function getMaxBoundary(): array
{
return $this->maxBoundary;
}
/**
* Get number of dimensions in the tree
* @return int
*/
public function getDimensionCount(): int
{
return $this->dimensions;
}
/**
* @param int $lo
* @param int $hi
* @param int $d
* @return Node
*/
private function buildSubTree(int $lo, int $hi, int $d): Node
{
$mid = (int)(($hi - $lo) / 2) + $lo;
$item = $this->select($mid, $lo, $hi, $d);
$node = new Node($item);
$d++;
$d = $d % $this->dimensions;
if ($mid > $lo) {
$node->setLeft($this->buildSubTree($lo, $mid - 1, $d));
}
if ($mid < $hi) {
$node->setRight($this->buildSubTree($mid + 1, $hi, $d));
}
return $node;
}
private function exch(int $i, int $j)
{
$tmp = $this->items[$i];
$this->items[$i] = $this->items[$j];
$this->items[$j] = $tmp;
}
/**
* @param int $k
* @param int $lo
* @param int $hi
* @param int $d
* @return ItemInterface
*/
private function select(int $k, int $lo, int $hi, int $d)
{
while ($hi > $lo) {
$j = $this->partition($lo, $hi, $d);
if ($j > $k) {
$hi = $j - 1;
} elseif ($j < $k) {
$lo = $j + 1;
} else {
return $this->items[$k];
}
}
return $this->items[$k];
}
/**
* @param int $lo
* @param int $hi
* @param int $d
* @return int
*/
private function partition(int $lo, int $hi, int $d)
{
$i = $lo;
$j = $hi + 1;
$v = $this->items[$lo];
$val = $v->getNthDimension($d);
do {
while ($this->items[++$i]->getNthDimension($d) < $val && $i != $hi) {
}
while ($this->items[--$j]->getNthDimension($d) > $val) {
}
if ($i < $j) {
$this->exch($i, $j);
}
} while ($i < $j);
$this->exch($lo, $j);
return $j;
}
/**
* Set boundaries for given item list
* @param ItemInterface[] $items
*/
private function setBoundaries(array $items)
{
$this->maxBoundary = [];
$this->minBoundary = [];
for ($i = 0; $i < $this->dimensions; $i++) {
$this->maxBoundary[$i] = -INF;
$this->minBoundary[$i] = INF;
}
foreach ($items as $item) {
for ($i = 0; $i < $this->dimensions; $i++) {
$this->maxBoundary[$i] = max($this->maxBoundary[$i], $item->getNthDimension($i));
$this->minBoundary[$i] = min($this->minBoundary[$i], $item->getNthDimension($i));
}
}
}
/**
* Build kd tree
*/
private function buildTree()
{
if ($this->length > 0) {
$hi = $this->length - 1;
$mid = (int)($hi / 2);
$item = $this->select($mid, 0, $hi, 0);
$this->root = new Node($item);
$nextDimension = 1 % $this->dimensions;
if ($mid > 0) {
$this->root->setLeft($this->buildSubTree(0, $mid - 1, $nextDimension));
}
if ($mid < $hi) {
$this->root->setRight($this->buildSubTree($mid + 1, $hi, $nextDimension));
}
}
}
}
+446
View File
@@ -0,0 +1,446 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Exception\ValidationException;
use Hexogen\KDTree\Interfaces\ItemInterface;
use Hexogen\KDTree\Interfaces\NodeInterface;
use Hexogen\KDTree\Interfaces\PointInterface;
use Hexogen\KDTree\Interfaces\SearchAbstract;
use SplPriorityQueue;
class NearestSearch extends SearchAbstract
{
/**
* @var SplPriorityQueue
*/
private $queue;
/**
* @var float
*/
private $maxQueuedDistance;
/**
* @var PointInterface
*/
private $point;
/**
* Search items it the tree by given algorithm
*
* @api
* @param PointInterface $point
* @param int $resultLength
* @return ItemInterface[]
*/
public function search(PointInterface $point, int $resultLength = 1) : array
{
$this->validatePoint($point);
$this->point = $point;
$upperBound = $this->tree->getMaxBoundary();
$lowerBound = $this->tree->getMinBoundary();
$root = $this->tree->getRoot();
if ($root == null) {
return [];
}
/**
* @var array orthogonal square distances to the point
*/
$orthogonalDistances = $this->getOrthogonalDistances($point, $upperBound, $lowerBound);
/**
* @var float possible Euclidean distance
*/
$possibleDistance = $this->getPossibleDistance($orthogonalDistances);
$this->prepareQueue($resultLength);
$this->searchNearest($root, 0, $upperBound, $lowerBound, $orthogonalDistances, $possibleDistance);
return $this->getItemsFromQueue();
}
/**
* Check that point has same number of dimensions that all items in the tree
*
* @param PointInterface $point
* @throws ValidationException
*/
private function validatePoint(PointInterface $point)
{
if ($point->getDimensionsCount() !== $this->tree->getDimensionCount()) {
throw new ValidationException(
'point dimensions count should be equal to ' . $this->tree->getDimensionCount()
);
}
}
/**
* Get orthogonal distances array from the point to multidimensional space that holds all the items in the tree
*
* @param PointInterface $point
* @param array $upperBound
* @param array $lowerBound
* @return array
*/
private function getOrthogonalDistances(PointInterface $point, array $upperBound, array $lowerBound): array
{
$orthogonalDistances = [];
for ($i = 0; $i < $this->dimensions; $i++) {
$coordinate = $point->getNthDimension($i);
$orthogonalDistances[$i] = $this->getPossibleOrthogonalDistance(
$coordinate,
$upperBound[$i],
$lowerBound[$i]
);
}
return $orthogonalDistances;
}
/**
* Calculate minimal possible Euclidean distance from the point to an item
*
* @param $orthogonalDistances
* @return float
*/
private function getPossibleDistance(array $orthogonalDistances) : float
{
$possibleDistance = 0.;
foreach ($orthogonalDistances as $orthogonalDistance) {
$possibleDistance += $orthogonalDistance;
}
return $possibleDistance;
}
/**
* Prepare queue for collecting nearest items.
* Queue size sets to be equal to result length or to tree size,
* if request result length is bigger then tree size
*
* @param int $resultLength
* @return SplPriorityQueue
*/
private function prepareQueue(int $resultLength)
{
$this->queue = new SplPriorityQueue();
$this->queue->setExtractFlags(SplPriorityQueue::EXTR_PRIORITY);
$itemsInTree = $this->tree->getItemCount();
if ($itemsInTree < $resultLength) {
$resultLength = $itemsInTree;
}
for ($i = 0; $i < $resultLength; $i++) {
$this->queue->insert(null, INF);
}
$this->maxQueuedDistance = INF;
}
/**
* Add an item to the queue if distance to the point is less than max queued,
* after it removes an item with the biggest distance, to keep queue size constant
*
* @param ItemInterface $item
* @param float $distance
*/
private function addToQueue(ItemInterface $item, float $distance)
{
if ($distance >= $this->maxQueuedDistance) {
return;
}
$this->queue->insert($item, $distance);
$this->queue->extract();
$this->maxQueuedDistance = $this->queue->current();
}
/**
* Calculate Euclidean distance between point and item
*
* @param ItemInterface $item
* @param PointInterface $point
* @return float
*/
private function calculateDistance(ItemInterface $item, PointInterface $point) : float
{
$distance = 0.;
for ($i = 0; $i < $this->dimensions; $i++) {
$distance += pow($item->getNthDimension($i) - $point->getNthDimension($i), 2);
}
return $distance;
}
/**
* Recursive search of N closest item in the tree to the given point
*
* @param NodeInterface $node
* @param int $dimension
* @param array $upperBound
* @param array $lowerBound
* @param array $orthogonalDistances
* @param float $possibleDistance
*/
private function searchNearest(
NodeInterface $node,
int $dimension,
array $upperBound,
array $lowerBound,
array $orthogonalDistances,
float $possibleDistance
) {
$item = $node->getItem();
$distance = $this->calculateDistance($item, $this->point);
$this->addToQueue($item, $distance);
$rightLowerBound = $lowerBound;
$leftUpperBound = $upperBound;
$rightLowerBound[$dimension] = $item->getNthDimension($dimension);
$leftUpperBound[$dimension] = $item->getNthDimension($dimension);
$rightNode = $node->getRight();
$leftNode = $node->getLeft();
if ($rightNode && $leftNode) {
$this->smartBranchesSearch(
$rightNode,
$leftNode,
$dimension,
$upperBound,
$rightLowerBound,
$leftUpperBound,
$lowerBound,
$orthogonalDistances,
$possibleDistance
);
return;
}
if ($rightNode) {
$this->branchSearch(
$rightNode,
$dimension,
$upperBound,
$rightLowerBound,
$orthogonalDistances,
$possibleDistance
);
}
if ($leftNode) {
$this->branchSearch(
$leftNode,
$dimension,
$leftUpperBound,
$lowerBound,
$orthogonalDistances,
$possibleDistance
);
}
}
/**
* Get Euclidean distance between point and an item in given dimension
*
* @param $pointCoordinate
* @param $upperBound
* @param $lowerBound
* @return float|number
*/
private function getPossibleOrthogonalDistance($pointCoordinate, $upperBound, $lowerBound)
{
if ($pointCoordinate > $upperBound) {
return pow($pointCoordinate - $upperBound, 2);
} elseif ($pointCoordinate < $lowerBound) {
return pow($lowerBound - $pointCoordinate, 2);
}
return 0.;
}
/**
* Recursive search in the given branch
*
* @param NodeInterface $branchNode
* @param int $dimension
* @param array $upperBound
* @param array $lowerBound
* @param array $orthogonalDistances
* @param float $possibleDistance
*/
private function branchSearch(
NodeInterface $branchNode,
int $dimension,
array $upperBound,
array $lowerBound,
array $orthogonalDistances,
float $possibleDistance
) {
// possible orthogonal distances to the right node
$branchOrthogonalDistances = $orthogonalDistances;
$branchPossibleDistance = $possibleDistance;
$nextDimension = ($dimension + 1) % $this->dimensions;
$branchOrthogonalDistances[$dimension] = $this->getPossibleOrthogonalDistance(
$this->point->getNthDimension($dimension),
$upperBound[$dimension],
$lowerBound[$dimension]
);
if ($orthogonalDistances[$dimension] != $branchOrthogonalDistances[$dimension]) {
$branchPossibleDistance += $branchOrthogonalDistances[$dimension] - $orthogonalDistances[$dimension];
}
if ($branchPossibleDistance <= $this->maxQueuedDistance) {
$this->searchNearest(
$branchNode,
$nextDimension,
$upperBound,
$lowerBound,
$branchOrthogonalDistances,
$branchPossibleDistance
);
}
}
/**
* extract all items from queue
*/
private function getItemsFromQueue() : array
{
$items = [];
$this->queue->setExtractFlags(SplPriorityQueue::EXTR_DATA);
while (!$this->queue->isEmpty()) {
$items[] = $this->queue->extract();
}
return array_reverse($items);
}
/**
* Nearest branch first approach
*
* @param NodeInterface $rightNode
* @param NodeInterface $leftNode
* @param int $dimension
* @param array $upperBound
* @param array $rightLowerBound
* @param array $leftUpperBound
* @param array $lowerBound
* @param array $orthogonalDistances
* @param float $possibleDistance
*/
private function smartBranchesSearch(
NodeInterface $rightNode,
NodeInterface $leftNode,
int $dimension,
array $upperBound,
array $rightLowerBound,
array $leftUpperBound,
array $lowerBound,
array $orthogonalDistances,
float $possibleDistance
) {
// possible orthogonal distances to the right node
$leftOrthogonalDistances = $rightOrthogonalDistances = $orthogonalDistances;
$leftPossibleDistance = $rightPossibleDistance = $possibleDistance;
$nextDimension = ($dimension + 1) % $this->dimensions;
$leftOrthogonalDistances[$dimension] = $this->getPossibleOrthogonalDistance(
$this->point->getNthDimension($dimension),
$leftUpperBound[$dimension],
$lowerBound[$dimension]
);
$rightOrthogonalDistances[$dimension] = $this->getPossibleOrthogonalDistance(
$this->point->getNthDimension($dimension),
$upperBound[$dimension],
$rightLowerBound[$dimension]
);
if ($orthogonalDistances[$dimension] != $leftOrthogonalDistances[$dimension]) {
$leftPossibleDistance += $leftOrthogonalDistances[$dimension] - $orthogonalDistances[$dimension];
}
if ($orthogonalDistances[$dimension] != $rightOrthogonalDistances[$dimension]) {
$rightPossibleDistance += $rightOrthogonalDistances[$dimension] - $orthogonalDistances[$dimension];
}
if ($leftPossibleDistance < $rightPossibleDistance) {
$this->prioritySearch(
$leftNode,
$rightNode,
$leftUpperBound,
$lowerBound,
$upperBound,
$rightLowerBound,
$leftOrthogonalDistances,
$rightOrthogonalDistances,
$leftPossibleDistance,
$rightPossibleDistance,
$nextDimension
);
return;
}
$this->prioritySearch(
$rightNode,
$leftNode,
$upperBound,
$rightLowerBound,
$leftUpperBound,
$lowerBound,
$rightOrthogonalDistances,
$leftOrthogonalDistances,
$rightPossibleDistance,
$leftPossibleDistance,
$nextDimension
);
}
public function prioritySearch(
NodeInterface $firstNode,
NodeInterface $secondNode,
array $firstUpperBound,
array $firstLowerBound,
array $secondUpperBound,
array $secondLowerBound,
array $firstOrthogonalDistances,
array $secondOrthogonalDistances,
float $firstPossibleDistance,
float $secondPossibleDistance,
int $nextDimension
) {
if ($firstPossibleDistance < $this->maxQueuedDistance) {
$this->searchNearest(
$firstNode,
$nextDimension,
$firstUpperBound,
$firstLowerBound,
$firstOrthogonalDistances,
$firstPossibleDistance
);
if ($secondPossibleDistance < $this->maxQueuedDistance) {
$this->searchNearest(
$secondNode,
$nextDimension,
$secondUpperBound,
$secondLowerBound,
$secondOrthogonalDistances,
$secondPossibleDistance
);
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Interfaces\NodeInterface;
use Hexogen\KDTree\Interfaces\ItemInterface;
class Node implements NodeInterface
{
/**
* @var ItemInterface
*/
private $item;
/**
* @var NodeInterface|null
*/
private $left;
/**
* @var NodeInterface|null
*/
private $right;
/**
* Node constructor.
* @param ItemInterface $item
*/
public function __construct(ItemInterface $item)
{
$this->item = $item;
$this->left = null;
$this->right = null;
}
/**
* @return ItemInterface get item from the node
*/
public function getItem() : ItemInterface
{
return $this->item;
}
/**
* @param NodeInterface $node set right node
*/
public function setRight(NodeInterface $node): void
{
$this->right = $node;
}
/**
* @param NodeInterface $node set left node
*/
public function setLeft(NodeInterface $node): void
{
$this->left = $node;
}
/**
* @return NodeInterface|null get right node
*/
public function getRight(): ?NodeInterface
{
return $this->right;
}
/**
* @return NodeInterface|null get left node
*/
public function getLeft(): ?NodeInterface
{
return $this->left;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace Hexogen\KDTree;
use Hexogen\KDTree\Exception\ValidationException;
use Hexogen\KDTree\Interfaces\PointInterface;
class Point implements PointInterface
{
private $dValues;
private $length;
/**
* Item constructor.
* @param array $dValues
*/
public function __construct(array $dValues)
{
$this->length = count($dValues);
$this->validateDValues($dValues);
$this->dValues = $dValues;
}
/**
* get nth dimension value from vector
*
* @api
* @param int $d
* @return float
*/
public function getNthDimension(int $d): float
{
if ($d < 0 || $d >= $this->length) {
throw new \OutOfRangeException('$d = ' . $d . ' should be between 0 and number of ' . $this->length);
}
return (float)$this->dValues[$d];
}
/**
* validate multi dimension vector
*
* @param array $dValues
* @throws ValidationException
*/
private function validateDValues(array $dValues)
{
if ($this->length == 0) {
throw new ValidationException('$dValues should be not empty');
}
for ($i = 0; $i < $this->length; $i++) {
if (!isset($dValues[$i]) || !is_numeric($dValues[$i])) {
throw new ValidationException('$dValues is not a simple array list');
}
}
}
/**
* @api
* @return int number of dimensions in the point
*/
public function getDimensionsCount(): int
{
return $this->length;
}
}