0
\$\begingroup\$

(Refer to the entire repository in GitHub.)


How it looks like

Jump point search in action


Intro

This time, I present my take on Jump point search that I translated from Javascript (PathFinding.js/src/finders/JumpPointFinderBase.js and related files) to Java.


Code

io.github.coderodde.pathfinding.finders..java:

package io.github.coderodde.pathfinding.finders;

import static io.github.coderodde.pathfinding.finders.Finder.expandPath;
import static io.github.coderodde.pathfinding.finders.Finder.searchSleep;
import io.github.coderodde.pathfinding.finders.jps.jumpers.DiagonalNonCrossingJumper;
import io.github.coderodde.pathfinding.finders.jps.jumpers.NoDiagonalJumper;
import io.github.coderodde.pathfinding.finders.jps.neighbourfinders.DiagonalCrossingNeighbourFinder;
import io.github.coderodde.pathfinding.finders.jps.neighbourfinders.DiagonalNoCrossingNeighbourFinder;
import io.github.coderodde.pathfinding.finders.jps.neighbourfinders.NoDiagonalNeighbourFinder;
import io.github.coderodde.pathfinding.heuristics.HeuristicFunction;
import io.github.coderodde.pathfinding.logic.GridCellNeighbourIterable;
import io.github.coderodde.pathfinding.logic.PathfindingSettings;
import io.github.coderodde.pathfinding.logic.SearchState;
import io.github.coderodde.pathfinding.logic.SearchStatistics;
import io.github.coderodde.pathfinding.model.GridModel;
import io.github.coderodde.pathfinding.utils.Cell;
import io.github.coderodde.pathfinding.utils.CellType;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;

/**
 * This class implements the Jump Point Search.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 19, 2025)
 * @since 1.0.0 (Oct 19, 2025)
 */
public final class JumpPointSearchFinder implements Finder {

    /**
     * This interface defines the API for computing neighbour cells of a given 
     * cell.
     */
    public interface NeighbourFinder {
        
        /**
         * Finds the neighbour cells of the cell {@code current}.
         * 
         * @param current    the cell whose neighbours to find.
         * @param parentsMap the map which maps cells to their respective parent
         *                   cells.
         * @param model      the grid model.
         * @param ps         the pathfinding settings.
         * @return 
         */
        List<Cell> findNeighbours(Cell current,
                                  Map<Cell, Cell> parentsMap,
                                  GridModel model,
                                  PathfindingSettings ps);
    }
    
    /**
     * This interface defines the API for jumping algorithms.
     */
    public interface Jumper {
        
        /**
         * Jumps to the next jump point cell.
         * 
         * @param x                the {@code x} coordinate of the current cell.
         * @param y                the {@code y} coordinate of the current cell.
         * @param px               the {@code x} coordinate of the parent cell.
         * @param py               the {@code y} coordinate of the parent cell.
         * @param model            the grid model.
         * @param searchStatistics the search statistics object.
         * 
         * @return the next jump point cell or {@code null} if there is no such.
         */
        Cell jump(int x,
                  int y,
                  int px,
                  int py,
                  GridModel model,
                  SearchStatistics searchStatistics);
    }
    
    @Override
    public List<Cell> findPath(GridModel model, 
                               GridCellNeighbourIterable neighbourIterable, 
                               PathfindingSettings pathfindingSettings, 
                               SearchState searchState, 
                               SearchStatistics searchStatistics) {
        
        NeighbourFinder neighbourFinder = 
                getNeighbourFinder(pathfindingSettings);
        
        Jumper jumper = getJumper(pathfindingSettings);
        
        Cell source = model.getSourceGridCell();
        Cell target = model.getTargetGridCell();
        
        Queue<HeapNode> open          = new PriorityQueue<>();
        Set<Cell> openSet             = new HashSet<>();
        Set<Cell> closed              = new HashSet<>();
        Map<Cell, Double> distanceMap = new HashMap<>();
        Map<Cell, Cell> parentsMap    = new HashMap<>();
        
        open.add(new HeapNode(source, 0.0));
        openSet.add(source);
        parentsMap.put(source, null);
        distanceMap.put(source, 0.0);
        
        while (!open.isEmpty()) {
            if (searchState.haltRequested()) {
                return List.of();
            }
            
            if (searchState.pauseRequested()) {
                searchSleep(pathfindingSettings);
                continue;
            }
            
            Cell current = open.remove().cell;
            
            if (model.getCellType(current) != CellType.SOURCE &&
                model.getCellType(current) != CellType.TARGET) {
                model.setCellType(current, CellType.VISITED);
            }
            
            if (current.equals(target)) {
                List<Cell> compressedPath = tracebackPath(target,
                                                          parentsMap);
                return expandPath(compressedPath, 
                                  model);
            }
            
            searchStatistics.incrementVisited();
            closed.add(current);
            
            identifySuccessors(current,
                               open,
                               closed,
                               openSet,
                               distanceMap,
                               parentsMap,
                               model,
                               pathfindingSettings,
                               searchState,
                               searchStatistics,
                               neighbourFinder,
                               jumper);
        }
        
        return List.of();
    }
    
