Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.66% covered (success)
99.66%
291 / 292
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
JumpPointSearch
99.66% covered (success)
99.66%
291 / 292
91.67% covered (success)
91.67%
11 / 12
174
0.00% covered (danger)
0.00%
0 / 1
 findPath
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
7
 identifySuccessors
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 findNeighbors
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 findNeighborsStraight
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 findNeighborsDiagonal
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
16
 findNeighborsDiagonalOneObstacle
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
19
 findNeighborsDiagonalNoObstacle
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
22
 jump
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 jumpStraight
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
17
 jumpDiagonal
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
22
 jumpDiagonalOneObstacle
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
24
 jumpDiagonalNoObstacle
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
21
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Algorithm\PathFinding
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 *
13 * Extended based on:
14 * MIT License
15 * (c) 2011-2012 Xueqiao Xu <xueqiaoxu@gmail.com>
16 * (c) PathFinding.js
17 */
18declare(strict_types=1);
19
20namespace phpOMS\Algorithm\PathFinding;
21
22use phpOMS\Stdlib\Base\Heap;
23
24/**
25 * Perform path finding.
26 *
27 * @package phpOMS\Algorithm\PathFinding
28 * @license OMS License 2.0
29 * @link    https://jingga.app
30 * @since   1.0.0
31 */
32final class JumpPointSearch implements PathFinderInterface
33{
34    /**
35     * {@inheritdoc}
36     */
37    public static function findPath(
38        int $startX, int $startY,
39        int $endX, int $endY,
40        Grid $grid,
41        int $heuristic, int $movement
42    ) : Path
43    {
44        /** @var null|JumpPointNode $startNode */
45        $startNode = $grid->getNode($startX, $startY);
46        /** @var null|JumpPointNode $endNode */
47        $endNode = $grid->getNode($endX, $endY);
48
49        if ($startNode === null || $endNode === null) {
50            return new Path($grid);
51        }
52
53        $startNode->setG(0.0);
54        $startNode->setF(0.0);
55        $startNode->setOpened(true);
56
57        $openList = new Heap(function($node1, $node2) {
58            return $node1->getF() - $node2->getF();
59        });
60
61        $openList->push($startNode);
62        $node = null;
63
64        while (!$openList->isEmpty()) {
65            $node = $openList->pop();
66            if ($node === null) {
67                break;
68            }
69
70            /** @var JumpPointNode $node */
71            $node->setClosed(true);
72
73            if ($node->isEqual($endNode)) {
74                break;
75            }
76
77            $openList = self::identifySuccessors($node, $grid, $heuristic, $movement, $endNode, $openList);
78        }
79
80        $path = new Path($grid);
81
82        while ($node !== null) {
83            $path->addNode($node);
84            $node = $node->parent;
85        }
86
87        return $path;
88    }
89
90    /**
91     * Find possible successor jump points
92     *
93     * @param JumpPointNode $node      Node to find successor for
94     * @param Grid          $grid      Grid of the nodes
95     * @param int           $heuristic Heuristic/metrics type for the distance calculation
96     * @param int           $movement  Movement type
97     * @param JumpPointNode $endNode   End node to find path to
98     * @param Heap          $openList  Heap of open nodes
99     *
100     * @return Heap
101     *
102     * @since 1.0.0
103     */
104    public static function identifySuccessors(JumpPointNode $node, Grid $grid, int $heuristic, int $movement, JumpPointNode $endNode, Heap $openList) : Heap
105    {
106        /** @var JumpPointNode[] $neighbors */
107        $neighbors       = self::findNeighbors($node, $movement, $grid);
108        $neighborsLength = \count($neighbors);
109
110        for ($i = 0; $i < $neighborsLength; ++$i) {
111            $neighbor = $neighbors[$i];
112
113            if ($neighbor === null) {
114                continue;
115            }
116
117            $jumpPoint = self::jump($neighbor, $node, $endNode, $movement, $grid);
118
119            if ($jumpPoint === null || $jumpPoint->isClosed()) {
120                continue;
121            }
122
123            $d  = Heuristic::metric($node->getCoordinates(), $jumpPoint->getCoordinates(), HeuristicType::OCTILE);
124            $ng = $node->getG() + $d;
125
126            if (!$jumpPoint->isOpened() || $ng < $jumpPoint->getG()) {
127                $jumpPoint->setG($ng);
128                $jumpPoint->setH($jumpPoint->getH() ?? Heuristic::metric($jumpPoint->getCoordinates(), $endNode->getCoordinates(), $heuristic));
129                $jumpPoint->setF($jumpPoint->getG() + $jumpPoint->getH());
130                $jumpPoint->parent = $node;
131
132                if (!$jumpPoint->isOpened()) {
133                    $openList->push($jumpPoint);
134                    $jumpPoint->setOpened(true);
135                } else {
136                    $openList->update($jumpPoint);
137                }
138            }
139        }
140
141        return $openList;
142    }
143
144    /**
145     * Find neighbor of node
146     *
147     * @param JumpPointNode $node     Node to find successor for
148     * @param int           $movement Movement type
149     * @param Grid          $grid     Grid of the nodes
150     *
151     * @return Node[] Neighbors of node
152     *
153     * @since 1.0.0
154     */
155    private static function findNeighbors(JumpPointNode $node, int $movement, Grid $grid) : array
156    {
157        if ($movement === MovementType::STRAIGHT) {
158            return self::findNeighborsStraight($node, $grid);
159        } elseif ($movement === MovementType::DIAGONAL) {
160            return self::findNeighborsDiagonal($node, $grid);
161        } elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
162            return self::findNeighborsDiagonalOneObstacle($node, $grid);
163        }
164
165        return self::findNeighborsDiagonalNoObstacle($node, $grid);
166    }
167
168    /**
169     * Find neighbor of node
170     *
171     * @param JumpPointNode $node Node to find successor for
172     * @param Grid          $grid Grid of the nodes
173     *
174     * @return Node[] Neighbors of node
175     *
176     * @since 1.0.0
177     */
178    private static function findNeighborsStraight(JumpPointNode $node, Grid $grid) : array
179    {
180        if ($node->parent === null) {
181            return $grid->getNeighbors($node, MovementType::STRAIGHT);
182        }
183
184        $x = $node->getX();
185        $y = $node->getY();
186
187        $px = $node->parent->getX();
188        $py = $node->parent->getY();
189
190        /** @var int $dx */
191        $dx = ($x - $px) / \max(\abs($x - $px), 1);
192        /** @var int $dy */
193        $dy = ($y - $py) / \max(\abs($y - $py), 1);
194
195        $neighbors = [];
196        if ($dx !== 0) {
197            if ($grid->isWalkable($x, $y - 1)) {
198                $neighbors[] = $grid->getNode($x, $y - 1);
199            }
200
201            if ($grid->isWalkable($x, $y + 1)) {
202                $neighbors[] = $grid->getNode($x, $y + 1);
203            }
204
205            if ($grid->isWalkable($x + $dx, $y)) {
206                $neighbors[] = $grid->getNode($x + $dx, $y);
207            }
208        } elseif ($dy !== 0) {
209            if ($grid->isWalkable($x - 1, $y)) {
210                $neighbors[] = $grid->getNode($x - 1, $y);
211            }
212
213            if ($grid->isWalkable($x + 1, $y)) {
214                $neighbors[] = $grid->getNode($x + 1, $y);
215            }
216
217            if ($grid->isWalkable($x, $y + $dy)) {
218                $neighbors[] = $grid->getNode($x, $y + $dy);
219            }
220        }
221
222        /** @var JumpPointNode[] $neighbors */
223        return $neighbors;
224    }
225
226    /**
227     * Find neighbor of node
228     *
229     * @param JumpPointNode $node Node to find successor for
230     * @param Grid          $grid Grid of the nodes
231     *
232     * @return Node[] Neighbors of node
233     *
234     * @since 1.0.0
235     */
236    private static function findNeighborsDiagonal(JumpPointNode $node, Grid $grid) : array
237    {
238        if ($node->parent === null) {
239            return $grid->getNeighbors($node, MovementType::DIAGONAL);
240        }
241
242        $x = $node->getX();
243        $y = $node->getY();
244
245        $px = $node->parent->getX();
246        $py = $node->parent->getY();
247
248        /** @var int $dx */
249        $dx = ($x - $px) / \max(\abs($x - $px), 1);
250        /** @var int $dy */
251        $dy = ($y - $py) / \max(\abs($y - $py), 1);
252
253        $neighbors = [];
254        if ($dx !== 0 && $dy !== 0) {
255            if ($grid->isWalkable($x, $y + $dy)) {
256                $neighbors[] = $grid->getNode($x, $y + $dy);
257            }
258
259            if ($grid->isWalkable($x + $dx, $y)) {
260                $neighbors[] = $grid->getNode($x + $dx, $y);
261            }
262
263            if ($grid->isWalkable($x + $dx, $y + $dy)) {
264                $neighbors[] = $grid->getNode($x + $dx, $y + $dy);
265            }
266
267            if (!$grid->isWalkable($x - $dx, $y)) {
268                $neighbors[] = $grid->getNode($x - $dx, $y + $dy);
269            }
270
271            if (!$grid->isWalkable($x, $y - $dy)) {
272                $neighbors[] = $grid->getNode($x + $dx, $y - $dy);
273            }
274        } elseif ($dx === 0) {
275            if ($grid->isWalkable($x, $y + $dy)) {
276                $neighbors[] = $grid->getNode($x, $y + $dy);
277            }
278
279            if (!$grid->isWalkable($x + 1, $y)) {
280                $neighbors[] = $grid->getNode($x + 1, $y + $dy);
281            }
282
283            if (!$grid->isWalkable($x - 1, $y)) {
284                $neighbors[] = $grid->getNode($x - 1, $y + $dy);
285            }
286        } else {
287            if ($grid->isWalkable($x + $dx, $y)) {
288                $neighbors[] = $grid->getNode($x + $dx, $y);
289            }
290
291            if (!$grid->isWalkable($x, $y + 1)) {
292                $neighbors[] = $grid->getNode($x + $dx, $y + 1);
293            }
294
295            if (!$grid->isWalkable($x, $y - 1)) {
296                $neighbors[] = $grid->getNode($x + $dx, $y - 1);
297            }
298        }
299
300        /** @var JumpPointNode[] $neighbors */
301        return $neighbors;
302    }
303
304    /**
305     * Find neighbor of node
306     *
307     * @param JumpPointNode $node Node to find successor for
308     * @param Grid          $grid Grid of the nodes
309     *
310     * @return Node[] Neighbors of node
311     *
312     * @since 1.0.0
313     */
314    private static function findNeighborsDiagonalOneObstacle(JumpPointNode $node, Grid $grid) : array
315    {
316        if ($node->parent === null) {
317            return $grid->getNeighbors($node, MovementType::DIAGONAL_ONE_OBSTACLE);
318        }
319
320        $x = $node->getX();
321        $y = $node->getY();
322
323        $px = $node->parent->getX();
324        $py = $node->parent->getY();
325
326        /** @var int $dx */
327        $dx = ($x - $px) / \max(\abs($x - $px), 1);
328        /** @var int $dy */
329        $dy = ($y - $py) / \max(\abs($y - $py), 1);
330
331        $neighbors = [];
332        if ($dx !== 0 && $dy !== 0) {
333            if ($grid->isWalkable($x, $y + $dy)) {
334                $neighbors[] = $grid->getNode($x, $y + $dy);
335            }
336
337            if ($grid->isWalkable($x + $dx, $y)) {
338                $neighbors[] = $grid->getNode($x + $dx, $y);
339            }
340
341            if ($grid->isWalkable($x, $y + $dy) || $grid->isWalkable($x + $dx, $y)) {
342                $neighbors[] = $grid->getNode($x + $dx, $y + $dy);
343            }
344
345            if (!$grid->isWalkable($x - $dx, $y) && $grid->isWalkable($x, $y + $dy)) {
346                $neighbors[] = $grid->getNode($x - $dx, $y + $dy);
347            }
348
349            if (!$grid->isWalkable($x, $y - $dy) && $grid->isWalkable($x + $dx, $y)) {
350                $neighbors[] = $grid->getNode($x + $dx, $y - $dy);
351            }
352        } elseif ($dx === 0) {
353            if ($grid->isWalkable($x, $y + $dy)) {
354                $neighbors[] = $grid->getNode($x, $y + $dy);
355                if (!$grid->isWalkable($x + 1, $y)) {
356                    $neighbors[] = $grid->getNode($x + 1, $y + $dy);
357                }
358
359                if (!$grid->isWalkable($x - 1, $y)) {
360                    $neighbors[] = $grid->getNode($x - 1, $y + $dy);
361                }
362            }
363        } elseif ($grid->isWalkable($x + $dx, $y)) {
364            $neighbors[] = $grid->getNode($x + $dx, $y);
365            if (!$grid->isWalkable($x, $y + 1)) {
366                $neighbors[] = $grid->getNode($x + $dx, $y + 1);
367            }
368
369            if (!$grid->isWalkable($x, $y - 1)) {
370                $neighbors[] = $grid->getNode($x + $dx, $y - 1);
371            }
372        }
373
374        /** @var JumpPointNode[] $neighbors */
375        return $neighbors;
376    }
377
378    /**
379     * Find neighbor of node
380     *
381     * @param JumpPointNode $node Node to find successor for
382     * @param Grid          $grid Grid of the nodes
383     *
384     * @return Node[] Neighbors of node
385     *
386     * @since 1.0.0
387     */
388    private static function findNeighborsDiagonalNoObstacle(JumpPointNode $node, Grid $grid) : array
389    {
390        if ($node->parent === null) {
391            return $grid->getNeighbors($node, MovementType::DIAGONAL_NO_OBSTACLE);
392        }
393
394        $x = $node->getX();
395        $y = $node->getY();
396
397        $px = $node->parent->getX();
398        $py = $node->parent->getY();
399
400        /** @var int $dx */
401        $dx = ($x - $px) / \max(\abs($x - $px), 1);
402        /** @var int $dy */
403        $dy = ($y - $py) / \max(\abs($y - $py), 1);
404
405        $neighbors = [];
406        if ($dx !== 0 && $dy !== 0) {
407            if ($grid->isWalkable($x, $y + $dy)) {
408                $neighbors[] = $grid->getNode($x, $y + $dy);
409            }
410
411            if ($grid->isWalkable($x + $dx, $y)) {
412                $neighbors[] = $grid->getNode($x + $dx, $y);
413            }
414
415            if ($grid->isWalkable($x, $y + $dy) || $grid->isWalkable($x + $dx, $y)) {
416                $neighbors[] = $grid->getNode($x + $dx, $y + $dy);
417            }
418        } elseif ($dx !== 0 && $dy === 0) {
419            $isNextWalkable   = $grid->isWalkable($x + $dx, $y);
420            $isTopWalkable    = $grid->isWalkable($x, $y + 1);
421            $isBottomWalkable = $grid->isWalkable($x, $y - 1);
422
423            if ($isNextWalkable) {
424                $neighbors[] = $grid->getNode($x + $dx, $y);
425                if ($isTopWalkable) {
426                    $neighbors[] = $grid->getNode($x + $dx, $y + 1);
427                }
428
429                if ($isBottomWalkable) {
430                    $neighbors[] = $grid->getNode($x + $dx, $y - 1);
431                }
432            }
433
434            if ($isTopWalkable) {
435                $neighbors[] = $grid->getNode($x, $y + 1);
436            }
437
438            if ($isBottomWalkable) {
439                $neighbors[] = $grid->getNode($x, $y - 1);
440            }
441        } elseif ($dx === 0 && $dy !== 0) {
442            $isNextWalkable  = $grid->isWalkable($x, $y + $dy);
443            $isRightWalkable = $grid->isWalkable($x + 1, $y);
444            $isLeftWalkable  = $grid->isWalkable($x - 1, $y);
445
446            if ($isNextWalkable) {
447                $neighbors[] = $grid->getNode($x, $y + $dy);
448                if ($isRightWalkable) {
449                    $neighbors[] = $grid->getNode($x + 1, $y + $dy);
450                }
451
452                if ($isLeftWalkable) {
453                    $neighbors[] = $grid->getNode($x - 1, $y + $dy);
454                }
455            }
456
457            if ($isRightWalkable) {
458                $neighbors[] = $grid->getNode($x + 1, $y);
459            }
460
461            if ($isLeftWalkable) {
462                $neighbors[] = $grid->getNode($x - 1, $y);
463            }
464        }
465
466        /** @var JumpPointNode[] $neighbors */
467        return $neighbors;
468    }
469
470    /**
471     * Find next jump point
472     *
473     * @param null|JumpPointNode $node     Node to find jump point from
474     * @param null|JumpPointNode $pNode    Parent node
475     * @param JumpPointNode      $endNode  End node to find path to
476     * @param int                $movement Movement type
477     * @param Grid               $grid     Grid of the nodes
478     *
479     * @return null|JumpPointNode
480     *
481     * @since 1.0.0
482     */
483    private static function jump(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, int $movement, Grid $grid) : ?JumpPointNode
484    {
485        if ($movement === MovementType::STRAIGHT) {
486            return self::jumpStraight($node, $pNode, $endNode, $grid);
487        } elseif ($movement === MovementType::DIAGONAL) {
488            return self::jumpDiagonal($node, $pNode, $endNode, $grid);
489        } elseif ($movement === MovementType::DIAGONAL_ONE_OBSTACLE) {
490            return self::jumpDiagonalOneObstacle($node, $pNode, $endNode, $grid);
491        }
492
493        return self::jumpDiagonalNoObstacle($node, $pNode, $endNode, $grid);
494    }
495
496    /**
497     * Find next jump point
498     *
499     * @param null|JumpPointNode $node    Node to find jump point from
500     * @param null|JumpPointNode $pNode   Parent node
501     * @param JumpPointNode      $endNode End node to find path to
502     * @param Grid               $grid    Grid of the nodes
503     *
504     * @return null|JumpPointNode
505     *
506     * @throws \Exception
507     *
508     * @since 1.0.0
509     */
510    private static function jumpStraight(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
511    {
512        if ($node === null || $pNode === null || !$node->isWalkable) {
513            return null;
514        }
515
516        $x = $node->getX();
517        $y = $node->getY();
518
519        $dx = $x - $pNode->getX();
520        $dy = $y - $pNode->getY();
521
522        // not always necessary but might be important for the future
523        $node->setTested(true);
524
525        if ($node->isEqual($endNode)) {
526            return $node;
527        }
528
529        if ($dx !== 0) {
530            if (($grid->isWalkable($x, $y - 1) && !$grid->isWalkable($x - $dx, $y - 1))
531                || ($grid->isWalkable($x, $y + 1) && !$grid->isWalkable($x - $dx, $y + 1))
532            ) {
533                return $node;
534            }
535        } elseif ($dy !== 0) {
536            if (($grid->isWalkable($x - 1, $y) && !$grid->isWalkable($x - 1, $y - $dy))
537                || ($grid->isWalkable($x + 1, $y) && !$grid->isWalkable($x + 1, $y - $dy))
538            ) {
539                return $node;
540            }
541
542            if (self::jumpStraight($grid->getNode($x + 1, $y), $node, $endNode, $grid) !== null
543                || self::jumpStraight($grid->getNode($x - 1, $y), $node, $endNode, $grid) !== null
544            ) {
545                return $node;
546            }
547        } else {
548            throw new \Exception('invalid movement'); // @codeCoverageIgnore
549        }
550
551        return self::jumpStraight($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
552    }
553
554    /**
555     * Find next jump point
556     *
557     * @param null|JumpPointNode $node    Node to find jump point from
558     * @param null|JumpPointNode $pNode   Parent node
559     * @param JumpPointNode      $endNode End node to find path to
560     * @param Grid               $grid    Grid of the nodes
561     *
562     * @return null|JumpPointNode
563     *
564     * @since 1.0.0
565     */
566    private static function jumpDiagonal(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
567    {
568        if ($node === null || $pNode === null || !$node->isWalkable) {
569            return null;
570        }
571
572        $x = $node->getX();
573        $y = $node->getY();
574
575        $dx = $x - $pNode->getX();
576        $dy = $y - $pNode->getY();
577
578        // not always necessary but might be important for the future
579        $node->setTested(true);
580
581        if ($node->isEqual($endNode)) {
582            return $node;
583        }
584
585        if ($dx !== 0 && $dy !== 0) {
586            if (($grid->isWalkable($x - $dx, $y + $dy) && !$grid->isWalkable($x - $dx, $y))
587                || ($grid->isWalkable($x + $dx, $y - $dy) && !$grid->isWalkable($x, $y - $dy))
588            ) {
589                return $node;
590            }
591
592            if (self::jumpDiagonal($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
593                || self::jumpDiagonal($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
594            ) {
595                return $node;
596            }
597        } elseif ($dx !== 0) {
598            if (($grid->isWalkable($x + $dx, $y + 1) && !$grid->isWalkable($x, $y + 1))
599                || ($grid->isWalkable($x + $dx, $y - 1) && !$grid->isWalkable($x, $y - 1))
600            ) {
601                return $node;
602            }
603        } elseif (($grid->isWalkable($x + 1, $y + $dy) && !$grid->isWalkable($x + 1, $y))
604            || ($grid->isWalkable($x - 1, $y + $dy) && !$grid->isWalkable($x - 1, $y))
605        ) {
606            return $node;
607        }
608
609        return self::jumpDiagonal($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
610    }
611
612    /**
613     * Find next jump point
614     *
615     * @param null|JumpPointNode $node    Node to find jump point from
616     * @param null|JumpPointNode $pNode   Parent node
617     * @param JumpPointNode      $endNode End node to find path to
618     * @param Grid               $grid    Grid of the nodes
619     *
620     * @return null|JumpPointNode
621     *
622     * @since 1.0.0
623     */
624    private static function jumpDiagonalOneObstacle(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
625    {
626        if ($node === null || $pNode === null || !$node->isWalkable) {
627            return null;
628        }
629
630        $x = $node->getX();
631        $y = $node->getY();
632
633        $dx = $x - $pNode->getX();
634        $dy = $y - $pNode->getY();
635
636        // not always necessary but might be important for the future
637        $node->setTested(true);
638
639        if ($node->isEqual($endNode)) {
640            return $node;
641        }
642
643        if ($dx !== 0 && $dy !== 0) {
644            if (($grid->isWalkable($x - $dx, $y + $dy) && !$grid->isWalkable($x - $dx, $y))
645                || ($grid->isWalkable($x + $dx, $y - $dy) && !$grid->isWalkable($x, $y - $dy))
646            ) {
647                return $node;
648            }
649
650            if (self::jumpDiagonalOneObstacle($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
651                || self::jumpDiagonalOneObstacle($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
652            ) {
653                return $node;
654            }
655        } elseif ($dx !== 0) {
656            if (($grid->isWalkable($x + $dx, $y + 1) && !$grid->isWalkable($x, $y + 1))
657                || ($grid->isWalkable($x + $dx, $y - 1) && !$grid->isWalkable($x, $y - 1))
658            ) {
659                return $node;
660            }
661        } elseif (($grid->isWalkable($x + 1, $y + $dy) && !$grid->isWalkable($x + 1, $y))
662            || ($grid->isWalkable($x - 1, $y + $dy) && !$grid->isWalkable($x - 1, $y))
663        ) {
664            return $node;
665        }
666
667        if ($grid->isWalkable($x + $dx, $y) || $grid->isWalkable($x, $y + $dy)) {
668            return self::jumpDiagonalOneObstacle($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
669        }
670
671        return null;
672    }
673
674    /**
675     * Find next jump point
676     *
677     * @param null|JumpPointNode $node    Node to find jump point from
678     * @param null|JumpPointNode $pNode   Parent node
679     * @param JumpPointNode      $endNode End node to find path to
680     * @param Grid               $grid    Grid of the nodes
681     *
682     * @return null|JumpPointNode
683     *
684     * @since 1.0.0
685     */
686    private static function jumpDiagonalNoObstacle(?JumpPointNode $node, ?JumpPointNode $pNode, JumpPointNode $endNode, Grid $grid) : ?JumpPointNode
687    {
688        if ($node === null || $pNode === null || !$node->isWalkable) {
689            return null;
690        }
691
692        $x = $node->getX();
693        $y = $node->getY();
694
695        $dx = $x - $pNode->getX();
696        $dy = $y - $pNode->getY();
697
698        // not always necessary but might be important for the future
699        $node->setTested(true);
700
701        if ($node->isEqual($endNode)) {
702            return $node;
703        }
704
705        if ($dx !== 0 && $dy !== 0) {
706            if (self::jumpDiagonalNoObstacle($grid->getNode($x + $dx, $y), $node, $endNode, $grid) !== null
707                || self::jumpDiagonalNoObstacle($grid->getNode($x, $y + $dy), $node, $endNode, $grid) !== null
708            ) {
709                return $node;
710            }
711        } elseif ($dx !== 0) {
712            if (($grid->isWalkable($x, $y - 1) && !$grid->isWalkable($x - $dx, $y - 1))
713                || ($grid->isWalkable($x, $y + 1) && !$grid->isWalkable($x - $dx, $y + 1))
714            ) {
715                return $node;
716            }
717        } elseif ($dy !== 0) {
718            if (($grid->isWalkable($x - 1, $y) && !$grid->isWalkable($x - 1, $y - $dy))
719                || ($grid->isWalkable($x + 1, $y) && !$grid->isWalkable($x + 1, $y - $dy))
720            ) {
721                return $node;
722            }
723        }
724
725        if ($grid->isWalkable($x + $dx, $y) || $grid->isWalkable($x, $y + $dy)) {
726            return self::jumpDiagonalNoObstacle($grid->getNode($x + $dx, $y + $dy), $node, $endNode, $grid);
727        }
728
729        return null;
730    }
731}