Skip to content

Commit

Permalink
front: nge saving node's positions
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion authored and sim51 committed Dec 5, 2024
1 parent c13b177 commit bc52f12
Show file tree
Hide file tree
Showing 5 changed files with 694 additions and 367 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { sortBy } from 'lodash';

import type {
MacroNodeResponse,
ScenarioResponse,
SearchResultItemOperationalPoint,
TrainScheduleResult,
} from 'common/api/osrdEditoastApi';

export type NodeIndex = {
node: MacroNodeResponse & { geocoord?: { lat: number; lng: number } };
saved: boolean;
};

export default class MacroEditorState {
/**
* Storing nodes by path item key
* It's the main storage for node.
* The saved attribut is to know if the data comes from the API
* If the value is a string, it's a key redirection
*/
nodesByPathKey: Record<string, NodeIndex | string>;

/**
* We keep a dictionnary of id/key to be able to find a node by its id
*/
nodesIdToKey: Record<number, string>;

/**
* Storing labels
*/
labels: Set<string>;

/**
* NGE resource
*/
ngeResource = { id: 1, capacity: 0 };

/**
* Default constructor
*/
constructor(
public readonly scenario: ScenarioResponse,
public trainSchedules: TrainScheduleResult[]
) {
// Empty
this.labels = new Set<string>([]);
this.nodesIdToKey = {};
this.nodesByPathKey = {};
this.ngeResource = { id: 1, capacity: trainSchedules.length };
}

/**
* Check if we have duplicates
* Ex: one key is trigram and an other is uic (with the same trigram), we should keep trigram
* What we do :
* - Make a list of key,trigram
* - aggregate on trigram to build a list of key
* - filter if the array is of size 1 (ie, no dedup todo)
* - sort the keys by priority
* - add redirection in the nodesByPathKey
*/
dedupNodes(): void {
const trigramAggreg = Object.entries(this.nodesByPathKey)
.filter(([_, value]) => typeof value !== 'string' && value.node.trigram)
.map(([key, value]) => ({ key, trigram: (value as NodeIndex).node.trigram! }))
.reduce(
(acc, curr) => {
acc[curr.trigram] = [...(acc[curr.trigram] || []), curr.key];
return acc;
},
{} as Record<string, string[]>
);

for (const trig of Object.keys(trigramAggreg)) {
if (trigramAggreg[trig].length < 2) {
delete trigramAggreg[trig];
}
trigramAggreg[trig] = sortBy(trigramAggreg[trig], (key) => {
if (key.startsWith('op_id:')) return 1;
if (key.startsWith('trigram:')) return 2;
if (key.startsWith('uic:')) return 3;
// default
return 4;
});
}

Object.values(trigramAggreg).forEach((mergeList) => {
const mainNodeKey = mergeList[0];
mergeList.slice(1).forEach((key) => {
this.nodesByPathKey[key] = mainNodeKey;
});
});
}

/**
* Store and index the node.
*/
indexNode(node: MacroNodeResponse, saved = false) {
// Remove in the id index, its previous value
const prevNode = this.getNodeByKey(node.path_item_key);
if (prevNode && typeof prevNode !== 'string') {
const prevId = prevNode.node.id;
delete this.nodesIdToKey[prevId];
}

// Index
this.nodesByPathKey[node.path_item_key] = { node, saved };
this.nodesIdToKey[node.id] = node.path_item_key;
node.labels.forEach((l) => {
if (l) this.labels.add(l);
});
}

/**
* Update node's data by its key
*/
updateNodeDataByKey(key: string, data: Partial<NodeIndex['node']>, saved?: boolean) {
const indexedNode = this.getNodeByKey(key);
if (indexedNode) {
this.indexNode(
{ ...indexedNode.node, ...data },
saved === undefined ? indexedNode.saved : saved
);
}
}

/**
* Delete a node by its key
*/
deleteNodeByKey(key: string) {
const indexedNode = this.getNodeByKey(key);
if (indexedNode) {
delete this.nodesIdToKey[indexedNode.node.id];
delete this.nodesByPathKey[key];
}
}

/**
* Get a node by its key.
*/
getNodeByKey(key: string): NodeIndex | null {
let result: NodeIndex | null = null;
let currentKey: string | null = key;
while (currentKey !== null) {
const found: string | NodeIndex | undefined = this.nodesByPathKey[currentKey];
if (typeof found === 'string') {
currentKey = found;
} else {
currentKey = null;
result = found || null;
}
}
return result;
}

/**
* Get a node by its id.
*/
getNodeById(id: number) {
const key = this.nodesIdToKey[id];
return this.getNodeByKey(key);
}

/**
* Given an path step, returns its pathKey
*/
static getPathKey(item: TrainScheduleResult['path'][0]): string {
if ('trigram' in item)
return `trigram:${item.trigram}${item.secondary_code ? `/${item.secondary_code}` : ''}`;
if ('operational_point' in item) return `op_id:${item.operational_point}`;
if ('uic' in item)
return `uic:${item.uic}${item.secondary_code ? `/${item.secondary_code}` : ''}`;

return `track_offset:${item.track}+${item.offset}`;
}

/**
* Given a search result item, returns all possible pathKeys, ordered by weight.
*/
static getPathKeys(item: SearchResultItemOperationalPoint): string[] {
const result = [];
result.push(`op_id:${item.obj_id}`);
result.push(`trigram:${item.trigram}${'ch' in item ? `/${item.ch}` : ''}`);
result.push(`uic:${item.uic}${'ch' in item ? `/${item.ch}` : ''}`);
item.track_sections.forEach((ts) => {
result.push(`track_offset:${ts.track}+${ts.position}`);
});
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { compact, uniq } from 'lodash';

import {
osrdEditoastApi,
type MacroNodeResponse,
type SearchResultItemOperationalPoint,
type TrainScheduleBase,
type TrainScheduleResult,
Expand All @@ -10,7 +11,7 @@ import type { AppDispatch } from 'store';
import { formatToIsoDate } from 'utils/date';
import { calculateTimeDifferenceInSeconds, formatDurationAsISO8601 } from 'utils/timeManipulation';

import nodeStore from './nodeStore';
import type MacroEditorState from './MacroEditorState';
import { DEFAULT_TRAINRUN_FREQUENCIES, DEFAULT_TRAINRUN_FREQUENCY } from './osrdToNge';
import type {
NetzgrafikDto,
Expand Down Expand Up @@ -372,28 +373,152 @@ const handleTrainrunOperation = async ({
}
};

const handleUpdateNode = (timeTableId: number, node: NodeDto) => {
const { betriebspunktName: trigram, positionX, positionY } = node;
nodeStore.set(timeTableId, { trigram, positionX, positionY });
const apiCreateNode = async (
state: MacroEditorState,
dispatch: AppDispatch,
node: Omit<MacroNodeResponse, 'id'>
) => {
try {
const createPromise = dispatch(
osrdEditoastApi.endpoints.postProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodes.initiate(
{
projectId: state.scenario.project.id,
studyId: state.scenario.study_id,
scenarioId: state.scenario.id,
macroNodeForm: node,
}
)
);
const newNode = await createPromise.unwrap();
state.indexNode(newNode, true);
} catch (e) {
console.error(e);
}
};

const apiUpdateNode = async (
state: MacroEditorState,
dispatch: AppDispatch,
node: MacroNodeResponse
) => {
try {
await dispatch(
osrdEditoastApi.endpoints.putProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodesNodeId.initiate(
{
projectId: state.scenario.project.id,
studyId: state.scenario.study_id,
scenarioId: state.scenario.id,
nodeId: node.id,
macroNodeForm: node,
}
)
);
state.indexNode(node, true);
} catch (e) {
console.error(e);
}
};

const apiDeleteNode = async (
state: MacroEditorState,
dispatch: AppDispatch,
node: MacroNodeResponse
) => {
try {
await dispatch(
osrdEditoastApi.endpoints.deleteProjectsByProjectIdStudiesAndStudyIdScenariosScenarioIdMacroNodesNodeId.initiate(
{
projectId: state.scenario.project.id,
studyId: state.scenario.study_id,
scenarioId: state.scenario.id,
nodeId: node.id,
}
)
);
state.deleteNodeByKey(node.path_item_key);
} catch (e) {
console.error(e);
}
};

const handleNodeOperation = ({
/**
* Cast a NGE node to a node.
*/
const castNgeNode = (
node: NetzgrafikDto['nodes'][0],
labels: NetzgrafikDto['labels']
): Omit<MacroNodeResponse, 'path_item_key'> => ({
id: node.id,
trigram: node.betriebspunktName,
full_name: node.fullName,
connection_time: node.connectionTime,
position_x: node.positionX,
position_y: node.positionY,
labels: node.labelIds
.map((id) => {
const ngeLabel = labels.find((e) => e.id === id);
if (ngeLabel) return ngeLabel.label;
return null;
})
.filter((n) => n !== null) as string[],
});

const handleNodeOperation = async ({
state,
type,
node,
timeTableId,
netzgrafikDto,
dispatch,
}: {
state: MacroEditorState;
type: NGEEvent['type'];
node: NodeDto;
timeTableId: number;
netzgrafikDto: NetzgrafikDto;
dispatch: AppDispatch;
}) => {
const indexNode = state.getNodeById(node.id);
switch (type) {
case 'create':
case 'update': {
handleUpdateNode(timeTableId, node);
if (indexNode) {
if (indexNode.saved) {
// Update the key if trigram has changed and key is based on it
let nodeKey = indexNode.node.path_item_key;
if (nodeKey.startsWith('trigram:') && indexNode.node.trigram !== node.betriebspunktName) {
nodeKey = `trigram:${node.betriebspunktName}`;
}
await apiUpdateNode(state, dispatch, {
...indexNode.node,
...castNgeNode(node, netzgrafikDto.labels),
id: indexNode.node.id,
path_item_key: nodeKey,
});
} else {
const newNode = {
...indexNode.node,
...castNgeNode(node, netzgrafikDto.labels),
};
// Create the node
await apiCreateNode(state, dispatch, newNode);
// keep track of the ID given by NGE
state.nodesIdToKey[node.id] = newNode.path_item_key;
}
} else {
// It's an unknown node, we need to create it in the db
// We assume that `betriebspunktName` is a trigram
const key = `trigram:${node.betriebspunktName}`;
// Create the node
await apiCreateNode(state, dispatch, {
...castNgeNode(node, netzgrafikDto.labels),
path_item_key: key,
});
// keep track of the ID given by NGE
state.nodesIdToKey[node.id] = key;
}
break;
}
case 'delete': {
nodeStore.delete(timeTableId, node.betriebspunktName);
if (indexNode) await apiDeleteNode(state, dispatch, indexNode.node);
break;
}
default:
Expand Down Expand Up @@ -442,6 +567,7 @@ const handleLabelOperation = async ({
const handleOperation = async ({
event,
dispatch,
state,
infraId,
timeTableId,
netzgrafikDto,
Expand All @@ -450,6 +576,7 @@ const handleOperation = async ({
}: {
event: NGEEvent;
dispatch: AppDispatch;
state: MacroEditorState;
infraId: number;
timeTableId: number;
netzgrafikDto: NetzgrafikDto;
Expand All @@ -459,7 +586,7 @@ const handleOperation = async ({
const { type } = event;
switch (event.objectType) {
case 'node':
handleNodeOperation({ type, node: event.node, timeTableId });
await handleNodeOperation({ state, dispatch, netzgrafikDto, type, node: event.node });
break;
case 'trainrun': {
await handleTrainrunOperation({
Expand Down
Loading

0 comments on commit bc52f12

Please sign in to comment.