Обновление клиента
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Hexogen\KDTree\Exception;
|
||||
|
||||
class ValidationException extends \Exception
|
||||
{
|
||||
}
|
||||
+185
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user