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

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
+15
View File
@@ -0,0 +1,15 @@
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at volodymyrbas@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
@@ -0,0 +1,32 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
We accept contributions via Pull Requests on [Github](https://github.com/hexogen/kdtree).
## Pull Requests
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``.
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
- **Create feature branches** - Don't ask us to pull from your master branch.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
## Running Tests
``` bash
$ composer test
```
**Happy coding**!
@@ -0,0 +1,17 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the bug.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,43 @@
name: Tests
on:
push:
pull_request:
jobs:
tests:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3']
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 10
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- name: Setup Problem Matchers
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP Dependencies
uses: nick-invision/retry@v2
with:
timeout_minutes: 5
max_attempts: 5
command: composer update --no-interaction --no-progress
- name: Execute PHPcs
run: vendor/bin/phpcs --standard=psr2 src/
- name: Execute PHPUnit
run: vendor/bin/phpunit
+21
View File
@@ -0,0 +1,21 @@
# Changelog
All Notable changes to `hexogen/kdtree` will be documented in this file.
Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## v0.1.1- 2018-09-04
### Added
- Support phpunit v7.x
### Deprecated
- PHP 7.0 support
## v0.2.0- 2018-12-23
### Changed
- PHP 7.0 support removed
- Interfaces now returns types according to documentation (where it was not possible accoridng
php 7.0 limitations)
+21
View File
@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2016 Volodymyr Basarab <volodymyrbas@gmail.com>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.
+126
View File
@@ -0,0 +1,126 @@
# K-D Tree
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Build Status][ico-tests]][link-tests]
[![Software License][ico-license]](LICENSE.md)
[![Total Downloads][ico-downloads]][link-downloads]
PHP multidimensional K-D Tree implementation.
To receive all benefits from K-D Tree, use file system implementation(FSKDTree). FSKDTree stores tree in binary format and uses lazy loading while traversing through nodes. Current approach provides much higher performance compared
to deserialization.
## Install
Via Composer
``` bash
$ composer require hexogen/kdtree
```
## Usage
### Tree creation
``` php
//Item container with 2 dimensional points
$itemList = new ItemList(2);
//Adding 2 - dimension items to the list
$itemList->addItem(new Item(1, [1.2, 4.3]));
$itemList->addItem(new Item(2, [1.3, 3.4]));
$itemList->addItem(new Item(3, [4.5, 1.2]));
$itemList->addItem(new Item(4, [5.2, 3.5]));
$itemList->addItem(new Item(5, [2.1, 3.6]));
//Building tree with given item list
$tree = new KDTree($itemList);
```
### Searching nearest items to the given point
``` php
//Creating search engine with custom algorithm (currently Nearest Search)
$searcher = new NearestSearch($tree);
//Retrieving a result ItemInterface[] array with given size (currently 2)
$result = $searcher->search(new Point([1.25, 3.5]), 2);
echo $result[0]->getId(); // 2
echo $result[0]->getNthDimension(0); // 1.3
echo $result[0]->getNthDimension(1); // 3.4
echo $result[1]->getId(); // 1
echo $result[1]->getNthDimension(0); // 1.2
echo $result[1]->getNthDimension(1); // 4.3
```
### Persist tree to a binary file
``` php
//Init tree writer
$persister = new FSTreePersister('/path/to/dir');
//Save the tree to /path/to/dir/treeName.bin
$persister->convert($tree, 'treeName.bin');
```
### File system version of the tree
``` php
//ItemInterface factory
$itemFactory = new ItemFactory();
//Then init new instance of file system version of the tree
$fsTree = new FSKDTree('/path/to/dir/treeName.bin', $itemFactory);
//Now use fs kdtree to search
$fsSearcher = new NearestSearch($fsTree);
//Retrieving a result ItemInterface[] array with given size (currently 2)
$result = $fsSearcher->search(new Point([1.25, 3.5]), 2);
echo $result[0]->getId(); // 2
echo $result[1]->getId(); // 1
```
## Change log
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Testing
``` bash
$ composer test
```
## Contributing
Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
## Security
If you discover any security related issues, please email volodymyrbas@gmail.com instead of using the issue tracker.
## Credits
- [Volodymyr Basarab][link-author]
- [All Contributors][link-contributors]
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/hexogen/kdtree.svg?style=flat-square
[ico-tests]: https://img.shields.io/github/actions/workflow/status/hexogen/kdtree/tests.yml?branch=master
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/hexogen/kdtree.svg?style=flat-square
[link-packagist]: https://packagist.org/packages/hexogen/kdtree
[link-tests]: https://github.com/hexogen/kdtree/actions?query=workflow%3ATests
[link-downloads]: https://packagist.org/packages/hexogen/kdtree
[link-author]: https://github.com/hexogen
[link-contributors]: ../../contributors
+54
View File
@@ -0,0 +1,54 @@
{
"name": "hexogen/kdtree",
"type": "library",
"description": "file system KDTree index",
"keywords": [
"hexogen",
"kdtree",
"data structures",
"algorithms",
"search"
],
"homepage": "https://github.com/hexogen/kdtree",
"license": "MIT",
"authors": [
{
"name": "Volodymyr Basarab",
"email": "volodymyrbas@gmail.com",
"homepage": "https://github.com/hexogen",
"role": "Developer"
}
],
"require": {
"php" : "^7.1|^8.0"
},
"require-dev": {
"league/csv": "^9.7.0",
"mockery/mockery": "dev-main",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5.0"
},
"autoload": {
"psr-4": {
"Hexogen\\KDTree\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Hexogen\\KDTree\\Tests\\": "tests"
}
},
"scripts": {
"test": "phpunit",
"check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests",
"fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests"
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"config": {
"sort-packages": true
}
}
@@ -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;
}
}