Skip to content

Commit 3c6565b

Browse files
emersionsim51
authored andcommitted
front: nge saving node's positions
Signed-off-by: Benoit Simard <[email protected]>
1 parent b3dfa2f commit 3c6565b

File tree

5 files changed

+692
-365
lines changed

5 files changed

+692
-365
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { sortBy } from 'lodash';
2+
3+
import type {
4+
MacroNodeResponse,
5+
ScenarioResponse,
6+
SearchResultItemOperationalPoint,
7+
TrainScheduleResult,
8+
} from 'common/api/osrdEditoastApi';
9+
10+
export type NodeIndex = {
11+
node: MacroNodeResponse & { geocoord?: { lat: number; lng: number } };
12+
saved: boolean;
13+
};
14+
15+
export default class MacroEditorState {
16+
/**
17+
* Storing nodes by path item key
18+
* It's the main storage for node.
19+
* The saved attribut is to know if the data comes from the API
20+
* If the value is a string, it's a key redirection
21+
*/
22+
nodesByPathKey: Record<string, NodeIndex | string>;
23+
24+
/**
25+
* We keep a dictionnary of id/key to be able to find a node by its id
26+
*/
27+
nodesIdToKey: Record<number, string>;
28+
29+
/**
30+
* Storing labels
31+
*/
32+
labels: Set<string>;
33+
34+
/**
35+
* NGE resource
36+
*/
37+
ngeResource = { id: 1, capacity: 0 };
38+
39+
/**
40+
* Default constructor
41+
*/
42+
constructor(
43+
public readonly scenario: ScenarioResponse,
44+
public trainSchedules: TrainScheduleResult[]
45+
) {
46+
// Empty
47+
this.labels = new Set<string>([]);
48+
this.nodesIdToKey = {};
49+
this.nodesByPathKey = {};
50+
this.ngeResource = { id: 1, capacity: trainSchedules.length };
51+
}
52+
53+
/**
54+
* Check if we have duplicates
55+
* Ex: one key is trigram and an other is uic (with the same trigram), we should keep trigram
56+
* What we do :
57+
* - Make a list of key,trigram
58+
* - aggregate on trigram to build a list of key
59+
* - filter if the array is of size 1 (ie, no dedup todo)
60+
* - sort the keys by priority
61+
* - add redirection in the nodesByPathKey
62+
*/
63+
dedupNodes(): void {
64+
const trigramAggreg = Object.entries(this.nodesByPathKey)
65+
.filter(([_, value]) => typeof value !== 'string' && value.node.trigram)
66+
.map(([key, value]) => ({ key, trigram: (value as NodeIndex).node.trigram! }))
67+
.reduce(
68+
(acc, curr) => {
69+
acc[curr.trigram] = [...(acc[curr.trigram] || []), curr.key];
70+
return acc;
71+
},
72+
{} as Record<string, string[]>
73+
);
74+
75+
for (const trig of Object.keys(trigramAggreg)) {
76+
if (trigramAggreg[trig].length < 2) {
77+
delete trigramAggreg[trig];
78+
}
79+
trigramAggreg[trig] = sortBy(trigramAggreg[trig], (key) => {
80+
if (key.startsWith('op_id:')) return 1;
81+
if (key.startsWith('trigram:')) return 2;
82+
if (key.startsWith('uic:')) return 3;
83+
// default
84+
return 4;
85+
});
86+
}
87+
88+
Object.values(trigramAggreg).forEach((mergeList) => {
89+
const mainNodeKey = mergeList[0];
90+
mergeList.slice(1).forEach((key) => {
91+
this.nodesByPathKey[key] = mainNodeKey;
92+
});
93+
});
94+
}
95+
96+
/**
97+
* Store and index the node.
98+
*/
99+
indexNode(node: MacroNodeResponse, saved = false) {
100+
// Remove in the id index, its previous value
101+
const prevNode = this.getNodeByKey(node.path_item_key);
102+
if (prevNode && typeof prevNode !== 'string') {
103+
const prevId = prevNode.node.id;
104+
delete this.nodesIdToKey[prevId];
105+
}
106+
107+
// Index
108+
this.nodesByPathKey[node.path_item_key] = { node, saved };
109+
this.nodesIdToKey[node.id] = node.path_item_key;
110+
node.labels.forEach((l) => {
111+
if (l) this.labels.add(l);
112+
});
113+
}
114+
115+
/**
116+
* Update node's data by its key
117+
*/
118+
updateNodeDataByKey(key: string, data: Partial<NodeIndex['node']>, saved?: boolean) {
119+
const indexedNode = this.getNodeByKey(key);
120+
if (indexedNode) {
121+
this.indexNode(
122+
{ ...indexedNode.node, ...data },
123+
saved === undefined ? indexedNode.saved : saved
124+
);
125+
}
126+
}
127+
128+
/**
129+
* Delete a node by its key
130+
*/
131+
deleteNodeByKey(key: string) {
132+
const indexedNode = this.getNodeByKey(key);
133+
if (indexedNode) {
134+
delete this.nodesIdToKey[indexedNode.node.id];
135+
delete this.nodesByPathKey[key];
136+
}
137+
}
138+
139+
/**
140+
* Get a node by its key.
141+
*/
142+
getNodeByKey(key: string): NodeIndex | null {
143+
let result: NodeIndex | null = null;
144+
let currentKey: string | null = key;
145+
while (currentKey !== null) {
146+
const found: string | NodeIndex | undefined = this.nodesByPathKey[currentKey];
147+
if (typeof found === 'string') {
148+
currentKey = found;
149+
} else {
150+
currentKey = null;
151+
result = found || null;
152+
}
153+
}
154+
return result;
155+
}
156+
157+
/**
158+
* Get a node by its id.
159+
*/
160+
getNodeById(id: number) {
161+
const key = this.nodesIdToKey[id];
162+
return this.getNodeByKey(key);
163+
}
164+
165+
/**
166+
* Given an path step, returns its pathKey
167+
*/
168+
static getPathKey(item: TrainScheduleResult['path'][0]): string {
169+
if ('trigram' in item)
170+
return `trigram:${item.trigram}${item.secondary_code ? `/${item.secondary_code}` : ''}`;
171+
if ('operational_point' in item) return `op_id:${item.operational_point}`;
172+
if ('uic' in item)
173+
return `uic:${item.uic}${item.secondary_code ? `/${item.secondary_code}` : ''}`;
174+
175+
return `track_offset:${item.track}+${item.offset}`;
176+
}
177+
178+
/**
179+
* Given a search result item, returns all possible pathKeys, ordered by weight.
180+
*/
181+
static getPathKeys(item: SearchResultItemOperationalPoint): string[] {
182+
const result = [];
183+
result.push(`op_id:${item.obj_id}`);
184+
result.push(`trigram:${item.trigram}${'ch' in item ? `/${item.ch}` : ''}`);
185+
result.push(`uic:${item.uic}${'ch' in item ? `/${item.ch}` : ''}`);
186+
item.track_sections.forEach((ts) => {
187+
result.push(`track_offset:${ts.track}+${ts.position}`);
188+
});
189+
return result;
190+
}
191+
}

