Skip to content

Commit bc52f12

Browse files
emersionsim51
authored andcommitted
front: nge saving node's positions
1 parent c13b177 commit bc52f12

File tree

5 files changed

+694
-367
lines changed

5 files changed

+694
-367
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,
@@ -372,28 +373,152 @@ const handleTrainrunOperation = async ({
372373
}
373374
};
374375

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

380-
const handleNodeOperation = ({
444+
/**
445+
* Cast a NGE node to a node.
446+
*/
447+
const castNgeNode = (
448+
node: NetzgrafikDto['nodes'][0],
449+
labels: NetzgrafikDto['labels']
450+
): Omit<MacroNodeResponse, 'path_item_key'> => ({
451+
id: node.id,
452+
trigram: node.betriebspunktName,
453+
full_name: node.fullName,
454+
connection_time: node.connectionTime,
455+
position_x: node.positionX,
456+
position_y: node.positionY,
457+
labels: node.labelIds
458+
.map((id) => {
459+
const ngeLabel = labels.find((e) => e.id === id);
460+
if (ngeLabel) return ngeLabel.label;
461+
return null;
462+
})
463+
.filter((n) => n !== null) as string[],
464+
});
465+
466+
const handleNodeOperation = async ({
467+
state,
381468
type,
382469
node,
383-
timeTableId,
470+
netzgrafikDto,
471+
dispatch,
384472
}: {
473+
state: MacroEditorState;
385474
type: NGEEvent['type'];
386475
node: NodeDto;
387-
timeTableId: number;
476+
netzgrafikDto: NetzgrafikDto;
477+
dispatch: AppDispatch;
388478
}) => {
479+
const indexNode = state.getNodeById(node.id);
389480
switch (type) {
390481
case 'create':
391482
case 'update': {
392-
handleUpdateNode(timeTableId, node);
483+
if (indexNode) {
484+
if (indexNode.saved) {
485+
// Update the key if trigram has changed and key is based on it
486+
let nodeKey = indexNode.node.path_item_key;
487+
if (nodeKey.startsWith('trigram:') && indexNode.node.trigram !== node.betriebspunktName) {
488+
nodeKey = `trigram:${node.betriebspunktName}`;
489+
}
490+
await apiUpdateNode(state, dispatch, {
491+
...indexNode.node,
492+
...castNgeNode(node, netzgrafikDto.labels),
493+
id: indexNode.node.id,
494+
path_item_key: nodeKey,
495+
});
496+
} else {
497+
const newNode = {
498+
...indexNode.node,
499+
...castNgeNode(node, netzgrafikDto.labels),
500+
};
501+
// Create the node
502+
await apiCreateNode(state, dispatch, newNode);
503+
// keep track of the ID given by NGE
504+
state.nodesIdToKey[node.id] = newNode.path_item_key;
505+
}
506+
} else {
507+
// It's an unknown node, we need to create it in the db
508+
// We assume that `betriebspunktName` is a trigram
509+
const key = `trigram:${node.betriebspunktName}`;
510+
// Create the node
511+
await apiCreateNode(state, dispatch, {
512+
...castNgeNode(node, netzgrafikDto.labels),
513+
path_item_key: key,
514+
});
515+
// keep track of the ID given by NGE
516+
state.nodesIdToKey[node.id] = key;
517+
}
393518
break;
394519
}
395520
case 'delete': {
396-
nodeStore.delete(timeTableId, node.betriebspunktName);
521+
if (indexNode) await apiDeleteNode(state, dispatch, indexNode.node);
397522
break;
398523
}
399524
default:
@@ -442,6 +567,7 @@ const handleLabelOperation = async ({
442567
const handleOperation = async ({
443568
event,
444569
dispatch,
570+
state,
445571
infraId,
446572
timeTableId,
447573
netzgrafikDto,
@@ -450,6 +576,7 @@ const handleOperation = async ({
450576
}: {
451577
event: NGEEvent;
452578
dispatch: AppDispatch;
579+
state: MacroEditorState;
453580
infraId: number;
454581
timeTableId: number;
455582
netzgrafikDto: NetzgrafikDto;
@@ -459,7 +586,7 @@ const handleOperation = async ({
459586
const { type } = event;
460587
switch (event.objectType) {
461588
case 'node':
462-
handleNodeOperation({ type, node: event.node, timeTableId });
589+
await handleNodeOperation({ state, dispatch, netzgrafikDto, type, node: event.node });
463590
break;
464591
case 'trainrun': {
465592
await handleTrainrunOperation({

0 commit comments

Comments
 (0)