Skip to content

Commit 43d6164

Browse files
authored
fix: optimize marker direction when metro router is relegated to fallbackRoute (#5029)
* fix: optimize marker direction when metro router is relegated to fallbackRoute
1 parent cd0f438 commit 43d6164

2 files changed

Lines changed: 155 additions & 44 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { Point } from '../../../src/geometry'
3+
4+
const manhattanMock = vi.hoisted(() => vi.fn())
5+
6+
vi.mock('../../../src/registry/router/manhattan/index', () => ({
7+
manhattan: manhattanMock,
8+
}))
9+
10+
import { metro } from '../../../src/registry/router/metro'
11+
12+
describe('metro router', () => {
13+
beforeEach(() => {
14+
manhattanMock.mockReset()
15+
})
16+
17+
it('should call manhattan with metro defaults', () => {
18+
manhattanMock.mockReturnValue([])
19+
20+
metro.call({} as any, [], {}, {} as any)
21+
22+
expect(manhattanMock).toHaveBeenCalledTimes(1)
23+
const [, options] = manhattanMock.mock.calls[0]
24+
expect(options.maxDirectionChange).toBe(45)
25+
expect(typeof options.fallbackRoute).toBe('function')
26+
expect(typeof options.directions).toBe('function')
27+
})
28+
29+
it('should allow overriding default options', () => {
30+
manhattanMock.mockReturnValue([])
31+
32+
metro.call({} as any, [], { maxDirectionChange: 90 }, {} as any)
33+
34+
const [, options] = manhattanMock.mock.calls[0]
35+
expect(options.maxDirectionChange).toBe(90)
36+
})
37+
38+
it('should generate 8-direction metro directions with diagonal costs', () => {
39+
manhattanMock.mockReturnValue([])
40+
41+
metro.call({} as any, [], { step: 10, cost: 1 } as any, {} as any)
42+
43+
const [, options] = manhattanMock.mock.calls[0]
44+
const dirs = options.directions.call(options)
45+
46+
expect(dirs).toHaveLength(8)
47+
expect(dirs[0]).toMatchObject({ cost: 1, offsetX: 10, offsetY: 0 })
48+
expect(dirs[1]).toMatchObject({ cost: 15, offsetX: 10, offsetY: 10 })
49+
expect(dirs[2]).toMatchObject({ cost: 1, offsetX: 0, offsetY: 10 })
50+
expect(dirs[3]).toMatchObject({ cost: 15, offsetX: -10, offsetY: 10 })
51+
expect(dirs[4]).toMatchObject({ cost: 1, offsetX: -10, offsetY: 0 })
52+
expect(dirs[5]).toMatchObject({ cost: 15, offsetX: -10, offsetY: -10 })
53+
expect(dirs[6]).toMatchObject({ cost: 1, offsetX: 0, offsetY: -10 })
54+
expect(dirs[7]).toMatchObject({ cost: 15, offsetX: 10, offsetY: -10 })
55+
})
56+
57+
it('fallbackRoute should not include from/to points and should set previousDirectionAngle', () => {
58+
manhattanMock.mockReturnValue([])
59+
60+
metro.call({} as any, [], {}, {} as any)
61+
62+
const [, options] = manhattanMock.mock.calls[0]
63+
const fallbackRoute = options.fallbackRoute as any
64+
65+
const resolvedOptions: any = {
66+
directions: Array.from({ length: 8 }, () => ({})),
67+
previousDirectionAngle: null,
68+
}
69+
70+
const from = new Point(0, 0)
71+
const to = new Point(10, 0)
72+
const route: Point[] = fallbackRoute(from, to, resolvedOptions)
73+
74+
expect(route.some((p) => p.equals(from))).toBe(false)
75+
expect(route.some((p) => p.equals(to))).toBe(false)
76+
expect(typeof resolvedOptions.previousDirectionAngle).toBe('number')
77+
expect(resolvedOptions.previousDirectionAngle % 45).toBe(0)
78+
})
79+
80+
it('fallbackRoute should return an intermediate point when lines intersect', () => {
81+
manhattanMock.mockReturnValue([])
82+
83+
metro.call({} as any, [], {}, {} as any)
84+
85+
const [, options] = manhattanMock.mock.calls[0]
86+
const fallbackRoute = options.fallbackRoute as any
87+
88+
const resolvedOptions: any = {
89+
directions: Array.from({ length: 8 }, () => ({})),
90+
previousDirectionAngle: null,
91+
}
92+
93+
const from = new Point(0, 0)
94+
const to = new Point(100, 80)
95+
const route: Point[] = fallbackRoute(from, to, resolvedOptions)
96+
97+
expect(route.length).toBeLessThanOrEqual(1)
98+
if (route.length === 1) {
99+
expect(route[0].equals(from)).toBe(false)
100+
expect(route[0].equals(to)).toBe(false)
101+
}
102+
expect(typeof resolvedOptions.previousDirectionAngle).toBe('number')
103+
expect(resolvedOptions.previousDirectionAngle % 45).toBe(0)
104+
})
105+
})

src/registry/router/metro.ts

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { FunctionExt } from '../../common'
2-
import { toRad, normalize, Line, Point } from '../../geometry'
2+
import { Line, normalize, Point, toRad } from '../../geometry'
33
import type { RouterDefinition } from './index'
44
import { manhattan } from './manhattan/index'
5-
import { type ManhattanRouterOptions, resolve } from './manhattan/options'
5+
import {
6+
type ManhattanRouterOptions,
7+
type ResolvedOptions,
8+
resolve,
9+
} from './manhattan/options'
610

711
export interface MetroRouterOptions extends ManhattanRouterOptions {}
812

@@ -28,49 +32,51 @@ const defaults: Partial<MetroRouterOptions> = {
2832
]
2933
},
3034

