-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathQueryStore.ts
More file actions
494 lines (445 loc) · 17.4 KB
/
QueryStore.ts
File metadata and controls
494 lines (445 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
import { coordinateToText, metersToText } from '@/Converters'
import Api, { ApiImpl } from '@/api/Api'
import Store from '@/stores/Store'
import Dispatcher, { Action } from '@/stores/Dispatcher'
import {
AddPoint,
ClearPoints,
ErrorAction,
InfoReceived,
InvalidatePoint,
MovePoint,
RemovePoint,
RouteRequestFailed,
RouteRequestSuccess,
SetCustomModel,
SetCustomModelEnabled,
SetPoint,
SetQueryPoints,
SetVehicleProfile,
} from '@/actions/Actions'
import { Bbox, RoutingArgs, RoutingProfile } from '@/api/graphhopper'
import { calcDist } from '@/distUtils'
import config from 'config'
import { customModel2prettyString, customModelExamples } from '@/sidebar/CustomModelExamples'
export interface Coordinate {
lat: number
lng: number
}
export function getBBoxFromCoord(c: Coordinate, offset: number = 0.005): Bbox {
return [c.lng - offset, c.lat - offset, c.lng + offset, c.lat + offset]
}
export interface QueryStoreState {
readonly profiles: RoutingProfile[]
readonly queryPoints: QueryPoint[]
readonly nextQueryPointId: number
readonly currentRequest: CurrentRequest
readonly maxAlternativeRoutes: number
readonly routingProfile: RoutingProfile
readonly customModelEnabled: boolean
readonly customModelStr: string
}
export interface QueryPoint {
readonly coordinate: Coordinate
readonly queryText: string
readonly streetName: string
readonly isInitialized: boolean
readonly color: string
readonly id: number
readonly type: QueryPointType
}
export interface CustomModel {
readonly speed?: object[]
readonly priority?: object[]
readonly distance_influence?: number
readonly areas?: object
}
export enum QueryPointType {
From,
To,
Via,
}
export interface CurrentRequest {
subRequests: SubRequest[]
}
export enum RequestState {
SENT,
SUCCESS,
FAILED,
}
export interface SubRequest {
readonly args: RoutingArgs
readonly state: RequestState
}
export default class QueryStore extends Store<QueryStoreState> {
private readonly api: Api
constructor(api: Api, initialCustomModelStr: string | null = null) {
super(QueryStore.getInitialState(initialCustomModelStr))
this.api = api
}
private static getInitialState(initialCustomModelStr: string | null): QueryStoreState {
const customModelEnabledInitially = initialCustomModelStr != null
if (!initialCustomModelStr)
initialCustomModelStr = customModel2prettyString(customModelExamples['default_example'])
// prettify the custom model if it can be parsed or leave it as is otherwise
try {
initialCustomModelStr = customModel2prettyString(JSON.parse(initialCustomModelStr))
} catch (e) {}
return {
profiles: [],
queryPoints: [
QueryStore.getEmptyPoint(0, QueryPointType.From),
QueryStore.getEmptyPoint(1, QueryPointType.To),
],
nextQueryPointId: 2,
currentRequest: {
subRequests: [],
},
maxAlternativeRoutes: 3,
routingProfile: {
name: '',
},
customModelEnabled: customModelEnabledInitially,
customModelStr: initialCustomModelStr,
}
}
reduce(state: QueryStoreState, action: Action): QueryStoreState {
if (action instanceof InvalidatePoint) {
const points = QueryStore.replacePoint(state.queryPoints, {
...action.point,
isInitialized: false,
})
return {
...state,
queryPoints: points,
}
} else if (action instanceof ClearPoints) {
const newPoints = state.queryPoints.map(point => {
return {
...point,
queryText: '',
point: { lat: 0, lng: 0 },
isInitialized: false,
}
})
return {
...state,
queryPoints: newPoints,
}
} else if (action instanceof SetPoint) {
const newState: QueryStoreState = {
...state,
queryPoints: QueryStore.replacePoint(state.queryPoints, action.point),
}
return this.routeIfReady(newState, action.zoomResponse)
} else if (action instanceof MovePoint) {
// Remove and Add in one action but with only one route request
const newPoints = QueryStore.movePoint(state.queryPoints, action.point, action.newIndex).map(
(point, index) => {
const type = QueryStore.getPointType(index, state.queryPoints.length)
return {
...point,
color: QueryStore.getMarkerColor(type),
type: type,
id: this.state.nextQueryPointId + index,
}
}
)
const newState = {
...state,
nextQueryPointId: state.nextQueryPointId + state.queryPoints.length,
queryPoints: newPoints,
}
return this.routeIfReady(newState, false)
} else if (action instanceof AddPoint) {
const tmp = state.queryPoints.slice()
const queryText = action.isInitialized ? coordinateToText(action.coordinate) : ''
// add new point at the desired index
tmp.splice(action.atIndex, 0, {
coordinate: action.coordinate,
id: state.nextQueryPointId,
queryText: queryText,
streetName: '',
color: '',
isInitialized: action.isInitialized,
type: QueryPointType.Via,
})
// determine colors for each point. I guess this could be smarter if this needs to be faster
const newPoints = tmp.map((point, i) => {
const type = QueryStore.getPointType(i, tmp.length)
return { ...point, color: QueryStore.getMarkerColor(type), type: type }
})
const newState: QueryStoreState = {
...state,
nextQueryPointId: state.nextQueryPointId + 1,
queryPoints: newPoints,
}
return this.routeIfReady(newState, action.zoom)
} else if (action instanceof SetQueryPoints) {
// make sure that some things are set correctly, regardless of what was passed in here.
const queryPoints = action.queryPoints.map((point, i) => {
const type = QueryStore.getPointType(i, action.queryPoints.length)
const queryText =
point.isInitialized && !point.queryText ? coordinateToText(point.coordinate) : point.queryText
return {
...point,
id: state.nextQueryPointId + i,
type: type,
color: QueryStore.getMarkerColor(type),
queryText: queryText,
}
})
// make sure there are always at least two input boxes
while (queryPoints.length < 2) {
const type = QueryStore.getPointType(queryPoints.length, 2)
queryPoints.push({
id: queryPoints.length,
type: type,
color: QueryStore.getMarkerColor(type),
streetName: '',
queryText: '',
isInitialized: false,
coordinate: { lat: 0, lng: 0 },
})
}
const nextId = state.nextQueryPointId + queryPoints.length
return this.routeIfReady(
{
...state,
queryPoints: queryPoints,
nextQueryPointId: nextId,
},
true
)
} else if (action instanceof RemovePoint) {
const newPoints = state.queryPoints
.filter(point => point.id !== action.point.id)
.map((point, i) => {
const type = QueryStore.getPointType(i, state.queryPoints.length - 1)
return { ...point, color: QueryStore.getMarkerColor(type), type: type }
})
const newState: QueryStoreState = {
...state,
queryPoints: newPoints,
}
return this.routeIfReady(newState, false)
} else if (action instanceof InfoReceived) {
// Do nothing if no routing profiles were received
if (action.result.profiles.length <= 0) return state
// if there are profiles defined in the config file use them, otherwise use the profiles from /info
const profiles: RoutingProfile[] = config.profiles
? Object.keys(config.profiles).map(profile => ({ name: profile }))
: action.result.profiles
// if a routing profile was in the url keep it, otherwise select the first entry as default profile
const profile = state.routingProfile.name ? state.routingProfile : profiles[0]
return this.routeIfReady(
{
...state,
profiles,
routingProfile: profile,
},
true
)
} else if (action instanceof SetVehicleProfile) {
const newState: QueryStoreState = {
...state,
routingProfile: action.profile,
}
return this.routeIfReady(newState, true)
} else if (action instanceof SetCustomModel) {
const newState = {
...state,
customModelStr: action.customModelStr,
}
return action.issueRoutingRequest ? this.routeIfReady(newState, true) : newState
} else if (action instanceof RouteRequestSuccess || action instanceof RouteRequestFailed) {
return QueryStore.handleFinishedRequest(state, action)
} else if (action instanceof SetCustomModelEnabled) {
const newState: QueryStoreState = {
...state,
customModelEnabled: action.enabled,
}
return this.routeIfReady(newState, true)
}
return state
}
private static handleFinishedRequest(
state: QueryStoreState,
action: RouteRequestSuccess | RouteRequestFailed
): QueryStoreState {
const newState = action instanceof RouteRequestSuccess ? RequestState.SUCCESS : RequestState.FAILED
const newSubrequests = QueryStore.replaceSubRequest(state.currentRequest.subRequests, action.request, newState)
return {
...state,
currentRequest: {
subRequests: newSubrequests,
},
}
}
private routeIfReady(state: QueryStoreState, zoom: boolean): QueryStoreState {
if (QueryStore.isReadyToRoute(state)) {
let requests
const maxDistance = getMaxDistance(state.queryPoints)
if (state.customModelEnabled) {
if (maxDistance < 200_000) {
// Use a single request, possibly including alternatives when custom models are enabled.
requests = [QueryStore.buildRouteRequest(state)]
} else if (maxDistance < 500_000) {
// Force no alternatives for longer custom model routes.
requests = [
QueryStore.buildRouteRequest({
...state,
maxAlternativeRoutes: 1,
}),
]
} else {
// Custom model requests with large distances take too long, so we just error.
// later: better usability if we just remove ch.disable? i.e. the request always succeeds
Dispatcher.dispatch(
new ErrorAction(
'Using the custom model feature is unfortunately not ' +
'possible when the request points are further than ' +
// todo: use settings#showDistanceInMiles, but not sure how to use state from another store here
metersToText(500_000, false) +
' apart.'
)
)
return state
}
} else {
requests = [
// We first send a fast request without alternatives ...
QueryStore.buildRouteRequest({
...state,
maxAlternativeRoutes: 1,
}),
]
// ... and then a second, slower request including alternatives if they are enabled.
if (
state.queryPoints.length === 2 &&
state.maxAlternativeRoutes > 1 &&
((ApiImpl.isMotorVehicle(state.routingProfile.name) && maxDistance < 7_000_000) ||
maxDistance < 500_000)
)
requests.push(QueryStore.buildRouteRequest(state))
}
return {
...state,
currentRequest: { subRequests: this.send(requests, zoom) },
}
}
return state
}
private send(args: RoutingArgs[], zoom: boolean) {
const subRequests = args.map(arg => {
return {
args: arg,
state: RequestState.SENT,
}
})
subRequests.forEach((subRequest, i) => this.api.routeWithDispatch(subRequest.args, i == 0 ? zoom : false))
return subRequests
}
private static isReadyToRoute(state: QueryStoreState) {
if (state.customModelEnabled)
try {
JSON.parse(state.customModelStr)
} catch {
return false
}
// Janek deliberately chose this style of if statements, to make this readable.
if (state.queryPoints.length <= 1) return false
if (!state.queryPoints.every(point => point.isInitialized)) return false
if (!state.routingProfile.name) return false
return true
}
private static movePoint(points: QueryPoint[], point: QueryPoint, newIndex: number): QueryPoint[] {
if (newIndex < 0) return points
const newPoints = points.filter((p, index) => {
if (p.id == point.id) {
if (index < newIndex) newIndex-- // index adjustment is important
return false
}
return true
})
if (newIndex >= points.length) return points
newPoints.splice(newIndex, 0, point)
return newPoints
}
private static replacePoint(points: QueryPoint[], point: QueryPoint) {
return replace(
points,
p => p.id === point.id,
() => point
)
}
private static replaceSubRequest(subRequests: SubRequest[], args: RoutingArgs, state: RequestState) {
return replace(
subRequests,
r => r.args === args,
r => {
return { ...r, state }
}
)
}
public static getMarkerColor(type: QueryPointType) {
switch (type) {
case QueryPointType.From:
return '#7cb342'
case QueryPointType.To:
return '#F97777'
default:
return '#76D0F7'
}
}
private static getPointType(index: number, numberOfPoints: number) {
if (index === 0) return QueryPointType.From
if (index === numberOfPoints - 1) return QueryPointType.To
return QueryPointType.Via
}
private static buildRouteRequest(state: QueryStoreState): RoutingArgs {
const coordinates = state.queryPoints.map(point => [point.coordinate.lng, point.coordinate.lat]) as [
number,
number
][]
let customModel = null
if (state.customModelEnabled)
try {
customModel = JSON.parse(state.customModelStr)
} catch {}
return {
points: coordinates,
pointHints: state.queryPoints.map(point => point.streetName),
profile: state.routingProfile.name,
maxAlternativeRoutes: state.maxAlternativeRoutes,
customModel: customModel,
}
}
private static getEmptyPoint(id: number, type: QueryPointType): QueryPoint {
return {
isInitialized: false,
queryText: '',
streetName: '',
coordinate: { lat: 0, lng: 0 },
id: id,
color: QueryStore.getMarkerColor(type),
type: type,
}
}
}
function replace<T>(array: T[], compare: { (element: T): boolean }, provider: { (element: T): T }) {
const result = []
for (const element of array) {
if (compare(element)) result.push(provider(element))
else result.push(element)
}
return result
}
function getMaxDistance(queryPoints: QueryPoint[]): number {
let max = 0
for (let idx = 1; idx < queryPoints.length; idx++) {
const dist = calcDist(queryPoints[idx - 1].coordinate, queryPoints[idx].coordinate)
max = Math.max(dist, max)
}
return max
}