    /**
     * Returns the required neighbour finder.
     * 
     * @param pathfindingSettings the pathfinding settings object.
     * 
     * @return an instance of the suitable 
     *         {@link io.github.coderodde.pathfinding.finders.JumpPointSearchFinder.NeighbourFinder}.
     */
    private static NeighbourFinder 
        getNeighbourFinder(PathfindingSettings pathfindingSettings) {
        
        if (pathfindingSettings.allowDiagonals()) {
            if (pathfindingSettings.dontCrossCorners()) {
                return new DiagonalNoCrossingNeighbourFinder();
            } else {
                return new DiagonalCrossingNeighbourFinder();
            }
        } else {
            return new NoDiagonalNeighbourFinder();
        }
    }
    
    /**
     * Returns the required jumper.
     * 
     * @param pathfindingSettings the pathfinding settings object.
     * 
     * @return an instance of the suitable 
     *         {@link io.github.coderodde.pathfinding.finders.JumpPointSearchFinder.Jumper}.
     */
    private static Jumper getJumper(PathfindingSettings pathfindingSettings) {
        
        if (pathfindingSettings.allowDiagonals()) {
            return new DiagonalNonCrossingJumper();
        } else {
            return new NoDiagonalJumper();
        }
    }
           
    private static void identifySuccessors(Cell current,
                                           Queue<HeapNode> open,
                                           Set<Cell> closed,
                                           Set<Cell> openSet,
                                           Map<Cell, Double> distanceMap,
                                           Map<Cell, Cell> parentsMap,
                                           GridModel model,
                                           PathfindingSettings ps,
                                           SearchState searchState,
                                           SearchStatistics searchStatistics,
                                           NeighbourFinder neighbourFinder,
                                           Jumper jumper) {
        
        List<Cell> neighbors = 
                neighbourFinder.findNeighbours(current,
                                               parentsMap,
                                               model,
                                               ps);
        
        int x = current.getx();
        int y = current.gety();
        HeuristicFunction hf = ps.getHeuristicFunction();
        
        for (Cell child : neighbors) {
            if (searchState.haltRequested()) {
                throw new HaltRequestedException();
            }
            
            while (searchState.pauseRequested()) {
                searchSleep(ps);
                
                if (searchState.haltRequested()) {
                    throw new HaltRequestedException();
                }
            }
            
            Cell jumpCell = jumper.jump(child.getx(),
                                        child.gety(),
                                        x,
                                        y,
                                        model,
                                        searchStatistics);
            
            if (jumpCell == null) {
                continue;
            }
            
            int jx = jumpCell.getx();
            int jy = jumpCell.gety();
            jumpCell = model.getCell(jx, jy);
            
            if (closed.contains(jumpCell)) {
                continue;
            }
            
            double distance = hf.estimate(jx - x,
                                          jy - y);
            
            double nextg = distanceMap.get(current) + distance;
            
            if (!openSet.contains(jumpCell) ||
                nextg < distanceMap.get(jumpCell)) {
                
                distanceMap.put(jumpCell, nextg);
                
                double f = 
                        nextg + 
                        hf.estimate(jx - model.getTargetGridCell().getx(),
                                    jy - model.getTargetGridCell().gety());
                
                parentsMap.put(jumpCell, current);
                
                if (!openSet.contains(jumpCell)) {
                     openSet.add(jumpCell);
                }
                
                open.add(new HeapNode(jumpCell, f));
                
                searchStatistics.incrementOpened();
                searchSleep(ps);
            }
        }
    }
}

io.github.coderodde.pathfinding.finders.jps.jumpers.DiagonalNonCrossingJumper.java:

package io.github.coderodde.pathfinding.finders.jps.jumpers;