31-
// a simple route used in situations when main routing method fails
32-
// (exceed max number of loop iterations, inaccessible)
33-
fallbackRoute(from, to, options) {
34-
// Find a route which breaks by 45 degrees ignoring all obstacles.
35-
36-
const theta = from.theta(to)
37-
38-
const route = []
39-
40-
let a = { x: to.x, y: from.y }
41-
let b = { x: from.x, y: to.y }
42-
43-
if (theta % 180 > 90) {
44-
const t = a
45-
a = b
46-
b = t
47-
}
48-
49-
const p1 = theta % 90 < 45 ? a : b
50-
const l1 = new Line(from, p1)
51-
52-
const alpha = 90 * Math.ceil(theta / 90)
53-
54-
const p2 = Point.fromPolar(l1.squaredLength(), toRad(alpha + 135), p1)
55-
const l2 = new Line(to, p2)
56-
57-
const intersectionPoint = l1.intersectsWithLine(l2)
58-
const point = intersectionPoint || to
59-
60-
const directionFrom = intersectionPoint ? point : from
61-
62-
const quadrant = 360 / options.directions.length
63-
const angleTheta = directionFrom.theta(to)
64-
const normalizedAngle = normalize(angleTheta + quadrant / 2)
65-
const directionAngle = quadrant * Math.floor(normalizedAngle / quadrant)
66-
67-
options.previousDirectionAngle = directionAngle
68-
69-
if (point) route.push(point.round())
70-
route.push(to)
35+
fallbackRoute: metroFallbackRoute,
36+
}
7137

72-
return route
73-
},
38+
function metroFallbackRoute(
39+
this: any,
40+
from: Point,
41+
to: Point,
42+
options: ResolvedOptions,
43+
) {
44+
const theta = from.theta(to)
45+
const route: Point[] = []
46+
47+
let a = { x: to.x, y: from.y }
48+
let b = { x: from.x, y: to.y }
49+
50+
if (theta % 180 > 90) {
51+
const t = a
52+
a = b
53+
b = t
54+
}
55+
56+
const p1 = theta % 90 < 45 ? a : b
57+
const l1 = new Line(from, p1)
58+
59+
const alpha = 90 * Math.ceil(theta / 90)
60+
61+
const p2 = Point.fromPolar(l1.squaredLength(), toRad(alpha + 135), p1)
62+
const l2 = new Line(to, p2)
63+
64+
const intersectionPoint = l1.intersectsWithLine(l2)
65+
const directionFrom = intersectionPoint || from
66+
const quadrant = 360 / options.directions.length
67+
const angleTheta = directionFrom.theta(to)
68+
const normalizedAngle = normalize(angleTheta + quadrant / 2)
69+
const directionAngle = quadrant * Math.floor(normalizedAngle / quadrant)
70+
options.previousDirectionAngle = directionAngle
71+
if (
72+
intersectionPoint &&
73+
!intersectionPoint.equals(from) &&
74+
!intersectionPoint.equals(to)
75+
) {
76+
route.push(intersectionPoint.round())
77+
}
78+
79+
return route
7480
}
7581

7682
export const metro: RouterDefinition<Partial<MetroRouterOptions>> = function (

0 commit comments

Comments
 (0)