Compare commits

..

6 Commits

2213 changed files with 42321 additions and 243720 deletions

37
.gitattributes vendored
View File

@ -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
View File

@ -0,0 +1 @@
A developer and contribution documentation can be found at https://orange-management.gitbooks.io/developer-guide/content/index.html.

View File

@ -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"

View File

@ -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.'

View File

@ -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 }}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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];
}
}

View File

@ -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
View 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;
}
}

View File

@ -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;
}
}

View File

@ -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
View 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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
{
}

View File

@ -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 [];
}
}

View File

@ -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 [];
}
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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 [];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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] ?? [];
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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 = [];
}

View File

@ -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 = [];
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 = [];
}

View File

@ -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 = [];
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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]);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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,
];
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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,
];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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),
];
}
}

View File

@ -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,
];
}
}

View File

@ -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);
}
}

View File

@ -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
{
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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