import io.github.coderodde.pathfinding.finders.JumpPointSearchFinder;
import io.github.coderodde.pathfinding.logic.SearchStatistics;
import io.github.coderodde.pathfinding.model.GridModel;
import io.github.coderodde.pathfinding.utils.Cell;
import io.github.coderodde.pathfinding.utils.CellType;

/**
 * This class implements the algorithm for doing jumps also diagonally but only 
 * if there is no obstacle wall crossing on the way.
 *
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 21, 2025)
 * @since 1.0.0 (Oct 21, 2025)
 */
public final class DiagonalNonCrossingJumper 
        implements JumpPointSearchFinder.Jumper {
    
    /**
     * This method implements jumping when diagonal moves crossing obstacle wall 
     * corners are not allowed.
     * 
     * @param x                the {@code X}-coordinate of the current cell.
     * @param y                the {@code Y}-coordinate of the current cell.
     * @param px               the {@code X}-coordinate of the parent cell.
     * @param py               the {@code Y}-coordinate of the parent cell.
     * @param model            the grid model.
     * @param searchStatistics the search statistics object.
     * 
     * @return the next cell.
     */
    @Override
    public Cell jump(int x,
                     int y,
                     int px, 
                     int py,
                     GridModel model,
                     SearchStatistics searchStatistics) {
        
        int dx = x - px;
        int dy = y - py;
        
        if (!model.isWalkable(x, y)) {
            return null;
        }
        
        if (!model.getCellType(x, y).equals(CellType.SOURCE) &&
            !model.getCellType(x, y).equals(CellType.TARGET)) {
            model.setCellType(x, y, CellType.TRACED);
            searchStatistics.incrementTraced();
        }
        
        if (model.getCell(x, y).equals(model.getTargetGridCell())) {
            return model.getTargetGridCell();
        }
        
        if (dx != 0 && dy != 0) {
            if (jump(x + dx,
                     y,
                     x,
                     y,
                     model,
                     searchStatistics) != null ||
                jump(x,
                     y + dy,
                     x,
                     y,
                     model,
                     searchStatistics) != null) {
                
                return model.getCell(x, y);
            }
        } else {
            if (dx != 0) {
                if ((model.isWalkable(x, y - 1) &&
                    !model.isWalkable(x - dx, y - 1)) ||
                    (model.isWalkable(x, y + 1) &&
                    !model.isWalkable(x - dx, y + 1))) {
                    
                    return model.getCell(x, y);
                }
            } else if (dy != 0) {
                if ((model.isWalkable(x - 1, y) &&
                    !model.isWalkable(x - 1, y - dy)) ||
                    (model.isWalkable(x + 1, y) &&
                    !model.isWalkable(x + 1, y - dy))) {
                    
                    return model.getCell(x, y);
                }
            }
        }
        
        if (model.isWalkable(x + dx, y) && model.isWalkable(x, y + dy)) {
            return jump(x + dx, 
                        y + dy, 
                        x, 
                        y, 
                        model,
                        searchStatistics);
        }
        
        return null;
    }
}

io.github.coderodde.pathfinding.finders.jps.jumpers.NoDiagonalJumper.java:

package io.github.coderodde.pathfinding.finders.jps.jumpers;

import io.github.coderodde.pathfinding.finders.JumpPointSearchFinder;
import io.github.coderodde.pathfinding.logic.SearchStatistics;
import io.github.coderodde.pathfinding.model.GridModel;
import io.github.coderodde.pathfinding.utils.Cell;
import io.github.coderodde.pathfinding.utils.CellType;

/**
 * This class implements the algorithm for doing jumps only vertically and
 * horizontally regardless the crossing obstacle walls.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 21, 2025)
 * @since 1.0.0 (Oct 21, 2025)
 */
public final class NoDiagonalJumper implements JumpPointSearchFinder.Jumper {

