Skip to content

Commit

Permalink
front: intervals editor, display operational points and allow snapping
Browse files Browse the repository at this point in the history
  • Loading branch information
clarani committed Aug 9, 2023
1 parent 5a68101 commit f9aa39b
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { MdOutlineHelpOutline } from 'react-icons/md';
import { useTranslation } from 'react-i18next';

import {
LinearMetadataItem,
getZoomedViewBox,
transalteViewBox,
splitAt,
Expand All @@ -22,6 +21,7 @@ import {
getLineStringDistance,
fixLinearMetadataItems,
} from 'common/IntervalsDataViz/data';
import { LinearMetadataItem } from 'common/IntervalsDataViz/types';
import { LinearMetadataDataviz } from 'common/IntervalsDataViz/dataviz';
import { useModal } from '../../../../common/BootstrapSNCF/ModalSNCF';
import HelpModal from './HelpModal';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import { JSONSchema7 } from 'json-schema';

import { LinearMetadataItem } from 'common/IntervalsDataViz/data';
import { LinearMetadataItem } from 'common/IntervalsDataViz/types';

interface LinearMetadataTooltipProps<T> {
item: LinearMetadataItem<T>;
Expand Down
44 changes: 17 additions & 27 deletions front/src/common/IntervalsDataViz/IntervalItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import React from 'react';
import cx from 'classnames';

import { INTERVAL_TYPES } from 'common/IntervalsEditor/types';
import { LinearMetadataItem } from './data';
import { computeStyleForDataValue, getPositionFromMouseEvent, isNilObject } from './utils';
import { IntervalItemBaseProps } from './types';
import {
computeStyleForDataValue,
getPositionFromMouseEventAndSegment,
isNilObject,
} from './utils';
import { IntervalItemBaseProps, LinearMetadataItem } from './types';

interface IntervalItemProps<T> extends IntervalItemBaseProps<T> {
dragingStartAt: number | null;
Expand All @@ -14,9 +17,9 @@ interface IntervalItemProps<T> extends IntervalItemBaseProps<T> {
resizing: { index: number | null; startAt: number } | null;
segment: LinearMetadataItem & { index: number };
setDraginStartAt: (dragingStartAt: number) => void;
setHoverAtx: (hoverAtx: number) => void;
setResizing: (resizing: { index: number | null; startAt: number } | null) => void;
wrapper: React.MutableRefObject<HTMLDivElement | null>;
setResizing: (
resizing: { index: number | null; startAt: number; startPosition: number } | null
) => void;
}

const IntervalItem = <T extends { [key: string]: string | number }>({
Expand All @@ -34,16 +37,13 @@ const IntervalItem = <T extends { [key: string]: string | number }>({
onCreate,
onDoubleClick,
onMouseEnter,
onMouseMove,
onMouseOver,
onWheel,
options,
resizing,
segment,
setDraginStartAt,
setHoverAtx,
setResizing,
wrapper,
}: IntervalItemProps<T>) => {
let valueText = '';
if (field && segment[field]) {
Expand Down Expand Up @@ -79,14 +79,14 @@ const IntervalItem = <T extends { [key: string]: string | number }>({
onClick={(e) => {
if (!dragingStartAt && onClick && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onClick(e, item, segment.index, point);
}
}}
onDoubleClick={(e) => {
if (!dragingStartAt && onDoubleClick && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onDoubleClick(e, item, segment.index, point);
}
}}
Expand All @@ -95,37 +95,27 @@ const IntervalItem = <T extends { [key: string]: string | number }>({
// handle mouse over
if (onMouseOver && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onMouseOver(e, item, segment.index, point);
}
}
}}
onFocus={() => undefined}
role="button"
tabIndex={0}
onMouseMove={(e) => {
// display vertical bar when hover element
setHoverAtx(e.clientX - (wrapper.current ? wrapper.current.getBoundingClientRect().x : 0));

if (!dragingStartAt && onMouseMove && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
onMouseMove(e, item, segment.index, point);
}
}}
onMouseEnter={(e) => {
if (!dragingStartAt && onMouseEnter && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onMouseEnter(e, item, segment.index, point);
}
}}
onMouseDown={(e) => {
if (onCreate && creating) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onCreate(point);
setResizing({ index: segment.index + 1, startAt: e.clientX });
setResizing({ index: segment.index + 1, startAt: e.clientX, startPosition: point + 1 });
} else {
// TODO use the frag tool context here
setDraginStartAt(e.clientX);
Expand All @@ -136,7 +126,7 @@ const IntervalItem = <T extends { [key: string]: string | number }>({
onWheel={(e) => {
if (!dragingStartAt && onWheel && data[segment.index]) {
const item = data[segment.index];
const point = getPositionFromMouseEvent(e, item);
const point = getPositionFromMouseEventAndSegment(e, item);
onWheel(e, item, segment.index, point);
}
}}
Expand Down Expand Up @@ -168,7 +158,7 @@ const IntervalItem = <T extends { [key: string]: string | number }>({
e.preventDefault();
}}
onMouseDown={(e) => {
setResizing({ index: segment.index, startAt: e.clientX });
setResizing({ index: segment.index, startAt: e.clientX, startPosition: segment.end });
e.stopPropagation();
e.preventDefault();
}}
Expand Down
10 changes: 2 additions & 8 deletions front/src/common/IntervalsDataViz/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@ import distance from '@turf/distance';
import along from '@turf/along';
import { LineString, FeatureCollection } from 'geojson';

import {
getLineStringDistance,
LinearMetadataItem,
update,
resizeSegment,
splitAt,
mergeIn,
} from './data';
import { getLineStringDistance, update, resizeSegment, splitAt, mergeIn } from './data';
import { LinearMetadataItem } from './types';

const DEBUG = false;

Expand Down
92 changes: 83 additions & 9 deletions front/src/common/IntervalsDataViz/data.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { Feature, Point, LineString, Position } from 'geojson';
import { last, differenceWith, cloneDeep, isEqual, sortBy, isArray, isNil, isObject } from 'lodash';
import {
last,
differenceWith,
cloneDeep,
isEqual,
isArray,
isNil,
isObject,
isEmpty,
sortBy,
} from 'lodash';
import { JSONSchema7, JSONSchema7Definition } from 'json-schema';
import { utils } from '@rjsf/core';
import lineSplit from '@turf/line-split';
import fnLength from '@turf/length';
import { EditorEntity } from 'types/editor';
import { LinearMetadataItem, OperationalPoint } from './types';

export const LINEAR_METADATA_FIELDS = ['slopes', 'curves'];
// Min size of a linear metadata segment
Expand All @@ -16,14 +27,6 @@ const ZOOM_RATIO = 0.75;
// Min size (in meter) of the viewbox
const MIN_SIZE_TO_DISPLAY = 10;

/**
* Generic type for Linear Metadata
*/
export type LinearMetadataItem<T = { [key: string]: unknown }> = T & {
begin: number;
end: number;
};

/**
* Cast a coordinate to its Feature representation.
*/
Expand Down Expand Up @@ -56,6 +59,25 @@ export function getLineStringDistance(line: LineString): number {
return Math.round(fnLength(lineStringToFeature(line)) * 1000);
}

/**
* Given a array of linearMetadata and a position, returns the first item
* containing the position
*
* @param linearMetadata linearMetadata in which we search
* @param hoveredPostion hoveredPosition
*/
export function getHoveredItem<T>(
linearMetadata: Array<LinearMetadataItem<T>>,
hoveredPostion: number
): { hoveredItem: LinearMetadataItem<T>; hoveredItemIndex: number } | null {
const hoveredItemIndex = linearMetadata.findIndex(
(item) => item.begin <= hoveredPostion && hoveredPostion <= item.end
);
return hoveredItemIndex !== -1
? { hoveredItem: linearMetadata[hoveredItemIndex], hoveredItemIndex }
: null;
}

/**
* When you change the size of a segment, you need to impact it on the chain.
* What we do is to substract the gap from its neighbor (see beginOrEnd).
Expand Down Expand Up @@ -588,6 +610,58 @@ export function cropForDatavizViewbox(
);
}

/**
* Given operationalPoints and viewbox, this function returns
* the cropped operationalPoints (and the computed position of the line in px)
*/
export function cropOperationPointsForDatavizViewbox(
operationalPoints: Array<OperationalPoint>,
currentViewBox: [number, number] | null,
wrapper: React.MutableRefObject<HTMLDivElement | null>,
fullLength: number
): Array<OperationalPoint & { positionInPx: number }> {
if (wrapper.current !== null && fullLength > 0) {
const wrapperWidth = wrapper.current.offsetWidth;
return (
[...operationalPoints]
// we filter operational points that are inside the viewbox
.filter(({ position }) => {
if (!currentViewBox) return true;
return !(position < currentViewBox[0] || currentViewBox[1] < position);
})
// we compute the vertical line position in px
.map((operationalPoint) => {
const lengthToDisplay = currentViewBox
? operationalPoint.position - currentViewBox[0]
: operationalPoint.position;
// subtract 2px for the display
const positionInPx = (lengthToDisplay * wrapperWidth) / fullLength - 2;
return {
...operationalPoint,
positionInPx,
};
})
);
}
return [];
}

/**
* Given operationalPoints and a point, this function returns
* the closest operationalPoint if it is closer than 10px to the point, else return null
*/
export function getClosestOperationalPoint(
position: number,
operationalPoints: Array<OperationalPoint & { positionInPx: number }>
): (OperationalPoint & { positionInPx: number }) | null {
if (isEmpty(operationalPoints)) return null;
const sortedOperationalPoints = sortBy(operationalPoints, (op) =>
Math.abs(position - op.positionInPx)
);
const closestPoint = sortedOperationalPoints[0];
return Math.abs(position - closestPoint.positionInPx) <= 10 ? closestPoint : null;
}

/**
* Given a JSON schema, return the props name that are a linear metadata.
* A Linear metadata is an array type with a ref
Expand Down
Loading

0 comments on commit f9aa39b

Please sign in to comment.