Skip to content

Commit

Permalink
front: nge saving node's positions
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Ser <contact@emersion.fr>
  • Loading branch information
emersion authored and sim51 committed Dec 6, 2024
1 parent a044bd9 commit 9fefabc
Show file tree
Hide file tree
Showing 6 changed files with 730 additions and 372 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { sortBy } from 'lodash';

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

export type NodeIndexed = Omit<MacroNodeResponse, 'id'> & {
ngeId: number;
dbId?: number;
geocoord?: { lat: number; lng: number };
};

export default class MacroEditorState {
/**
* Scenario data
*/
scenario: ScenarioResponse;

/**
* Train schedules
*/
trainSchedules: TrainScheduleResult[];

/**
* Nodes storage
* Type null is here due to deletion, to avoid recomputing indices.
* We are not building a db engine, so we can afford to have some null values.
*/
nodes: Array<NodeIndexed | null> = [];

/**
* Given a path key, returns the node index in the nodes storage.
*/
indexByPathKey: Record<string, number>;

/**
* Given a nge ID, returns the node index in the nodes storage.
*/
indexByNgeId: Record<string, number>;

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

/**
* NGE resource
*/
ngeResource: { id: number; capacity: number };

/**
* Default constructor
*/
constructor(scenario: ScenarioResponse, trainSchedules: TrainScheduleResult[]) {
// Empty
this.labels = new Set<string>([]);
this.nodes = [];
this.indexByPathKey = {};
this.indexByNgeId = {};
this.scenario = scenario;
this.trainSchedules = trainSchedules;
this.ngeResource = { id: 1, capacity: this.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.indexByPathKey)
.map(([_, indexInStorage]) => {
const node = this.nodes[indexInStorage];
return node ? { key: node.path_item_key, trigram: node.trigram } : null;
})
.filter((i) => i !== null && i.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];
const mainNodeIndex = this.indexByPathKey[mainNodeKey];
mergeList.slice(1).forEach((key) => {
// Delete the node
const nodeIndexInStorage = this.indexByPathKey[key];
this.deleteByIndexStorage(nodeIndexInStorage);
// Update the indices to redirect to the main one
this.indexByPathKey[key] = mainNodeIndex;
});
});
}

/**
* Store and index the node.
*/
indexNodeByKey(key: string, node: NodeIndexed) {
let nodeIndexInStorage = this.indexByPathKey[key];
if (nodeIndexInStorage !== undefined) {
// if there is previous value, we clean the indices
const prevNode = this.nodes[nodeIndexInStorage]!;
delete this.indexByNgeId[prevNode.ngeId];
delete this.indexByPathKey[key];
// replace the node
this.nodes[nodeIndexInStorage] = node;
} else {
// we add the new node in the storage
nodeIndexInStorage = this.nodes.length;
this.nodes.push(node);
}

// Update the indices
this.indexByPathKey[node.path_item_key] = nodeIndexInStorage;
this.indexByNgeId[node.ngeId] = nodeIndexInStorage;

// Index labels
node.labels.forEach((l) => {
if (l) this.labels.add(l);
});
}

/**
* Update node's data by its key
*/
updateNodeDataByKey(key: string, data: Partial<NodeIndexed>) {
const indexedNode = this.getNodeByKey(key);
if (indexedNode) {
this.indexNodeByKey(key, { ...indexedNode, ...data });
}
}

/**
* Delete a node by its nge ID
*/
deleteNodeByNgeId(ngeId: number) {
const indexInStorage = this.indexByNgeId[ngeId];
const node = this.nodes[indexInStorage];
if (node) {
this.deleteByIndexStorage(indexInStorage);
}
}

/**
* Get a node by its key.
*/
getNodeByKey(key: string): NodeIndexed | null {
const index = this.indexByPathKey[key];
return this.nodes[index] || null;
}

/**
* Get a node by its NGE ID.
*/
getNodeByNgeId(id: number): NodeIndexed | null {
const index = this.indexByNgeId[id];
return this.nodes[index] || null;
}

private deleteByIndexStorage(indexInStorage: number) {
// delete all refs in indices
[this.indexByPathKey, this.indexByPathKey, this.indexByNgeId].forEach((index) => {
Object.keys(index).forEach((key) => {
if (index[key] === indexInStorage) delete index[key];
});
});
// we set value to null to avoid recomputing indices
this.nodes[indexInStorage] = null;
}

/**
* 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}/${item.ch}`);
result.push(`uic:${item.uic}/${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 @@ -10,8 +10,10 @@ 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 type { NodeIndexed } from './MacroEditorState';
import { DEFAULT_TRAINRUN_FREQUENCIES, DEFAULT_TRAINRUN_FREQUENCY } from './osrdToNge';
import { createMacroNode, deleteMacroNodeByNgeId, updateMacroNode } from './utils';
import type {
NetzgrafikDto,
NGEEvent,
Expand Down Expand Up @@ -375,28 +377,85 @@ const handleTrainrunOperation = async ({
}
};

const handleUpdateNode = (timeTableId: number, node: NodeDto) => {
const { betriebspunktName: trigram, positionX, positionY } = node;
nodeStore.set(timeTableId, { trigram, positionX, positionY });
};
/**
* Cast a NGE node to a node.
*/
const castNgeNode = (
node: NetzgrafikDto['nodes'][0],
labels: NetzgrafikDto['labels']
): Omit<NodeIndexed, 'path_item_key' | 'dbId'> => ({
ngeId: 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 = ({
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.getNodeByNgeId(node.id);
switch (type) {
case 'create':
case 'update': {
handleUpdateNode(timeTableId, node);
if (indexNode) {
if (indexNode.dbId) {
// Update the key if trigram has changed and key is based on it
let nodeKey = indexNode.path_item_key;
if (nodeKey.startsWith('trigram:') && indexNode.trigram !== node.betriebspunktName) {
nodeKey = `trigram:${node.betriebspunktName}`;
}
await updateMacroNode(state, dispatch, {
...indexNode,
...castNgeNode(node, netzgrafikDto.labels),
dbId: indexNode.dbId,
path_item_key: nodeKey,
});
} else {
const newNode = {
...indexNode,
...castNgeNode(node, netzgrafikDto.labels),
};
// Create the node
await createMacroNode(state, dispatch, newNode, node.id);
}
} 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 createMacroNode(
state,
dispatch,
{
...castNgeNode(node, netzgrafikDto.labels),
path_item_key: key,
},
node.id
);
}
break;
}
case 'delete': {
nodeStore.delete(timeTableId, node.betriebspunktName);
if (indexNode) await deleteMacroNodeByNgeId(state, dispatch, node.id);
break;
}
default:
Expand Down Expand Up @@ -445,6 +504,7 @@ const handleLabelOperation = async ({
const handleOperation = async ({
event,
dispatch,
state,
infraId,
timeTableId,
netzgrafikDto,
Expand All @@ -453,6 +513,7 @@ const handleOperation = async ({
}: {
event: NGEEvent;
dispatch: AppDispatch;
state: MacroEditorState;
infraId: number;
timeTableId: number;
netzgrafikDto: NetzgrafikDto;
Expand All @@ -462,7 +523,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 9fefabc

Please sign in to comment.