    /**
     * This method implements jumping when diagonal moves are not allowed.
     * 
     * @param x                the {@code X}-coordinate of the current cell.
     * @param y                the {@code Y}-coordinate of the current cell.
     * @param px               the {@code X}-coordinate of the parent cell.
     * @param py               the {@code Y}-coordinate of the parent cell.
     * @param model            the grid model.
     * @param searchStatistics the search statistics object.
     * 
     * @return the next cell.
     */
    @Override
    public Cell jump(int x, 
                     int y, 
                     int px, 
                     int py, 
                     GridModel model,
                     SearchStatistics searchStatistics) {
        
        int dx = x - px;
        int dy = y - py;
        
        if (!model.isWalkable(x, y)) {
            return null;
        }
        
        if (!model.getCellType(x, y).equals(CellType.SOURCE) &&
            !model.getCellType(x, y).equals(CellType.TARGET)) {
            model.setCellType(x, y, CellType.TRACED);
        }
        
        if (model.getCell(x, y).equals(model.getTargetGridCell())) {
            return model.getTargetGridCell();
        }
        
        if (dx != 0) {
            if ((model.isWalkable(x, y - 1) &&
                !model.isWalkable(x - dx, y - 1)) ||
                (model.isWalkable(x, y + 1) &&
                !model.isWalkable(x - dx, y + 1))) {
                
                return model.getCell(x, y);
            }
        } else if (dy != 0) {
            if ((model.isWalkable(x - 1, y) &&
                !model.isWalkable(x - 1, y - dy)) ||
                (model.isWalkable(x + 1, y) &&
                !model.isWalkable(x + 1, y - dy))) {
                
                return model.getCell(x, y);
            }
            
            if (jump(x + 1, y, x, y, model, searchStatistics) != null ||
                jump(x - 1, y, x, y, model, searchStatistics) != null) {
                
                return model.getCell(x, y);
            }
        } else {
            throw new IllegalStateException("Should not get here");
        }
        
        return jump(x + dx, 
                    y + dy,
                    x,
                    y, 
                    model, 
                    searchStatistics);
    }
}

io.github.coderodde.pathfinding.finders.jps.neighbourfinders.DiagonalNoCrossingNeighbourFinder.java:

package io.github.coderodde.pathfinding.finders.jps.neighbourfinders;

import io.github.coderodde.pathfinding.finders.JumpPointSearchFinder;
import io.github.coderodde.pathfinding.logic.GridNodeExpander;
import io.github.coderodde.pathfinding.logic.PathfindingSettings;
import io.github.coderodde.pathfinding.model.GridModel;
import io.github.coderodde.pathfinding.utils.Cell;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class implements the 
 * {@link io.github.coderodde.pathfinding.finders.JumpPointSearchFinder.NeighbourFinder} 
 * interface for computing neighbours with diagonal movement that does not 
 * involve crossing an obstacle wall.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 21, 2025)
 * @since 1.0.0 (Oct 21, 2025)
 */
public final class DiagonalNoCrossingNeighbourFinder 
        implements JumpPointSearchFinder.NeighbourFinder {

    @Override
    public List<Cell> findNeighbours(Cell current, 
                                     Map<Cell, Cell> parentsMap, 
                                     GridModel model, 
                                     PathfindingSettings ps) {
        
        List<Cell> neighbours = new ArrayList<>();
        Cell parent = parentsMap.get(current);
        
        int x = current.getx();
        int y = current.gety();
        int px;
        int py;
        int dx;
        int dy;
        
        if (parent != null) {
            px = parent.getx();
            py = parent.gety();
            dx = (x - px) / Math.max(Math.abs(x - px), 1);
            dy = (y - py) / Math.max(Math.abs(y - py), 1);
            
            // Diagonal search:
            if (dx != 0 && dy != 0) {
                if (model.isWalkable(x, y + dy)) {
                    neighbours.add(model.getCell(x, y + dy));
                }
                
                if (model.isWalkable(x + dx, y)) {
                    neighbours.add(model.getCell(x + dx, y));
                }
                
                if (model.isWalkable(x, y + dy) &&
                    model.isWalkable(x + dx, y)) {
                    neighbours.add(model.getCell(x + dx, y + dy));
                }
            } else {
                // Once here, search horizontally or vertically:
                boolean isNextWalkable;
                
                if (dx != 0) {
                    isNextWalkable = model.isWalkable(x + dx, y);
                    boolean isTopWalkable = model.isWalkable(x, y + 1);
                    boolean isBottomWalkable = model.isWalkable(x, y - 1);
                    
                    if (isNextWalkable) {
                        neighbours.add(model.getCell(x + dx, y));
                        
                        if (isTopWalkable) {
                            neighbours.add(model.getCell(x + dx, y + 1));
                        }
                        
                        if (isBottomWalkable) {
                            neighbours.add(model.getCell(x + dx, y - 1));
                        }
                    }
                    
                    if (isTopWalkable) {
                        neighbours.add(model.getCell(x, y + 1));
                    }
                    
                    if (isBottomWalkable) {
                        neighbours.add(model.getCell(x, y - 1));
                    }
                } else if (dy != 0) {
                    isNextWalkable = model.isWalkable(x, y + dy);
                    boolean isRightWalkable = model.isWalkable(x + 1, y);
                    boolean isLeftWalkable  = model.isWalkable(x - 1, y);
                    
                    if (isNextWalkable) {
                        neighbours.add(model.getCell(x, y + dy));
                        
                        if (isRightWalkable) {
                            neighbours.add(model.getCell(x + 1, y + dy));
                        }
                        
                        if (isLeftWalkable) {
                            neighbours.add(model.getCell(x - 1, y + dy));
                        }
                    }
                    
                    if (isRightWalkable) {
                        neighbours.add(model.getCell(x + 1, y));
                    }
                    
                    if (isLeftWalkable) {
                        neighbours.add(model.getCell(x - 1, y));
                    }
                }
            }
        } else {
            // Once here, return all neighbours:
            neighbours.addAll(
                    new GridNodeExpander(model, 
                                         ps).expand(current));
        }
            
        return neighbours;
    }
}