front/src/applications/operationalStudies/components/MacroEditor/ngeToOsrd.ts

+137-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { compact, uniq } from 'lodash';
22

33
import {
44
osrdEditoastApi,
5+
type MacroNodeResponse,
56
type SearchResultItemOperationalPoint,
67
type TrainScheduleBase,
78
type TrainScheduleResult,
@@ -10,7 +11,7 @@ import type { AppDispatch } from 'store';
1011
import { formatToIsoDate } from 'utils/date';
1112
import { calculateTimeDifferenceInSeconds, formatDurationAsISO8601 } from 'utils/timeManipulation';
1213

13-
import nodeStore from './nodeStore';
14+
import type MacroEditorState from './MacroEditorState';
1415
import { DEFAULT_TRAINRUN_FREQUENCIES, DEFAULT_TRAINRUN_FREQUENCY } from './osrdToNge';
1516
import type {
1617
NetzgrafikDto,
@@ -375,28 +376,152 @@ const handleTrainrunOperation = async ({
375376
}
376377
};
377378

378-
const handleUpdateNode = (timeTableId: number, node: NodeDto) => {
379-
const { betriebspunktName: trigram, positionX, positionY } = node;
380-
nodeStore.set(timeTableId, { trigram, positionX, positionY });
379+
const apiCreateNode = async (
380+
state: MacroEditorState,
381+
dispatch: AppDispatch,
382+
node: Omit<MacroNodeResponse, 'id'>
383+
) => {
384+
try {
385+
const createPromise = dispatch(
386+
osrdEditoastApi.endpoints.postProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodes.initiate(
387+
{
388+
projectId: state.scenario.project.id,
389+
studyId: state.scenario.study_id,
390+
scenarioId: state.scenario.id,
391+
macroNodeForm: node,
392+
}
393+
)
394+
);
395+
const newNode = await createPromise.unwrap();
396+
state.indexNode(newNode, true);
397+
} catch (e) {
398+
console.error(e);
399+
}
400+
};
401+
402+
const apiUpdateNode = async (
403+
state: MacroEditorState,
404+
dispatch: AppDispatch,
405+
node: MacroNodeResponse
406+
) => {
407+
try {
408+
await dispatch(
409+
osrdEditoastApi.endpoints.putProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodesNodeId.initiate(
410+
{
411+
projectId: state.scenario.project.id,
412+
studyId: state.scenario.study_id,
413+
scenarioId: state.scenario.id,
414+
nodeId: node.id,
415+
macroNodeForm: node,
416+
}
417+
)
418+
);
419+
state.indexNode(node, true);
420+
} catch (e) {
421+
console.error(e);
422+
}
423+
};
424+
425+
const apiDeleteNode = async (
426+
state: MacroEditorState,
427+
dispatch: AppDispatch,
428+
node: MacroNodeResponse
429+
) => {
430+
try {
431+
await dispatch(
432+
osrdEditoastApi.endpoints.deleteProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodesNodeId.initiate(
433+
{
434+
projectId: state.scenario.project.id,
435+
studyId: state.scenario.study_id,
436+
scenarioId: state.scenario.id,
437+
nodeId: node.id,
438+
}
439+
)
440+
);
441+
state.deleteNodeByKey(node.path_item_key);
442+
} catch (e) {
443+
console.error(e);
444+
}
381445
};
382446

383-
const handleNodeOperation = ({
447+
/**
448+
* Cast a NGE node to a node.
449+
*/
450+
const castNgeNode = (
451+
node: NetzgrafikDto['nodes'][0],
452+
labels: NetzgrafikDto['labels']
453+
): Omit<MacroNodeResponse, 'path_item_key'> => ({
454+
id: node.id,
455+
trigram: node.betriebspunktName,
456+
full_name: node.fullName,
457+
connection_time: node.connectionTime,
458+
position_x: node.positionX,
459+
position_y: node.positionY,
460+
labels: node.labelIds
461+
.map((id) => {
462+
const ngeLabel = labels.find((e) => e.id === id);
463+
if (ngeLabel) return ngeLabel.label;
464+
return null;
465+
})
466+
.filter((n) => n !== null) as string[],
467+
});
468+
469+
const handleNodeOperation = async ({
470+
state,
384471
type,
385472
node,
386-
timeTableId,
473+
netzgrafikDto,
474+
dispatch,
387475
}: {
476+
state: MacroEditorState;
388477
type: NGEEvent['type'];
389478
node: NodeDto;
390-
timeTableId: number;
479+
netzgrafikDto: NetzgrafikDto;
480+
dispatch: AppDispatch;
391481
}) => {
482+
const indexNode = state.getNodeById(node.id);
392483
switch (type) {
393484
case 'create':
394485
case 'update': {
395-
handleUpdateNode(timeTableId, node);
486+
if (indexNode) {
487+
if (indexNode.saved) {
488+
// Update the key if trigram has changed and key is based on it
489+
let nodeKey = indexNode.node.path_item_key;
490+
if (nodeKey.startsWith('trigram:') && indexNode.node.trigram !== node.betriebspunktName) {
491+
nodeKey = `trigram:${node.betriebspunktName}`;
492+
}
493+
await apiUpdateNode(state, dispatch, {
494+
...indexNode.node,
495+
...castNgeNode(node, netzgrafikDto.labels),
496+
id: indexNode.node.id,
497+
path_item_key: nodeKey,
498+
});
499+
} else {
500+
const newNode = {
501+
...indexNode.node,
502+
...castNgeNode(node, netzgrafikDto.labels),
503+
};
504+
// Create the node
505+
await apiCreateNode(state, dispatch, newNode);
506+
// keep track of the ID given by NGE
507+
state.nodesIdToKey[node.id] = newNode.path_item_key;
508+
}
509+
} else {
510+
// It's an unknown node, we need to create it in the db
511+
// We assume that `betriebspunktName` is a trigram
512+
const key = `trigram:${node.betriebspunktName}`;
513+
// Create the node
514+
await apiCreateNode(state, dispatch, {
515+
...castNgeNode(node, netzgrafikDto.labels),
516+
path_item_key: key,
517+
});
518+
// keep track of the ID given by NGE
519+
state.nodesIdToKey[node.id] = key;
520+
}
396521
break;
397522
}
398523
case 'delete': {
399-
nodeStore.delete(timeTableId, node.betriebspunktName);
524+
if (indexNode) await apiDeleteNode(state, dispatch, indexNode.node);
400525
break;
401526
}
402527
default:
@@ -445,6 +570,7 @@ const handleLabelOperation = async ({
445570
const handleOperation = async ({
446571
event,
447572
dispatch,
573+
state,
448574
infraId,
449575
timeTableId,
450576
netzgrafikDto,
@@ -453,6 +579,7 @@ const handleOperation = async ({
453579
}: {
454580
event: NGEEvent;
455581
dispatch: AppDispatch;
582+
state: MacroEditorState;
456583
infraId: number;
457584
timeTableId: number;
458585
netzgrafikDto: NetzgrafikDto;
@@ -462,7 +589,7 @@ const handleOperation = async ({
462589
const { type } = event;
463590
switch (event.objectType) {
464591
case 'node':
465-
handleNodeOperation({ type, node: event.node, timeTableId });
592+
await handleNodeOperation({ state, dispatch, netzgrafikDto, type, node: event.node });
466593
break;
467594
case 'trainrun': {
468595
await handleTrainrunOperation({

0 commit comments

Comments
 (0)