Skip to content

Commit ace99bd

Browse files
CopilotbedaHovorka
andauthored
Add Kotlin-idiomatic Array2DMap extensions for pathfinding algorithms (#74)
* Add Kotlin-idiomatic Array2DMap extensions for pathfinding - Add ordered navigation methods (firstPoint, lastPoint, higherPoint, lowerPoint, ceilingPoint, floorPoint) - Add range view methods (subMap, headMap, tailMap) - Add neighbor iteration helpers (neighbors4, neighbors8, neighborEntries4, neighborEntries8) - Add spatial query methods (pointsWithinManhattan, pointsInRegion) - All methods use APIs with fallback to filter operations for multiplatform compatibility - Comprehensive test suite with 24 tests covering all new functionality - All existing tests pass (724 passing, 1 skipped) - Code quality checks pass (ktlint, detekt) Co-authored-by: bedaHovorka <5263405+bedaHovorka@users.noreply.github.com>
1 parent 74599ce commit ace99bd

2 files changed

Lines changed: 558 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/* Brno University of Technology
2+
* Faculty of Information Technology
3+
*
4+
* BSc Thesis 2006/2007
5+
*
6+
* Railway Interlocking Simulator
7+
*
8+
* Bedrich Hovorka
9+
*/
10+
package cz.vutbr.fit.interlockSim.util
11+
12+
/**
13+
* Kotlin-idiomatic extension functions for Array2DMap to support pathfinding algorithms.
14+
*
15+
* These extensions provide ordered navigation and spatial queries without requiring
16+
* Java's NavigableMap interface, making the API multiplatform-friendly and more
17+
* Kotlin-idiomatic.
18+
*/
19+
20+
/**
21+
* Returns the first (smallest) point in the map, or null if the map is empty.
22+
* Points are ordered by row (y) first, then by column (x).
23+
*
24+
* Useful for: Starting point for grid traversal in pathfinding algorithms.
25+
*/
26+
fun <V> Array2DMap<V>.firstPoint(): Point? = keys.firstOrNull()
27+
28+
/**
29+
* Returns the last (largest) point in the map, or null if the map is empty.
30+
* Points are ordered by row (y) first, then by column (x).
31+
*
32+
* Useful for: End point for grid traversal, bounds checking.
33+
*/
34+
fun <V> Array2DMap<V>.lastPoint(): Point? = keys.lastOrNull()
35+
36+
/**
37+
* Returns the first point-value entry in the map, or null if the map is empty.
38+
*/
39+
fun <V> Array2DMap<V>.firstEntry(): Map.Entry<Point, V>? =
40+
firstPoint()?.let { point -> entries.find { it.key == point } }
41+
42+
/**
43+
* Returns the last point-value entry in the map, or null if the map is empty.
44+
*/
45+
fun <V> Array2DMap<V>.lastEntry(): Map.Entry<Point, V>? = lastPoint()?.let { point -> entries.find { it.key == point } }
46+
47+
/**
48+
* Returns the least point strictly greater than the given point, or null if no such point exists.
49+
*
50+
* Useful for: Advancing to the next cell during grid traversal.
51+
*
52+
* @param point the reference point
53+
* @return the next point after the given point, or null
54+
*/
55+
fun <V> Array2DMap<V>.higherPoint(point: Point): Point? {
56+
val comparator = Array2DMap.POINT_COMPARATOR
57+
return keys.filter { comparator.compare(it, point) > 0 }
58+
.minWithOrNull(comparator)
59+
}
60+
61+
/**
62+
* Returns the greatest point strictly less than the given point, or null if no such point exists.
63+
*
64+
* Useful for: Moving backwards during grid traversal.
65+
*
66+
* @param point the reference point
67+
* @return the previous point before the given point, or null
68+
*/
69+
fun <V> Array2DMap<V>.lowerPoint(point: Point): Point? {
70+
val comparator = Array2DMap.POINT_COMPARATOR
71+
return keys.filter { comparator.compare(it, point) < 0 }
72+
.maxWithOrNull(comparator)
73+
}
74+
75+
/**
76+
* Returns the least point greater than or equal to the given point, or null if no such point exists.
77+
*
78+
* Useful for: Finding the starting point for a range query in pathfinding.
79+
*
80+
* @param point the reference point
81+
* @return the ceiling point, or null
82+
*/
83+
fun <V> Array2DMap<V>.ceilingPoint(point: Point): Point? {
84+
val comparator = Array2DMap.POINT_COMPARATOR
85+
return keys.filter { comparator.compare(it, point) >= 0 }
86+
.minWithOrNull(comparator)
87+
}
88+
89+
/**
90+
* Returns the greatest point less than or equal to the given point, or null if no such point exists.
91+
*
92+
* Useful for: Finding the ending point for a range query.
93+
*
94+
* @param point the reference point
95+
* @return the floor point, or null
96+
*/
97+
fun <V> Array2DMap<V>.floorPoint(point: Point): Point? {
98+
val comparator = Array2DMap.POINT_COMPARATOR
99+
return keys.filter { comparator.compare(it, point) <= 0 }
100+
.maxWithOrNull(comparator)
101+
}
102+
103+
/**
104+
* Returns a view of the portion of this map whose points range from [fromPoint] (inclusive)
105+
* to [toPoint] (exclusive).
106+
*
107+
* Useful for: Limiting pathfinding search to a specific rectangular region.
108+
*
109+
* @param fromPoint low endpoint (inclusive) of the points in the returned map
110+
* @param toPoint high endpoint (exclusive) of the points in the returned map
111+
* @return a view of the specified range within this map
112+
*/
113+
fun <V> Array2DMap<V>.subMap(
114+
fromPoint: Point,
115+
toPoint: Point
116+
): Map<Point, V> {
117+
val comparator = Array2DMap.POINT_COMPARATOR
118+
return keys.filter { comparator.compare(it, fromPoint) >= 0 && comparator.compare(it, toPoint) < 0 }
119+
.associateWith { point -> this[point]!! }
120+
}
121+
122+
/**
123+
* Returns a view of the portion of this map whose points are strictly less than [toPoint].
124+
*
125+
* Useful for: Getting all cells before a certain point in grid order.
126+
*
127+
* @param toPoint high endpoint (exclusive) of the points in the returned map
128+
* @return a view of the portion of this map whose points are less than toPoint
129+
*/
130+
fun <V> Array2DMap<V>.headMap(toPoint: Point): Map<Point, V> {
131+
val comparator = Array2DMap.POINT_COMPARATOR
132+
return keys.filter { comparator.compare(it, toPoint) < 0 }
133+
.associateWith { point -> this[point]!! }
134+
}
135+
136+
/**
137+
* Returns a view of the portion of this map whose points are greater than or equal to [fromPoint].
138+
*
139+
* Useful for: Getting all cells from a certain point onwards in grid order.
140+
*
141+
* @param fromPoint low endpoint (inclusive) of the points in the returned map
142+
* @return a view of the portion of this map whose points are greater than or equal to fromPoint
143+
*/
144+
fun <V> Array2DMap<V>.tailMap(fromPoint: Point): Map<Point, V> {
145+
val comparator = Array2DMap.POINT_COMPARATOR
146+
return keys.filter { comparator.compare(it, fromPoint) >= 0 }
147+
.associateWith { point -> this[point]!! }
148+
}
149+
150+
/**
151+
* Returns the 4-connected neighbors (up, down, left, right) of the given point that exist in the map.
152+
*
153+
* Useful for: A* pathfinding, Dijkstra's algorithm - getting adjacent cells to explore.
154+
*
155+
* @param point the center point
156+
* @return sequence of neighboring points that exist in this map
157+
*/
158+
fun <V> Array2DMap<V>.neighbors4(point: Point): Sequence<Point> =
159+
sequence {
160+
val candidates =
161+
listOf(
162+
Point(point.x, point.y - 1), // up
163+
Point(point.x, point.y + 1), // down
164+
Point(point.x - 1, point.y), // left
165+
Point(point.x + 1, point.y) // right
166+
)
167+
for (candidate in candidates) {
168+
if (containsKey(candidate)) {
169+
yield(candidate)
170+
}
171+
}
172+
}
173+
174+
/**
175+
* Returns the 8-connected neighbors (including diagonals) of the given point that exist in the map.
176+
*
177+
* Useful for: Pathfinding with diagonal movement allowed.
178+
*
179+
* @param point the center point
180+
* @return sequence of neighboring points that exist in this map
181+
*/
182+
fun <V> Array2DMap<V>.neighbors8(point: Point): Sequence<Point> =
183+
sequence {
184+
for (dy in -1..1) {
185+
for (dx in -1..1) {
186+
if (dx == 0 && dy == 0) continue // skip self
187+
val candidate = Point(point.x + dx, point.y + dy)
188+
if (containsKey(candidate)) {
189+
yield(candidate)
190+
}
191+
}
192+
}
193+
}
194+
195+
/**
196+
* Returns the 4-connected neighbor entries (up, down, left, right) of the given point.
197+
*
198+
* Useful for: Pathfinding with access to both position and value.
199+
*
200+
* @param point the center point
201+
* @return sequence of neighboring point-value pairs
202+
*/
203+
fun <V> Array2DMap<V>.neighborEntries4(point: Point): Sequence<Pair<Point, V>> =
204+
neighbors4(point).map { p -> p to this[p]!! }
205+
206+
/**
207+
* Returns the 8-connected neighbor entries (including diagonals) of the given point.
208+
*
209+
* Useful for: Pathfinding with diagonal movement and access to values.
210+
*
211+
* @param point the center point
212+
* @return sequence of neighboring point-value pairs
213+
*/
214+
fun <V> Array2DMap<V>.neighborEntries8(point: Point): Sequence<Pair<Point, V>> =
215+
neighbors8(point).map { p -> p to this[p]!! }
216+
217+
/**
218+
* Returns all points within a Manhattan distance of [distance] from the given point.
219+
*
220+
* Useful for: Finding all cells within a certain movement cost (e.g., train reach).
221+
*
222+
* @param point the center point
223+
* @param distance the maximum Manhattan distance
224+
* @return sequence of points within the distance that exist in this map
225+
*/
226+
fun <V> Array2DMap<V>.pointsWithinManhattan(
227+
point: Point,
228+
distance: Int
229+
): Sequence<Point> =
230+
sequence {
231+
for (dy in -distance..distance) {
232+
val remainingDist = distance - kotlin.math.abs(dy)
233+
for (dx in -remainingDist..remainingDist) {
234+
val candidate = Point(point.x + dx, point.y + dy)
235+
if (containsKey(candidate)) {
236+
yield(candidate)
237+
}
238+
}
239+
}
240+
}
241+
242+
/**
243+
* Returns all points in a rectangular region defined by the given bounds.
244+
*
245+
* Useful for: Region-based queries in pathfinding, checking occupancy in an area.
246+
*
247+
* @param minX minimum x coordinate (inclusive)
248+
* @param minY minimum y coordinate (inclusive)
249+
* @param maxX maximum x coordinate (inclusive)
250+
* @param maxY maximum y coordinate (inclusive)
251+
* @return sequence of points in the region that exist in this map
252+
*/
253+
fun <V> Array2DMap<V>.pointsInRegion(
254+
minX: Int,
255+
minY: Int,
256+
maxX: Int,
257+
maxY: Int
258+
): Sequence<Point> {
259+
return keys.asSequence()
260+
.filter { it.x in minX..maxX && it.y in minY..maxY }
261+
}

0 commit comments

Comments
 (0)