io.github.coderodde.pathfinding.finders.jps.neighbourfinders.NoDiagonalNeighbourFinder.java:

package io.github.coderodde.pathfinding.finders.jps.neighbourfinders;

import io.github.coderodde.pathfinding.finders.JumpPointSearchFinder;
import io.github.coderodde.pathfinding.logic.GridNodeExpander;
import io.github.coderodde.pathfinding.logic.PathfindingSettings;
import io.github.coderodde.pathfinding.model.GridModel;
import io.github.coderodde.pathfinding.utils.Cell;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class implements the 
 * {@link io.github.coderodde.pathfinding.finders.JumpPointSearchFinder.NeighbourFinder} 
 * interface for computing neighbours with no diagonal movement.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.0.0 (Oct 21, 2025)
 * @since 1.0.0 (Oct 21, 2025)
 */
public final class NoDiagonalNeighbourFinder 
        implements JumpPointSearchFinder.NeighbourFinder {

    @Override
    public List<Cell> findNeighbours(Cell current,
                                     Map<Cell, Cell> parentsMap, 
                                     GridModel model, 
                                     PathfindingSettings ps) {
        
        List<Cell> neighbours = new ArrayList<>();
        Cell parent = parentsMap.get(current);
        
        int x = current.getx();
        int y = current.gety();
        int px;
        int py;
        int dx;
        int dy;
        
        if (parent != null) {
            px = parent.getx();
            py = parent.gety();
            dx = (x - px) / Math.max(Math.abs(x - px), 1);
            dy = (y - py) / Math.max(Math.abs(y - py), 1);
            
            if (dx != 0) {
                if (model.isWalkable(x, y - 1)) {
                    neighbours.add(model.getCell(x, y - 1));
                }
                
                if (model.isWalkable(x, y + 1)) {
                    neighbours.add(model.getCell(x, y + 1));
                }
                
                if (model.isWalkable(x + dx, y)) {
                    neighbours.add(model.getCell(x + dx, y));
                }
            } else if (dy != 0) {
                if (model.isWalkable(x - 1, y)) {
                    neighbours.add(model.getCell(x - 1, y));
                }
                
                if (model.isWalkable(x + 1, y)) {
                    neighbours.add(model.getCell(x + 1, y));
                }
                
                if (model.isWalkable(x, y + dy)) {
                    neighbours.add(model.getCell(x, y + dy));
                }
            }
        } else {
            // Once here, return all neighbours:
            neighbours.addAll(
                    new GridNodeExpander(model, 
                                         ps).expand(current));
        }
        
        return neighbours;
    }
}

Critique request

As always, I am eager to receive constructive commentary on my work. Especially, I would like some directions on how to code up a jumper and its neighbour finder for dealing with diagonal moves with crossing walls.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Very nice, though about "faster". If one would do both: search from the start, and search back from the end, then that halves the cost: like two circles 2.πr² instead of one double π(2r)² = 4.πr². Of course more complicated. \$\endgroup\$ Commented Oct 26 at 20:53
  • 1
    \$\begingroup\$ @JoopEggen Bidirectional search. I know the topic. But what comes to JPS, I have no clue how to implement it. Also, it might be not possible to begin with, or it won't yield any speed up. \$\endgroup\$ Commented Oct 27 at 5:05

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.