mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-12 02:08:40 +00:00
Compare commits
6 Commits
master
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bb778faf3 | |||
| 02fd06188f | |||
| eede030e76 | |||
| 79f5b646dc | |||
| 7c24bdbbcc | |||
| c67ac5befa |
37
.gitattributes
vendored
37
.gitattributes
vendored
|
|
@ -1,37 +0,0 @@
|
|||
* text=false
|
||||
*.php ident
|
||||
|
||||
# Force the following filetypes to have unix eols, so Windows does not break them
|
||||
*.php text eol=lf
|
||||
*.js text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.css text eol=lf
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.htaccess text eol=lf
|
||||
*.json text eol=lf
|
||||
*.c text eol=lf
|
||||
*.cpp text eol=lf
|
||||
*.h text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.flv binary
|
||||
*.fla binary
|
||||
*.swf binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.pyc binary
|
||||
*.pdf binary
|
||||
*.dat binary
|
||||
*.z binary
|
||||
1
.github/contributing.md
vendored
Normal file
1
.github/contributing.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
A developer and contribution documentation can be found at https://orange-management.gitbooks.io/developer-guide/content/index.html.
|
||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
|
@ -1,11 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "composer" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
0
tests/Module/Testmodule/Application/Moduletestapplication/index.tpl.php → .github/issue_template.md
vendored
Executable file → Normal file
0
tests/Module/Testmodule/Application/Moduletestapplication/index.tpl.php → .github/issue_template.md
vendored
Executable file → Normal file
13
.github/workflows/greetings.yml
vendored
13
.github/workflows/greetings.yml
vendored
|
|
@ -1,13 +0,0 @@
|
|||
name: Greetings
|
||||
|
||||
on: [pull_request, issues]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thank you for createing this issue. We will check it as soon as possible.'
|
||||
pr-message: 'Thank you for your pull request. We will check it as soon as possible.'
|
||||
24
.github/workflows/image.yml
vendored
24
.github/workflows/image.yml
vendored
|
|
@ -1,24 +0,0 @@
|
|||
name: Compress images
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.jpg'
|
||||
- '**.png'
|
||||
- '**.webp'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.jpg'
|
||||
- '**.png'
|
||||
- '**.webp'
|
||||
jobs:
|
||||
build:
|
||||
name: calibreapp/image-actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: Compress Images
|
||||
uses: calibreapp/image-actions@main
|
||||
with:
|
||||
githubToken: ${{ secrets.GH_TOKEN }}
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
|
|
@ -1,11 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
general_module_workflow_php:
|
||||
uses: Karaka-Management/Karaka/.github/workflows/php_template.yml@develop
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
18
.gitignore
vendored
Executable file → Normal file
18
.gitignore
vendored
Executable file → Normal file
|
|
@ -1,17 +1 @@
|
|||
*.log
|
||||
.directory
|
||||
Build
|
||||
*.cache
|
||||
.directory
|
||||
Vagrantfile
|
||||
vendor
|
||||
bower_components
|
||||
node_modules
|
||||
*.log
|
||||
.vagrant
|
||||
.vscode
|
||||
.sass-cache
|
||||
cache
|
||||
Cache
|
||||
Libraries
|
||||
.idea*.cache
|
||||
*.log
|
||||
508
Account/Account.php
Executable file → Normal file
508
Account/Account.php
Executable file → Normal file
|
|
@ -1,20 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
use phpOMS\Contract\ArrayableInterface;
|
||||
use phpOMS\Localization\Localization;
|
||||
use phpOMS\Localization\NullLocalization;
|
||||
use phpOMS\Validation\Network\Email;
|
||||
|
||||
/**
|
||||
|
|
@ -23,20 +25,21 @@ use phpOMS\Validation\Network\Email;
|
|||
* The account class is the base model for accounts. This model contains the most common account
|
||||
* information. This model is not comparable to a profile which contains much more information.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Account implements \JsonSerializable
|
||||
class Account implements ArrayableInterface, \JsonSerializable
|
||||
{
|
||||
|
||||
/**
|
||||
* Id.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $id = 0;
|
||||
protected $id = 0;
|
||||
|
||||
/**
|
||||
* Names.
|
||||
|
|
@ -44,7 +47,7 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name1 = '';
|
||||
protected $name1 = '';
|
||||
|
||||
/**
|
||||
* Names.
|
||||
|
|
@ -52,7 +55,7 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name2 = '';
|
||||
protected $name2 = '';
|
||||
|
||||
/**
|
||||
* Names.
|
||||
|
|
@ -60,7 +63,7 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name3 = '';
|
||||
protected $name3 = '';
|
||||
|
||||
/**
|
||||
* Email.
|
||||
|
|
@ -68,7 +71,7 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $email = '';
|
||||
protected $email = '';
|
||||
|
||||
/**
|
||||
* Ip.
|
||||
|
|
@ -78,15 +81,15 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $origin = '';
|
||||
protected $origin = '';
|
||||
|
||||
/**
|
||||
* Login.
|
||||
*
|
||||
* @var null|string
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?string $login = null;
|
||||
protected $login = '';
|
||||
|
||||
/**
|
||||
* Last activity.
|
||||
|
|
@ -94,23 +97,31 @@ class Account implements \JsonSerializable
|
|||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $lastActive;
|
||||
protected $lastActive = null;
|
||||
|
||||
/**
|
||||
* Last activity.
|
||||
*
|
||||
* @var \DateTimeImmutable
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTimeImmutable $createdAt;
|
||||
protected $createdAt = null;
|
||||
|
||||
/**
|
||||
* Permissions.
|
||||
*
|
||||
* @var PermissionAbstract[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $permissions = [];
|
||||
|
||||
/**
|
||||
* Groups.
|
||||
*
|
||||
* @var Group[]
|
||||
* @var int[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $groups = [];
|
||||
protected $groups = [];
|
||||
|
||||
/**
|
||||
* Password.
|
||||
|
|
@ -118,7 +129,7 @@ class Account implements \JsonSerializable
|
|||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $password = '';
|
||||
protected $password = '';
|
||||
|
||||
/**
|
||||
* Account type.
|
||||
|
|
@ -126,7 +137,7 @@ class Account implements \JsonSerializable
|
|||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $type = AccountType::USER;
|
||||
protected $type = AccountType::USER;
|
||||
|
||||
/**
|
||||
* Account status.
|
||||
|
|
@ -134,7 +145,7 @@ class Account implements \JsonSerializable
|
|||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $status = AccountStatus::INACTIVE;
|
||||
protected $status = AccountStatus::INACTIVE;
|
||||
|
||||
/**
|
||||
* Localization.
|
||||
|
|
@ -142,49 +153,7 @@ class Account implements \JsonSerializable
|
|||
* @var Localization
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public Localization $l11n;
|
||||
|
||||
use PermissionHandlingTrait;
|
||||
|
||||
/**
|
||||
* Has permission.
|
||||
*
|
||||
* @param int $permission Permission
|
||||
* @param int|null $unit Unit
|
||||
* @param int|null $app App
|
||||
* @param string|null $module Module
|
||||
* @param int|null $category Category
|
||||
* @param int|null $element Element
|
||||
* @param int|null $component Component
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasPermission(
|
||||
int $permission,
|
||||
?int $unit = null,
|
||||
?int $app = null,
|
||||
?string $module = null,
|
||||
?int $category = null,
|
||||
?int $element = null,
|
||||
?int $component = null
|
||||
) : bool
|
||||
{
|
||||
foreach ($this->groups as $group) {
|
||||
if ($group->hasPermission($permission, $unit, $app, $module, $category, $element, $component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->permissions as $p) {
|
||||
if ($p->hasPermission($permission, $unit, $app, $module, $category, $element, $component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
protected $l11n = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
|
@ -193,14 +162,14 @@ class Account implements \JsonSerializable
|
|||
*
|
||||
* @param int $id Account id
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $id = 0)
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable('now');
|
||||
$this->createdAt = new \DateTime('now');
|
||||
$this->lastActive = new \DateTime('now');
|
||||
$this->id = $id;
|
||||
$this->l11n = new Localization();
|
||||
$this->l11n = new NullLocalization();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,75 +177,160 @@ class Account implements \JsonSerializable
|
|||
*
|
||||
* @return int Account id
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getId() : int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localization.
|
||||
*
|
||||
* Every account can have a different localization which can be accessed here.
|
||||
*
|
||||
* @return Localization
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getL11n() : Localization
|
||||
{
|
||||
return $this->l11n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups.
|
||||
*
|
||||
* Every account can belong to multiple groups.
|
||||
* These groups usually are used for permissions and categorize accounts.
|
||||
*
|
||||
* @return Group[] Returns array of all groups
|
||||
* @return array Returns array of all groups
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getGroups() : array
|
||||
{
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ids of groups
|
||||
*
|
||||
* @return int[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getGroupIds() : array
|
||||
{
|
||||
/*
|
||||
$ids = [];
|
||||
foreach ($this->groups as $group) {
|
||||
$ids[] = $group->id;
|
||||
}
|
||||
|
||||
return $ids;
|
||||
*/
|
||||
return \array_keys($this->groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add group.
|
||||
*
|
||||
* @param Group $group Group to add
|
||||
* @param mixed $group Group to add
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addGroup(Group $group) : void
|
||||
public function addGroup($group) /* : void */
|
||||
{
|
||||
$this->groups[] = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* User has group.
|
||||
* Set localization.
|
||||
*
|
||||
* @param int $id Group id
|
||||
* @param Localization $l11n Localization
|
||||
*
|
||||
* @return bool
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasGroup(int $id) : bool
|
||||
public function setL11n(Localization $l11n) /* : void */
|
||||
{
|
||||
foreach ($this->groups as $group) {
|
||||
if ($group->id === $id) {
|
||||
$this->l11n = $l11n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set permissions.
|
||||
*
|
||||
* The method accepts an array of permissions. All existing permissions are replaced.
|
||||
*
|
||||
* @param PermissionAbstract[] $permissions
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setPermissions(array $permissions) /* : void */
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permissions.
|
||||
*
|
||||
* Adds permissions to the account
|
||||
*
|
||||
* @param PermissionAbstract[] $permissions Array of permissions to add to the account
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addPermissions(array $permissions) /* : void */
|
||||
{
|
||||
$this->permissions = array_merge($this->permissions, $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permission.
|
||||
*
|
||||
* Adds a single permission to the account
|
||||
*
|
||||
* @param PermissionAbstract $permission Permission to add to the account
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addPermission(PermissionAbstract $permission) /* : void */
|
||||
{
|
||||
$this->permissions[] = $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getPermissions() : array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has permissions.
|
||||
*
|
||||
* Checks if the account has a permission defined
|
||||
*
|
||||
* @param int $permission Permission to check
|
||||
* @param int $unit Unit Unit to check (null if all are acceptable)
|
||||
* @param string $app App App to check (null if all are acceptable)
|
||||
* @param int $module Module Module to check (null if all are acceptable)
|
||||
* @param int $type Type (e.g. customer) (null if all are acceptable)
|
||||
* @param int $element (e.g. customer id) (null if all are acceptable)
|
||||
* @param int $component (e.g. address) (null if all are acceptable)
|
||||
*
|
||||
* @return bool Returns true if the account has the permission, false otherwise
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasPermission(int $permission, int $unit = null, string $app = null, int $module = null, int $type = null, $element = null, $component = null) : bool
|
||||
{
|
||||
$app = isset($app) ? strtolower($app) : $app;
|
||||
|
||||
foreach ($this->permissions as $p) {
|
||||
if (($p->getUnit() === $unit || $p->getUnit() === null || !isset($unit))
|
||||
&& ($p->getApp() === $app || $p->getApp() === null || !isset($app))
|
||||
&& ($p->getModule() === $module || $p->getModule() === null || !isset($module))
|
||||
&& ($p->getType() === $type || $p->getType() === null || !isset($type))
|
||||
&& ($p->getElement() === $element || $p->getElement() === null || !isset($element))
|
||||
&& ($p->getComponent() === $component || $p->getComponent() === null || !isset($component))
|
||||
&& ($p->getPermission() | $permission) === $p->getPermission()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -284,12 +338,102 @@ class Account implements \JsonSerializable
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name1.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName1() : string
|
||||
{
|
||||
return $this->name1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name1.
|
||||
*
|
||||
* @param string $name Name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setName1(string $name) /* : void */
|
||||
{
|
||||
$this->name1 = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name2.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName2() : string
|
||||
{
|
||||
return $this->name2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name2.
|
||||
*
|
||||
* @param string $name Name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setName2(string $name) /* : void */
|
||||
{
|
||||
$this->name2 = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name3.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName3() : string
|
||||
{
|
||||
return $this->name3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name3.
|
||||
*
|
||||
* @param string $name Name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setName3(string $name) /* : void */
|
||||
{
|
||||
$this->name3 = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email.
|
||||
*
|
||||
* @return string Returns the email address
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getEmail() : string
|
||||
{
|
||||
|
|
@ -305,27 +449,103 @@ class Account implements \JsonSerializable
|
|||
*
|
||||
* @throws \InvalidArgumentException Exception is thrown if the provided string is not a valid email
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setEmail(string $email) : void
|
||||
public function setEmail(string $email) /* : void */
|
||||
{
|
||||
if ($email !== '' && !Email::isValid($email)) {
|
||||
if (!Email::isValid($email)) {
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
$this->email = \mb_strtolower($email);
|
||||
$this->email = mb_strtolower($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status.
|
||||
*
|
||||
* AccountStatus
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getStatus() : int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status.
|
||||
*
|
||||
* @param int $status Status
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setStatus(int $status) /* : void */
|
||||
{
|
||||
if (!AccountStatus::isValidValue($status)) {
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* AccountType
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getType() : int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @param int $type Type
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setType(int $type) /* : void */
|
||||
{
|
||||
if (!AccountType::isValidValue($type)) {
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last activity.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
* @return \DateTime
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getLastActive() : \DateTimeInterface
|
||||
public function getLastActive() : \DateTime
|
||||
{
|
||||
return $this->lastActive ?? $this->createdAt;
|
||||
return $this->lastActive ?? $this->getCreatedAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created at.
|
||||
*
|
||||
* @return \DateTime
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCreatedAt() : \DateTime
|
||||
{
|
||||
return $this->createdAt ?? new \DateTime('NOW');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -335,19 +555,31 @@ class Account implements \JsonSerializable
|
|||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception Throws this exception if the password_hash function fails
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function generatePassword(string $password) : void
|
||||
public function generatePassword(string $password) /* : void */
|
||||
{
|
||||
$temp = \password_hash($password, \PASSWORD_BCRYPT);
|
||||
$this->password = \password_hash($password, \PASSWORD_DEFAULT);
|
||||
|
||||
if ($temp === false) {
|
||||
throw new \Exception('Internal password_hash error.'); // @codeCoverageIgnore
|
||||
if ($this->password === false) {
|
||||
throw new \Exception();
|
||||
}
|
||||
}
|
||||
|
||||
$this->password = $temp;
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name Name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setName(string $name) /* : void */
|
||||
{
|
||||
$this->login = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -355,23 +587,23 @@ class Account implements \JsonSerializable
|
|||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function updateLastActive() : void
|
||||
public function updateLastActive() /* : void */
|
||||
{
|
||||
$this->lastActive = new \DateTime('now');
|
||||
$this->lastActive = new \DateTime('NOW');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string representation.
|
||||
*
|
||||
* @return string Returns the json_encode of this object
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __toString() : string
|
||||
public function __toString()
|
||||
{
|
||||
return (string) \json_encode($this->toArray());
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -380,8 +612,8 @@ class Account implements \JsonSerializable
|
|||
public function toArray() : array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => [
|
||||
'id' => $this->id,
|
||||
'name' => [
|
||||
$this->name1,
|
||||
$this->name2,
|
||||
$this->name3,
|
||||
|
|
@ -396,9 +628,13 @@ class Account implements \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* Json serialize.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function jsonSerialize() : mixed
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
|
|
|||
61
Account/AccountManager.php
Executable file → Normal file
61
Account/AccountManager.php
Executable file → Normal file
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
|
|
@ -20,22 +20,23 @@ use phpOMS\DataStorage\Session\SessionInterface;
|
|||
/**
|
||||
* Account manager class.
|
||||
*
|
||||
* The account manager is used to manage accounts.
|
||||
* The account manager is used to manage multiple accounts.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class AccountManager implements \Countable
|
||||
class AccountManager implements \Countable
|
||||
{
|
||||
|
||||
/**
|
||||
* Accounts.
|
||||
*
|
||||
* @var Account[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $accounts = [];
|
||||
private $accounts = [];
|
||||
|
||||
/**
|
||||
* Session.
|
||||
|
|
@ -43,14 +44,14 @@ final class AccountManager implements \Countable
|
|||
* @var SessionInterface
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private SessionInterface $session;
|
||||
private $session = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param SessionInterface $session Session
|
||||
* @param SessionInterface $session Session
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(SessionInterface $session)
|
||||
{
|
||||
|
|
@ -64,15 +65,15 @@ final class AccountManager implements \Countable
|
|||
*
|
||||
* @return Account
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function get(int $id = 0) : Account
|
||||
{
|
||||
if ($id === 0) {
|
||||
$account = new Account(Auth::authenticate($this->session));
|
||||
|
||||
if (!isset($this->accounts[$account->id])) {
|
||||
$this->accounts[$account->id] = $account;
|
||||
if (!isset($this->accounts[$account->getId()])) {
|
||||
$this->accounts[$account->getId()] = $account;
|
||||
}
|
||||
|
||||
return $account;
|
||||
|
|
@ -86,14 +87,14 @@ final class AccountManager implements \Countable
|
|||
*
|
||||
* @param Account $account Account
|
||||
*
|
||||
* @return bool Returns true if the account could be added otherwise false is returned
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function add(Account $account) : bool
|
||||
{
|
||||
if (!isset($this->accounts[$account->id])) {
|
||||
$this->accounts[$account->id] = $account;
|
||||
if (!isset($this->accounts[$account->getId()])) {
|
||||
$this->accounts[$account->getId()] = $account;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -106,9 +107,9 @@ final class AccountManager implements \Countable
|
|||
*
|
||||
* @param int $id Account id
|
||||
*
|
||||
* @return bool Returns true if the account could be removed otherwise false
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function remove(int $id) : bool
|
||||
{
|
||||
|
|
@ -124,12 +125,12 @@ final class AccountManager implements \Countable
|
|||
/**
|
||||
* Get accounts count.
|
||||
*
|
||||
* @return int Returns the amount of accounts in the manager (>= 0)
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function count() : int
|
||||
{
|
||||
return \count($this->accounts);
|
||||
return count($this->accounts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
Account/AccountStatus.php
Executable file → Normal file
35
Account/AccountStatus.php
Executable file → Normal file
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
|
|
@ -19,18 +19,15 @@ use phpOMS\Stdlib\Base\Enum;
|
|||
/**
|
||||
* Account status enum.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class AccountStatus extends Enum
|
||||
{
|
||||
public const ACTIVE = 1;
|
||||
|
||||
public const INACTIVE = 2;
|
||||
|
||||
public const TIMEOUT = 3;
|
||||
|
||||
public const BANNED = 4;
|
||||
/* public */ const ACTIVE = 1;
|
||||
/* public */ const INACTIVE = 2;
|
||||
/* public */ const TIMEOUT = 3;
|
||||
/* public */ const BANNED = 4;
|
||||
}
|
||||
|
|
|
|||
29
Account/AccountType.php
Executable file → Normal file
29
Account/AccountType.php
Executable file → Normal file
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
|
|
@ -19,14 +19,13 @@ use phpOMS\Stdlib\Base\Enum;
|
|||
/**
|
||||
* Account type enum.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class AccountType extends Enum
|
||||
{
|
||||
public const USER = 0;
|
||||
|
||||
public const GROUP = 1;
|
||||
/* public */ const USER = 0;
|
||||
/* public */ const GROUP = 1;
|
||||
}
|
||||
|
|
|
|||
174
Account/Group.php
Executable file → Normal file
174
Account/Group.php
Executable file → Normal file
|
|
@ -1,60 +1,63 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
use phpOMS\Contract\ArrayableInterface;
|
||||
|
||||
/**
|
||||
* Account group class.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Group implements \JsonSerializable
|
||||
class Group implements ArrayableInterface, \JsonSerializable
|
||||
{
|
||||
|
||||
/**
|
||||
* Group id.
|
||||
* Account id.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $id = 0;
|
||||
protected $id = 0;
|
||||
|
||||
/**
|
||||
* Group name.
|
||||
* Account name.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name = '';
|
||||
protected $name = '';
|
||||
|
||||
/**
|
||||
* Group name.
|
||||
* Account name.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $description = '';
|
||||
protected $description = '';
|
||||
|
||||
/**
|
||||
* Group members.
|
||||
* Account name.
|
||||
*
|
||||
* @var array
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $members = [];
|
||||
protected $members = [];
|
||||
|
||||
/**
|
||||
* Parents.
|
||||
|
|
@ -62,7 +65,7 @@ class Group implements \JsonSerializable
|
|||
* @var int[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $parents = [];
|
||||
protected $parents = [];
|
||||
|
||||
/**
|
||||
* Group status.
|
||||
|
|
@ -70,32 +73,131 @@ class Group implements \JsonSerializable
|
|||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $status = GroupStatus::INACTIVE;
|
||||
protected $status = GroupStatus::INACTIVE;
|
||||
|
||||
use PermissionHandlingTrait;
|
||||
/**
|
||||
* Permissions.
|
||||
*
|
||||
* @var int[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $permissions = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group id.
|
||||
*
|
||||
* @return int Returns the id of the group
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getId() : int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group name.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group name.
|
||||
*
|
||||
* @param string $name Group name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setName(string $name) /* : void */
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group description.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getDescription() : string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group description.
|
||||
*
|
||||
* @param string $description Group description
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setDescription(string $description) /* : void */
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group status.
|
||||
*
|
||||
* @return int Group status
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getStatus() : int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group status.
|
||||
*
|
||||
* @param int $status Group status
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws InvalidEnumValue
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setStatus(int $status) /* : void */
|
||||
{
|
||||
if (!GroupStatus::isValidValue($status)) {
|
||||
throw new InvalidEnumValue($status);
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string representation.
|
||||
*
|
||||
* @return string Returns the json_encode of this object
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __toString() : string
|
||||
public function __toString()
|
||||
{
|
||||
return (string) \json_encode($this->toArray());
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -115,15 +217,11 @@ class Group implements \JsonSerializable
|
|||
/**
|
||||
* Json serialize.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize() : mixed
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
|
|
|||
34
Account/GroupStatus.php
Executable file → Normal file
34
Account/GroupStatus.php
Executable file → Normal file
|
|
@ -1,34 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Group status enum.
|
||||
* Accept status enum.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class GroupStatus extends Enum
|
||||
{
|
||||
public const ACTIVE = 1;
|
||||
|
||||
public const INACTIVE = 2;
|
||||
|
||||
public const HIDDEN = 4;
|
||||
/* public */ const ACTIVE = 1;
|
||||
/* public */ const INACTIVE = 2;
|
||||
/* public */ const HIDDEN = 4;
|
||||
}
|
||||
|
|
|
|||
45
Account/NullAccount.php
Executable file → Normal file
45
Account/NullAccount.php
Executable file → Normal file
|
|
@ -1,46 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
/**
|
||||
* Null account class.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class NullAccount extends Account
|
||||
class NullAccount extends Account
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $id Model id
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $id = 0)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize() : mixed
|
||||
{
|
||||
return ['id' => $this->id];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
/**
|
||||
* Null group class.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class NullGroup extends Group
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $id Model id
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $id = 0)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize() : mixed
|
||||
{
|
||||
return ['id' => $this->id];
|
||||
}
|
||||
}
|
||||
429
Account/PermissionAbstract.php
Executable file → Normal file
429
Account/PermissionAbstract.php
Executable file → Normal file
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
|
|
@ -20,12 +20,12 @@ namespace phpOMS\Account;
|
|||
* This permission abstract is the basis for all permissions. Contrary to it's name it is not an
|
||||
* abstract class and can be used directly if needed.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class PermissionAbstract implements \JsonSerializable
|
||||
class PermissionAbstract
|
||||
{
|
||||
/**
|
||||
* Permission id.
|
||||
|
|
@ -33,198 +33,276 @@ class PermissionAbstract implements \JsonSerializable
|
|||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $id = 0;
|
||||
protected $id = 0;
|
||||
|
||||
/**
|
||||
* Unit id.
|
||||
*
|
||||
* @var null|int
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?int $unit = null;
|
||||
protected $unit = null;
|
||||
|
||||
/**
|
||||
* App name.
|
||||
*
|
||||
* @var null|int
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?int $app = null;
|
||||
protected $app = null;
|
||||
|
||||
/**
|
||||
* Module id.
|
||||
*
|
||||
* @var null|string
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?string $module = null;
|
||||
protected $module = null;
|
||||
|
||||
/**
|
||||
* Providing module id.
|
||||
*
|
||||
* @var string
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?string $from = null;
|
||||
protected $from = 0;
|
||||
|
||||
/**
|
||||
* Type.
|
||||
*
|
||||
* @var null|int
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?int $category = null;
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* Element id.
|
||||
*
|
||||
* null === all
|
||||
* int === specific
|
||||
*
|
||||
* @var null|int
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?int $element = null;
|
||||
protected $element = null;
|
||||
|
||||
/**
|
||||
* Component id.
|
||||
*
|
||||
* null === all
|
||||
* int === specific
|
||||
* 0 === own data
|
||||
*
|
||||
* @var null|int
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?int $component = null;
|
||||
protected $component = null;
|
||||
|
||||
/**
|
||||
* Permission.
|
||||
*
|
||||
* @var bool
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $hasRead = false;
|
||||
protected $permission = PermissionType::NONE;
|
||||
|
||||
/**
|
||||
* Permission.
|
||||
* Get permission id.
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $hasModify = false;
|
||||
public function getId() : int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission.
|
||||
* Get unit id.
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $hasCreate = false;
|
||||
public function getUnit() /* : ?int */
|
||||
{
|
||||
return $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default create permissions
|
||||
* Set unit id.
|
||||
*
|
||||
* @var null|string
|
||||
* @since 1.0.0
|
||||
* @param int $unit Unit
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?string $defaultCPermissions = null;
|
||||
public function setUnit(int $unit = null) /* : void */
|
||||
{
|
||||
$this->unit = $unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission.
|
||||
* Get app name.
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $hasDelete = false;
|
||||
public function getApp() /* : ?string */
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission.
|
||||
* Set app name.
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
* @param string $app App name
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $hasPermission = false;
|
||||
public function setApp(string $app = null) /* : void */
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permission permissions
|
||||
* Get module id.
|
||||
*
|
||||
* @var null|string
|
||||
* @since 1.0.0
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?string $defaultPPermissions = null;
|
||||
public function getModule() /* : ?int */
|
||||
{
|
||||
return $this->module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Set module id.
|
||||
*
|
||||
* @param null|int $unit Unit to check (null if all are acceptable)
|
||||
* @param null|int $app App to check (null if all are acceptable)
|
||||
* @param null|string $module Module Module to check (null if all are acceptable)
|
||||
* @param null|string $from Provided by which module
|
||||
* @param null|int $category Category (e.g. customer) (null if all are acceptable)
|
||||
* @param null|int $element (e.g. customer id) (null if all are acceptable)
|
||||
* @param null|int $component (e.g. address) (null if all are acceptable)
|
||||
* @param int $permission Permission to check
|
||||
* @param int $module Module
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
?int $unit = null,
|
||||
?int $app = null,
|
||||
?string $module = null,
|
||||
?string $from = null,
|
||||
?int $category = null,
|
||||
?int $element = null,
|
||||
?int $component = null,
|
||||
int $permission = PermissionType::NONE
|
||||
) {
|
||||
$this->unit = $unit;
|
||||
$this->app = $app;
|
||||
$this->module = $module;
|
||||
$this->from = $from;
|
||||
$this->category = $category;
|
||||
$this->element = $element;
|
||||
public function setModule(int $module = null) /* : void */
|
||||
{
|
||||
$this->module = $module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providing module id.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getFrom() /* : ?int */
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set providing module id.
|
||||
*
|
||||
* @param int $from Providing module
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setFrom(int $from = null) /* : void */
|
||||
{
|
||||
$this->from = $from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getType() /* : ?int */
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type.
|
||||
*
|
||||
* @param int $type Type
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setType(int $type = null) /* : void */
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element id.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getElement() /* : ?int */
|
||||
{
|
||||
return $this->element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set element id.
|
||||
*
|
||||
* @param int $element Element id
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setElement(int $element = null) /* : void */
|
||||
{
|
||||
$this->element = $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get component id.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getComponent() /* : ?int */
|
||||
{
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set component id.
|
||||
*
|
||||
* @param int $component Component
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setComponent(int $component = null) /* : void */
|
||||
{
|
||||
$this->component = $component;
|
||||
|
||||
$this->hasRead = ($permission & PermissionType::READ) === PermissionType::READ;
|
||||
$this->hasCreate = ($permission & PermissionType::CREATE) === PermissionType::CREATE;
|
||||
$this->hasModify = ($permission & PermissionType::MODIFY) === PermissionType::MODIFY;
|
||||
$this->hasDelete = ($permission & PermissionType::DELETE) === PermissionType::DELETE;
|
||||
$this->hasPermission = ($permission & PermissionType::PERMISSION) === PermissionType::PERMISSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permission
|
||||
*
|
||||
* @return int Returns the permission (PermissionType)
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getPermission() : int
|
||||
{
|
||||
$permission = 0;
|
||||
|
||||
if ($this->hasRead) {
|
||||
$permission |= PermissionType::READ;
|
||||
}
|
||||
|
||||
if ($this->hasCreate) {
|
||||
$permission |= PermissionType::CREATE;
|
||||
}
|
||||
|
||||
if ($this->hasModify) {
|
||||
$permission |= PermissionType::MODIFY;
|
||||
}
|
||||
|
||||
if ($this->hasDelete) {
|
||||
$permission |= PermissionType::DELETE;
|
||||
}
|
||||
|
||||
if ($this->hasPermission) {
|
||||
$permission |= PermissionType::PERMISSION;
|
||||
}
|
||||
|
||||
return $permission === 0 ? PermissionType::NONE : $permission;
|
||||
return $this->permission;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -234,15 +312,11 @@ class PermissionAbstract implements \JsonSerializable
|
|||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setPermission(int $permission = 0) : void
|
||||
public function setPermission(int $permission = 0) /* : void */
|
||||
{
|
||||
$this->hasRead = ($permission & PermissionType::READ) === PermissionType::READ;
|
||||
$this->hasCreate = ($permission & PermissionType::CREATE) === PermissionType::CREATE;
|
||||
$this->hasModify = ($permission & PermissionType::MODIFY) === PermissionType::MODIFY;
|
||||
$this->hasDelete = ($permission & PermissionType::DELETE) === PermissionType::DELETE;
|
||||
$this->hasPermission = ($permission & PermissionType::PERMISSION) === PermissionType::PERMISSION;
|
||||
$this->permission = $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -252,27 +326,11 @@ class PermissionAbstract implements \JsonSerializable
|
|||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addPermission(int $permission = 0) : void
|
||||
public function addPermission(int $permission = 0) /* : void */
|
||||
{
|
||||
switch($permission) {
|
||||
case PermissionType::READ:
|
||||
$this->hasRead = true;
|
||||
break;
|
||||
case PermissionType::CREATE:
|
||||
$this->hasCreate = true;
|
||||
break;
|
||||
case PermissionType::MODIFY:
|
||||
$this->hasModify = true;
|
||||
break;
|
||||
case PermissionType::DELETE:
|
||||
$this->hasDelete = true;
|
||||
break;
|
||||
case PermissionType::PERMISSION:
|
||||
$this->hasPermission = true;
|
||||
break;
|
||||
}
|
||||
$this->permission |= $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -280,87 +338,12 @@ class PermissionAbstract implements \JsonSerializable
|
|||
*
|
||||
* @param int $permission Permission
|
||||
*
|
||||
* @return bool Returns true if the permission is set otherwise returns false
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasPermissionFlags(int $permission) : bool
|
||||
public function hasPermission(int $permission) : bool
|
||||
{
|
||||
return ($this->getPermission() & $permission) === $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has permissions.
|
||||
*
|
||||
* Checks if the permission is defined
|
||||
*
|
||||
* @param int $permission Permission to check
|
||||
* @param null|int $unit Unit Unit to check (null if all are acceptable)
|
||||
* @param null|int $app App App to check (null if all are acceptable)
|
||||
* @param null|string $module Module Module to check (null if all are acceptable)
|
||||
* @param null|int $category Category (e.g. customer) (null if all are acceptable)
|
||||
* @param null|int $element (e.g. customer id) (null if all are acceptable)
|
||||
* @param null|int $component (e.g. address) (null if all are acceptable)
|
||||
*
|
||||
* @return bool Returns true if the permission is set, false otherwise
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasPermission(
|
||||
int $permission,
|
||||
?int $unit = null,
|
||||
?int $app = null,
|
||||
?string $module = null,
|
||||
?int $category = null,
|
||||
?int $element = null,
|
||||
?int $component = null
|
||||
) : bool
|
||||
{
|
||||
return $permission === PermissionType::NONE ||
|
||||
(($unit === null || $this->unit === null || $this->unit === $unit)
|
||||
&& ($app === null || $this->app === null || $this->app === $app)
|
||||
&& ($module === null || $this->module === null || $this->module === $module)
|
||||
&& ($category === null || $this->category === null || $this->category === $category)
|
||||
&& ($element === null || $this->element === null || $this->element === $element)
|
||||
&& ($component === null || $this->component === null || $this->component === $component)
|
||||
&& ($this->getPermission() & $permission) === $permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is equals.
|
||||
*
|
||||
* @param self $permission Permission
|
||||
*
|
||||
* @return bool Returns true if the permission is the same
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isEqual(self $permission) : bool
|
||||
{
|
||||
return $this->unit === $permission->unit
|
||||
&& $this->app === $permission->app
|
||||
&& $this->module === $permission->module
|
||||
&& $this->category === $permission->category
|
||||
&& $this->element === $permission->element
|
||||
&& $this->component === $permission->component
|
||||
&& $this->getPermission() === $permission->getPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize() : mixed
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'unit' => $this->unit,
|
||||
'app' => $this->app,
|
||||
'module' => $this->module,
|
||||
'from' => $this->from,
|
||||
'category' => $this->category,
|
||||
'element' => $this->element,
|
||||
'component' => $this->component,
|
||||
'permission' => $this->getPermission(),
|
||||
];
|
||||
return ($this->permission | $permission) === $this->permission;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
/**
|
||||
* Permission handling trait.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
trait PermissionHandlingTrait
|
||||
{
|
||||
/**
|
||||
* Permissions.
|
||||
*
|
||||
* @var PermissionAbstract[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $permissions = [];
|
||||
|
||||
/**
|
||||
* Set permissions.
|
||||
*
|
||||
* The method accepts an array of permissions. All existing permissions are replaced.
|
||||
*
|
||||
* @param PermissionAbstract[] $permissions Permissions
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setPermissions(array $permissions) : void
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permissions.
|
||||
*
|
||||
* Adds permissions
|
||||
*
|
||||
* @param array<array|PermissionAbstract> $permissions Array of permissions to add
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addPermissions(array $permissions) : void
|
||||
{
|
||||
foreach ($permissions as $permission) {
|
||||
if (\is_array($permission)) {
|
||||
$this->permissions = \array_merge($this->permissions, $permission);
|
||||
} else {
|
||||
$this->permissions[] = $permission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permission.
|
||||
*
|
||||
* Adds a single permission
|
||||
*
|
||||
* @param PermissionAbstract $permission Permission to add
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addPermission(PermissionAbstract $permission) : void
|
||||
{
|
||||
$this->permissions[] = $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove permission.
|
||||
*
|
||||
* @param PermissionAbstract $permission Permission to remove
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function removePermission(PermissionAbstract $permission) : void
|
||||
{
|
||||
foreach ($this->permissions as $key => $p) {
|
||||
if ($p->isEqual($permission)) {
|
||||
unset($this->permissions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions.
|
||||
*
|
||||
* @return PermissionAbstract[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getPermissions() : array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has permissions.
|
||||
*
|
||||
* Checks if the permission is defined
|
||||
*
|
||||
* @param int $permission Permission to check
|
||||
* @param null|int $unit Unit Unit to check (null if all are acceptable)
|
||||
* @param null|int $app App App to check (null if all are acceptable)
|
||||
* @param null|string $module Module Module to check (null if all are acceptable)
|
||||
* @param null|int $category Type (e.g. customer) (null if all are acceptable)
|
||||
* @param null|int $element (e.g. customer id) (null if all are acceptable)
|
||||
* @param null|int $component (e.g. address) (null if all are acceptable)
|
||||
*
|
||||
* @return bool Returns true if the permission is set, false otherwise
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function hasPermission(
|
||||
int $permission,
|
||||
?int $unit = null,
|
||||
?int $app = null,
|
||||
?string $module = null,
|
||||
?int $category = null,
|
||||
?int $element = null,
|
||||
?int $component = null
|
||||
) : bool
|
||||
{
|
||||
foreach ($this->permissions as $p) {
|
||||
if ($p->hasPermission($permission, $unit, $app, $module, $category, $element, $component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Permision type/owner enum.
|
||||
*
|
||||
* A permission can be long to a group or an account.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class PermissionOwner extends Enum
|
||||
{
|
||||
public const GROUP = 1;
|
||||
|
||||
public const ACCOUNT = 2;
|
||||
}
|
||||
41
Account/PermissionType.php
Executable file → Normal file
41
Account/PermissionType.php
Executable file → Normal file
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
* Orange Management
|
||||
*
|
||||
* PHP Version 8.2
|
||||
* PHP Version 7.1
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
* @package phpOMS\Account
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 1.0
|
||||
* @version 1.0.0
|
||||
* @link http://website.orange-management.de
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace phpOMS\Account;
|
||||
|
||||
|
|
@ -19,22 +19,17 @@ use phpOMS\Stdlib\Base\Enum;
|
|||
/**
|
||||
* Permission type enum.
|
||||
*
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @package phpOMS\Account
|
||||
* @license OMS License 1.0
|
||||
* @link http://website.orange-management.de
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class PermissionType extends Enum
|
||||
{
|
||||
public const NONE = 1; // No permission
|
||||
|
||||
public const READ = 2; // Is able to read models/data
|
||||
|
||||
public const CREATE = 4; // Is able to create models/data
|
||||
|
||||
public const MODIFY = 8; // Is able to modify models/data
|
||||
|
||||
public const DELETE = 16; // Is able to delete models/data
|
||||
|
||||
public const PERMISSION = 32; // Is able to change permissions
|
||||
/* public */ const NONE = 1;
|
||||
/* public */ const READ = 2;
|
||||
/* public */ const CREATE = 4;
|
||||
/* public */ const MODIFY = 8;
|
||||
/* public */ const DELETE = 16;
|
||||
/* public */ const PERMISSION = 32;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Ai\NeuralNetwork
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Ai\NeuralNetwork;
|
||||
|
||||
/**
|
||||
* Neuron
|
||||
*
|
||||
* @package phpOMS\Ai\NeuralNetwork
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Neuron
|
||||
{
|
||||
/**
|
||||
* Neuron inputs
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $inputs = [];
|
||||
|
||||
/**
|
||||
* Input weights
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $weights = [];
|
||||
|
||||
/**
|
||||
* Bias
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $bias = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $inputs Neuron inputs/connections
|
||||
* @param array $weights Input weights
|
||||
* @param float $bias Input bias
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(array $inputs = [], array $weights = [], float $bias = 0.0)
|
||||
{
|
||||
$this->inputs = $inputs;
|
||||
$this->weights = $weights;
|
||||
$this->bias = $bias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add neuron input
|
||||
*
|
||||
* @param mixed $input Input
|
||||
* @param float $weight Weight of input
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addInput(mixed $input, float $weight) : void
|
||||
{
|
||||
$this->inputs[] = $input;
|
||||
$this->weights[] = $weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create node output
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function output() : float
|
||||
{
|
||||
$length = \count($this->inputs);
|
||||
$output = 0.0;
|
||||
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$output += $this->inputs[$i]->output() * $this->weights[$i];
|
||||
}
|
||||
|
||||
return $output + $this->bias;
|
||||
// return $this->activationFunction($output + $this->bias);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Ai\Ocr
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Ai\Ocr;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
use phpOMS\System\File\PathException;
|
||||
|
||||
/**
|
||||
* Basic OCR implementation for MNIST data
|
||||
*
|
||||
* @package phpOMS\Ai\Ocr
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class BasicOcr
|
||||
{
|
||||
/**
|
||||
* Dataset on which the OCR is trained on.
|
||||
*
|
||||
* The data needs to be MNIST data.
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $Xtrain = [];
|
||||
|
||||
/**
|
||||
* Resultset on which the OCR is trained on.
|
||||
*
|
||||
* These are the actual values for the Xtrain data and must therefore have the same dimension.
|
||||
*
|
||||
* The labels need to be MNIST labels.
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $ytrain = [];
|
||||
|
||||
/**
|
||||
* Train OCR with data and result/labels
|
||||
*
|
||||
* @param string $dataPath Impage path to read
|
||||
* @param string $labelPath Label path to read
|
||||
* @param int $limit Limit (0 = unlimited)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function trainWith(string $dataPath, string $labelPath, int $limit = 0) : void
|
||||
{
|
||||
$Xtrain = $this->readImages($dataPath, $limit);
|
||||
$ytrain = $this->readLabels($labelPath, $limit);
|
||||
|
||||
$this->Xtrain = \array_merge($this->Xtrain, $Xtrain);
|
||||
$this->ytrain = \array_merge($this->ytrain, $ytrain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read image from path
|
||||
*
|
||||
* @param string $path Image to read
|
||||
* @param int $limit Limit
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws PathException
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function readImages(string $path, int $limit = 0) : array
|
||||
{
|
||||
if (!\is_file($path)) {
|
||||
throw new PathException($path);
|
||||
}
|
||||
|
||||
$fp = \fopen($path, 'r');
|
||||
if ($fp === false) {
|
||||
throw new PathException($path); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// $magicNumber = $unpack[1];
|
||||
// 2051 === image data (should always be this)
|
||||
// 2049 === label data
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
$numberOfImages = $unpack[1];
|
||||
|
||||
if ($limit > 0) {
|
||||
$numberOfImages = \min($numberOfImages, $limit);
|
||||
}
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/** @var int<0, max> $numberOfRows */
|
||||
$numberOfRows = (int) $unpack[1];
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/** @var int<0, max> $numberOfColumns */
|
||||
$numberOfColumns = (int) $unpack[1];
|
||||
|
||||
$images = [];
|
||||
for ($i = 0; $i < $numberOfImages; ++$i) {
|
||||
if (($read = \fread($fp, $numberOfRows * $numberOfColumns)) === false
|
||||
|| ($unpack = \unpack('C*', $read)) === false
|
||||
) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$images[] = \array_values($unpack);
|
||||
}
|
||||
|
||||
\fclose($fp);
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read labels from from path
|
||||
*
|
||||
* @param string $path Labels path
|
||||
* @param int $limit Limit
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws PathException
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function readLabels(string $path, int $limit = 0) : array
|
||||
{
|
||||
if (!\is_file($path)) {
|
||||
throw new PathException($path);
|
||||
}
|
||||
|
||||
$fp = \fopen($path, 'r');
|
||||
if ($fp === false) {
|
||||
throw new PathException($path); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// $magicNumber = $unpack[1];
|
||||
// 2051 === image data
|
||||
// 2049 === label data (should always be this)
|
||||
|
||||
if (($read = \fread($fp, 4)) === false || ($unpack = \unpack('N', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
$numberOfLabels = $unpack[1];
|
||||
|
||||
if ($limit > 0) {
|
||||
$numberOfLabels = \min($numberOfLabels, $limit);
|
||||
}
|
||||
|
||||
$labels = [];
|
||||
for ($i = 0; $i < $numberOfLabels; ++$i) {
|
||||
if (($read = \fread($fp, 1)) === false || ($unpack = \unpack('C', $read)) === false) {
|
||||
return []; // @codeCoverageIgnore
|
||||
}
|
||||
$labels[] = $unpack[1];
|
||||
}
|
||||
|
||||
\fclose($fp);
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the k-nearest matches for test data
|
||||
*
|
||||
* @param array $Xtrain Image data used for training
|
||||
* @param array $ytrain Labels associated with the trained data
|
||||
* @param array $Xtest Image data from the image to categorize
|
||||
* @param int $k Amount of best fits that should be found
|
||||
*/
|
||||
private function kNearest(array $Xtrain, array $ytrain, array $Xtest, int $k = 3) : array
|
||||
{
|
||||
$predictedLabels = [];
|
||||
foreach ($Xtest as $sample) {
|
||||
$distances = $this->getDistances($Xtrain, $sample);
|
||||
\asort($distances);
|
||||
|
||||
$keys = \array_keys($distances);
|
||||
|
||||
$candidateLabels = [];
|
||||
for ($i = 0; $i < $k; ++$i) {
|
||||
$candidateLabels[] = $ytrain[$keys[$i]];
|
||||
}
|
||||
|
||||
// find best match
|
||||
$countedCandidates = \array_count_values($candidateLabels);
|
||||
|
||||
foreach ($candidateLabels as $i => $label) {
|
||||
$predictedLabels[] = [
|
||||
'label' => $label,
|
||||
'prob' => $countedCandidates[$label] / $k,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $predictedLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fitting method in order to see how similar two datasets are.
|
||||
*
|
||||
* @param array $Xtrain Image data used for training
|
||||
* @param array $sample Image data to compare against
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function getDistances(array $Xtrain, array $sample) : array
|
||||
{
|
||||
$dist = [];
|
||||
foreach ($Xtrain as $train) {
|
||||
$dist[] = MetricsND::euclidean($train, $sample);
|
||||
}
|
||||
|
||||
return $dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MNIST file from images
|
||||
*
|
||||
* @param string[] $images Images
|
||||
* @param string $out Output file
|
||||
* @param int $resolution Resolution of the iomages
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function imagesToMNIST(array $images, string $out, int $resolution) : void
|
||||
{
|
||||
$out = \fopen($out, 'wb');
|
||||
if ($out === false) {
|
||||
return; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
\fwrite($out, \pack('N', 2051));
|
||||
\fwrite($out, \pack('N', \count($images)));
|
||||
\fwrite($out, \pack('N', $resolution));
|
||||
\fwrite($out, \pack('N', $resolution));
|
||||
|
||||
$size = $resolution * $resolution;
|
||||
|
||||
foreach ($images as $in) {
|
||||
$inString = \file_get_contents($in);
|
||||
if ($inString === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$im = \imagecreatefromstring($inString);
|
||||
if ($im === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new = \imagescale($im, $resolution, $resolution);
|
||||
if ($new === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert the image to grayscale and normalize the pixel values
|
||||
$mnist = [];
|
||||
for ($i = 0; $i < $resolution; ++$i) {
|
||||
for ($j = 0; $j < $resolution; ++$j) {
|
||||
$pixel = \imagecolorat($new, $j, $i);
|
||||
$gray = \round(
|
||||
(
|
||||
0.299 * (($pixel >> 16) & 0xFF)
|
||||
+ 0.587 * (($pixel >> 8) & 0xFF)
|
||||
+ 0.114 * ($pixel & 0xFF)
|
||||
) / 255,
|
||||
3
|
||||
);
|
||||
|
||||
$mnist[] = $gray;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
\fwrite($out, \pack('C', (int) \round($mnist[$i] * 255)));
|
||||
}
|
||||
}
|
||||
|
||||
\fclose($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert labels to MNIST format
|
||||
*
|
||||
* @param string[] $data Labels (one char per label)
|
||||
* @param string $out Output path
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function labelsToMNIST(array $data, string $out) : void
|
||||
{
|
||||
// Only allows single char labels
|
||||
$out = \fopen($out, 'wb');
|
||||
if ($out === false) {
|
||||
return; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
\fwrite($out, \pack('N', 2049));
|
||||
\fwrite($out, \pack('N', \count($data)));
|
||||
|
||||
foreach ($data as $e) {
|
||||
\fwrite($out, \pack('C', $e));
|
||||
}
|
||||
|
||||
\fclose($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize an unknown image
|
||||
*
|
||||
* @param string $path Path to the image to categorize/evaluate/match against the training data
|
||||
* @param int $comparison Amount of comparisons
|
||||
* @param int $limit Limit (0 = unlimited)
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function matchImage(string $path, int $comparison = 3, int $limit = 0) : array
|
||||
{
|
||||
$Xtest = $this->readImages($path, $limit);
|
||||
|
||||
return $this->kNearest($this->Xtrain, $this->ytrain, $Xtest, $comparison);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Ai\Ocr\Tesseract
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Ai\Ocr\Tesseract;
|
||||
|
||||
use phpOMS\System\File\PathException;
|
||||
use phpOMS\System\SystemUtils;
|
||||
|
||||
/**
|
||||
* Tesseract api
|
||||
*
|
||||
* @package phpOMS\Ai\Ocr\Tesseract
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TesseractOcr
|
||||
{
|
||||
/**
|
||||
* Tesseract path.
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected static string $bin = '/usr/bin/tesseract';
|
||||
|
||||
/**
|
||||
* Set tesseract binary.
|
||||
*
|
||||
* @param string $path tesseract path
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws PathException This exception is thrown if the binary path doesn't exist
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function setBin(string $path) : void
|
||||
{
|
||||
if (\realpath($path) === false) {
|
||||
throw new PathException($path);
|
||||
}
|
||||
|
||||
self::$bin = \realpath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prase image
|
||||
*
|
||||
* @param string $image Image path
|
||||
* @param array $languages Languages to use
|
||||
* @param int $psm Page segmentation mode (0 - 13)
|
||||
* 0 Orientation and script detection (OSD) only.
|
||||
* 1 Automatic page segmentation with OSD.
|
||||
* 2 Automatic page segmentation, but no OSD, or OCR.
|
||||
* 3 Fully automatic page segmentation, but no OSD. (Default)
|
||||
* 4 Assume a single column of text of variable sizes.
|
||||
* 5 Assume a single uniform block of vertically aligned text.
|
||||
* 6 Assume a single uniform block of text.
|
||||
* 7 Treat the image as a single text line.
|
||||
* 8 Treat the image as a single word.
|
||||
* 9 Treat the image as a single word in a circle.
|
||||
* 10 Treat the image as a single character.
|
||||
* 11 Sparse text. Find as much text as possible in no particular order.
|
||||
* 12 Sparse text with OSD.
|
||||
* 13 Raw line. Treat the image as a single text line, bypassing hacks that are Tesseract-specific.
|
||||
* @param int $oem OCR engine modes
|
||||
* 0 Legacy engine only.
|
||||
* 1 Neural nets LSTM engine only.
|
||||
* 2 Legacy + LSTM engines.
|
||||
* 3 Default, based on what is available
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function parseImage(string $image, array $languages = ['eng', 'deu'], int $psm = 3, int $oem = 3) : string
|
||||
{
|
||||
$temp = \tempnam(\sys_get_temp_dir(), 'oms_ocr_');
|
||||
if ($temp === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$extension = 'png';
|
||||
try {
|
||||
// Tesseract needs higher dpi to work properly (identify + adjust if necessary)
|
||||
$dpi = (int) \trim(\implode('', SystemUtils::runProc(
|
||||
'identify',
|
||||
'-quiet -format "%x" ' . $image
|
||||
)));
|
||||
|
||||
if ($dpi < 300) {
|
||||
$split = \explode('.', $image);
|
||||
$extension = \end($split);
|
||||
|
||||
SystemUtils::runProc(
|
||||
'convert',
|
||||
'-units PixelsPerInch ' . $image . ' -resample 300 ' . $temp . '.' . $extension
|
||||
);
|
||||
|
||||
$image = $temp . '.' . $extension;
|
||||
}
|
||||
|
||||
// Do actual parsing
|
||||
SystemUtils::runProc(
|
||||
self::$bin,
|
||||
$image . ' '
|
||||
. $temp
|
||||
. ' -c preserve_interword_spaces=1'
|
||||
. ' --psm ' . $psm
|
||||
. ' --oem ' . $oem
|
||||
. (empty($languages) ? '' : ' -l ' . \implode('+', $languages))
|
||||
);
|
||||
} catch (\Throwable $_) {
|
||||
if (\is_file($temp . '.' . $extension)) {
|
||||
\unlink($temp . '.' . $extension);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (\is_file($temp . '.' . $extension)) {
|
||||
\unlink($temp . '.' . $extension);
|
||||
}
|
||||
|
||||
$filepath = \is_file($temp . '.txt')
|
||||
? $temp . '.txt'
|
||||
: $temp;
|
||||
|
||||
if (!\is_file($filepath)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
\unlink($temp);
|
||||
|
||||
return '';
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$parsed = \file_get_contents($filepath);
|
||||
if ($parsed === false) {
|
||||
$parsed = '';
|
||||
}
|
||||
|
||||
\unlink($filepath);
|
||||
\unlink($temp);
|
||||
|
||||
return \trim($parsed);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class AffinityPropagation
|
||||
{
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* The parent category of this clustering algorithm is hierarchical clustering.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license Base: MIT Copyright (c) 2020 Greene Laboratory
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./DivisiveClustering.php
|
||||
* @see ./clustering_overview.png
|
||||
* @see https://en.wikipedia.org/wiki/Hierarchical_clustering
|
||||
* @see https://github.com/greenelab/hclust/blob/master/README.md
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
* @todo Implement missing linkage functions
|
||||
*/
|
||||
final class AgglomerativeClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \Closure $metric;
|
||||
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \Closure $linkage;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(?\Closure $metric = null, ?\Closure $linkage = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (Point $a, Point $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
$this->linkage = $linkage ?? function (array $a, array $b, array $distances) {
|
||||
return self::averageDistanceLinkage($a, $b, $distances);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum/Complete-Linkage clustering
|
||||
*/
|
||||
public static function maximumDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$max = \PHP_INT_MIN;
|
||||
foreach ($setA as $a) {
|
||||
foreach ($setB as $b) {
|
||||
if ($distances[$a][$b] > $max) {
|
||||
$max = $distances[$a][$b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum/Single-Linkage clustering
|
||||
*/
|
||||
public static function minimumDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$min = \PHP_INT_MAX;
|
||||
foreach ($setA as $a) {
|
||||
foreach ($setB as $b) {
|
||||
if ($distances[$a][$b] < $min) {
|
||||
$min = $distances[$a][$b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unweighted average linkage clustering (UPGMA)
|
||||
*/
|
||||
public static function averageDistanceLinkage(array $setA, array $setB, array $distances) : float
|
||||
{
|
||||
$distance = 0;
|
||||
foreach ($setA as $a) {
|
||||
$distance += \array_sum($distances[$a]);
|
||||
}
|
||||
|
||||
return $distance / \count($setA) / \count($setB);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Birch implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering interface.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Get cluster centroids
|
||||
*
|
||||
* @return Point[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCentroids() : array;
|
||||
|
||||
/**
|
||||
* Get cluster assignments of the training data
|
||||
*
|
||||
* @return Point[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getClusters() : array;
|
||||
|
||||
/**
|
||||
* Cluster a single point
|
||||
*
|
||||
* This point doesn't have to be in the training data.
|
||||
*
|
||||
* @param Point $point Point to cluster
|
||||
*
|
||||
* @return null|Point
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point;
|
||||
|
||||
/**
|
||||
* Get noise data.
|
||||
*
|
||||
* Data points from the training data that are not part of a cluster.
|
||||
*
|
||||
* @return Point[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getNoise() : array;
|
||||
|
||||
// Not possible to interface due to different implementations
|
||||
// public function generateClusters(...) : void
|
||||
}
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Geometry\ConvexHull\MonotoneChain;
|
||||
use phpOMS\Math\Geometry\Shape\D2\Polygon;
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Expand to n dimensions
|
||||
*/
|
||||
final class DBSCAN implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Epsilon for float comparison.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const EPSILON = 4.88e-04;
|
||||
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $metric;
|
||||
|
||||
/**
|
||||
* Points outside of any cluster
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $noisePoints = [];
|
||||
|
||||
/**
|
||||
* All points
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusterCenters = [];
|
||||
|
||||
/**
|
||||
* Clusters
|
||||
*
|
||||
* Array of points assigned to a cluster
|
||||
*
|
||||
* @var array<int, Point[]>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
/**
|
||||
* Convex hull of all clusters
|
||||
*
|
||||
* @var array<array>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $convexHulls = [];
|
||||
|
||||
/**
|
||||
* Cluster points
|
||||
*
|
||||
* Points in clusters (helper to avoid looping the cluster array)
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusteredPoints = [];
|
||||
|
||||
/**
|
||||
* Distance matrix
|
||||
*
|
||||
* Distances between points
|
||||
*
|
||||
* @var array<float[]>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $distanceMatrix = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(?\Closure $metric = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (Point $a, Point $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand cluster with additional point and potential neighbors.
|
||||
*
|
||||
* @param Point $point Point to add to a cluster
|
||||
* @param array $neighbors Neighbors of point
|
||||
* @param int $c Cluster id
|
||||
* @param float $epsilon Max distance
|
||||
* @param int $minPoints Min amount of points required for a cluster
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function expandCluster(
|
||||
Point $point,
|
||||
array $neighbors,
|
||||
int $c,
|
||||
float $epsilon,
|
||||
int $minPoints
|
||||
) : void
|
||||
{
|
||||
$this->clusters[$c][] = $point;
|
||||
$this->clusteredPoints[] = $point;
|
||||
$nPoint = \reset($neighbors);
|
||||
|
||||
while ($nPoint) {
|
||||
$neighbors2 = $this->findNeighbors($nPoint, $epsilon);
|
||||
|
||||
if (\count($neighbors2) >= $minPoints) {
|
||||
foreach ($neighbors2 as $nPoint2) {
|
||||
if (!isset($neighbors[$nPoint2->name])) {
|
||||
$neighbors[$nPoint2->name] = $nPoint2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($nPoint->name, $this->clusteredPoints)) {
|
||||
$this->clusters[$c][] = $nPoint;
|
||||
$this->clusteredPoints[] = $nPoint;
|
||||
}
|
||||
|
||||
$nPoint = \next($neighbors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbors of a point
|
||||
*
|
||||
* @param Point $point Base point for potential neighbors
|
||||
* @param float $epsilon Max distance to neighbor
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findNeighbors(Point $point, float $epsilon) : array
|
||||
{
|
||||
$neighbors = [];
|
||||
foreach ($this->points as $point2) {
|
||||
if ($point->isEquals($point2)) {
|
||||
$distance = isset($this->distanceMatrix[$point->name])
|
||||
? $this->distanceMatrix[$point->name][$point2->name]
|
||||
: $this->distanceMatrix[$point2->name][$point->name];
|
||||
|
||||
if ($distance < $epsilon) {
|
||||
$neighbors[$point2->name] = $point2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate distances between points
|
||||
*
|
||||
* @param array $points Array of all points
|
||||
*
|
||||
* @return float[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function generateDistanceMatrix(array $points) : array
|
||||
{
|
||||
$distances = [];
|
||||
foreach ($points as $point) {
|
||||
$distances[$point->name] = [];
|
||||
foreach ($points as $point2) {
|
||||
$distances[$point->name][$point2->name] = ($this->metric)($point, $point2);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var float[] $distances */
|
||||
return $distances;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
if ($this->convexHulls === []) {
|
||||
foreach ($this->clusters as $c => $cluster) {
|
||||
$points = [];
|
||||
foreach ($cluster as $p) {
|
||||
$points[] = $p->coordinates;
|
||||
}
|
||||
|
||||
// @todo this is only good for 2D. Fix this for ND.
|
||||
$this->convexHulls[$c] = MonotoneChain::createConvexHull($points);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->convexHulls as $c => $hull) {
|
||||
if (Polygon::isPointInPolygon($point->coordinates, $hull) <= 0) {
|
||||
return $hull;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the clusters of the points
|
||||
*
|
||||
* @param Point[] $points Points to cluster
|
||||
* @param float $epsilon Max distance
|
||||
* @param int $minPoints Min amount of points required for a cluster
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function generateClusters(array $points, float $epsilon, int $minPoints) : void
|
||||
{
|
||||
$this->noisePoints = [];
|
||||
$this->clusters = [];
|
||||
$this->clusteredPoints = [];
|
||||
$this->points = $points;
|
||||
$this->convexHulls = [];
|
||||
|
||||
$this->distanceMatrix = $this->generateDistanceMatrix($points);
|
||||
|
||||
$c = 0;
|
||||
$this->clusters[$c] = [];
|
||||
|
||||
foreach ($this->points as $point) {
|
||||
$neighbors = $this->findNeighbors($point, $epsilon);
|
||||
|
||||
if (\count($neighbors) < $minPoints) {
|
||||
$this->noisePoints[] = $point->name;
|
||||
} elseif (!\in_array($point->name, $this->clusteredPoints)) {
|
||||
$this->expandCluster($point, $neighbors, $c, $epsilon, $minPoints);
|
||||
++$c;
|
||||
$this->clusters[$c] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
if (!empty($this->clusterCenters)) {
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
$dim = \count(\reset($this->points)->getCoordinates());
|
||||
foreach ($this->clusters as $cluster) {
|
||||
$middle = \array_fill(0, $dim, 0);
|
||||
foreach ($cluster as $point) {
|
||||
for ($i = 0; $i < $dim; ++$i) {
|
||||
$middle[$i] += $point->getCoordinate($i);
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $dim; ++$i) {
|
||||
$middle[$i] /= \count($cluster);
|
||||
}
|
||||
|
||||
$this->clusterCenters = new Point($middle);
|
||||
}
|
||||
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return $this->noisePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return $this->clusters;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* The parent category of this clustering algorithm is hierarchical clustering.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./AgglomerativeClustering.php
|
||||
* @see ./clustering_overview.png
|
||||
* @see https://en.wikipedia.org/wiki/Hierarchical_clustering
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class DivisiveClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Kmeans implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Epsilon for float comparison.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const EPSILON = 4.88e-04;
|
||||
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $metric;
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusterCenters = [];
|
||||
|
||||
/**
|
||||
* Points of the clusters
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
/**
|
||||
* Points
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(?\Closure $metric = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (Point $a, Point $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
$bestCluster = null;
|
||||
$bestDistance = \PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($this->clusterCenters as $center) {
|
||||
if (($distance = ($this->metric)($center, $point)) < $bestDistance) {
|
||||
$bestCluster = $center;
|
||||
$bestDistance = $distance;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestCluster;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the clusters of the points
|
||||
*
|
||||
* @param Point[] $points Points to cluster
|
||||
* @param int<1, max> $clusters Amount of clusters
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function generateClusters(array $points, int $clusters) : void
|
||||
{
|
||||
$this->points = $points;
|
||||
$n = \count($points);
|
||||
$clusterCenters = $this->kpp($points, $clusters);
|
||||
$coordinates = \count($points[0]->coordinates);
|
||||
|
||||
while (true) {
|
||||
foreach ($clusterCenters as $center) {
|
||||
for ($i = 0; $i < $coordinates; ++$i) {
|
||||
$center->setCoordinate($i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($points as $point) {
|
||||
$clusterPoint = $clusterCenters[$point->group];
|
||||
|
||||
++$clusterPoint->group;
|
||||
for ($i = 0; $i < $coordinates; ++$i) {
|
||||
$clusterPoint->setCoordinate($i, $clusterPoint->getCoordinate($i) + $point->getCoordinate($i));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($clusterCenters as $center) {
|
||||
for ($i = 0; $i < $coordinates; ++$i) {
|
||||
$center->setCoordinate($i, $center->getCoordinate($i) / $center->group);
|
||||
}
|
||||
}
|
||||
|
||||
$changed = 0;
|
||||
foreach ($points as $point) {
|
||||
$min = $this->nearestClusterCenter($point, $clusterCenters)[0];
|
||||
|
||||
if ($clusters !== $point->group) {
|
||||
++$changed;
|
||||
$point->group = $min;
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed <= $n * self::EPSILON || $n * self::EPSILON < 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($clusterCenters as $key => $center) {
|
||||
$center->group = $key;
|
||||
$center->name = (string) $key;
|
||||
}
|
||||
|
||||
$this->clusterCenters = $clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index and distance to the nearest cluster center
|
||||
*
|
||||
* @param Point $point Point to get the cluster for
|
||||
* @param Point[] $clusterCenters All cluster centers
|
||||
*
|
||||
* @return array [index, distance]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function nearestClusterCenter(Point $point, array $clusterCenters) : array
|
||||
{
|
||||
$index = $point->group;
|
||||
$dist = \PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($clusterCenters as $key => $cPoint) {
|
||||
$d = ($this->metric)($cPoint, $point);
|
||||
|
||||
if ($dist > $d) {
|
||||
$dist = $d;
|
||||
$index = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return [$index, $dist];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cluster centers
|
||||
*
|
||||
* @param Point[] $points Points to use for the cluster center initialization
|
||||
* @param int<0, max> $n Amount of clusters to use
|
||||
*
|
||||
* @return Point[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function kpp(array $points, int $n) : array
|
||||
{
|
||||
$clusters = [clone $points[\array_rand($points, 1)]];
|
||||
|
||||
$d = \array_fill(0, $n, 0.0);
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
$sum = 0;
|
||||
|
||||
foreach ($points as $key => $point) {
|
||||
$d[$key] = $this->nearestClusterCenter($point, $clusters)[1];
|
||||
$sum += $d[$key];
|
||||
}
|
||||
|
||||
$sum *= \mt_rand(0, \mt_getrandmax()) / \mt_getrandmax();
|
||||
|
||||
$found = false;
|
||||
foreach ($d as $key => $di) {
|
||||
$sum -= $di;
|
||||
|
||||
// The in array check is important to avoid duplicate cluster centers
|
||||
if ($sum <= 0 && !\in_array($c = $points[$key], $clusters)) {
|
||||
$clusters[$i] = clone $c;
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
while (!$found) {
|
||||
if (!\in_array($c = $points[\array_rand($points)], $clusters)) {
|
||||
$clusters[$i] = clone $c;
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($points as $point) {
|
||||
$point->group = $this->nearestClusterCenter($point, $clusters)[0];
|
||||
}
|
||||
|
||||
return $clusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
if (!empty($this->clusters)) {
|
||||
return $this->clusters;
|
||||
}
|
||||
|
||||
foreach ($this->points as $point) {
|
||||
$c = $this->cluster($point);
|
||||
$this->clusters[$c?->name] = $point;
|
||||
}
|
||||
|
||||
return $this->clusters;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\KernelsND;
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement noise points
|
||||
*/
|
||||
final class MeanShift implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* Min distance for clustering
|
||||
*
|
||||
* As long as a point is further away as the min distance the shifting is performed
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const MIN_DISTANCE = 0.001;
|
||||
|
||||
/**
|
||||
* Kernel function
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $kernel;
|
||||
|
||||
/**
|
||||
* Metric function
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $metric;
|
||||
|
||||
private array $points;
|
||||
|
||||
/**
|
||||
* Points outside of any cluster
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $noisePoints = [];
|
||||
|
||||
/**
|
||||
* Cluster points
|
||||
*
|
||||
* Points in clusters (helper to avoid looping the cluster array)
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
*
|
||||
* @var Point[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusterCenters = [];
|
||||
|
||||
/**
|
||||
* Max distance to cluster to be still considered part of cluster
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $groupDistanceTolerance = 0.1;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Both the metric and kernel function need to be of the same dimension.
|
||||
*
|
||||
* @param null|\Closure $metric Metric to use for the distance between two points
|
||||
* @param null|\Closure $kernel Kernel
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(?\Closure $metric = null, ?\Closure $kernel = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (Point $a, Point $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
$this->kernel = $kernel ?? function (array $distances, array $bandwidths) {
|
||||
return KernelsND::gaussianKernel($distances, $bandwidths);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the clusters of the points
|
||||
*
|
||||
* @param Point[] $points Points to cluster
|
||||
* @param array<int|float> $bandwidth Bandwidth(s)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function generateClusters(array $points, array $bandwidth) : void
|
||||
{
|
||||
$this->points = $points;
|
||||
$shiftPoints = $points;
|
||||
$maxMinDist = 1;
|
||||
|
||||
$stillShifting = \array_fill(0, \count($points), true);
|
||||
|
||||
$pointLength = \count($shiftPoints);
|
||||
|
||||
while ($maxMinDist > self::MIN_DISTANCE) {
|
||||
$maxMinDist = 0;
|
||||
|
||||
for ($i = 0; $i < $pointLength; ++$i) {
|
||||
if (!$stillShifting[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pNew = $shiftPoints[$i];
|
||||
$pNewStart = $pNew;
|
||||
$pNew = $this->shiftPoint($pNew, $points, $bandwidth);
|
||||
$dist = ($this->metric)($pNew, $pNewStart);
|
||||
|
||||
if ($dist > $maxMinDist) {
|
||||
$maxMinDist = $dist;
|
||||
}
|
||||
|
||||
if ($dist < self::MIN_DISTANCE) {
|
||||
$stillShifting[$i] = false;
|
||||
}
|
||||
|
||||
$shiftPoints[$i] = $pNew;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo create an array of noisePoints like in the DBSCAN. That array can be empty or not depending on the bandwidth defined
|
||||
|
||||
$this->clusters = $this->groupPoints($shiftPoints);
|
||||
$this->clusterCenters = $shiftPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform shift on a point
|
||||
*
|
||||
* @param Point $point Point to shift
|
||||
* @param Point $points Array of all points
|
||||
* @param array<int|float> $bandwidth Bandwidth(s)
|
||||
*
|
||||
* @return Point
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function shiftPoint(Point $point, array $points, array $bandwidth) : Point
|
||||
{
|
||||
$scaleFactor = 0.0;
|
||||
|
||||
$shifted = clone $point;
|
||||
|
||||
foreach ($points as $pTemp) {
|
||||
$dist = ($this->metric)($point, $pTemp);
|
||||
$weight = ($this->kernel)($dist, $bandwidth);
|
||||
|
||||
foreach ($point->coordinates as $idx => $_) {
|
||||
if (!isset($shifted->coordinates[$idx])) {
|
||||
$shifted->coordinates[$idx] = 0;
|
||||
}
|
||||
|
||||
$shifted->coordinates[$idx] += $pTemp->coordinates[$idx] * $weight;
|
||||
}
|
||||
|
||||
$scaleFactor += $weight;
|
||||
}
|
||||
|
||||
foreach ($shifted->coordinates as $idx => $_) {
|
||||
$shifted->coordinates[$idx] /= $scaleFactor;
|
||||
}
|
||||
|
||||
return $shifted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group points together into clusters
|
||||
*
|
||||
* @param Point[] $points Array of points to assign to groups
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function groupPoints(array $points) : array
|
||||
{
|
||||
$groupAssignment = [];
|
||||
$groups = [];
|
||||
$groupIndex = 0;
|
||||
|
||||
foreach ($points as $point) {
|
||||
$nearestGroupIndex = $this->findNearestGroup($point, $groups);
|
||||
|
||||
if ($nearestGroupIndex === -1) {
|
||||
// create new group
|
||||
$groups[] = [$point];
|
||||
$groupAssignment[] = $groupIndex;
|
||||
|
||||
++$groupIndex;
|
||||
} else {
|
||||
$groupAssignment[] = $nearestGroupIndex;
|
||||
$groups[$nearestGroupIndex][] = $point;
|
||||
}
|
||||
}
|
||||
|
||||
return $groupAssignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest cluster/group of a point
|
||||
*
|
||||
* @param Point $point Point to find the cluster for
|
||||
* @param array<Point[]> $groups Clusters
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function findNearestGroup(Point $point, array $groups) : int
|
||||
{
|
||||
$nearestGroupIndex = -1;
|
||||
$index = 0;
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$distanceToGroup = $this->distanceToGroup($point, $group);
|
||||
|
||||
if ($distanceToGroup < $this->groupDistanceTolerance) {
|
||||
$nearestGroupIndex = $index;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
++$index;
|
||||
}
|
||||
|
||||
return $nearestGroupIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find distance of point to best cluster/group
|
||||
*
|
||||
* @param Point $point Point to find the cluster for
|
||||
* @param Point[] $group Clusters
|
||||
*
|
||||
* @return float Distance
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function distanceToGroup(Point $point, array $group) : float
|
||||
{
|
||||
$minDistance = \PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($group as $pt) {
|
||||
$dist = ($this->metric)($point, $pt);
|
||||
|
||||
if ($dist < $minDistance) {
|
||||
$minDistance = $dist;
|
||||
}
|
||||
}
|
||||
|
||||
return $minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return $this->clusterCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
$clusterId = $this->findNearestGroup($point, $this->clusters);
|
||||
|
||||
return $this->clusterCenters[$clusterId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return $this->noisePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return $this->clusters;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Point for clustering
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Point implements PointInterface
|
||||
{
|
||||
/**
|
||||
* Coordinates of the point
|
||||
*
|
||||
* @var array<int, int|float>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $coordinates = [];
|
||||
|
||||
/**
|
||||
* Group or cluster this point belongs to
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $group = 0;
|
||||
|
||||
/**
|
||||
* Name of the point
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<int, int|float> $coordinates Coordinates of the point
|
||||
* @param string $name Name of the point
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(array $coordinates, string $name = '')
|
||||
{
|
||||
$this->coordinates = $coordinates;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCoordinates() : array
|
||||
{
|
||||
return $this->coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCoordinate(int $index) : int | float
|
||||
{
|
||||
return $this->coordinates[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setCoordinate(int $index, int | float $value) : void
|
||||
{
|
||||
$this->coordinates[$index] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEquals(Point $point) : bool
|
||||
{
|
||||
return $this->name === $point->name && $this->coordinates === $point->coordinates;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Point interface.
|
||||
*
|
||||
* @property int $group Group
|
||||
* @property string $name Name
|
||||
* @property array $coordinates Coordinates
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @property array<int, int|float> $coordinates
|
||||
* @property string $name
|
||||
* @property int $group
|
||||
*/
|
||||
interface PointInterface
|
||||
{
|
||||
/**
|
||||
* Get the point coordinates
|
||||
*
|
||||
* @return array<int, int|float>
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCoordinates() : array;
|
||||
|
||||
/**
|
||||
* Get the coordinate of the point
|
||||
*
|
||||
* @param int $index Index of the coordinate (e.g. 0 = x);
|
||||
*
|
||||
* @return int|float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCoordinate(int $index) : int | float;
|
||||
|
||||
/**
|
||||
* Set the coordinate of the point
|
||||
*
|
||||
* @param int $index Index of the coordinate (e.g. 0 = x);
|
||||
* @param int|float $value Value of the coordinate
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setCoordinate(int $index, int | float $value) : void;
|
||||
|
||||
/**
|
||||
* Check if two points are equal
|
||||
*
|
||||
* @param Point $point Point to compare with
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isEquals(Point $point) : bool;
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see ./clustering_overview.png
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class SpectralClustering implements ClusteringInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCentroids() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClusters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cluster(Point $point) : ?Point
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNoise() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 710 KiB |
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\CoinMatching
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\CoinMatching;
|
||||
|
||||
/**
|
||||
* Matching a value with a set of coins
|
||||
*
|
||||
* @package phpOMS\Algorithm\CoinMatching
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class MinimumCoinProblem
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the minimum amount of coins that are required to match a value
|
||||
*
|
||||
* @param array $coins Types of coins available (every coin has infinite availablity)
|
||||
* @param int $value Value to match with the coins
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function getMinimumCoinsForValueI(array $coins, int $value) : array
|
||||
{
|
||||
// amount of required coins for different values
|
||||
$table = [0];
|
||||
$usedCoins = [];
|
||||
|
||||
for ($i = 1; $i <= $value; ++$i) {
|
||||
$table[$i] = \PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$m = \count($coins);
|
||||
|
||||
for ($i = 1; $i <= $value; ++$i) {
|
||||
for ($j = 0; $j < $m; ++$j) {
|
||||
if ($coins[$j] <= $i) {
|
||||
$subRes = $table[$i - $coins[$j]];
|
||||
|
||||
if ($subRes !== \PHP_INT_MAX
|
||||
&& $subRes + 1 < $table[$i]
|
||||
) {
|
||||
$table[$i] = $subRes + 1;
|
||||
$usedCoins[$i] = $coins[$j] === null ? ($usedCoins[$i] ?? []) : \array_merge($usedCoins[$i - $coins[$j]] ?? [], [$coins[$j]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $usedCoins[$value] ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Frequency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Frequency;
|
||||
|
||||
/**
|
||||
* Apriori algorithm.
|
||||
*
|
||||
* The algorithm checks how often a set exists in a given set of sets.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Frequency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Apriori
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all possible subsets
|
||||
*
|
||||
* @param array $arr Array of elements
|
||||
*
|
||||
* @return array<array>
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generateSubsets(array $arr) : array
|
||||
{
|
||||
$subsets = [[]];
|
||||
|
||||
foreach ($arr as $element) {
|
||||
$newSubsets = [];
|
||||
|
||||
foreach ($subsets as $subset) {
|
||||
$newSubsets[] = $subset;
|
||||
$newSubsets[] = \array_merge($subset, [$element]);
|
||||
}
|
||||
|
||||
$subsets = $newSubsets;
|
||||
}
|
||||
|
||||
unset($subsets[0]);
|
||||
|
||||
return $subsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the apriori algorithm.
|
||||
*
|
||||
* The algorithm cheks how often a set exists in a given set of sets.
|
||||
*
|
||||
* @param array<string[]> $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]])
|
||||
* @param string[] $subset Subset to check for (empty array -> all subsets are checked)
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function apriori(array $sets, array $subset = []) : array
|
||||
{
|
||||
// Unique single items
|
||||
$totalSet = [];
|
||||
foreach ($sets as &$s) {
|
||||
\sort($s);
|
||||
|
||||
foreach ($s as $item) {
|
||||
$totalSet[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$totalSet = \array_unique($totalSet);
|
||||
\sort($totalSet);
|
||||
\sort($subset);
|
||||
|
||||
// Combinations of items
|
||||
$combinations = self::generateSubsets($totalSet);
|
||||
|
||||
// Table
|
||||
$table = [];
|
||||
foreach ($combinations as &$c) {
|
||||
\sort($c);
|
||||
if (!empty($subset) && $c !== $subset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$table[\implode(':', $c)] = 0;
|
||||
}
|
||||
|
||||
foreach ($combinations as $combination) {
|
||||
if (!empty($subset) && $combination !== $subset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($sets as $set) {
|
||||
foreach ($combination as $item) {
|
||||
if (!\in_array($item, $set)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
++$table[\implode(':', $combination)];
|
||||
}
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Graph;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Graph;
|
||||
|
||||
/**
|
||||
* Dependency resolver class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Graph;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class DependencyResolver
|
||||
{
|
||||
/**
|
||||
* Resolve dependencies
|
||||
*
|
||||
* @param array $graph Graph to resolve
|
||||
*
|
||||
* @return null|array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function resolve(array $graph) : ?array
|
||||
{
|
||||
$resolved = [];
|
||||
$unresolved = [];
|
||||
foreach ($graph as $table => $_) {
|
||||
self::dependencyResolve($table, $graph, $resolved, $unresolved);
|
||||
}
|
||||
|
||||
return empty($unresolved) ? $resolved : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm to resolve dependencies
|
||||
*
|
||||
* @param int|string $item Item id
|
||||
* @param array<int|string, array> $items All items
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function dependencyResolve(int | string $item, array $items, array &$resolved, array &$unresolved) : void
|
||||
{
|
||||
$unresolved[] = $item;
|
||||
|
||||
if (!isset($items[$item])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($items[$item] as $dependency) {
|
||||
if (!\in_array($dependency, $unresolved)) {
|
||||
$unresolved[] = $dependency;
|
||||
self::dependencyResolve($dependency, $items, $resolved, $unresolved);
|
||||
} else {
|
||||
return; // circular dependency
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($item, $resolved)) {
|
||||
$resolved[] = $item;
|
||||
}
|
||||
|
||||
foreach ($unresolved as $key => $unres) {
|
||||
if ($unres === $item) {
|
||||
unset($unresolved[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Graph
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Graph;
|
||||
|
||||
/**
|
||||
* Markov chain
|
||||
*
|
||||
* @package phpOMS\Algorithm\Graph
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class MarkovChain
|
||||
{
|
||||
/**
|
||||
* Order of the markov chain
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private int $order = 1;
|
||||
|
||||
/**
|
||||
* Trained data
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $order Order of the markov chain
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $order = 1)
|
||||
{
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create markov chain based on input
|
||||
*
|
||||
* @param array $values Training values
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function train(array $values) : void
|
||||
{
|
||||
$temp = [];
|
||||
$length = \count($values) - $this->order;
|
||||
|
||||
$unique = \array_unique($values);
|
||||
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$key = [];
|
||||
for ($j = 0; $j < $this->order; ++$j) {
|
||||
$key[] = $values[$i + $j];
|
||||
}
|
||||
|
||||
$keyString = \implode(' ', $key);
|
||||
|
||||
if (!isset($temp[$keyString])) {
|
||||
foreach ($unique as $value) {
|
||||
$temp[$keyString][$value] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
++$temp[$keyString][$values[$i + 1]];
|
||||
}
|
||||
|
||||
foreach ($temp as $key => $values) {
|
||||
$sum = \array_sum($values);
|
||||
foreach ($values as $idx => $value) {
|
||||
$this->data[$key][$idx] = $value / $sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set training data
|
||||
*
|
||||
* @param array<array<int, int>> $values Training values
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setTraining(array $values) : void
|
||||
{
|
||||
$this->data = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a markov chain based on the training data.
|
||||
*
|
||||
* @param int $length Length of the markov chain
|
||||
* @param array $start Start values of the markov chain
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function generate(int $length, ?array $start = null) : array
|
||||
{
|
||||
$orderKeys = \array_keys($this->data);
|
||||
$orderValues = \array_keys(\reset($this->data));
|
||||
|
||||
$output = $start ?? \explode(' ', $orderKeys[\array_rand($orderKeys)]);
|
||||
$key = $output;
|
||||
|
||||
for ($i = $this->order; $i < $length; ++$i) {
|
||||
$keyString = \implode(' ', $key);
|
||||
|
||||
$prob = \mt_rand(1, 100) / 100;
|
||||
$cProb = 0.0;
|
||||
$val = null;
|
||||
$new = null;
|
||||
|
||||
foreach (($this->data[$keyString] ?? []) as $val => $p) {
|
||||
$cProb += $p;
|
||||
|
||||
if ($prob <= $cProb) {
|
||||
$new = $val;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find possible key
|
||||
$new ??= $orderValues[\array_rand($orderValues)];
|
||||
|
||||
$output[] = $new;
|
||||
$key[] = $new;
|
||||
|
||||
\array_shift($key);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the probability for a certain markov chain.
|
||||
*
|
||||
* @param array $path Markov chain
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function pathProbability(array $path) : float
|
||||
{
|
||||
$length = \count($path);
|
||||
if ($length <= $this->order) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$key = \array_slice($path, 0, $this->order);
|
||||
|
||||
$prob = 1.0;
|
||||
for ($i = $this->order; $i < $length; ++$i) {
|
||||
$prob *= $this->data[\implode(' ', $key)][$path[$i]] ?? 0.0;
|
||||
|
||||
$key[] = $path[$i];
|
||||
\array_shift($key);
|
||||
}
|
||||
|
||||
return $prob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the probability for a certain state change in a markov chain
|
||||
*
|
||||
* @param array $state Current state of the markov chain
|
||||
* @param mixed $next Next markov state
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function stepProbability(array $state, mixed $next) : float
|
||||
{
|
||||
if (\count($state) !== $this->order) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return $this->data[\implode(' ', $state)][$next] ?? 0.0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\JobScheduling;
|
||||
|
||||
/**
|
||||
* Job for scheduling
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Job implements JobInterface
|
||||
{
|
||||
/**
|
||||
* Value of the job
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $value = 0.0;
|
||||
|
||||
/**
|
||||
* Start time of the job
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \DateTime $start;
|
||||
|
||||
/**
|
||||
* End time of the job
|
||||
*
|
||||
* @var \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private ?\DateTime $end = null;
|
||||
|
||||
/**
|
||||
* Name of the job
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param float $value Value of the job
|
||||
* @param \DateTime $start Start time of the job
|
||||
* @param null|\DateTime $end End time of the job
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(float $value, \DateTime $start, ?\DateTime $end, string $name = '')
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValue() : float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStart() : \DateTime
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEnd() : ?\DateTime
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\JobScheduling;
|
||||
|
||||
/**
|
||||
* Job interface.
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface JobInterface
|
||||
{
|
||||
/**
|
||||
* Get value of the job
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getValue() : float;
|
||||
|
||||
/**
|
||||
* Get start time of the job
|
||||
*
|
||||
* @return \DateTime
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getStart() : \DateTime;
|
||||
|
||||
/**
|
||||
* Get end time of the job
|
||||
*
|
||||
* @return \DateTime
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getEnd() : ?\DateTime;
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\JobScheduling;
|
||||
|
||||
/**
|
||||
* Job scheduling algorithm with no overlapping jobs
|
||||
*
|
||||
* @package phpOMS\Algorithm\JobScheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Weighted
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort jobs by end date.
|
||||
*
|
||||
* @param JobInterface $j1 Job 1
|
||||
* @param JobInterface $j2 Job 2
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function sortByEnd(JobInterface $j1, JobInterface $j2) : int
|
||||
{
|
||||
if ($j1->getEnd() === null && $j2->getEnd() !== null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($j1->getEnd() === null && $j2->getEnd() === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($j1->getEnd() !== null && $j2->getEnd() === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $j1->getEnd()->getTimestamp() <=> $j2->getEnd()->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a none-conflicting job that comes before a defined job
|
||||
*
|
||||
* @param JobInterface[] $jobs List of jobs
|
||||
* @param int $pivot Job to find the previous job to
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function binarySearch(array $jobs, int $pivot) : int
|
||||
{
|
||||
$lo = 0;
|
||||
$hi = $pivot - 1;
|
||||
|
||||
while ($lo <= $hi) {
|
||||
$mid = (int) (($lo + $hi) / 2);
|
||||
|
||||
if ($jobs[$mid]->getEnd() !== null
|
||||
&& $jobs[$mid]->getEnd()->getTimestamp() <= $jobs[$pivot]->getStart()->getTimestamp()
|
||||
) {
|
||||
if ($jobs[$mid + 1]->getEnd() !== null
|
||||
&& $jobs[$mid + 1]->getEnd()->getTimestamp() <= $jobs[$pivot]->getStart()->getTimestamp()
|
||||
) {
|
||||
$lo = $mid + 1;
|
||||
} else {
|
||||
return $mid;
|
||||
}
|
||||
} else {
|
||||
$hi = $mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximize the value of the job execution without overlapping jobs
|
||||
*
|
||||
* @param JobInterface[] $jobs Jobs to filter
|
||||
*
|
||||
* @return JobInterface[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function solve(array $jobs) : array
|
||||
{
|
||||
$n = \count($jobs);
|
||||
|
||||
if ($n < 2) {
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
\usort($jobs, function (\phpOMS\Algorithm\JobScheduling\JobInterface $j1, \phpOMS\Algorithm\JobScheduling\JobInterface $j2) : int {
|
||||
return self::sortByEnd($j1, $j2);
|
||||
});
|
||||
|
||||
$valueTable = [$jobs[0]->getValue()];
|
||||
|
||||
$resultTable = [];
|
||||
$resultTable[0] = [$jobs[0]];
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
$value = $jobs[$i]->getValue();
|
||||
$jList = [$jobs[$i]];
|
||||
$l = self::binarySearch($jobs, $i);
|
||||
|
||||
if ($l != -1) {
|
||||
$value += $valueTable[$l];
|
||||
$jList = \array_merge($resultTable[$l], $jList);
|
||||
}
|
||||
|
||||
if ($value > $valueTable[$i - 1]) {
|
||||
$valueTable[$i] = $value;
|
||||
$resultTable[$i] = $jList;
|
||||
} else {
|
||||
$valueTable[$i] = $valueTable[$i - 1];
|
||||
$resultTable[$i] = $resultTable[$i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return $resultTable[$n - 1];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine type.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class IdleIntervalType
|
||||
{
|
||||
public const ACTIVE_TIME = 1; // every x hours of activity
|
||||
|
||||
public const JOB_TIME = 2; // every x jobs
|
||||
|
||||
public const FIXED_TIME = 3; // datetime
|
||||
|
||||
public const GENERAL_TIME = 4; // every x hours
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Idle time.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class IdleTime
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $type = 0; // setup, shutdown, cleaning, maintenance, general, ...
|
||||
|
||||
public int $intervalType = IdleIntervalType::ACTIVE_TIME;
|
||||
|
||||
public int $interval = 0;
|
||||
|
||||
public int $duration = 0; // in seconds
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Job step.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class JobStep
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $order = 0;
|
||||
|
||||
public int $l11n = 0;
|
||||
|
||||
public int $machineType = 0;
|
||||
|
||||
public bool $machineParallelization = false;
|
||||
|
||||
public array $machines = [];
|
||||
|
||||
public int $workerType = 0;
|
||||
|
||||
public array $workerQualifications = []; // qualifications needed
|
||||
|
||||
public bool $workerParallelization = false;
|
||||
|
||||
public array $workers = [];
|
||||
|
||||
public int $material = 0;
|
||||
|
||||
public int $materialQuantity = 0;
|
||||
|
||||
public int $duration = 0; // in seconds
|
||||
|
||||
public int $maxHoldTime = -1; // minutes it can be halted if necessary (-1 infinite, 0 not at all)
|
||||
|
||||
public int $maxHoldAfterCompletion = -1; // minutes the next processing step can be postponed (-1 infinite, 0 not at all)
|
||||
|
||||
private int $realDuration = 0;
|
||||
|
||||
// depending on job completions
|
||||
private array $jobDependencies = [];
|
||||
|
||||
public bool $shouldBeParallel = false;
|
||||
|
||||
/**
|
||||
* Duration
|
||||
* + machine type/machine specific times (e.g. setup time etc.)
|
||||
* + machine-job specific times (e.g. setup time for this job which could be different from the general machine setup time)
|
||||
*/
|
||||
public function calculateDuration() : void
|
||||
{
|
||||
$this->realDuration = $this->duration;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Machine
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public MachineType $type;
|
||||
|
||||
public array $idle = [];
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine type.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MachineType
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = []; // qualifications needed
|
||||
|
||||
// array of arrays, where each operator type requires certain qualifications
|
||||
public array $workerTypes = [];
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Material.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Material
|
||||
{
|
||||
public int $id = 0;
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Material.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Qualification
|
||||
{
|
||||
public int $id = 0;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Worker.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Worker
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $type = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = [];
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Worker.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class WorkerType
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = [];
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
/**
|
||||
* Job.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Job
|
||||
{
|
||||
/**
|
||||
* Id
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $id = 0;
|
||||
|
||||
/**
|
||||
* Time of the execution
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $executionTime = 0;
|
||||
|
||||
/**
|
||||
* Priority.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $priority = 0.0;
|
||||
|
||||
/**
|
||||
* Value this job generates.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $value = 0.0;
|
||||
|
||||
/**
|
||||
* Cost of executing this job.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $cost = 0.0;
|
||||
|
||||
/**
|
||||
* How many iterations has this job been on hold in the queue.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $onhold = 0;
|
||||
|
||||
/**
|
||||
* How many iterations has this job been in process in the queue.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $inprocessing = 0;
|
||||
|
||||
/**
|
||||
* What is the deadline for this job?
|
||||
*
|
||||
* @param \DateTime
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public \DateTime $deadline;
|
||||
|
||||
/**
|
||||
* Which steps must be taken during the job execution
|
||||
*
|
||||
* @var JobStep[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $steps = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->deadline = new \DateTime('now');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profit of the job
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getProfit() : float
|
||||
{
|
||||
return $this->value - $this->cost;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# Notes
|
||||
|
||||
## Job / Item
|
||||
|
||||
1. output item
|
||||
2. output quantity
|
||||
3. output scale factor
|
||||
4. instruction manuals []
|
||||
5. steps []
|
||||
|
||||
### Data
|
||||
|
||||
For single item, for this specific job (quantity plays a role) and for the current state
|
||||
|
||||
1. Work time planned/actual
|
||||
1.1. Per worker type
|
||||
1.2. Total
|
||||
2. Machine time planned/actual
|
||||
2.1. Per machine type
|
||||
2.2. Total
|
||||
3. Total duration planned/actual (is NOT work time + machine time)
|
||||
4. Machines types required incl. quantity
|
||||
5. Worker types required incl. quantity
|
||||
6. Material costs
|
||||
7. Worker costs
|
||||
7.1. Per worker type
|
||||
7.2. Total
|
||||
8. Machine costs
|
||||
8.1. Per machine type
|
||||
8.2. Total
|
||||
9. Progress status in %
|
||||
10. Progress type (time based, step based, manual)
|
||||
11. Value planned/actual
|
||||
11. Costs planned/actual
|
||||
12. Current step
|
||||
|
||||
## Steps
|
||||
|
||||
1. Setup machine
|
||||
1.1. worker types required []
|
||||
1.1.1. qualifications required by worker type []
|
||||
1.1.2. defined after algorithm: workers []
|
||||
1.1.2.1. worker specific qualifications available []
|
||||
1.2. amount of workers per type required
|
||||
1.3. worker scale factor (0 = no scaling, 1 = 100% scaling)
|
||||
1.4. machine types required []
|
||||
1.4.1. qualifications required by machine type []
|
||||
1.4.2. min capacity
|
||||
1.4.3. max capacity
|
||||
1.4.4. defined after algorithm: machines []
|
||||
1.4.4.1. machine specific qualifications required by machine type []
|
||||
1.4.4.2. machine specific min capacity
|
||||
1.4.4.3. machine specific max capacity
|
||||
1.5. amount of machines per type required
|
||||
1.6. machine scale factor (0 = no scaling, 1 = 100% scaling)
|
||||
1.7. worker / machine correlation (1 = equal scaling required, > 1 = more workers required per machine scale, < 1 = less workers required per machine scale (e.g. 1.5 -> 150% additional worker required if machines are scaled by 100%, 0.8 -> 80% additional worker required if machines are scaled by 100%))
|
||||
1.8. worker duration
|
||||
1.8.1. planned
|
||||
1.8.1. current/actual
|
||||
1.9. machine duration
|
||||
1.9.1. planned
|
||||
1.9.1. current/actual
|
||||
1.10. total duration
|
||||
1.10.1. planned
|
||||
1.10.1. current/actual
|
||||
1.11. duration scale factor (1 = duration equally scaled as machine/worker scaling, > 1 = longer duration with scaling, < 1 = shorter duration with scaling (e.g. 1.1 -> 110% additional duration if scaled by 100%, 0.9 -> 90 % additional duration if scaled by 100%)). The scale factor is max(worker scale, machine scale);
|
||||
1.12. depends on steps []
|
||||
1.13. try to parallelize? (planned/actual)
|
||||
1.14. material required []
|
||||
1.14.1. material id
|
||||
1.14.2. planned quantity
|
||||
1.14.2. actual quantity
|
||||
1.15. instruction checklist []
|
||||
1.16. hold time during
|
||||
1.16. hold time until next stip
|
||||
|
||||
2. Insert material 1
|
||||
3. Insert material 2
|
||||
4. Mix material
|
||||
5. Quality control
|
||||
6. Average correction
|
||||
7. Insert material 3
|
||||
8. Insert material 4
|
||||
9. Mix material
|
||||
10. Quality control
|
||||
11. Average correction
|
||||
12. Fill into large bindings
|
||||
13. Fill into smaller bindings
|
||||
14. Quality control
|
||||
15. Packaging
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. Try to manufacture in one go (no large breaks in between)
|
||||
2. Try to parallelize (minimize time needed for production)
|
||||
3. Match deadline (if no deadline available go to "find earliest possible deadline")
|
||||
3.1. Priorize close or early to deadline finish (settings dependant)
|
||||
3.2. If not possilbe re-adjust pending production
|
||||
3.2.1. Focus on (value, cost, ...) (settings dependant)
|
||||
3.2.2. If not possible re-adjust ongoing production
|
||||
3.2.2.1. Focus on (value, cost, ...) (settings dependant)
|
||||
3.2.2.2. If not possible find earliest possible deadline
|
||||
|
||||
Constraints / To consider
|
||||
|
||||
1. Deadline (maybe not defined)
|
||||
2. Machines
|
||||
2.1. Available
|
||||
2.2.1. Other jobs
|
||||
2.2.2. General maintenance cleaning
|
||||
2.2.3. Unforseable maintenance
|
||||
2.2. Scalability by a factor
|
||||
3. Worker
|
||||
2.2. Available
|
||||
2.2.1. Other jobs
|
||||
2.2.2. General maintenance cleaning
|
||||
2.2.3. Vacation/sick
|
||||
2.2. Qualification
|
||||
2.3. Scalability by a factor
|
||||
4. Job variance (multiple corrections required)
|
||||
5. Material
|
||||
4.1. Available
|
||||
4.2. Delivery time
|
||||
6. Parallelizability
|
||||
7. Stock space
|
||||
8. Putting job steps on hold
|
||||
9. max/min capacities
|
||||
10. Scaling factors
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Priority type enum.
|
||||
*
|
||||
* Defines the different priorities in which elements from the queue can be extracted.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class PriorityMode extends Enum
|
||||
{
|
||||
public const FIFO = 1; // First in first out
|
||||
|
||||
public const LIFO = 2; // Last in first out
|
||||
|
||||
public const PRIORITY = 4;
|
||||
|
||||
public const VALUE = 8;
|
||||
|
||||
public const COST = 16;
|
||||
|
||||
public const PROFIT = 32;
|
||||
|
||||
public const HOLD = 64; // Longest on hold
|
||||
|
||||
public const EARLIEST_DEADLINE = 128; // EDF
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
/**
|
||||
* Scheduler.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class ScheduleQueue
|
||||
{
|
||||
/**
|
||||
* Queue
|
||||
*
|
||||
* @var Job[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $queue = [];
|
||||
|
||||
/**
|
||||
* Get element from queue
|
||||
*
|
||||
* @param int $size Amount of elements to return
|
||||
* @param int $type Priority type to use for return
|
||||
*
|
||||
* @return Job[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function get(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = [];
|
||||
$keys = \array_keys($this->queue);
|
||||
|
||||
switch ($type) {
|
||||
case PriorityMode::FIFO:
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
$jobs[$i] = $this->queue[$keys[$i]];
|
||||
}
|
||||
|
||||
break;
|
||||
case PriorityMode::LIFO:
|
||||
for ($i = \count($this->queue) - $size - 1; $i < $size; ++$i) {
|
||||
$jobs[$i] = $this->queue[$keys[$i]];
|
||||
}
|
||||
|
||||
break;
|
||||
case PriorityMode::PRIORITY:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->priority <=> $b->priority;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::VALUE:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->value <=> $a->value;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::COST:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->cost <=> $b->cost;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::PROFIT:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->getProfit() <=> $a->getProfit();
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::HOLD:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->onhold <=> $a->onhold;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::EARLIEST_DEADLINE:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->deadline->getTimestamp() <=> $b->deadline->getTimestamp();
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new element into queue
|
||||
*
|
||||
* @param int $id Element id
|
||||
* @param Job $job Element to add
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function insert(int $id, Job $job) : void
|
||||
{
|
||||
$this->queue[$id] = $job;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop elements from the queue.
|
||||
*
|
||||
* This also removes the elements from the queue
|
||||
*
|
||||
* @param int $size Amount of elements to return
|
||||
* @param int $type Priority type to use for return
|
||||
*
|
||||
* @return Job[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function pop(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = $this->get($size, $type);
|
||||
foreach ($jobs as $id => $_) {
|
||||
unset($this->queue[$id]);
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the hold counter of an element
|
||||
*
|
||||
* @param int $id Id of the element (0 = all elements)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function bumpHold(int $id = 0) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
foreach ($this->queue as $job) {
|
||||
++$job->onhold;
|
||||
}
|
||||
} else {
|
||||
++$this->queue[$id]->onhold;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the priority of an element
|
||||
*
|
||||
* @param int $id Id of the element (0 = all elements)
|
||||
* @param float $priority Priority to increase by
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function adjustPriority(int $id = 0, float $priority = 0.1) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
foreach ($this->queue as $job) {
|
||||
$job->priority += $priority;
|
||||
}
|
||||
} else {
|
||||
$this->queue[$id]->priority += $priority;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an element from the queue
|
||||
*
|
||||
* @param int $id Id of the element
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function remove(int $id) : void
|
||||
{
|
||||
unset($this->queue[$id]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Backpack for the Knapsack problem
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Backpack implements BackpackInterface
|
||||
{
|
||||
/**
|
||||
* Maximum amount of cost this backpack can hold
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $maxCost = 0.0;
|
||||
|
||||
/**
|
||||
* Current value
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $value = 0.0;
|
||||
|
||||
/**
|
||||
* Current cost
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $cost = 0.0;
|
||||
|
||||
/**
|
||||
* Items inside the backpack
|
||||
*
|
||||
* @var array<int, array{item:ItemInterface, quantity:int|float}>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $items = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param float $maxCost Maximum amount of costs the backpack can hold
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(float $maxCost)
|
||||
{
|
||||
$this->maxCost = $maxCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValue() : float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCost() : float
|
||||
{
|
||||
return $this->cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMaxCost() : float
|
||||
{
|
||||
return $this->maxCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getItems() : array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addItem(ItemInterface $item, int | float $quantity = 1) : void
|
||||
{
|
||||
$this->items[] = ['item' => $item, 'quantity' => $quantity];
|
||||
$this->value += $item->getValue() * $quantity;
|
||||
$this->cost += $item->getCost() * $quantity;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Backpack interface.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface BackpackInterface
|
||||
{
|
||||
/**
|
||||
* Get the value of the stored items
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getValue() : float;
|
||||
|
||||
/**
|
||||
* Get the cost of the stored items
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCost() : float;
|
||||
|
||||
/**
|
||||
* Get the max allowed costs for the items
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getMaxCost() : float;
|
||||
|
||||
/**
|
||||
* Get items
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getItems() : array;
|
||||
|
||||
/**
|
||||
* Add item to backpack
|
||||
*
|
||||
* @param ItemInterface $item Item
|
||||
* @param int|float $quantity Quantity of the item
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addItem(ItemInterface $item, int | float $quantity = 1) : void;
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Bounded knapsack algorithm
|
||||
*
|
||||
* This algorithm only works for integer cost, values and quantities!
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Bounded
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the backpack with items
|
||||
*
|
||||
* This algorithm only works for integer cost, values and quantities!
|
||||
*
|
||||
* @param array $items Items to fill the backpack with ['item' => Item, 'quantity' => ?]
|
||||
* @param BackpackInterface $backpack Backpack to fill
|
||||
*
|
||||
* @return BackpackInterface
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function solve(array $items, BackpackInterface $backpack) : BackpackInterface
|
||||
{
|
||||
$n = \count($items);
|
||||
|
||||
// @var int<0, max> $maxCost
|
||||
$maxCost = (int) $backpack->getMaxCost();
|
||||
$mm = \array_fill(0, ($maxCost + 1), 0);
|
||||
$m = [];
|
||||
$m[0] = $mm;
|
||||
|
||||
for ($i = 1; $i <= $n; ++$i) {
|
||||
$m[$i] = $mm;
|
||||
|
||||
for ($j = 0; $j <= $maxCost; ++$j) {
|
||||
$m[$i][$j] = $m[$i - 1][$j];
|
||||
|
||||
for ($k = 1; $k <= $items[$i - 1]['quantity']; ++$k) {
|
||||
if ($k * ((int) $items[$i - 1]['item']->getCost()) > $j) {
|
||||
break;
|
||||
}
|
||||
|
||||
$v = $m[$i - 1][$j - $k * ((int) $items[$i - 1]['item']->getCost())] + $k * ((int) $items[$i - 1]['item']->getValue());
|
||||
|
||||
if ($v > $m[$i][$j]) {
|
||||
$m[$i][$j] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$s = 0;
|
||||
for ($i = $n, $j = $maxCost; $i > 0; --$i) {
|
||||
$s = 0;
|
||||
$v = $m[$i][$j];
|
||||
|
||||
$value = (int) $items[$i - 1]['item']->getValue();
|
||||
|
||||
for ($k = 0; $v !== $m[$i - 1][$j] + $k * $value; ++$k) {
|
||||
++$s;
|
||||
$j -= (int) $items[$i - 1]['item']->getCost();
|
||||
}
|
||||
|
||||
if ($s > 0) {
|
||||
$backpack->addItem($items[$i - 1]['item'], $s);
|
||||
}
|
||||
}
|
||||
|
||||
return $backpack;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Continuous knapsack algorithm
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Continuous
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparing items
|
||||
*
|
||||
* @param Item[] $a Item
|
||||
* @param Item[] $b Item
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function continuousComparator(array $a, array $b) : int
|
||||
{
|
||||
return $b['item']->getValue() / $b['item']->getCost() <=> $a['item']->getValue() / $a['item']->getCost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the backpack with items
|
||||
*
|
||||
* @param array $items Items to fill the backpack with ['item' => Item, 'quantity' => ?]
|
||||
* @param BackpackInterface $backpack Backpack to fill
|
||||
*
|
||||
* @return BackpackInterface
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function solve(array $items, BackpackInterface $backpack) : BackpackInterface
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
\usort($items, ['self', 'continuousComparator']);
|
||||
|
||||
$availableSpace = $backpack->getMaxCost();
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($availableSpace <= 0.0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$backpack->addItem(
|
||||
$item['item'],
|
||||
$quantity = \min($item['quantity'], $availableSpace / $item['item']->getCost())
|
||||
);
|
||||
|
||||
$availableSpace -= $quantity * $item['item']->getCost();
|
||||
}
|
||||
|
||||
return $backpack;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Item in the Knapsack
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Item implements ItemInterface
|
||||
{
|
||||
/**
|
||||
* Value of the item
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $value = 0.0;
|
||||
|
||||
/**
|
||||
* Cost of the item
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $cost = 0.0;
|
||||
|
||||
/**
|
||||
* Name of the item
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public string $name = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param float $value Value of the item
|
||||
* @param float $cost Cost of the item
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(float $value, float $cost, string $name = '')
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->cost = $cost;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValue() : float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCost() : float
|
||||
{
|
||||
return $this->cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Knapsack;
|
||||
|
||||
/**
|
||||
* Item interface.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Knapsack;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface ItemInterface
|
||||
{
|
||||
/**
|
||||
* Get value of the item
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getValue() : float;
|
||||
|
||||
/**
|
||||
* Get value of the item
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCost() : float;
|
||||
|
||||
/**
|
||||
* Get the name of the item
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getName() : string;
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Maze
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Maze;
|
||||
|
||||
/**
|
||||
* Maze generator
|
||||
*
|
||||
* @package phpOMS\Algorithm\Maze
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MazeGenerator
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random maze
|
||||
*
|
||||
* @param int<0, max> $width Width
|
||||
* @param int<0, max> $height Height
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function random(int $width, int $height) : array
|
||||
{
|
||||
$n = $height * $width - 1;
|
||||
$horizontal = \array_fill(0, $height, []);
|
||||
$vertical = \array_fill(0, $height, []);
|
||||
|
||||
$pos = [\mt_rand(0, $height) - 1, \mt_rand(0, $width) - 1];
|
||||
$path = [$pos];
|
||||
$unvisited = [];
|
||||
|
||||
for ($i = 0; $i < $height + 2; ++$i) {
|
||||
$unvisited[] = [];
|
||||
|
||||
for ($j = 0; $j < $width + 1; ++$j) {
|
||||
$unvisited[$i][] = $i > 0 && $i < $height + 1 && $j > 0 && ($i !== $pos[0] + 1 || $j != $pos[1] + 1);
|
||||
}
|
||||
}
|
||||
|
||||
while ($n > 0) {
|
||||
$potential = [
|
||||
[$pos[0] + 1, $pos[1]],
|
||||
[$pos[0], $pos[1] + 1],
|
||||
[$pos[0] - 1, $pos[1]],
|
||||
[$pos[0], $pos[1] - 1],
|
||||
];
|
||||
|
||||
$neighbors = [];
|
||||
|
||||
for ($i = 0; $i < 4; ++$i) {
|
||||
if ($unvisited[$potential[$i][0] + 1][$potential[$i][1] + 1] ?? false) {
|
||||
$neighbors[] = $potential[$i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($neighbors)) {
|
||||
--$n;
|
||||
|
||||
$next = $neighbors[\array_rand($neighbors, 1)];
|
||||
$unvisited[$next[0] + 1][$next[1] + 1] = false;
|
||||
|
||||
if ($next[0] === $pos[0]) {
|
||||
$horizontal[$next[0]][($next[1] + $pos[1] - 1) / 2] = true;
|
||||
} else {
|
||||
$vertical[($next[0] + $pos[0] - 1) / 2][$next[1]] = true;
|
||||
}
|
||||
|
||||
$path[] = $next;
|
||||
$pos = $next;
|
||||
} else {
|
||||
$pos = \array_pop($path);
|
||||
|
||||
if ($pos === null) {
|
||||
break; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$maze = [];
|
||||
for ($i = 0; $i < $height * 2 + 1; ++$i) {
|
||||
$line = [];
|
||||
|
||||
if ($i % 2 === 0) {
|
||||
for ($j = 0; $j < $width * 4 + 1; ++$j) {
|
||||
if ($j % 4 === 0) {
|
||||
$line[$j] = '+'; // 9
|
||||
} else {
|
||||
$line[$j] = $i > 0 && ($vertical[$i / 2 - 1][(int) \floor($j / 4)] ?? false) ? ' ' : '-'; // 9
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for ($j = 0; $j < $width * 4 + 1; ++$j) { // 2
|
||||
if ($j % 4 === 0) {
|
||||
$line[$j] = $j > 0 && ($horizontal[($i - 1) / 2][$j / 4 - 1] ?? false) ? ' ' : '|'; // 0 | 9
|
||||
} else {
|
||||
$line[$j] = ' '; // 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
$line[1] = $line[2] = $line[3] = ' '; // 0
|
||||
}
|
||||
|
||||
if ($height * 2 - 1 === $i) {
|
||||
$line[4 * $width] = ' '; // 2 - 0
|
||||
}
|
||||
|
||||
$maze[] = $line;
|
||||
}
|
||||
|
||||
return $maze;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a maze
|
||||
*
|
||||
* @param array<int, int[]> $maze Maze to render
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function render(array $maze) : string
|
||||
{
|
||||
$rendered = '';
|
||||
foreach ($maze as $row) {
|
||||
foreach ($row as $column) {
|
||||
$rendered .= $column;
|
||||
}
|
||||
|
||||
$rendered .= "\n";
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform ant colony algorithm.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class AntColonyOptimization
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform bees algorithm.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class BeesAlgorithm
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform firefly algorithm.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class FireflyAlgorithm
|
||||
{
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform genetic algorithm (GA).
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class GeneticOptimization
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
// Fitness function (may require to pass solution space as \Closure variable)
|
||||
// E.g.
|
||||
// highest value of some sorts (e.g. profit)
|
||||
// most elements (e.g. jobs)
|
||||
// lowest costs
|
||||
// combination of criteria = points (where some criteria are mandatory/optional)
|
||||
public static function fitness($x)
|
||||
{
|
||||
return $x;
|
||||
}
|
||||
|
||||
public static function mutate($parameters, $mutationRate)
|
||||
{
|
||||
for ($i = 0; $i < \count($parameters); $i++) {
|
||||
if (\mt_rand(0, 1000) / 1000 < $mutationRate) {
|
||||
$parameters[$i] = 1 - $parameters[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public static function crossover($parent1, $parent2, $parameterCount)
|
||||
{
|
||||
$crossoverPoint = \mt_rand(1, $parameterCount - 1);
|
||||
|
||||
$child1 = \array_merge(
|
||||
\array_slice($parent1, 0, $crossoverPoint),
|
||||
\array_slice($parent2, $crossoverPoint)
|
||||
);
|
||||
|
||||
$child2 = \array_merge(
|
||||
\array_slice($parent2, 0, $crossoverPoint),
|
||||
\array_slice($parent1, $crossoverPoint)
|
||||
);
|
||||
|
||||
return [$child1, $child2];
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform optimization
|
||||
*
|
||||
* @example See unit test for example use case
|
||||
*
|
||||
* @param array<array> $population List of all elements with their parameters (i.e. list of "objects" as arrays).
|
||||
* The constraints are defined as array values.
|
||||
* @param \Closure $fitness Fitness function calculates score/feasibility of solution
|
||||
* @param \Closure $mutate Mutation function to change the parameters of an "object"
|
||||
* @param \Closure $crossover Crossover function to exchange parameter values between "objects".
|
||||
* Sometimes single parameters can be exchanged but sometimes interdependencies exist between parameters which is why this function is required.
|
||||
* @param int $generations Number of generations to create
|
||||
* @param float $mutationRate Rate at which parameters are changed.
|
||||
* How this is used depends on the mutate function.
|
||||
*
|
||||
* @return array{solutions:array, fitnesses:float[]}
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function optimize(
|
||||
array $population,
|
||||
\Closure $fitness,
|
||||
\Closure $mutate,
|
||||
\Closure $crossover,
|
||||
int $generations = 500,
|
||||
float $mutationRate = 0.1
|
||||
) : array
|
||||
{
|
||||
$populationSize = \count($population);
|
||||
$parameterCount = $populationSize === 0 ? 0 : \count(\reset($population));
|
||||
|
||||
// Genetic Algorithm Loop
|
||||
for ($generation = 0; $generation < $generations; ++$generation) {
|
||||
$fitnessScores = [];
|
||||
foreach ($population as $parameters) {
|
||||
$fitnessScores[] = ($fitness)($parameters);
|
||||
}
|
||||
|
||||
// Select parents for crossover based on fitness scores
|
||||
$parents = [];
|
||||
for ($i = 0; $i < $populationSize; ++$i) {
|
||||
do {
|
||||
$parentIndex1 = \array_rand($population);
|
||||
$parentIndex2 = \array_rand($population);
|
||||
} while ($parentIndex1 === $parentIndex2);
|
||||
|
||||
$parents[] = $fitnessScores[$parentIndex1] > $fitnessScores[$parentIndex2]
|
||||
? $population[$parentIndex1]
|
||||
: $population[$parentIndex2];
|
||||
}
|
||||
|
||||
// Crossover and mutation to create next generation
|
||||
$newPopulation = [];
|
||||
for ($i = 0; $i < $populationSize; $i += 2) {
|
||||
$crossover = ($crossover)($parents[$i], $parents[$i + 1], $parameterCount);
|
||||
|
||||
$child1 = ($mutate)($crossover[0], $mutationRate);
|
||||
$child2 = ($mutate)($crossover[1], $mutationRate);
|
||||
|
||||
$newPopulation[] = $child1;
|
||||
$newPopulation[] = $child2;
|
||||
}
|
||||
|
||||
$population = $newPopulation;
|
||||
}
|
||||
|
||||
$fitnesses = [];
|
||||
|
||||
foreach ($population as $key => $parameters) {
|
||||
$fitnesses[$key] = ($fitness)($parameters);
|
||||
}
|
||||
|
||||
\asort($fitnesses);
|
||||
|
||||
return [
|
||||
'solutions' => $population,
|
||||
'fitnesses' => $fitnesses,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform harmony search algorithm.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class HarmonySearch
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform intelligent water drops algorithm.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class IntelligentWaterDrops
|
||||
{
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform simulated annealing (SA).
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class SimulatedAnnealing
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
public static function costFunction($x)
|
||||
{
|
||||
return $x;
|
||||
}
|
||||
|
||||
// can be many things, e.g. swapping parameters, increasing/decreasing, random generation
|
||||
public static function neighbor(array $generation, $parameterCount)
|
||||
{
|
||||
$newGeneration = $generation;
|
||||
$randomIndex1 = \mt_rand(0, $parameterCount - 1);
|
||||
$randomIndex2 = \mt_rand(0, $parameterCount - 1);
|
||||
|
||||
// Swap two cities in the route
|
||||
$temp = $newGeneration[$randomIndex1];
|
||||
$newGeneration[$randomIndex1] = $newGeneration[$randomIndex2];
|
||||
$newGeneration[$randomIndex2] = $temp;
|
||||
|
||||
return $newGeneration;
|
||||
}
|
||||
*/
|
||||
|
||||
// Simulated Annealing algorithm
|
||||
// @todo allow to create a solution space (currently all solutions need to be in space)
|
||||
// @todo currently only replacing generations, not altering them
|
||||
/**
|
||||
* Perform optimization
|
||||
*
|
||||
* @example See unit test for example use case
|
||||
*
|
||||
* @param array $space List of all elements with their parameters (i.e. list of "objects" as arrays).
|
||||
* The constraints are defined as array values.
|
||||
* @param int $initialTemperature Starting temperature
|
||||
* @param \Closure $costFunction Fitness function calculates score/feasibility of solution
|
||||
* @param \Closure $neighbor Neighbor function to find a new solution/neighbor
|
||||
* @param float $coolingRate Rate at which cooling takes place
|
||||
* @param int $iterations Number of iterations
|
||||
*
|
||||
* @return array{solutions:array, costs:float[]}
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function optimize(
|
||||
array $space,
|
||||
int $initialTemperature,
|
||||
\Closure $costFunction,
|
||||
\Closure $neighbor,
|
||||
float $coolingRate = 0.98,
|
||||
int $iterations = 1000
|
||||
) : array
|
||||
{
|
||||
$parameterCount = \count($space);
|
||||
$currentGeneration = \reset($space);
|
||||
|
||||
$currentCost = ($costFunction)($currentGeneration);
|
||||
|
||||
for ($i = 0; $i < $iterations; ++$i) {
|
||||
$newGeneration = ($neighbor)($currentGeneration, $parameterCount);
|
||||
|
||||
$newCost = ($costFunction)($newGeneration);
|
||||
|
||||
$temperature = $initialTemperature * \pow($coolingRate, $i);
|
||||
|
||||
if ($newCost < $currentCost
|
||||
|| \mt_rand() / \mt_getrandmax() < \exp(($currentCost - $newCost) / $temperature)
|
||||
) {
|
||||
$currentGeneration = $newGeneration;
|
||||
$currentCost = $newCost;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'solutions' => $currentGeneration,
|
||||
'costs' => $currentCost,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Optimization;
|
||||
|
||||
/**
|
||||
* Perform tabu search.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class TabuSearch
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/*
|
||||
// Define your fitness function here
|
||||
public static function fitness($solution) {
|
||||
// Calculate and return the fitness of the solution
|
||||
// This function should be tailored to your specific problem
|
||||
return $solution;
|
||||
}
|
||||
|
||||
// Define your neighborhood generation function here
|
||||
public static function generateNeighbor($currentSolution) {
|
||||
// Generate a neighboring solution based on the current solution
|
||||
// This function should be tailored to your specific problem
|
||||
return $currentSolution;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform optimization
|
||||
*
|
||||
* @example See unit test for example use case
|
||||
*
|
||||
* @param array $initialSolution List of all elements with ther parameters (i.e. list of "objects" as arrays).
|
||||
* The constraints are defined as array values.
|
||||
* @param \Closure $fitness Fitness function calculates score/feasability of solution
|
||||
* @param \Closure $neighbor Neighbor function to find a new solution/neighbor
|
||||
* @param int $tabuListSize ????
|
||||
* @param int $iterations Number of iterations
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function optimize(
|
||||
array $initialSolution,
|
||||
\Closure $fitness,
|
||||
\Closure $neighbor,
|
||||
int $tabuListSize,
|
||||
int $iterations
|
||||
) : array
|
||||
{
|
||||
$currentSolution = $initialSolution;
|
||||
$bestSolution = $currentSolution;
|
||||
$bestFitness = \PHP_FLOAT_MIN;
|
||||
$tabuList = [];
|
||||
|
||||
for ($i = 0; $i < $iterations; ++$i) {
|
||||
$neighbors = [];
|
||||
for ($j = 0; $j < $tabuListSize; ++$j) {
|
||||
$neighbor = ($neighbor)($currentSolution);
|
||||
$neighbors[] = $neighbor;
|
||||
}
|
||||
|
||||
$bestNeighbor = null;
|
||||
foreach ($neighbors as $neighbor) {
|
||||
if (!\in_array($neighbor, $tabuList) &&
|
||||
($bestNeighbor === null
|
||||
|| ($fitness)($neighbor) > ($fitness)($bestNeighbor))
|
||||
) {
|
||||
$bestNeighbor = $neighbor;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestNeighbor === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
$tabuList[] = $bestNeighbor;
|
||||
if (\count($tabuList) > $tabuListSize) {
|
||||
\array_shift($tabuList);
|
||||
}
|
||||
|
||||
$currentSolution = $bestNeighbor;
|
||||
|
||||
if (($score = ($fitness)($bestNeighbor)) > $bestFitness) {
|
||||
$bestSolution = $bestNeighbor;
|
||||
$bestFitness = $score;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestSolution;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*
|
||||
* Extended based on:
|
||||
* MIT License
|
||||
* (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
|
||||
* (c) PathFinding.js
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Stdlib\Base\Heap;
|
||||
|
||||
/**
|
||||
* Perform path finding.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class AStar implements PathFinderInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function findPath(
|
||||
int $startX, int $startY,
|
||||
int $endX, int $endY,
|
||||
Grid $grid,
|
||||
int $heuristic, int $movement
|
||||
) : Path
|
||||
{
|
||||
/** @var null|AStarNode $startNode */
|
||||
$startNode = $grid->getNode($startX, $startY);
|
||||
/** @var null|AStarNode $endNode */
|
||||
$endNode = $grid->getNode($endX, $endY);
|
||||
|
||||
if ($startNode === null || $endNode === null) {
|
||||
return new Path($grid);
|
||||
}
|
||||
|
||||
$startNode->setG(0.0);
|
||||
$startNode->setF(0.0);
|
||||
$startNode->setOpened(true);
|
||||
|
||||
$openList = new Heap(function (AStarNode $node1, AStarNode $node2) {
|
||||
return $node1->getF() - $node2->getF();
|
||||
});
|
||||
|
||||
$openList->push($startNode);
|
||||
$node = null;
|
||||
|
||||
while (!$openList->isEmpty()) {
|
||||
$node = $openList->pop();
|
||||
if ($node === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
/** @var AStarNode $node */
|
||||
$node->setClosed(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
break;
|
||||
}
|
||||
|
||||
/** @var AStarNode[] $neighbors */
|
||||
$neighbors = $grid->getNeighbors($node, $movement);
|
||||
$neighborsLength = \count($neighbors);
|
||||
for ($i = 0; $i < $neighborsLength; ++$i) {
|
||||
$neighbor = $neighbors[$i];
|
||||
|
||||
if ($neighbor->isClosed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ng = $node->getG() + (($neighbor->getX() - $node->getX() === 0 || $neighbor->getY() - $node->getY() === 0) ? 1 : \sqrt(2));
|
||||
|
||||
if (!$neighbor->isOpened() || $ng < $neighbor->getG()) {
|
||||
$neighbor->setG($ng);
|
||||
$neighbor->setH($neighbor->getH() ?? (
|
||||
$neighbor->getWeight() * Heuristic::metric($neighbor->getCoordinates(), $endNode->getCoordinates(), $heuristic)
|
||||
));
|
||||
$neighbor->setF($neighbor->getG() + $neighbor->getH());
|
||||
$neighbor->parent = $node;
|
||||
|
||||
if (!$neighbor->isOpened()) {
|
||||
$openList->push($neighbor);
|
||||
$neighbor->setOpened(true);
|
||||
} else {
|
||||
$openList->update($neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$path = new Path($grid);
|
||||
|
||||
while ($node !== null) {
|
||||
$path->addNode($node);
|
||||
$node = $node->parent;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Node on grid.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class AStarNode extends Node
|
||||
{
|
||||
/**
|
||||
* The g score is cost of the path
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $g = 0.0;
|
||||
|
||||
/**
|
||||
* The heuristic distance is the cost to the end node
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private ?float $h = null;
|
||||
|
||||
/**
|
||||
* The f score is defined as f(n) = g(n) + h(n)
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $f = 0.0;
|
||||
|
||||
/**
|
||||
* Define as checked node
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private bool $isClosed = false;
|
||||
|
||||
/**
|
||||
* Define as potential candidate
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private bool $isOpened = false;
|
||||
|
||||
/**
|
||||
* Is checked?
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isClosed() : bool
|
||||
{
|
||||
return $this->isClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is potential candidate
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isOpened() : bool
|
||||
{
|
||||
return $this->isOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set check status
|
||||
*
|
||||
* @param bool $isClosed Is closed
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setClosed(bool $isClosed) : void
|
||||
{
|
||||
$this->isClosed = $isClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set potential candidate
|
||||
*
|
||||
* @param bool $isOpened Is potential candidate
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setOpened(bool $isOpened) : void
|
||||
{
|
||||
$this->isOpened = $isOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the g score
|
||||
*
|
||||
* @param float $g G score
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setG(float $g) : void
|
||||
{
|
||||
$this->g = $g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the heuristic distance
|
||||
*
|
||||
* @param float $h H distance
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setH(?float $h) : void
|
||||
{
|
||||
$this->h = $h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the f score
|
||||
*
|
||||
* @param float $f F score
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setF(float $f) : void
|
||||
{
|
||||
$this->f = $f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the g score
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getG() : float
|
||||
{
|
||||
return $this->g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the heuristic distance
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getH() : ?float
|
||||
{
|
||||
return $this->h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the f score
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getF() : float
|
||||
{
|
||||
return $this->f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*
|
||||
* Extended based on:
|
||||
* MIT License
|
||||
* (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
|
||||
* (c) PathFinding.js
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Grid of nodes.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Grid
|
||||
{
|
||||
/**
|
||||
* Grid system containing all nodes
|
||||
*
|
||||
* @var array<int, array<int, Node>>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $nodes = [[]];
|
||||
|
||||
/**
|
||||
* Create a grid from an array
|
||||
*
|
||||
* [
|
||||
* [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* [0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 9, 9, 9, 9, 9, 0, 9, 0, 0, 0, 0,],
|
||||
* [0, 0, 1, 0, 9, 0, 0, 0, 0, 0, 9, 0, 9, 9, 9,],
|
||||
* [0, 0, 0, 0, 9, 0, 0, 9, 9, 9, 9, 0, 0, 0, 0,],
|
||||
* [0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 9, 9, 9, 0,],
|
||||
* [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 9, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 9, 2, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0,],
|
||||
* ]
|
||||
*
|
||||
* @param array<int, int[]> $gridArray Grid defined in an array (0 = empty, 1 = start, 2 = end, 9 = not walkable)
|
||||
* @param string $node Node type name
|
||||
*
|
||||
* @return Grid
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function createGridFromArray(array $gridArray, string $node) : self
|
||||
{
|
||||
$grid = new self();
|
||||
foreach ($gridArray as $y => $yRow) {
|
||||
foreach ($yRow as $x => $xElement) {
|
||||
if ($xElement === 0 || $xElement === 1 || $xElement === 2) {
|
||||
/** @var \phpOMS\Algorithm\PathFinding\Node $empty */
|
||||
$empty = new $node($x, $y, 1.0, true);
|
||||
$grid->setNode($x, $y, $empty);
|
||||
} elseif ($xElement === 9) {
|
||||
/** @var \phpOMS\Algorithm\PathFinding\Node $wall */
|
||||
$wall = new $node($x, $y, 1.0, false);
|
||||
$grid->setNode($x, $y, $wall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set node at position
|
||||
*
|
||||
* @param int $x X-Coordinate
|
||||
* @param int $y Y-Coordinate
|
||||
* @param Node $node Node to set
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setNode(int $x, int $y, Node $node) : void
|
||||
{
|
||||
$this->nodes[$y][$x] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node at position
|
||||
*
|
||||
* @param int $x X-Coordinate
|
||||
* @param int $y Y-Coordinate
|
||||
*
|
||||
* @return null|Node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getNode(int $x, int $y) : ?Node
|
||||
{
|
||||
if (!isset($this->nodes[$y]) || !isset($this->nodes[$y][$x])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->nodes[$y][$x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is node walkable"
|
||||
*
|
||||
* @param int $x X-Coordinate
|
||||
* @param int $y Y-Coordinate
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isWalkable(int $x, int $y) : bool
|
||||
{
|
||||
return isset($this->nodes[$y]) && isset($this->nodes[$y][$x]) && $this->nodes[$y][$x]->isWalkable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get neighbors of node
|
||||
*
|
||||
* @param Node $node Node to get neighbors from
|
||||
* @param int $movement Allowed movements
|
||||
*
|
||||
* @return Node[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getNeighbors(Node $node, int $movement) : array
|
||||
{
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$neighbors = [];
|
||||
|
||||
$s0 = false;
|
||||
$s1 = false;
|
||||
$s2 = false;
|
||||
$s3 = false;
|
||||
$d0 = false;
|
||||
$d1 = false;
|
||||
$d2 = false;
|
||||
$d3 = false;
|
||||
|
||||
if ($this->isWalkable($x, $y - 1)) {
|
||||
$neighbors[] = $this->getNode($x, $y - 1);
|
||||
$s0 = true;
|
||||
}
|
||||
|
||||
if ($this->isWalkable($x + 1, $y)) {
|
||||
$neighbors[] = $this->getNode($x + 1, $y);
|
||||
$s1 = true;
|
||||
}
|
||||
|
||||
if ($this->isWalkable($x, $y + 1)) {
|
||||
$neighbors[] = $this->getNode($x, $y + 1);
|
||||
$s2 = true;
|
||||
}
|
||||
|
||||
if ($this->isWalkable($x - 1, $y)) {
|
||||
$neighbors[] = $this->getNode($x - 1, $y);
|
||||
$s3 = true;
|
||||
}
|
||||
|
||||
if ($movement === MovementType::STRAIGHT) {
|
||||
/** @var Node[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
if ($movement === MovementType::DIAGONAL_NO_OBSTACLE) {
|
||||
$d0 = $s3 && $s0;
|
||||
$d1 = $s0 && $s1;
|
||||
$d2 = $s1 && $s2;
|
||||
$d3 = $s2 && $s3;
|
||||
} elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
|
||||
$d0 = $s3 || $s0;
|
||||
$d1 = $s0 || $s1;
|
||||
$d2 = $s1 || $s2;
|
||||
$d3 = $s2 || $s3;
|
||||
} elseif ($movement === MovementType::DIAGONAL) {
|
||||
$d0 = true;
|
||||
$d1 = true;
|
||||
$d2 = true;
|
||||
$d3 = true;
|
||||
}
|
||||
|
||||
if ($d0 && $this->isWalkable($x - 1, $y - 1)) {
|
||||
$neighbors[] = $this->getNode($x - 1, $y - 1);
|
||||
}
|
||||
|
||||
if ($d1 && $this->isWalkable($x + 1, $y - 1)) {
|
||||
$neighbors[] = $this->getNode($x + 1, $y - 1);
|
||||
}
|
||||
|
||||
if ($d2 && $this->isWalkable($x + 1, $y + 1)) {
|
||||
$neighbors[] = $this->getNode($x + 1, $y + 1);
|
||||
}
|
||||
|
||||
if ($d3 && $this->isWalkable($x - 1, $y + 1)) {
|
||||
$neighbors[] = $this->getNode($x - 1, $y + 1);
|
||||
}
|
||||
|
||||
/** @var Node[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Math\Topology\Metrics2D;
|
||||
|
||||
/**
|
||||
* Node on grid.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Heuristic
|
||||
{
|
||||
/**
|
||||
* Calculate metric/distance between two nodes.
|
||||
*
|
||||
* @param array<string, int|float> $node1 Array with 'x' and 'y' coordinate
|
||||
* @param array<string, int|float> $node2 Array with 'x' and 'y' coordinate
|
||||
* @param int $heuristic Heuristic to use for calculation
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function metric(array $node1, array $node2, int $heuristic) : float
|
||||
{
|
||||
if ($heuristic === HeuristicType::MANHATTAN) {
|
||||
return Metrics2D::manhattan($node1, $node2);
|
||||
} elseif ($heuristic === HeuristicType::EUCLIDEAN) {
|
||||
return Metrics2D::euclidean($node1, $node2);
|
||||
} elseif ($heuristic === HeuristicType::OCTILE) {
|
||||
return Metrics2D::octile($node1, $node2);
|
||||
} elseif ($heuristic === HeuristicType::MINKOWSKI) {
|
||||
return Metrics2D::minkowski($node1, $node2, 1);
|
||||
} elseif ($heuristic === HeuristicType::CANBERRA) {
|
||||
return Metrics2D::canberra($node1, $node2);
|
||||
} elseif ($heuristic === HeuristicType::BRAY_CURTIS) {
|
||||
return Metrics2D::brayCurtis($node1, $node2);
|
||||
}
|
||||
|
||||
return Metrics2D::chebyshev($node1, $node2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Heuristic type enum.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class HeuristicType extends Enum
|
||||
{
|
||||
public const MANHATTAN = 1;
|
||||
|
||||
public const EUCLIDEAN = 2;
|
||||
|
||||
public const OCTILE = 4;
|
||||
|
||||
public const CHEBYSHEV = 8;
|
||||
|
||||
public const MINKOWSKI = 16;
|
||||
|
||||
public const CANBERRA = 32;
|
||||
|
||||
public const BRAY_CURTIS = 64;
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Node on grid.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class JumpPointNode extends Node
|
||||
{
|
||||
/**
|
||||
* The g score is cost of the path
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $g = 0.0;
|
||||
|
||||
/**
|
||||
* The heuristic distance is the cost to the end node
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private ?float $h = null;
|
||||
|
||||
/**
|
||||
* The f score is defined as f(n) = g(n) + h(n)
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $f = 0.0;
|
||||
|
||||
/**
|
||||
* Define as checked node
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private bool $isClosed = false;
|
||||
|
||||
/**
|
||||
* Define as potential candidate
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private bool $isOpened = false;
|
||||
|
||||
/**
|
||||
* The node was already tested?
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private bool $isTested = false;
|
||||
|
||||
/**
|
||||
* Is checked?
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isClosed() : bool
|
||||
{
|
||||
return $this->isClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is potential candidate
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isOpened() : bool
|
||||
{
|
||||
return $this->isOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is already tested
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isTested() : bool
|
||||
{
|
||||
return $this->isTested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set check status
|
||||
*
|
||||
* @param bool $isClosed Is closed
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setClosed(bool $isClosed) : void
|
||||
{
|
||||
$this->isClosed = $isClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set potential candidate
|
||||
*
|
||||
* @param bool $isOpened Is potential candidate
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setOpened(bool $isOpened) : void
|
||||
{
|
||||
$this->isOpened = $isOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set tested
|
||||
*
|
||||
* @param bool $isTested Node tested?
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setTested(bool $isTested) : void
|
||||
{
|
||||
$this->isTested = $isTested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the g score
|
||||
*
|
||||
* @param float $g G score
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setG(float $g) : void
|
||||
{
|
||||
$this->g = $g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the heuristic distance
|
||||
*
|
||||
* @param float $h H distance
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setH(?float $h) : void
|
||||
{
|
||||
$this->h = $h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the f score
|
||||
*
|
||||
* @param float $f F score
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function setF(float $f) : void
|
||||
{
|
||||
$this->f = $f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the g score
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getG() : float
|
||||
{
|
||||
return $this->g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the heuristic distance
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getH() : ?float
|
||||
{
|
||||
return $this->h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the f score
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getF() : float
|
||||
{
|
||||
return $this->f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,731 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*
|
||||
* Extended based on:
|
||||
* MIT License
|
||||
* (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
|
||||
* (c) PathFinding.js
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Stdlib\Base\Heap;
|
||||
|
||||
/**
|
||||
* Perform path finding.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class JumpPointSearch implements PathFinderInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function findPath(
|
||||
int $startX, int $startY,
|
||||
int $endX, int $endY,
|
||||
Grid $grid,
|
||||
int $heuristic, int $movement
|
||||
) : Path
|
||||
{
|
||||
/** @var null|JumpPointNode $startNode */
|
||||
$startNode = $grid->getNode($startX, $startY);
|
||||
/** @var null|JumpPointNode $endNode */
|
||||
$endNode = $grid->getNode($endX, $endY);
|
||||
|
||||
if ($startNode === null || $endNode === null) {
|
||||
return new Path($grid);
|
||||
}
|
||||
|
||||
$startNode->setG(0.0);
|
||||
$startNode->setF(0.0);
|
||||
$startNode->setOpened(true);
|
||||
|
||||
$openList = new Heap(function($node1, $node2) {
|
||||
return $node1->getF() - $node2->getF();
|
||||
});
|
||||
|
||||
$openList->push($startNode);
|
||||
$node = null;
|
||||
|
||||
while (!$openList->isEmpty()) {
|
||||
$node = $openList->pop();
|
||||
if ($node === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
/** @var JumpPointNode $node */
|
||||
$node->setClosed(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$openList = self::identifySuccessors($node, $grid, $heuristic, $movement, $endNode, $openList);
|
||||
}
|
||||
|
||||
$path = new Path($grid);
|
||||
|
||||
while ($node !== null) {
|
||||
$path->addNode($node);
|
||||
$node = $node->parent;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find possible successor jump points
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param Grid $grid Grid of the nodes
|
||||
* @param int $heuristic Heuristic/metrics type for the distance calculation
|
||||
* @param int $movement Movement type
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param Heap $openList Heap of open nodes
|
||||
*
|
||||
* @return Heap
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function identifySuccessors(JumpPointNode $node, Grid $grid, int $heuristic, int $movement, JumpPointNode $endNode, Heap $openList) : Heap
|
||||
{
|
||||
/** @var JumpPointNode[] $neighbors */
|
||||
$neighbors = self::findNeighbors($node, $movement, $grid);
|
||||
$neighborsLength = \count($neighbors);
|
||||
|
||||
for ($i = 0; $i < $neighborsLength; ++$i) {
|
||||
$neighbor = $neighbors[$i];
|
||||
|
||||
if ($neighbor === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$jumpPoint = self::jump($neighbor, $node, $endNode, $movement, $grid);
|
||||
|
||||
if ($jumpPoint === null || $jumpPoint->isClosed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$d = Heuristic::metric($node->getCoordinates(), $jumpPoint->getCoordinates(), HeuristicType::OCTILE);
|
||||
$ng = $node->getG() + $d;
|
||||
|
||||
if (!$jumpPoint->isOpened() || $ng < $jumpPoint->getG()) {
|
||||
$jumpPoint->setG($ng);
|
||||
$jumpPoint->setH($jumpPoint->getH() ?? Heuristic::metric($jumpPoint->getCoordinates(), $endNode->getCoordinates(), $heuristic));
|
||||
$jumpPoint->setF($jumpPoint->getG() + $jumpPoint->getH());
|
||||
$jumpPoint->parent = $node;
|
||||
|
||||
if (!$jumpPoint->isOpened()) {
|
||||
$openList->push($jumpPoint);
|
||||
$jumpPoint->setOpened(true);
|
||||
} else {
|
||||
$openList->update($jumpPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $openList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbor of node
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param int $movement Movement type
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return Node[] Neighbors of node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function findNeighbors(JumpPointNode $node, int $movement, Grid $grid) : array
|
||||
{
|
||||
if ($movement === MovementType::STRAIGHT) {
|
||||
return self::findNeighborsStraight($node, $grid);
|
||||
} elseif ($movement === MovementType::DIAGONAL) {
|
||||
return self::findNeighborsDiagonal($node, $grid);
|
||||
} elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
|
||||
return self::findNeighborsDiagonalOneObstacle($node, $grid);
|
||||
}
|
||||
|
||||
return self::findNeighborsDiagonalNoObstacle($node, $grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbor of node
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return Node[] Neighbors of node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function findNeighborsStraight(JumpPointNode $node, Grid $grid) : array
|
||||
{
|
||||
if ($node->parent === null) {
|
||||
return $grid->getNeighbors($node, MovementType::STRAIGHT);
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$px = $node->parent->getX();
|
||||
$py = $node->parent->getY();
|
||||
|
||||
/** @var int $dx */
|
||||
$dx = ($x - $px) / \max(\abs($x - $px), 1);
|
||||
/** @var int $dy */
|
||||
$dy = ($y - $py) / \max(\abs($y - $py), 1);
|
||||
|
||||
$neighbors = [];
|
||||
if ($dx !== 0) {
|
||||
if ($grid->isWalkable($x, $y - 1)) {
|
||||
$neighbors[] = $grid->getNode($x, $y - 1);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x, $y + 1)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + 1);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
}
|
||||
} elseif ($dy !== 0) {
|
||||
if ($grid->isWalkable($x - 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x - 1, $y);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + 1, $y);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var JumpPointNode[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbor of node
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return Node[] Neighbors of node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function findNeighborsDiagonal(JumpPointNode $node, Grid $grid) : array
|
||||
{
|
||||
if ($node->parent === null) {
|
||||
return $grid->getNeighbors($node, MovementType::DIAGONAL);
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$px = $node->parent->getX();
|
||||
$py = $node->parent->getY();
|
||||
|
||||
/** @var int $dx */
|
||||
$dx = ($x - $px) / \max(\abs($x - $px), 1);
|
||||
/** @var int $dy */
|
||||
$dy = ($y - $py) / \max(\abs($y - $py), 1);
|
||||
|
||||
$neighbors = [];
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x - $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x - $dx, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x, $y - $dy)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y - $dy);
|
||||
}
|
||||
} elseif ($dx === 0) {
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x + 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + 1, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x - 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x - 1, $y + $dy);
|
||||
}
|
||||
} else {
|
||||
if ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x, $y + 1)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + 1);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x, $y - 1)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var JumpPointNode[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbor of node
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return Node[] Neighbors of node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function findNeighborsDiagonalOneObstacle(JumpPointNode $node, Grid $grid) : array
|
||||
{
|
||||
if ($node->parent === null) {
|
||||
return $grid->getNeighbors($node, MovementType::DIAGONAL_ONE_OBSTACLE);
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$px = $node->parent->getX();
|
||||
$py = $node->parent->getY();
|
||||
|
||||
/** @var int $dx */
|
||||
$dx = ($x - $px) / \max(\abs($x - $px), 1);
|
||||
/** @var int $dy */
|
||||
$dy = ($y - $py) / \max(\abs($y - $py), 1);
|
||||
|
||||
$neighbors = [];
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x, $y + $dy) || $grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x - $dx, $y) && $grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x - $dx, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x, $y - $dy) && $grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y - $dy);
|
||||
}
|
||||
} elseif ($dx === 0) {
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
if (!$grid->isWalkable($x + 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + 1, $y + $dy);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x - 1, $y)) {
|
||||
$neighbors[] = $grid->getNode($x - 1, $y + $dy);
|
||||
}
|
||||
}
|
||||
} elseif ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
if (!$grid->isWalkable($x, $y + 1)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + 1);
|
||||
}
|
||||
|
||||
if (!$grid->isWalkable($x, $y - 1)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var JumpPointNode[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find neighbor of node
|
||||
*
|
||||
* @param JumpPointNode $node Node to find successor for
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return Node[] Neighbors of node
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function findNeighborsDiagonalNoObstacle(JumpPointNode $node, Grid $grid) : array
|
||||
{
|
||||
if ($node->parent === null) {
|
||||
return $grid->getNeighbors($node, MovementType::DIAGONAL_NO_OBSTACLE);
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$px = $node->parent->getX();
|
||||
$py = $node->parent->getY();
|
||||
|
||||
/** @var int $dx */
|
||||
$dx = ($x - $px) / \max(\abs($x - $px), 1);
|
||||
/** @var int $dy */
|
||||
$dy = ($y - $py) / \max(\abs($y - $py), 1);
|
||||
|
||||
$neighbors = [];
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if ($grid->isWalkable($x, $y + $dy)) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x, $y + $dy) || $grid->isWalkable($x + $dx, $y)) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + $dy);
|
||||
}
|
||||
} elseif ($dx !== 0 && $dy === 0) {
|
||||
$isNextWalkable = $grid->isWalkable($x + $dx, $y);
|
||||
$isTopWalkable = $grid->isWalkable($x, $y + 1);
|
||||
$isBottomWalkable = $grid->isWalkable($x, $y - 1);
|
||||
|
||||
if ($isNextWalkable) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y);
|
||||
if ($isTopWalkable) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y + 1);
|
||||
}
|
||||
|
||||
if ($isBottomWalkable) {
|
||||
$neighbors[] = $grid->getNode($x + $dx, $y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isTopWalkable) {
|
||||
$neighbors[] = $grid->getNode($x, $y + 1);
|
||||
}
|
||||
|
||||
if ($isBottomWalkable) {
|
||||
$neighbors[] = $grid->getNode($x, $y - 1);
|
||||
}
|
||||
} elseif ($dx === 0 && $dy !== 0) {
|
||||
$isNextWalkable = $grid->isWalkable($x, $y + $dy);
|
||||
$isRightWalkable = $grid->isWalkable($x + 1, $y);
|
||||
$isLeftWalkable = $grid->isWalkable($x - 1, $y);
|
||||
|
||||
if ($isNextWalkable) {
|
||||
$neighbors[] = $grid->getNode($x, $y + $dy);
|
||||
if ($isRightWalkable) {
|
||||
$neighbors[] = $grid->getNode($x + 1, $y + $dy);
|
||||
}
|
||||
|
||||
if ($isLeftWalkable) {
|
||||
$neighbors[] = $grid->getNode($x - 1, $y + $dy);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isRightWalkable) {
|
||||
$neighbors[] = $grid->getNode($x + 1, $y);
|
||||
}
|
||||
|
||||
if ($isLeftWalkable) {
|
||||
$neighbors[] = $grid->getNode($x - 1, $y);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var JumpPointNode[] $neighbors */
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next jump point
|
||||
*
|
||||
* @param null|JumpPointNode $node Node to find jump point from
|
||||
* @param null|JumpPointNode $pNode Parent node
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param int $movement Movement type
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return null|JumpPointNode
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function jump(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, int $movement, Grid $grid) : ?JumpPointNode
|
||||
{
|
||||
if ($movement === MovementType::STRAIGHT) {
|
||||
return self::jumpStraight($node, $pNode, $endNode, $grid);
|
||||
} elseif ($movement === MovementType::DIAGONAL) {
|
||||
return self::jumpDiagonal($node, $pNode, $endNode, $grid);
|
||||
} elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
|
||||
return self::jumpDiagonalOneObstacle($node, $pNode, $endNode, $grid);
|
||||
}
|
||||
|
||||
return self::jumpDiagonalNoObstacle($node, $pNode, $endNode, $grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next jump point
|
||||
*
|
||||
* @param null|JumpPointNode $node Node to find jump point from
|
||||
* @param null|JumpPointNode $pNode Parent node
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return null|JumpPointNode
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function jumpStraight(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
|
||||
{
|
||||
if ($node === null || $pNode === null || !$node->isWalkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$dx = $x - $pNode->getX();
|
||||
$dy = $y - $pNode->getY();
|
||||
|
||||
// not always necessary but might be important for the future
|
||||
$node->setTested(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($dx !== 0) {
|
||||
if (($grid->isWalkable($x, $y - 1) && !$grid->isWalkable($x - $dx, $y - 1))
|
||||
|| ($grid->isWalkable($x, $y + 1) && !$grid->isWalkable($x - $dx, $y + 1))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif ($dy !== 0) {
|
||||
if (($grid->isWalkable($x - 1, $y) && !$grid->isWalkable($x - 1, $y - $dy))
|
||||
|| ($grid->isWalkable($x + 1, $y) && !$grid->isWalkable($x + 1, $y - $dy))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if (self::jumpStraight($grid->getNode($x + 1, $y), $node, $endNode, $grid) !== null
|
||||
|| self::jumpStraight($grid->getNode($x - 1, $y), $node, $endNode, $grid) !== null
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('invalid movement'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return self::jumpStraight($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next jump point
|
||||
*
|
||||
* @param null|JumpPointNode $node Node to find jump point from
|
||||
* @param null|JumpPointNode $pNode Parent node
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return null|JumpPointNode
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function jumpDiagonal(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
|
||||
{
|
||||
if ($node === null || $pNode === null || !$node->isWalkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$dx = $x - $pNode->getX();
|
||||
$dy = $y - $pNode->getY();
|
||||
|
||||
// not always necessary but might be important for the future
|
||||
$node->setTested(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if (($grid->isWalkable($x - $dx, $y + $dy) && !$grid->isWalkable($x - $dx, $y))
|
||||
|| ($grid->isWalkable($x + $dx, $y - $dy) && !$grid->isWalkable($x, $y - $dy))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if (self::jumpDiagonal($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
|
||||
|| self::jumpDiagonal($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif ($dx !== 0) {
|
||||
if (($grid->isWalkable($x + $dx, $y + 1) && !$grid->isWalkable($x, $y + 1))
|
||||
|| ($grid->isWalkable($x + $dx, $y - 1) && !$grid->isWalkable($x, $y - 1))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif (($grid->isWalkable($x + 1, $y + $dy) && !$grid->isWalkable($x + 1, $y))
|
||||
|| ($grid->isWalkable($x - 1, $y + $dy) && !$grid->isWalkable($x - 1, $y))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
return self::jumpDiagonal($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next jump point
|
||||
*
|
||||
* @param null|JumpPointNode $node Node to find jump point from
|
||||
* @param null|JumpPointNode $pNode Parent node
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return null|JumpPointNode
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function jumpDiagonalOneObstacle(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
|
||||
{
|
||||
if ($node === null || $pNode === null || !$node->isWalkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$dx = $x - $pNode->getX();
|
||||
$dy = $y - $pNode->getY();
|
||||
|
||||
// not always necessary but might be important for the future
|
||||
$node->setTested(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if (($grid->isWalkable($x - $dx, $y + $dy) && !$grid->isWalkable($x - $dx, $y))
|
||||
|| ($grid->isWalkable($x + $dx, $y - $dy) && !$grid->isWalkable($x, $y - $dy))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if (self::jumpDiagonalOneObstacle($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
|
||||
|| self::jumpDiagonalOneObstacle($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif ($dx !== 0) {
|
||||
if (($grid->isWalkable($x + $dx, $y + 1) && !$grid->isWalkable($x, $y + 1))
|
||||
|| ($grid->isWalkable($x + $dx, $y - 1) && !$grid->isWalkable($x, $y - 1))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif (($grid->isWalkable($x + 1, $y + $dy) && !$grid->isWalkable($x + 1, $y))
|
||||
|| ($grid->isWalkable($x - 1, $y + $dy) && !$grid->isWalkable($x - 1, $y))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y) || $grid->isWalkable($x, $y + $dy)) {
|
||||
return self::jumpDiagonalOneObstacle($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next jump point
|
||||
*
|
||||
* @param null|JumpPointNode $node Node to find jump point from
|
||||
* @param null|JumpPointNode $pNode Parent node
|
||||
* @param JumpPointNode $endNode End node to find path to
|
||||
* @param Grid $grid Grid of the nodes
|
||||
*
|
||||
* @return null|JumpPointNode
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function jumpDiagonalNoObstacle(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
|
||||
{
|
||||
if ($node === null || $pNode === null || !$node->isWalkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = $node->getX();
|
||||
$y = $node->getY();
|
||||
|
||||
$dx = $x - $pNode->getX();
|
||||
$dy = $y - $pNode->getY();
|
||||
|
||||
// not always necessary but might be important for the future
|
||||
$node->setTested(true);
|
||||
|
||||
if ($node->isEqual($endNode)) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
if ($dx !== 0 && $dy !== 0) {
|
||||
if (self::jumpDiagonalNoObstacle($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
|
||||
|| self::jumpDiagonalNoObstacle($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif ($dx !== 0) {
|
||||
if (($grid->isWalkable($x, $y - 1) && !$grid->isWalkable($x - $dx, $y - 1))
|
||||
|| ($grid->isWalkable($x, $y + 1) && !$grid->isWalkable($x - $dx, $y + 1))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
} elseif ($dy !== 0) {
|
||||
if (($grid->isWalkable($x - 1, $y) && !$grid->isWalkable($x - 1, $y - $dy))
|
||||
|| ($grid->isWalkable($x + 1, $y) && !$grid->isWalkable($x + 1, $y - $dy))
|
||||
) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
if ($grid->isWalkable($x + $dx, $y) || $grid->isWalkable($x, $y + $dy)) {
|
||||
return self::jumpDiagonalNoObstacle($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Movement type enum.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class MovementType extends Enum
|
||||
{
|
||||
public const DIAGONAL = 1;
|
||||
|
||||
public const STRAIGHT = 2;
|
||||
|
||||
public const DIAGONAL_ONE_OBSTACLE = 4;
|
||||
|
||||
public const DIAGONAL_NO_OBSTACLE = 8;
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
use phpOMS\Stdlib\Base\HeapItemInterface;
|
||||
|
||||
/**
|
||||
* Node on grid.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Node implements HeapItemInterface
|
||||
{
|
||||
/**
|
||||
* X-Coordinate.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private int $x = 0;
|
||||
|
||||
/**
|
||||
* Y-Coordinate.
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private int $y = 0;
|
||||
|
||||
/**
|
||||
* Cost of the node.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private float $weight = 1.0;
|
||||
|
||||
/**
|
||||
* Can be walked?
|
||||
*
|
||||
* @var bool
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public bool $isWalkable = true;
|
||||
|
||||
/**
|
||||
* Parent node.
|
||||
*
|
||||
* @var null|Node
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public ?Node $parent = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $x X-Coordinate
|
||||
* @param int $y Y-Coordinate
|
||||
* @param float $weight Cost of reaching this node
|
||||
* @param bool $isWalkable Can be walked on?
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(int $x, int $y, float $weight = 1.0, bool $isWalkable = true)
|
||||
{
|
||||
$this->x = $x;
|
||||
$this->y = $y;
|
||||
$this->weight = $weight;
|
||||
$this->isWalkable = $isWalkable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cost to walk on this node
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getWeight() : float
|
||||
{
|
||||
return $this->weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get x-coordinate
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getX() : int
|
||||
{
|
||||
return $this->x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get y-coordinate
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getY() : int
|
||||
{
|
||||
return $this->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is node equal to another node?
|
||||
*
|
||||
* @param Node $node Node to compare to
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function isEqual(HeapItemInterface $node) : bool
|
||||
{
|
||||
return $this->x === $node->getX() && $this->y === $node->getY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coordinates of this node.
|
||||
*
|
||||
* @return array<string, int> ['x' => ?, 'y' => ?]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getCoordinates() : array
|
||||
{
|
||||
return ['x' => $this->x, 'y' => $this->y];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Null node.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class NullJumpPointNode extends JumpPointNode
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Null node.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class NullNode extends Node
|
||||
{
|
||||
}
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*
|
||||
* Extended based on:
|
||||
* MIT License
|
||||
* (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
|
||||
* (c) PathFinding.js
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Path in grids.
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Path
|
||||
{
|
||||
/**
|
||||
* Nodes in the path
|
||||
*
|
||||
* @var Node[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public array $nodes = [];
|
||||
|
||||
/**
|
||||
* Grid this path belongs to
|
||||
*
|
||||
* @var Grid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private Grid $grid;
|
||||
|
||||
/**
|
||||
* Nodes in the path
|
||||
*
|
||||
* @var Node[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $expandedNodes = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Grid $grid Grid this path belongs to
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(Grid $grid)
|
||||
{
|
||||
$this->grid = $grid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add node to the path
|
||||
*
|
||||
* @param Node $node Node
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function addNode(Node $node) : void
|
||||
{
|
||||
$this->nodes[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path length (euclidean)
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getLength() : float
|
||||
{
|
||||
$n = \count($this->nodes);
|
||||
|
||||
$dist = 0.0;
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
$dx = $this->nodes[$i - 1]->getX() - $this->nodes[$i]->getX();
|
||||
$dy = $this->nodes[$i - 1]->getY() - $this->nodes[$i]->getY();
|
||||
|
||||
$dist += \sqrt($dx * $dx + $dy * $dy);
|
||||
}
|
||||
|
||||
return $dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the incomplete node path
|
||||
*
|
||||
* @return Node[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getPath() : array
|
||||
{
|
||||
return $this->nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete node path
|
||||
*
|
||||
* The path may only contain the jump points or pivot points.
|
||||
* In order to get every node it needs to be expanded.
|
||||
*
|
||||
* @return Node[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function expandPath() : array
|
||||
{
|
||||
if (empty($this->expandedNodes)) {
|
||||
//$reverse = \array_reverse($this->nodes);
|
||||
$reverse = $this->nodes;
|
||||
$length = \count($reverse);
|
||||
|
||||
if ($length < 2) {
|
||||
return $reverse;
|
||||
}
|
||||
|
||||
$expanded = [];
|
||||
for ($i = 0; $i < $length - 1; ++$i) {
|
||||
$coord0 = $reverse[$i];
|
||||
$coord1 = $reverse[$i + 1];
|
||||
|
||||
$interpolated = $this->interpolate($coord0, $coord1);
|
||||
$expanded = \array_merge($expanded, $interpolated);
|
||||
}
|
||||
|
||||
$expanded[] = $reverse[$length - 1];
|
||||
$this->expandedNodes = $expanded;
|
||||
}
|
||||
|
||||
return $this->expandedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nodes in between two nodes.
|
||||
*
|
||||
* The path may only contain the jump points or pivot points.
|
||||
* In order to get every node it needs to be expanded.
|
||||
*
|
||||
* @param Node $node1 Node
|
||||
* @param Node $node2 Node
|
||||
*
|
||||
* @return Node[]
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function interpolate(Node $node1, Node $node2) : array
|
||||
{
|
||||
$dx = \abs($node2->getX() - $node1->getX());
|
||||
$dy = \abs($node2->getY() - $node1->getY());
|
||||
|
||||
$sx = ($node1->getX() < $node2->getX()) ? 1 : -1;
|
||||
$sy = ($node1->getY() < $node2->getY()) ? 1 : -1;
|
||||
|
||||
$node = $node1;
|
||||
$err = $dx - $dy;
|
||||
|
||||
$x0 = $node->getX();
|
||||
$y0 = $node->getY();
|
||||
|
||||
$line = [];
|
||||
while (true) {
|
||||
if ($node->getX() === $node2->getX() && $node->getY() === $node2->getY()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$line[] = $node;
|
||||
|
||||
$e2 = 2 * $err;
|
||||
|
||||
if ($e2 > -$dy) {
|
||||
$err -= $dy;
|
||||
$x0 += $sx;
|
||||
}
|
||||
|
||||
if ($e2 < $dx) {
|
||||
$err += $dx;
|
||||
$y0 += $sy;
|
||||
}
|
||||
|
||||
$node = $this->grid->getNode($x0, $y0);
|
||||
|
||||
if ($node === null) {
|
||||
break; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\PathFinding;
|
||||
|
||||
/**
|
||||
* Path finder interface
|
||||
*
|
||||
* @package phpOMS\Algorithm\PathFinding
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
interface PathFinderInterface
|
||||
{
|
||||
/**
|
||||
* Find path from one point to another
|
||||
*
|
||||
* @param int $startX Start point X-Coordinate
|
||||
* @param int $startY Start point Y-Coordinate
|
||||
* @param int $endX End point X-Coordinate
|
||||
* @param int $endY End point Y-Coordinate
|
||||
* @param Grid $grid Grid with the walkable points
|
||||
* @param int $heuristic Heuristic algorithm to use in order to calculate the distance for a good path
|
||||
* @param int $movement Allowed movement (e.g. straight, diagonal, ...)
|
||||
*
|
||||
* @return Path
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function findPath(
|
||||
int $startX, int $startY,
|
||||
int $endX, int $endY,
|
||||
Grid $grid,
|
||||
int $heuristic, int $movement
|
||||
) : Path;
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
/**
|
||||
* Calculate rating strength using the Bradley Terry model
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Bradley%E2%80%93Terry_model
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class BradleyTerry
|
||||
{
|
||||
/**
|
||||
* Rate the strongest to the weakest team based on historic performances (wins/losses)
|
||||
*
|
||||
* The following example contains match results (matrix) of teams A-D facing each other (each point is a victory).
|
||||
* @example rating(
|
||||
* [
|
||||
* 'A' => ['A' => 0, 'B' => 2, 'C' => 0, 'D' => 1],
|
||||
* 'B' => ['A' => 3, 'B' => 0, 'C' => 5, 'D' => 0],
|
||||
* 'C' => ['A' => 0, 'B' => 3, 'C' => 0, 'D' => 1],
|
||||
* 'D' => ['A' => 4, 'B' => 0, 'C' => 3, 'D' => 0],
|
||||
* ],
|
||||
* 10
|
||||
* ) // [0.640, 1.043, 0.660, 2.270] -> D is strongest
|
||||
*
|
||||
* @param array[] $history Historic results
|
||||
* @param int $iterations Iterations for estimation
|
||||
*
|
||||
* @return float[] Array of "strength" scores (highest = strongest)
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rating(array $history, int $iterations = 20) : array
|
||||
{
|
||||
$keys = \array_keys($history);
|
||||
$pOld = [];
|
||||
foreach ($keys as $key) {
|
||||
$pOld[$key] = 1;
|
||||
}
|
||||
|
||||
$p = $pOld;
|
||||
for ($i = 0; $i < $iterations; ++$i) {
|
||||
foreach ($history as $idx => $row) {
|
||||
$W = \array_sum($row);
|
||||
|
||||
$d = 0;
|
||||
foreach ($history as $idx2 => $_) {
|
||||
if ($idx === $idx2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$d += ($history[$idx][$idx2] + $history[$idx2][$idx])
|
||||
/ ($pOld[$idx] + $pOld[$idx2]);
|
||||
}
|
||||
|
||||
$p[$idx] = $W / $d;
|
||||
}
|
||||
|
||||
$norm = \array_sum($p);
|
||||
foreach ($p as $idx => $_) {
|
||||
$p[$idx] /= $norm;
|
||||
}
|
||||
|
||||
$pOld = $p;
|
||||
}
|
||||
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
/**
|
||||
* Elo rating calculation using Elo rating
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Elo_rating_system
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Elo
|
||||
{
|
||||
/**
|
||||
* ELO change rate
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $K = 32;
|
||||
|
||||
/**
|
||||
* Default elo to use for new players
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $DEFAULT_ELO = 1500;
|
||||
|
||||
/**
|
||||
* Lowest elo allowed
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $MIN_ELO = 100;
|
||||
|
||||
/**
|
||||
* Calculate the elo rating
|
||||
*
|
||||
* @param int $elo Current player elo
|
||||
* @param int[] $oElo Current elo of all opponents
|
||||
* @param float[] $s Match results against the opponents (1 = victor, 0 = loss, 0.5 = draw)
|
||||
*
|
||||
* @return array{elo:int}
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rating(int $elo, array $oElo, array $s) : array
|
||||
{
|
||||
$eloNew = $elo;
|
||||
foreach ($oElo as $idx => $o) {
|
||||
$expected = 1 / (1 + 10 ** (($o - $elo) / 400));
|
||||
$r = $this->K * ($s[$idx] - $expected);
|
||||
|
||||
$eloNew += (int) \round($r);
|
||||
}
|
||||
|
||||
return [
|
||||
'elo' => (int) \max($eloNew, $this->MIN_ELO),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an approximated win probability based on elo points.
|
||||
*
|
||||
* @param int $elo1 Elo of the player we want to calculate the win probability for
|
||||
* @param int $elo2 Opponent elo
|
||||
* @param bool $canDraw Is a draw possible?
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function winProbability(int $elo1, int $elo2, bool $canDraw = false) : float
|
||||
{
|
||||
return $canDraw
|
||||
? -1.0 // @todo implement
|
||||
: 1 / (1 + \pow(10, ($elo2 - $elo1) / 400));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate an approximated draw probability based on elo points.
|
||||
*
|
||||
* @param int $elo1 Elo of the player we want to calculate the win probability for
|
||||
* @param int $elo2 Opponent elo
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function drawProbability(int $elo1, int $elo2) : float
|
||||
{
|
||||
return -1.0; // @todo implement
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
/**
|
||||
* Elo rating calculation using Glicko-1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Glicko_rating_system
|
||||
* @see http://www.glicko.net/glicko/glicko.pdf
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Glicko1
|
||||
{
|
||||
/**
|
||||
* Helper constant
|
||||
*
|
||||
* @latex Q = ln(10) / 400
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private const Q = 0.00575646273;
|
||||
|
||||
/**
|
||||
* Default elo to use for new players
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $DEFAULT_ELO = 1500;
|
||||
|
||||
/**
|
||||
* Default rd to use for new players
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $DEFAULT_RD = 350;
|
||||
|
||||
/**
|
||||
* C (constant) for RD caclulation
|
||||
*
|
||||
* This is used to adjust the RD value based on the time from the last time a player played a match
|
||||
*
|
||||
* @latex RD = min\left(\sqrt{RD_0^2 + c^2t}, 350\right)
|
||||
*
|
||||
* @see calculateC();
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $DEFAULT_C = 34.6;
|
||||
|
||||
/**
|
||||
* Lowest elo allowed
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $MIN_ELO = 100;
|
||||
|
||||
/**
|
||||
* Lowest rd allowed
|
||||
*
|
||||
* @example 50 means that the player rating is probably between -100 / +100 of the current rating
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $MIN_RD = 50;
|
||||
|
||||
/**
|
||||
* Calculate the C value.
|
||||
*
|
||||
* This is only necessary if you change the DEFAULT_RD, want a different rating period or have significantly different average RD values.
|
||||
*
|
||||
* @param int $ratingPeriods Time without matches until the RD returns to the default RD
|
||||
* @param int $avgRD Average RD
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function calculateC(int $ratingPeriods = 100, int $avgRD = 50) : void
|
||||
{
|
||||
$this->DEFAULT_C = \sqrt(($this->DEFAULT_RD ** 2 - $avgRD ** 2) / $ratingPeriods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the glicko-1 elo
|
||||
*
|
||||
* @param int $elo Current player "elo"
|
||||
* @param int $rdOld Current player deviation (RD)
|
||||
* @param int $lastMatchDate Last match date used to calculate the time difference (can be days, months, ... depending on your match interval)
|
||||
* @param int $matchDate Match date (usually day)
|
||||
* @param int[] $oElo Opponent "elo"
|
||||
* @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw)
|
||||
* @param int[] $oRd Opponent deviation (RD)
|
||||
*
|
||||
* @return array{elo:int, rd:int}
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rating(
|
||||
int $elo = 1500,
|
||||
int $rdOld = 50,
|
||||
int $lastMatchDate = 0,
|
||||
int $matchDate = 0,
|
||||
array $oElo = [],
|
||||
array $s = [],
|
||||
array $oRd = []
|
||||
) : array
|
||||
{
|
||||
// Step 1:
|
||||
$E = [];
|
||||
$gRD = [];
|
||||
|
||||
$RD = \min(
|
||||
350,
|
||||
\max(
|
||||
\sqrt(
|
||||
$rdOld * $rdOld
|
||||
+ $this->DEFAULT_C * $this->DEFAULT_C * \max(0, $matchDate - $lastMatchDate)
|
||||
),
|
||||
$this->MIN_RD
|
||||
)
|
||||
);
|
||||
|
||||
// Step 2:
|
||||
foreach ($oElo as $id => $e) {
|
||||
$gRD_t = 1 / (\sqrt(1 + 3 * self::Q * self::Q * $oRd[$id] * $oRd[$id] / (\M_PI * \M_PI)));
|
||||
$gRD[$id] = $gRD_t;
|
||||
$E[$id] = 1 / (1 + \pow(10, $gRD_t * ($elo - $e) / -400));
|
||||
}
|
||||
|
||||
$d = 0;
|
||||
foreach ($E as $id => $_) {
|
||||
$d += $gRD[$id] * $gRD[$id] * $E[$id] * (1 - $E[$id]);
|
||||
}
|
||||
$d2 = 1 / (self::Q * self::Q * $d);
|
||||
|
||||
$r = 0;
|
||||
foreach ($E as $id => $_) {
|
||||
$r += $gRD[$id] * ($s[$id] - $E[$id]);
|
||||
}
|
||||
$r = $elo + self::Q / (1 / ($RD * $RD) + 1 / $d2) * $r;
|
||||
|
||||
// Step 3:
|
||||
$RD_ = \sqrt(1 / (1 / ($RD * $RD) + 1 / $d2));
|
||||
|
||||
return [
|
||||
'elo' => (int) \max((int) $r, $this->MIN_ELO),
|
||||
'rd' => (int) \max($RD_, $this->MIN_RD),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
use phpOMS\Math\Solver\Root\Bisection;
|
||||
|
||||
/**
|
||||
* Elo rating calculation using Glicko-2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Glicko_rating_system
|
||||
* @see http://www.glicko.net/glicko/glicko2.pdf
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Glicko2
|
||||
{
|
||||
/**
|
||||
* Glicko scale factor
|
||||
*
|
||||
* @latex Q = 400 / ln(10)
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private const Q = 173.7177927613;
|
||||
|
||||
/**
|
||||
* Constraint for the volatility over time (smaller = stronger constraint)
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $tau = 0.5;
|
||||
|
||||
/**
|
||||
* Default elo to use for new players
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $DEFAULT_ELO = 1500;
|
||||
|
||||
/**
|
||||
* Default rd to use for new players
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $DEFAULT_RD = 350;
|
||||
|
||||
/**
|
||||
* Valatility (sigma)
|
||||
*
|
||||
* Expected flactuation = how erratic is the player's performance
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public float $DEFAULT_VOLATILITY = 0.06;
|
||||
|
||||
/**
|
||||
* Lowest elo allowed
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $MIN_ELO = 100;
|
||||
|
||||
/**
|
||||
* Lowest rd allowed
|
||||
*
|
||||
* @example 50 means that the player rating is probably between -100 / +100 of the current rating
|
||||
*
|
||||
* @var int
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public int $MIN_RD = 50;
|
||||
|
||||
/**
|
||||
* Calculate the glicko-2 elo
|
||||
*
|
||||
* @example $glicko->elo(1500, 200, 0.06, [1,0,0], [1400,1550,1700], [30,100,300]) // 1464, 151, 0.059
|
||||
*
|
||||
* @param int $elo Current player "elo"
|
||||
* @param int $rdOld Current player deviation (RD)
|
||||
* @param float $volOld Last match date used to calculate the time difference (can be days, months, ... depending on your match interval)
|
||||
* @param int[] $oElo Opponent "elo"
|
||||
* @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw)
|
||||
* @param int[] $oRd Opponent deviation (RD)
|
||||
*
|
||||
* @return array{elo:int, rd:int, vol:float}
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function rating(
|
||||
int $elo = 1500,
|
||||
int $rdOld = 50,
|
||||
float $volOld = 0.06,
|
||||
array $oElo = [],
|
||||
array $s = [],
|
||||
array $oRd = []
|
||||
) : array
|
||||
{
|
||||
$tau = $this->tau;
|
||||
|
||||
// Step 0:
|
||||
$rdOld /= self::Q;
|
||||
$elo = ($elo - $this->DEFAULT_ELO) / self::Q;
|
||||
|
||||
foreach ($oElo as $idx => $value) {
|
||||
$oElo[$idx] = ($value - $this->DEFAULT_ELO) / self::Q;
|
||||
}
|
||||
|
||||
foreach ($oRd as $idx => $value) {
|
||||
$oRd[$idx] = $value / self::Q;
|
||||
}
|
||||
|
||||
// Step 1:
|
||||
$g = [];
|
||||
foreach ($oRd as $idx => $rd) {
|
||||
$g[$idx] = 1 / \sqrt(1 + 3 * $rd * $rd / (\M_PI * \M_PI));
|
||||
}
|
||||
|
||||
$E = [];
|
||||
foreach ($oElo as $idx => $oe) {
|
||||
$E[$idx] = 1 / (1 + \exp(-$g[$idx] * ($elo - $oe)));
|
||||
}
|
||||
|
||||
$v = 0;
|
||||
foreach ($g as $idx => $t) {
|
||||
$v += $t * $t * $E[$idx] * (1 - $E[$idx]);
|
||||
}
|
||||
$v = 1 / $v;
|
||||
|
||||
$tDelta = 0;
|
||||
foreach ($g as $idx => $t) {
|
||||
$tDelta += $t * ($s[$idx] - $E[$idx]);
|
||||
}
|
||||
$Delta = $v * $tDelta;
|
||||
|
||||
// Step 2:
|
||||
$fn = function($x) use ($Delta, $rdOld, $v, $tau, $volOld)
|
||||
{
|
||||
return 0.5 * (\exp($x) * ($Delta ** 2 - $rdOld ** 2 - $v - \exp($x))) / (($rdOld ** 2 + $v + \exp($x)) ** 2)
|
||||
- ($x - \log($volOld ** 2)) / ($tau ** 2);
|
||||
};
|
||||
|
||||
$root = Bisection::root($fn, -100, 100, 1000);
|
||||
$vol = \exp($root / 2);
|
||||
|
||||
// Step 3:
|
||||
$RD = 1 / \sqrt(1 / ($rdOld ** 2 + $vol ** 2) + 1 / $v);
|
||||
$r = $elo + $RD ** 2 * $tDelta;
|
||||
|
||||
// Undo step 0:
|
||||
$RD = self::Q * $RD;
|
||||
$r = self::Q * $r + $this->DEFAULT_ELO;
|
||||
|
||||
return [
|
||||
'elo' => (int) \max($r, $this->MIN_ELO),
|
||||
'rd' => (int) \max($RD, $this->MIN_RD),
|
||||
'vol' => $vol,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,290 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Microsoft
|
||||
* @license This algorithm may be patented by Microsoft, verify and acquire a license if necessary
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
use phpOMS\Math\Stochastic\Distribution\NormalDistribution;
|
||||
|
||||
/**
|
||||
* Elo rating calculation using Elo rating
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @todo Implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py
|
||||
* https://github.com/Karaka-Management/phpOMS/issues/337
|
||||
*/
|
||||
// phpcs:ignoreFile
|
||||
class TrueSkill
|
||||
{
|
||||
public const DEFAULT_MU = 25;
|
||||
|
||||
public const DEFAULT_SIGMA = 25 / 3;
|
||||
|
||||
public const DEFAULT_BETA = 25 / 3 / 2;
|
||||
|
||||
public const DEFAULT_TAU = 25 / 3 / 100;
|
||||
|
||||
public const DEFAULT_DRAW_PROBABILITY = 0.1;
|
||||
|
||||
private float $mu = 0.0;
|
||||
|
||||
private float $sigma = 0.0;
|
||||
|
||||
private float $beta = 0.0;
|
||||
|
||||
private float $tau = 0.0;
|
||||
|
||||
private float $drawProbability = 0.0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param null|float $mu Mu
|
||||
* @param null|float $sigma Sigma
|
||||
* @param null|float $beta Beta
|
||||
* @param null|float $tau Tau
|
||||
* @param null|float $drawProbability Draw probability
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
?float $mu = null,
|
||||
?float $sigma = null,
|
||||
?float $beta = null,
|
||||
?float $tau = null,
|
||||
?float $drawProbability = null)
|
||||
{
|
||||
$this->mu = $mu ?? self::DEFAULT_MU;
|
||||
$this->sigma = $sigma ?? self::DEFAULT_SIGMA;
|
||||
$this->beta = $beta ?? self::DEFAULT_BETA;
|
||||
$this->tau = $tau ?? self::DEFAULT_TAU;
|
||||
$this->drawProbability = $drawProbability ?? self::DEFAULT_DRAW_PROBABILITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate win probability
|
||||
*
|
||||
* @param array $team1 Team 1
|
||||
* @param array $team2 Team 2
|
||||
* @param float $drawMargin Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function winProbability(array $team1, array $team2, float $drawMargin = 0.0) : float
|
||||
{
|
||||
$sigmaSum = 0.0;
|
||||
$mu1 = 0.0;
|
||||
foreach ($team1 as $player) {
|
||||
$mu1 += $player->mu;
|
||||
$sigmaSum += $player->sigma * $player->sigma;
|
||||
}
|
||||
|
||||
$mu2 = 0.0;
|
||||
foreach ($team2 as $player) {
|
||||
$mu2 += $player->mu;
|
||||
$sigmaSum += $player->sigma * $player->sigma;
|
||||
}
|
||||
|
||||
$deltaMu = $mu1 - $mu2;
|
||||
|
||||
return NormalDistribution::getCdf(
|
||||
($deltaMu - $drawMargin) / \sqrt((\count($team1) + \count($team2)) * ($this->beta * $this->beta) + $sigmaSum),
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Draw margin = epsilon
|
||||
/**
|
||||
* P_{draw} = 2\Phi\left(\dfrac{\epsilon}{\sqrt{n_1 + n_2} * \beta}\right) - 1
|
||||
*/
|
||||
public function drawProbability(float $drawMargin, int $n1, int $n2, float $beta)
|
||||
{
|
||||
return 2 * NormalDistribution::getCdf($drawMargin / (\sqrt($n1 + $n2) * $beta), 0.0, 1.0) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* \epsilon = \Phi^{-1}\left(\dfrac{P_{draw} + 1}{2}\right) * \sqrt{n_1 + n_2} * \beta
|
||||
*/
|
||||
public function drawMargin(float $drawProbability, int $n1, int $n2, float $beta)
|
||||
{
|
||||
return NormalDistribution::getIcdf(($drawProbability + 1) / 2.0, 0.0, 1.0) * \sqrt($n1 + $n2) * $beta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mean additive truncated gaussion function "v" for wins
|
||||
*
|
||||
* @latex c = \sqrt{2 * \beta^2 + \sigma_{winner}^2 + \sigma_{loser}^2}
|
||||
* @latex \mu_{winner} = \mu_{winner} + \dfrac{\sigma_{winner}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
|
||||
* @latex \mu_{loser} = \mu_{loser} + \dfrac{\sigma_{loser}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
|
||||
* @latex t = \dfrac{\mu_{winner} - \mu_{loser}}{c}
|
||||
*
|
||||
* @latex \nu = \dfrac{\mathcal{N}(t - \epsilon)}{\Phi(t - \epsilon)}
|
||||
*
|
||||
* @param float $t Difference winner and loser mu
|
||||
* @param float $epsilon Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function vWin(float $t, float $epsilon) : float
|
||||
{
|
||||
return NormalDistribution::getPdf($t - $epsilon, 0, 1.0) / NormalDistribution::getCdf($t - $epsilon, 0.0, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mean additive truncated gaussion function "v" for draws
|
||||
*
|
||||
* @latex c = \sqrt{2 * \beta^2 + \sigma_{winner}^2 + \sigma_{loser}^2}
|
||||
* @latex \mu_{winner} = \mu_{winner} + \dfrac{\sigma_{winner}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
|
||||
* @latex \mu_{loser} = \mu_{loser} + \dfrac{\sigma_{loser}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
|
||||
* @latex t = \dfrac{\mu_{winner} - \mu_{loser}}{c}
|
||||
* @latex \dfrac{\mathcal{N}(t - \epsilon)}{\Phi(t - \epsilon)}
|
||||
*
|
||||
* @latex \nu = \dfrac{\mathcal{N}(-\epsilon - t) - \mathcal{N}(\epsilon - t)}{\Phi(\epsilon - t) - \Phi(-\epsilon - t)}
|
||||
*
|
||||
* @param float $t Difference winner and loser mu
|
||||
* @param float $epsilon Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function vDraw(float $t, float $epsilon) : float
|
||||
{
|
||||
$tAbs = \abs($t);
|
||||
$a = $epsilon - $tAbs;
|
||||
$b = -$epsilon - $tAbs;
|
||||
|
||||
$aPdf = NormalDistribution::getPdf($a, 0.0, 1.0);
|
||||
$bPdf = NormalDistribution::getPdf($b, 0.0, 1.0);
|
||||
$numer = $bPdf - $aPdf;
|
||||
|
||||
$aCdf = NormalDistribution::getCdf($a, 0.0, 1.0);
|
||||
$bCdf = NormalDistribution::getCdf($b, 0.0, 1.0);
|
||||
$denom = $aCdf - $bCdf;
|
||||
|
||||
return $numer / $denom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variance multiplicative function "w" for draws
|
||||
*
|
||||
* @latex w = \nu * (\nu + t - \epsilon)
|
||||
*
|
||||
* @param float $t Difference winner and loser mu
|
||||
* @param float $epsilon Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function wWin(float $t, float $epsilon) : float
|
||||
{
|
||||
$v = $this->vWin($t, $epsilon);
|
||||
|
||||
return $v * ($v + $t - $epsilon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variance multiplicative function "w" for draws
|
||||
*
|
||||
* @latex w = \nu^2 + \dfrac{(\epsilon - t) * \mathcal{N}(\epsilon - t) + (\epsilon + t) * \mathcal{N}(\epsilon + t)}{\Phi(\epsilon - t) - \Phi(-\epsilon - t)}
|
||||
*
|
||||
* @param float $t Difference winner and loser mu
|
||||
* @param float $epsilon Draw margin
|
||||
*
|
||||
* @return float
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function wDraw(float $t, float $epsilon) : float
|
||||
{
|
||||
$tAbs = \abs($t);
|
||||
|
||||
$v = $this->vDraw($t, $epsilon);
|
||||
|
||||
return $v * $v
|
||||
+ (($epsilon - $t) * NormalDistribution::getPdf($epsilon - $tAbs, 0.0, 1.0) + ($epsilon + $tAbs) * NormalDistribution::getPdf($epsilon + $tAbs, 0.0, 1.0))
|
||||
/ (NormalDistribution::getCdf($epsilon - $tAbs, 0.0, 1.0) - NormalDistribution::getCdf(-$epsilon - $tAbs, 0.0, 1.0));
|
||||
}
|
||||
|
||||
private function buildRatingLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
private function buildPerformanceLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
private function buildTeamPerformanceLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
private function buildTruncLayer() : void
|
||||
{
|
||||
}
|
||||
|
||||
private function factorGraphBuilders()
|
||||
{
|
||||
// Rating layer
|
||||
|
||||
// Performance layer
|
||||
|
||||
// Team Performance layer
|
||||
|
||||
// Trunc layer
|
||||
|
||||
return [
|
||||
'rating_layer' => $ratingLayer,
|
||||
'performance_layer' => $ratingLayer,
|
||||
'team_performance_layer' => $ratingLayer,
|
||||
'trunc_layer' => $ratingLayer,
|
||||
];
|
||||
}
|
||||
|
||||
public function rating() : void
|
||||
{
|
||||
// Start values
|
||||
$mu = 25;
|
||||
$sigma = $mu / 3;
|
||||
$beta = $sigma / 2;
|
||||
$tau = $sigma / 100;
|
||||
$Pdraw = 0.1;
|
||||
|
||||
$alpha = 0.25;
|
||||
|
||||
// Partial update
|
||||
$sigmaPartial = $sigmaOld * $sigmaNew / \sqrt($alpha * $sigmaOld * $sigmaOld - ($alpha - 1) * $sigmaNew * $sigmaNew);
|
||||
$muPartial = $muOld * ($alpha - 1) * $sigmaNew * $sigmaNew - $muNew * $alpha * $sigmaOld * $sigmaOld
|
||||
/ (($alpha - 1) * $sigmaNew * $sigmaNew - $alpha * $sigmaOld * $sigmaOld);
|
||||
|
||||
// New
|
||||
$tau = $pi * $mu;
|
||||
|
||||
$P = NormalDistribution::getCdf(($s1 - $s2) / (\sqrt(2) * $beta));
|
||||
$Delta = $alpha * $beta * \sqrt($pi) * (($y + 1) / 2 - $P);
|
||||
|
||||
$K = NormalDistribution::getCdf();
|
||||
|
||||
$pi = 1 / ($sigma * $sigma);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Rating;
|
||||
|
||||
/**
|
||||
* Elo rating calculation using Elo rating
|
||||
*
|
||||
* @package phpOMS\Algorithm\Rating
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @see https://en.wikipedia.org/wiki/Elo_rating_system
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class TrueSkillFactoryGraph
|
||||
{
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* BitonicSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class BitonicSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$first = self::sort(\array_slice($list, 0, (int) ($n / 2)), SortOrder::ASC);
|
||||
$second = self::sort(\array_slice($list, (int) ($n / 2)), SortOrder::DESC);
|
||||
|
||||
return self::merge(\array_merge($first, $second), $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splitting, merging and sorting list
|
||||
*
|
||||
* @param array $list List to sort
|
||||
* @param int $order Sort order
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function merge(array $list, int $order) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n === 1) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$dist = $n / 2;
|
||||
for ($i = 0; $i < $dist; ++$i) {
|
||||
if ($list[$i]->compare($list[$i + $dist], $order)) {
|
||||
$old = $list[$i];
|
||||
$list[$i] = $list[$i + $dist];
|
||||
$list[$i + $dist] = $old;
|
||||
}
|
||||
}
|
||||
|
||||
$first = self::merge(\array_slice($list, 0, (int) ($n / 2)), $order);
|
||||
$second = self::merge(\array_slice($list, (int) ($n / 2)), $order);
|
||||
|
||||
return \array_merge($first, $second);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* Bubblesort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class BubbleSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
do {
|
||||
$newN = 0;
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
if ($list[$i - 1]->compare($list[$i], $order)) {
|
||||
$old = $list[$i - 1];
|
||||
$list[$i - 1] = $list[$i];
|
||||
$list[$i] = $old;
|
||||
|
||||
$newN = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$n = $newN;
|
||||
} while ($n > 1);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* Bucketsort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class BucketSort
|
||||
{
|
||||
/**
|
||||
* Sort array
|
||||
*
|
||||
* @param array $list List of sortable elements
|
||||
* @param int $bucketCount Buckets to divide the list into
|
||||
* @param string $algo Algorithm to use for sort
|
||||
* @param int $order Sort order
|
||||
*
|
||||
* @return array Sorted array
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function sort(array $list, int $bucketCount, string $algo = InsertionSort::class, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$buckets = [];
|
||||
$M = $list[0]::max($list);
|
||||
|
||||
if ($bucketCount < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (\count($list) < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
foreach ($list as $element) {
|
||||
$buckets[(int) \floor(($bucketCount - 1) * $element->getValue() / $M)][] = $element;
|
||||
}
|
||||
|
||||
$sorted = [];
|
||||
foreach ($buckets as $bucket) {
|
||||
$sorted[] = $algo::sort($bucket, SortOrder::ASC);
|
||||
}
|
||||
|
||||
return $order === SortOrder::ASC ? \array_merge(...$sorted) : \array_reverse(\array_merge(...$sorted), false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* CocktailShakerSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class CocktailShakerSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$start = 0;
|
||||
$end = \count($list) - 1;
|
||||
|
||||
if ($end < 1) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
while ($start <= $end) {
|
||||
$newStart = $end;
|
||||
$newEnd = $start;
|
||||
|
||||
for ($i = $start; $i < $end; ++$i) {
|
||||
if ($list[$i]->compare($list[$i + 1], $order)) {
|
||||
$old = $list[$i];
|
||||
$list[$i] = $list[$i + 1];
|
||||
$list[$i + 1] = $old;
|
||||
|
||||
$newEnd = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$end = $newEnd - 1;
|
||||
|
||||
for ($i = $end; $i >= $start; --$i) {
|
||||
if ($list[$i]->compare($list[$i + 1], $order)) {
|
||||
$old = $list[$i];
|
||||
$list[$i] = $list[$i + 1];
|
||||
$list[$i + 1] = $old;
|
||||
|
||||
$newStart = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$start = $newStart + 1;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* CombSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class CombSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$sorted = false;
|
||||
$n = \count($list);
|
||||
$gap = $n;
|
||||
$shrink = 1.3;
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
while (!$sorted) {
|
||||
$gap = (int) \floor($gap / $shrink);
|
||||
|
||||
if ($gap < 2) {
|
||||
$gap = 1;
|
||||
$sorted = true;
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
while ($i + $gap < $n) {
|
||||
if ($list[$i]->compare($list[$i + $gap], $order)) {
|
||||
$old = $list[$i];
|
||||
$list[$i] = $list[$i + 1];
|
||||
$list[$i + 1] = $old;
|
||||
|
||||
$sorted = false;
|
||||
}
|
||||
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* CycleSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class CycleSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
for ($start = 0; $start < $n - 1; ++$start) {
|
||||
$item = $list[$start];
|
||||
|
||||
$pos = $start;
|
||||
$length0 = \count($list);
|
||||
for ($i = $start + 1; $i < $length0; ++$i) {
|
||||
if (!$list[$i]->compare($item, $order)) {
|
||||
++$pos;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pos === $start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while ($item->equals($list[$pos])) {
|
||||
++$pos;
|
||||
}
|
||||
|
||||
if ($pos !== $start) {
|
||||
$old = $list[$pos];
|
||||
$list[$pos] = $item;
|
||||
$item = $old;
|
||||
}
|
||||
|
||||
while ($pos !== $start) {
|
||||
$pos = $start;
|
||||
|
||||
for ($i = $start + 1; $i < $n; ++$i) {
|
||||
if (!$list[$i]->compare($item, $order)) {
|
||||
++$pos;
|
||||
}
|
||||
}
|
||||
|
||||
while (isset($list[$pos]) && $item->equals($list[$pos])) {
|
||||
++$pos;
|
||||
}
|
||||
|
||||
if (isset($list[$pos])) {
|
||||
$old = $list[$pos];
|
||||
$list[$pos] = $item;
|
||||
$item = $old;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* GnomeSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class GnomeSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
$j = $i;
|
||||
|
||||
while ($j > 0 && $list[$j - 1]->compare($list[$j], $order)) {
|
||||
$old = $list[$j - 1];
|
||||
$list[$j - 1] = $list[$j];
|
||||
$list[$j] = $old;
|
||||
|
||||
--$j;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* HeapSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class HeapSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$copy = $list;
|
||||
|
||||
for ($p = (int) ($n / 2 - 1); $p >= 0; --$p) {
|
||||
self::heapify($copy, $n, $p, $order);
|
||||
}
|
||||
|
||||
for ($i = $n - 1; $i > 0; --$i) {
|
||||
$temp = $copy[$i];
|
||||
$copy[$i] = $copy[0];
|
||||
$copy[0] = $temp;
|
||||
|
||||
--$n;
|
||||
self::heapify($copy, $n, 0, $order);
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert into heap data structure
|
||||
*
|
||||
* @param array $list Data to sort
|
||||
* @param int $size Heap size
|
||||
* @param int $index Index element
|
||||
* @param int $order Sort order
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function heapify(array &$list, int $size, int $index, int $order) : void
|
||||
{
|
||||
$left = ($index + 1) * 2 - 1;
|
||||
$right = ($index + 1) * 2;
|
||||
$pivot = 0;
|
||||
|
||||
$pivot = $left < $size && $list[$left]->compare($list[$index], $order) ? $left : $index;
|
||||
|
||||
if ($right < $size && $list[$right]->compare($list[$pivot], $order)) {
|
||||
$pivot = $right;
|
||||
}
|
||||
|
||||
if ($pivot !== $index) {
|
||||
$temp = $list[$index];
|
||||
$list[$index] = $list[$pivot];
|
||||
$list[$pivot] = $temp;
|
||||
|
||||
self::heapify($list, $size, $pivot, $order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* InsertionSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class InsertionSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
for ($i = 1; $i < $n; ++$i) {
|
||||
$pivot = $list[$i];
|
||||
$j = $i - 1;
|
||||
|
||||
while ($j >= 0 && $list[$j]->compare($pivot, $order)) {
|
||||
$list[$j + 1] = $list[$j];
|
||||
--$j;
|
||||
}
|
||||
|
||||
$list[$j + 1] = $pivot;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* IntroSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class IntroSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$clone = $list;
|
||||
$size = self::partition($clone, 0, \count($list) - 1, $order);
|
||||
|
||||
if ($size < 16) {
|
||||
return InsertionSort::sort($clone, $order);
|
||||
}
|
||||
|
||||
if ($size > \log(\count($list)) * 2) {
|
||||
return HeapSort::sort($clone, $order);
|
||||
}
|
||||
|
||||
return QuickSort::sort($clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition list and return the size
|
||||
*
|
||||
* @param array $list List reference
|
||||
* @param int $lo Low or left side
|
||||
* @param int $hi High or right side
|
||||
* @param int $order Order type
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function partition(array &$list, int $lo, int $hi, int $order) : int
|
||||
{
|
||||
$pivot = $list[$hi];
|
||||
$i = $lo;
|
||||
|
||||
for ($j = $lo; $j < $hi; ++$j) {
|
||||
if ($list[$j]->compare($pivot, $order)) {
|
||||
$temp = $list[$j];
|
||||
$list[$j] = $list[$i];
|
||||
$list[$i] = $temp;
|
||||
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
|
||||
$list[$hi] = $list[$i];
|
||||
$list[$i] = $pivot;
|
||||
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.2
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Sort;
|
||||
|
||||
/**
|
||||
* MergeSort class.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Sort;
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class MergeSort implements SortInterface
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function sort(array $list, int $order = SortOrder::ASC) : array
|
||||
{
|
||||
$n = \count($list);
|
||||
|
||||
if ($n < 2) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$clone = $list;
|
||||
self::sortHalve($clone, 0, $n - 1, $order);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive sorting of halve of the list and then merging it
|
||||
*
|
||||
* @param array $list Data to sort
|
||||
* @param int $lo Start of the list to sort
|
||||
* @param int $hi End of the list to sort
|
||||
* @param int $order Sort order
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function sortHalve(array &$list, int $lo, int $hi, int $order) : void
|
||||
{
|
||||
if ($lo >= $hi) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mi = (int) ($lo + ($hi - $lo) / 2);
|
||||
|
||||
self::sortHalve($list, $lo, $mi, $order);
|
||||
self::sortHalve($list, $mi + 1, $hi, $order);
|
||||
|
||||
self::merge($list, $lo, $mi, $hi, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge and sort sub list
|
||||
*
|
||||
* @param array $list Data to sort
|
||||
* @param int $lo Start of the list to sort
|
||||
* @param int $mi Middle point of the list to sort
|
||||
* @param int $hi End of the list to sort
|
||||
* @param int $order Sort order
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function merge(array &$list, int $lo, int $mi, int $hi, int $order) : void
|
||||
{
|
||||
$n1 = $mi - $lo + 1;
|
||||
$n2 = $hi - $mi;
|
||||
|
||||
$loList = [];
|
||||
$hiList = [];
|
||||
|
||||
for ($i = 0; $i < $n1; ++$i) {
|
||||
$loList[$i] = $list[$lo + $i];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $n2; ++$i) {
|
||||
$hiList[$i] = $list[$mi + 1 + $i];
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$j = 0;
|
||||
$k = $lo;
|
||||
|
||||
while ($i < $n1 && $j < $n2) {
|
||||
if (!$loList[$i]->compare($hiList[$j], $order)) {
|
||||
$list[$k] = $loList[$i];
|
||||
++$i;
|
||||
} else {
|
||||
$list[$k] = $hiList[$j];
|
||||
++$j;
|
||||
}
|
||||
|
||||
++$k;
|
||||
}
|
||||
|
||||
while ($i < $n1) {
|
||||
$list[$k] = $loList[$i];
|
||||
++$i;
|
||||
++$k;
|
||||
}
|
||||
|
||||
while ($j < $n2) {
|
||||
$list[$k] = $hiList[$j];
|
||||
++$j;
|
||||
++$k;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user