From 809121937db4b1836062353e7234ba7d4b7046d4 Mon Sep 17 00:00:00 2001 From: Yohh Date: Mon, 2 Dec 2024 15:02:24 +0100 Subject: [PATCH 1/3] ui-trackoccupancydiagram: add through train - create drawThroughTrain and drawDefaultZone function Co-authored-by: Uriel-Sautron Signed-off-by: Yohh --- .../occupancyZones.ts | 6 +-- .../drawElements/drawOccupancyZones.ts | 52 +++++++++++++++---- .../drawElements/drawOccupancyZonesTexts.ts | 25 +++++---- .../helpers/drawElements/drawTrack.ts | 2 + 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts index f91fae4c4..91241eaf5 100644 --- a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts +++ b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts @@ -8,7 +8,7 @@ const OccupancyZones = [ originStation: 'FOO', destinationStation: 'BAR', arrivalTime: new Date('2024/04/02 01:30'), - departureTime: new Date('2024/04/02 01:31'), + departureTime: new Date('2024/04/02 01:30'), }, { id: '2', @@ -96,7 +96,7 @@ const OccupancyZones = [ arrivalTime: new Date('2024/04/02 00:40'), originStation: 'FOO', destinationStation: 'BAR', - departureTime: new Date('2024/04/02 00:41'), + departureTime: new Date('2024/04/02 00:40'), }, { id: '10', @@ -228,7 +228,7 @@ const OccupancyZones = [ originStation: 'FOO', destinationStation: 'BAR', arrivalTime: new Date('2024/04/02 02:59'), - departureTime: new Date('2024/04/02 03:00'), + departureTime: new Date('2024/04/02 02:59'), }, ]; diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts index 9ad3d6081..4e6110ff2 100644 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts @@ -8,6 +8,40 @@ import { } from '../../consts'; import type { OccupancyZone, Track } from '../../types'; +type DrawZone = { + ctx: CanvasRenderingContext2D; + arrivalTime: number; + departureTime: number; +}; + +const drawDefaultZone = ({ ctx, arrivalTime, departureTime }: DrawZone) => { + ctx.beginPath(); + ctx.rect(arrivalTime, OCCUPANCY_ZONE_START, departureTime! - arrivalTime, OCCUPANCY_ZONE_HEIGHT); + ctx.fill(); + ctx.stroke(); +}; + +const ARROW_OFFSET_X = 1; +const ARROW_OFFSET_Y = 1.5; +const ARROW_WIDTH = 4.5; +const ARROW_TOP_Y = 3.5; +const ARROW_BOTTOM_Y = 6.5; + +const drawThroughTrain = ({ ctx, arrivalTime }: Omit) => { + ctx.beginPath(); + ctx.moveTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + ctx.lineTo(arrivalTime - ARROW_WIDTH, OCCUPANCY_ZONE_START - ARROW_TOP_Y); + ctx.lineTo(arrivalTime + ARROW_WIDTH, OCCUPANCY_ZONE_START - ARROW_TOP_Y); + ctx.lineTo(arrivalTime + ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + ctx.lineTo(arrivalTime + ARROW_WIDTH, OCCUPANCY_ZONE_START + ARROW_BOTTOM_Y); + ctx.lineTo(arrivalTime - ARROW_WIDTH, OCCUPANCY_ZONE_START + ARROW_BOTTOM_Y); + ctx.lineTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + ctx.fill(); + ctx.moveTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + ctx.lineTo(arrivalTime + ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + ctx.stroke(); +}; + export const drawOccupancyZones = ({ ctx, width, @@ -36,25 +70,25 @@ export const drawOccupancyZones = ({ trackOccupancyZones.forEach((zone) => { const arrivalTime = getTimePixel(zone.arrivalTime.getTime()); const departureTime = getTimePixel(zone.departureTime.getTime()); + const isThroughTrain = arrivalTime === departureTime; ctx.fillStyle = zone.color; ctx.strokeStyle = COLORS.WHITE_100; ctx.lineWidth = 1; - ctx.beginPath(); - ctx.rect( - arrivalTime, - OCCUPANCY_ZONE_START, - departureTime - arrivalTime, - OCCUPANCY_ZONE_HEIGHT - ); - ctx.fill(); - ctx.stroke(); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + if (isThroughTrain) { + drawThroughTrain({ ctx, arrivalTime }); + } else { + drawDefaultZone({ ctx, arrivalTime, departureTime }); + } drawOccupancyZonesTexts({ ctx, zone, arrivalTime, departureTime, + isThroughTrain, }); }); }); diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts index ed7f7ac6e..10bbd36c2 100644 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts @@ -24,6 +24,7 @@ export const drawOccupancyZonesTexts = ({ zone, arrivalTime, departureTime, + isThroughTrain, }: { ctx: CanvasRenderingContext2D; zone: { @@ -35,6 +36,7 @@ export const drawOccupancyZonesTexts = ({ }; arrivalTime: number; departureTime: number; + isThroughTrain: boolean; }) => { const zoneOccupancyLength = departureTime - arrivalTime - STROKE_WIDTH; @@ -82,17 +84,18 @@ export const drawOccupancyZonesTexts = ({ stroke: textStroke, }); - drawText({ - ctx, - text: zone.departureTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), - x: departureTime, - y: OCCUPANCY_ZONE_START + MINUTES_TEXT_OFFSET, - color: GREY_80, - xPosition: xDeparturePosition, - yPosition: 'top', - font: SANS, - stroke: textStroke, - }); + if (!isThroughTrain) + drawText({ + ctx, + text: zone.departureTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), + x: departureTime, + y: OCCUPANCY_ZONE_START + MINUTES_TEXT_OFFSET, + color: GREY_80, + xPosition: xDeparturePosition, + yPosition: 'top', + font: '400 12px IBM Plex Sans', + stroke: textStroke, + }); // origin & destination drawText({ diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts index 288b6d006..434faa545 100644 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts @@ -19,6 +19,8 @@ const drawRails = ({ ctx: CanvasRenderingContext2D; }) => { ctx.clearRect(xStart, yStart, width, 9); + ctx.beginPath(); + ctx.fillStyle = WHITE_50; ctx.strokeStyle = stroke; ctx.beginPath(); ctx.rect(xStart, yStart, width, 8); From 164e905da2810d7c61cd137f4b7f77f501b0ca4e Mon Sep 17 00:00:00 2001 From: Yohh Date: Fri, 13 Dec 2024 10:10:52 +0100 Subject: [PATCH 2/3] ui-trackoccupancydiagram: display simultaneous occupancy zones Co-authored-by: Uriel-Sautron Signed-off-by: Yohh --- .../TrackOccupancyDiagram.stories.tsx | 0 .../occupancyZones.ts | 156 +++++++++++++++++- .../src/components/TrackOccupancyCanvas.tsx | 0 .../components/TrackOccupancyManchette.tsx | 4 +- .../src/components/consts.ts | 5 +- .../drawElements/drawOccupancyZones.ts | 132 ++++++++++++--- .../drawElements/drawOccupancyZonesTexts.ts | 20 ++- .../components/layers/OccupancyZonesLayer.tsx | 0 .../src/components/types.ts | 0 ui-trackoccupancydiagram/src/styles/main.css | 1 - 10 files changed, 282 insertions(+), 36 deletions(-) mode change 100644 => 100755 storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx mode change 100644 => 100755 storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/TrackOccupancyManchette.tsx mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/consts.ts mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx mode change 100644 => 100755 ui-trackoccupancydiagram/src/components/types.ts mode change 100644 => 100755 ui-trackoccupancydiagram/src/styles/main.css diff --git a/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx b/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx old mode 100644 new mode 100755 diff --git a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts old mode 100644 new mode 100755 index 91241eaf5..e124f853c --- a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts +++ b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts @@ -41,7 +41,7 @@ const OccupancyZones = [ arrivalTime: new Date('2024/04/02 00:24'), originStation: 'FOO', destinationStation: 'FOO', - departureTime: new Date('2024/04/02 01:29'), + departureTime: new Date('2024/04/02 00:50'), }, { id: '5', @@ -230,6 +230,160 @@ const OccupancyZones = [ arrivalTime: new Date('2024/04/02 02:59'), departureTime: new Date('2024/04/02 02:59'), }, + { + id: '23', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:34'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 01:05'), + }, + { + id: '24', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:57'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 01:02'), + }, + { + id: '25', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:46'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 01:11'), + }, + { + id: '26', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 01:49'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:14'), + }, + { + id: '27', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 01:54'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:07'), + }, + { + id: '28', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:00'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:11'), + }, + { + id: '29', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:09'), + }, + { + id: '30', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:10'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:17'), + }, + { + id: '31', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:19'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:29'), + }, + { + id: '32', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:24'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:26'), + }, + { + id: '33', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:29'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:57'), + }, + { + id: '34', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:34'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:53'), + }, + { + id: '33', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:09'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 03:07'), + }, + { + id: '34', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 02:12'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 02:59'), + }, ]; export default OccupancyZones; diff --git a/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx b/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx old mode 100644 new mode 100755 diff --git a/ui-trackoccupancydiagram/src/components/TrackOccupancyManchette.tsx b/ui-trackoccupancydiagram/src/components/TrackOccupancyManchette.tsx old mode 100644 new mode 100755 index df0e90675..4e059d0f9 --- a/ui-trackoccupancydiagram/src/components/TrackOccupancyManchette.tsx +++ b/ui-trackoccupancydiagram/src/components/TrackOccupancyManchette.tsx @@ -1,11 +1,13 @@ import React from 'react'; +import { TRACK_HEIGHT_CONTAINER } from './consts'; import { type TrackOccupancyManchetteProps } from './types'; const TrackOccupancyManchette = ({ tracks }: TrackOccupancyManchetteProps) => (
{tracks.map((track) => ( -
+ // height is shared between manchette and canvas components +
{track.line}
{track.name}
diff --git a/ui-trackoccupancydiagram/src/components/consts.ts b/ui-trackoccupancydiagram/src/components/consts.ts old mode 100644 new mode 100755 index d4210cbb4..eb6a44dfe --- a/ui-trackoccupancydiagram/src/components/consts.ts +++ b/ui-trackoccupancydiagram/src/components/consts.ts @@ -1,6 +1,6 @@ -export const TRACK_HEIGHT_CONTAINER = 73; +export const TRACK_HEIGHT_CONTAINER = 100; export const CANVAS_PADDING = 10; -export const OCCUPANCY_ZONE_START = 35; +export const OCCUPANCY_ZONE_START = TRACK_HEIGHT_CONTAINER / 2 - 1.5; export const OCCUPANCY_ZONE_HEIGHT = 3; export const MINUTES_TEXT_OFFSET = 6.5; export const STATION_TEXT_OFFSET = 5; @@ -17,6 +17,7 @@ export const COLORS = { GREY_80: 'rgb(49, 46, 43)', HOUR_BACKGROUND: 'rgba(243, 248, 253, 0.5)', RAIL_TICK: 'rgb(33, 112, 185)', + REMAINING_TRAINS_BACKGROUND: 'rgba(0, 0, 0, 0.7)', WHITE_50: 'rgba(255, 255, 255, 0.5)', WHITE_100: 'rgb(255, 255, 255)', }; diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts old mode 100644 new mode 100755 index 4e6110ff2..fc2a132de --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts @@ -8,15 +8,23 @@ import { } from '../../consts'; import type { OccupancyZone, Track } from '../../types'; +const { REMAINING_TRAINS_BACKGROUND, WHITE_100 } = COLORS; +const REMAINING_TRAINS_WIDTH = 70; +const REMAINING_TRAINS_HEIGHT = 24; +const REMAINING_TEXT_OFFSET = 12; +const Y_OFFSET_INCREMENT = 4; +const MAX_ZONES = 9; + type DrawZone = { ctx: CanvasRenderingContext2D; arrivalTime: number; departureTime: number; + yPosition: number; }; -const drawDefaultZone = ({ ctx, arrivalTime, departureTime }: DrawZone) => { +const drawDefaultZone = ({ ctx, arrivalTime, departureTime, yPosition }: DrawZone) => { ctx.beginPath(); - ctx.rect(arrivalTime, OCCUPANCY_ZONE_START, departureTime! - arrivalTime, OCCUPANCY_ZONE_HEIGHT); + ctx.rect(arrivalTime, yPosition, departureTime! - arrivalTime, OCCUPANCY_ZONE_HEIGHT); ctx.fill(); ctx.stroke(); }; @@ -27,7 +35,7 @@ const ARROW_WIDTH = 4.5; const ARROW_TOP_Y = 3.5; const ARROW_BOTTOM_Y = 6.5; -const drawThroughTrain = ({ ctx, arrivalTime }: Omit) => { +const drawThroughTrain = ({ ctx, arrivalTime }: Omit) => { ctx.beginPath(); ctx.moveTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); ctx.lineTo(arrivalTime - ARROW_WIDTH, OCCUPANCY_ZONE_START - ARROW_TOP_Y); @@ -42,6 +50,26 @@ const drawThroughTrain = ({ ctx, arrivalTime }: Omit) ctx.stroke(); }; +type DrawRemainingTrainsBox = { + ctx: CanvasRenderingContext2D; + text: string; + textX: number; + textY: number; +}; + +const drawRemainingTrainsBox = ({ ctx, text, textX, textY }: DrawRemainingTrainsBox) => { + ctx.fillStyle = REMAINING_TRAINS_BACKGROUND; + ctx.beginPath(); + ctx.rect(textX, textY, REMAINING_TRAINS_WIDTH, REMAINING_TRAINS_HEIGHT); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = WHITE_100; + ctx.font = '400 12px IBM Plex Sans'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(text, textX + REMAINING_TRAINS_WIDTH / 2, textY + REMAINING_TRAINS_HEIGHT / 2); +}; + export const drawOccupancyZones = ({ ctx, width, @@ -63,33 +91,91 @@ export const drawOccupancyZones = ({ const trackTranslate = index === 0 ? CANVAS_PADDING : TRACK_HEIGHT_CONTAINER; ctx.translate(0, trackTranslate); - const trackOccupancyZones = occupancyZones?.filter((zone) => zone.trackId === track.id); + const filteredOccupancyZone = occupancyZones?.filter((zone) => zone.trackId === track.id); + const sortedOccupancyZone = filteredOccupancyZone?.sort( + (a, b) => a.arrivalTime.getTime() - b.arrivalTime.getTime() + ); + + if (!sortedOccupancyZone) return; - if (!trackOccupancyZones) return; + let primaryArrivalTimePixel = 0; + let primaryDepartureTimePixel = 0; + let lastDepartureTimePixel = 0; + let yPosition = OCCUPANCY_ZONE_START; + let yOffset = 0; + let counter = 0; - trackOccupancyZones.forEach((zone) => { + if (sortedOccupancyZone && sortedOccupancyZone.length > 1) { + primaryArrivalTimePixel = getTimePixel(sortedOccupancyZone[0].arrivalTime.getTime()); + primaryDepartureTimePixel = getTimePixel(sortedOccupancyZone[0].departureTime.getTime()); + lastDepartureTimePixel = primaryDepartureTimePixel; + } + + // use a for of loop to be able to break the loop if there are more than 9 zones + for (const [sortedIndex, zone] of sortedOccupancyZone.entries()) { const arrivalTime = getTimePixel(zone.arrivalTime.getTime()); const departureTime = getTimePixel(zone.departureTime.getTime()); const isThroughTrain = arrivalTime === departureTime; - ctx.fillStyle = zone.color; - ctx.strokeStyle = COLORS.WHITE_100; - ctx.lineWidth = 1; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + // reset the overlap check if the zone is not overlapping + if (arrivalTime > lastDepartureTimePixel) { + yPosition = OCCUPANCY_ZONE_START; + primaryArrivalTimePixel = arrivalTime; + primaryDepartureTimePixel = departureTime; + lastDepartureTimePixel = departureTime; + yOffset = 0; + counter = 0; + } + + // figure out if the zone is overlapping with any previous one + // if so and it's an even index, move it to the bottom, if it's an odd index, move it to the top + if (arrivalTime >= primaryArrivalTimePixel && arrivalTime < lastDepartureTimePixel) { + if (counter % 2 === 0) { + yPosition -= yOffset; + } else { + yPosition += yOffset; + } + yOffset += Y_OFFSET_INCREMENT; + counter += 1; + } + + if (departureTime >= lastDepartureTimePixel) lastDepartureTimePixel = departureTime; + + // draw at least 9 zones and zones texts + // if there are more than 9 zones, draw a box with the remaining trains + if (counter <= MAX_ZONES) { + ctx.fillStyle = zone.color; + ctx.strokeStyle = WHITE_100; + ctx.lineWidth = 1; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + if (isThroughTrain) { + drawThroughTrain({ ctx, arrivalTime }); + } else { + drawDefaultZone({ ctx, arrivalTime, departureTime, yPosition }); + } + + drawOccupancyZonesTexts({ + ctx, + zone, + arrivalTime, + departureTime, + yPosition, + isThroughTrain, + }); + } else if (counter === MAX_ZONES + 1) { + const textX = + primaryArrivalTimePixel + + (lastDepartureTimePixel - primaryArrivalTimePixel) / 2 - + REMAINING_TRAINS_WIDTH / 2; + const textY = OCCUPANCY_ZONE_START - REMAINING_TEXT_OFFSET; + const text = `+${sortedOccupancyZone.length - sortedIndex} trains`; + + drawRemainingTrainsBox({ ctx, text, textX, textY }); - if (isThroughTrain) { - drawThroughTrain({ ctx, arrivalTime }); - } else { - drawDefaultZone({ ctx, arrivalTime, departureTime }); + break; } - drawOccupancyZonesTexts({ - ctx, - zone, - arrivalTime, - departureTime, - isThroughTrain, - }); - }); + } }); }; diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts old mode 100644 new mode 100755 index 10bbd36c2..0f4e6171a --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts @@ -24,10 +24,12 @@ export const drawOccupancyZonesTexts = ({ zone, arrivalTime, departureTime, + yPosition, isThroughTrain, }: { ctx: CanvasRenderingContext2D; zone: { + id: string; arrivalTrainName: string; arrivalTime: Date; departureTime: Date; @@ -36,6 +38,7 @@ export const drawOccupancyZonesTexts = ({ }; arrivalTime: number; departureTime: number; + yPosition: number; isThroughTrain: boolean; }) => { const zoneOccupancyLength = departureTime - arrivalTime - STROKE_WIDTH; @@ -44,6 +47,7 @@ export const drawOccupancyZonesTexts = ({ zoneOccupancyLength < BREAKPOINTS[breakpoint]; const textLength = ctx.measureText(zone.originStation!).width; + const { xName, yName } = { xName: isBelowBreakpoint('medium') ? arrivalTime - textLength + STROKE_WIDTH @@ -64,7 +68,7 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, text: zone.arrivalTrainName, - x: xName, + x: isThroughTrain ? xName - 4 : xName, y: yName, color: GREY_50, rotateAngle: -30, @@ -75,8 +79,8 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, text: zone.arrivalTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), - x: arrivalTime, - y: OCCUPANCY_ZONE_START + MINUTES_TEXT_OFFSET, + x: isThroughTrain ? arrivalTime - 4 : arrivalTime, + y: yPosition + MINUTES_TEXT_OFFSET, color: GREY_80, xPosition: xArrivalPosition, yPosition: 'top', @@ -89,7 +93,7 @@ export const drawOccupancyZonesTexts = ({ ctx, text: zone.departureTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), x: departureTime, - y: OCCUPANCY_ZONE_START + MINUTES_TEXT_OFFSET, + y: yPosition + MINUTES_TEXT_OFFSET, color: GREY_80, xPosition: xDeparturePosition, yPosition: 'top', @@ -101,8 +105,8 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, text: zone.originStation!, - x: arrivalTime, - y: OCCUPANCY_ZONE_START - STATION_TEXT_OFFSET, + x: isThroughTrain ? arrivalTime - 4 : arrivalTime, + y: yPosition - STATION_TEXT_OFFSET, color: GREY_60, xPosition: 'right', yPosition: 'bottom', @@ -113,8 +117,8 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, text: zone.destinationStation!, - x: departureTime, - y: OCCUPANCY_ZONE_START - STATION_TEXT_OFFSET, + x: isThroughTrain ? departureTime + 4 : departureTime, + y: yPosition - STATION_TEXT_OFFSET, color: GREY_60, xPosition: 'left', yPosition: 'bottom', diff --git a/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx b/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx old mode 100644 new mode 100755 diff --git a/ui-trackoccupancydiagram/src/components/types.ts b/ui-trackoccupancydiagram/src/components/types.ts old mode 100644 new mode 100755 diff --git a/ui-trackoccupancydiagram/src/styles/main.css b/ui-trackoccupancydiagram/src/styles/main.css old mode 100644 new mode 100755 index ec01f97c0..e3f7c6e4b --- a/ui-trackoccupancydiagram/src/styles/main.css +++ b/ui-trackoccupancydiagram/src/styles/main.css @@ -27,7 +27,6 @@ } .track { - height: 73px; display: flex; justify-content: flex-end; align-items: center; From d7b5903756fdd0613233c9830bb666ae17f9ea9d Mon Sep 17 00:00:00 2001 From: Yohh Date: Fri, 13 Dec 2024 11:21:17 +0100 Subject: [PATCH 3/3] ui-trackoccupancydiagram: highlight selected train Co-authored-by: Uriel-Sautron Signed-off-by: Yohh --- .../TrackOccupancyDiagram.stories.tsx | 5 + .../occupancyZones.ts | 143 +++++++++ .../src/components/TrackOccupancyCanvas.tsx | 9 +- .../src/components/consts.ts | 5 +- .../drawElements/drawOccupancyZones.ts | 289 ++++++++++++------ .../drawElements/drawOccupancyZonesTexts.ts | 121 +++++--- .../helpers/drawElements/drawTrack.ts | 2 +- .../components/layers/OccupancyZonesLayer.tsx | 5 +- .../src/components/types.ts | 1 + .../src/components/utils.ts | 2 +- 10 files changed, 443 insertions(+), 139 deletions(-) diff --git a/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx b/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx index f37a94033..71c202d79 100755 --- a/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx +++ b/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx @@ -38,11 +38,13 @@ type TrackOccupancyDiagramProps = { yOffset: number; spaceScaleType: 'linear' | 'proportional'; emptyData: boolean; + selectedTrainId: string; }; const OP_ID = 'story'; const X_ZOOM_LEVEL = 6; const Y_ZOOM_LEVEL = 3; +const SELECTED_TRAIN_ID = '5'; const TrackOccupancyDiagram = ({ xZoomLevel, @@ -212,6 +214,7 @@ const TrackOccupancyDiagram = ({ opId={OP_ID} useDraw={useDraw} setCanvasesRoot={setCanvasesRoot} + selectedTrainId={SELECTED_TRAIN_ID} />
@@ -246,6 +249,7 @@ const meta: Meta = { yOffset: 0, spaceScaleType: 'linear', emptyData: false, + selectedTrainId: SELECTED_TRAIN_ID, }, render: (args) => , @@ -261,5 +265,6 @@ export const TrackOccupancyDiagramStoryDefault: Story = { xOffset: 0, xZoomLevel: X_ZOOM_LEVEL, yZoomLevel: Y_ZOOM_LEVEL, + selectedTrainId: SELECTED_TRAIN_ID, }, }; diff --git a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts index e124f853c..f1a83b730 100755 --- a/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts +++ b/storybook/stories/samples/TrackOccupancyDiagramSamples/occupancyZones.ts @@ -384,6 +384,149 @@ const OccupancyZones = [ destinationStation: 'FOO', departureTime: new Date('2024/04/02 02:59'), }, + { + id: '35', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 03:12'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 03:20'), + }, + { + id: '36', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '37', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 01:20'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 01:25'), + }, + { + id: '38', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '39', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '40', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '41', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '42', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '43', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '44', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '45', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '46', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, + { + id: '47', + trackId: '4', + arrivalTrainName: '643152', + departureTrainName: '643153', + color: 'rgb(121, 118, 113)', + arrivalTime: new Date('2024/04/02 00:05'), + originStation: 'FOO', + destinationStation: 'FOO', + departureTime: new Date('2024/04/02 00:10'), + }, ]; export default OccupancyZones; diff --git a/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx b/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx index a61ad94d1..05a1e4795 100755 --- a/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx +++ b/ui-trackoccupancydiagram/src/components/TrackOccupancyCanvas.tsx @@ -8,14 +8,19 @@ new FontFace('IBM Plex Mono', 'url(/assets/IBMPlexMono-Regular.ttf)').load().the document.fonts.add(result); }); -const TrackOccupancyCanvas = ({ opId, useDraw, setCanvasesRoot }: TrackOccupancyCanvasProps) => ( +const TrackOccupancyCanvas = ({ + opId, + useDraw, + setCanvasesRoot, + selectedTrainId, +}: TrackOccupancyCanvasProps) => (
- +
); diff --git a/ui-trackoccupancydiagram/src/components/consts.ts b/ui-trackoccupancydiagram/src/components/consts.ts index eb6a44dfe..843abc0b0 100755 --- a/ui-trackoccupancydiagram/src/components/consts.ts +++ b/ui-trackoccupancydiagram/src/components/consts.ts @@ -1,8 +1,8 @@ export const TRACK_HEIGHT_CONTAINER = 100; export const CANVAS_PADDING = 10; -export const OCCUPANCY_ZONE_START = TRACK_HEIGHT_CONTAINER / 2 - 1.5; +export const OCCUPANCY_ZONE_Y_START = TRACK_HEIGHT_CONTAINER / 2 - 1.5; export const OCCUPANCY_ZONE_HEIGHT = 3; -export const MINUTES_TEXT_OFFSET = 6.5; +export const MINUTES_TEXT_OFFSET = 8.5; export const STATION_TEXT_OFFSET = 5; export const FONTS = { @@ -18,6 +18,7 @@ export const COLORS = { HOUR_BACKGROUND: 'rgba(243, 248, 253, 0.5)', RAIL_TICK: 'rgb(33, 112, 185)', REMAINING_TRAINS_BACKGROUND: 'rgba(0, 0, 0, 0.7)', + SELECTION_20: 'rgb(255, 242, 179)', WHITE_50: 'rgba(255, 255, 255, 0.5)', WHITE_100: 'rgb(255, 255, 255)', }; diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts index fc2a132de..def7f98a9 100755 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZones.ts @@ -2,29 +2,40 @@ import { drawOccupancyZonesTexts } from './drawOccupancyZonesTexts'; import { TRACK_HEIGHT_CONTAINER, CANVAS_PADDING, - OCCUPANCY_ZONE_START, + OCCUPANCY_ZONE_Y_START, OCCUPANCY_ZONE_HEIGHT, + FONTS, COLORS, } from '../../consts'; import type { OccupancyZone, Track } from '../../types'; -const { REMAINING_TRAINS_BACKGROUND, WHITE_100 } = COLORS; +const { SANS } = FONTS; +const { REMAINING_TRAINS_BACKGROUND, WHITE_100, SELECTION_20 } = COLORS; const REMAINING_TRAINS_WIDTH = 70; const REMAINING_TRAINS_HEIGHT = 24; const REMAINING_TEXT_OFFSET = 12; const Y_OFFSET_INCREMENT = 4; const MAX_ZONES = 9; +const X_BACKGROUND_PADDING = 4; +const X_TROUGHTRAIN_BACKGROUND_PADDING = 8; +const BACKGROUND_HEIGHT = 40; +const SELECTED_TRAIN_ID_GRADIANT = 2; type DrawZone = { ctx: CanvasRenderingContext2D; - arrivalTime: number; - departureTime: number; + arrivalTimePixel: number; + departureTimePixel: number; yPosition: number; }; -const drawDefaultZone = ({ ctx, arrivalTime, departureTime, yPosition }: DrawZone) => { +const drawDefaultZone = ({ ctx, arrivalTimePixel, departureTimePixel, yPosition }: DrawZone) => { ctx.beginPath(); - ctx.rect(arrivalTime, yPosition, departureTime! - arrivalTime, OCCUPANCY_ZONE_HEIGHT); + ctx.rect( + arrivalTimePixel, + yPosition, + departureTimePixel - arrivalTimePixel, + OCCUPANCY_ZONE_HEIGHT + ); ctx.fill(); ctx.stroke(); }; @@ -35,39 +46,118 @@ const ARROW_WIDTH = 4.5; const ARROW_TOP_Y = 3.5; const ARROW_BOTTOM_Y = 6.5; -const drawThroughTrain = ({ ctx, arrivalTime }: Omit) => { +const drawThroughTrain = ({ + ctx, + arrivalTimePixel, +}: Omit) => { + // Through trains are materialized by converging arrows like the following ones + // ___ + // \_/ + // / \ + // ‾‾‾ ctx.beginPath(); - ctx.moveTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); - ctx.lineTo(arrivalTime - ARROW_WIDTH, OCCUPANCY_ZONE_START - ARROW_TOP_Y); - ctx.lineTo(arrivalTime + ARROW_WIDTH, OCCUPANCY_ZONE_START - ARROW_TOP_Y); - ctx.lineTo(arrivalTime + ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); - ctx.lineTo(arrivalTime + ARROW_WIDTH, OCCUPANCY_ZONE_START + ARROW_BOTTOM_Y); - ctx.lineTo(arrivalTime - ARROW_WIDTH, OCCUPANCY_ZONE_START + ARROW_BOTTOM_Y); - ctx.lineTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + // draw the upper part + ctx.moveTo(arrivalTimePixel - ARROW_OFFSET_X, OCCUPANCY_ZONE_Y_START + ARROW_OFFSET_Y); + ctx.lineTo(arrivalTimePixel - ARROW_WIDTH, OCCUPANCY_ZONE_Y_START - ARROW_TOP_Y); + ctx.lineTo(arrivalTimePixel + ARROW_WIDTH, OCCUPANCY_ZONE_Y_START - ARROW_TOP_Y); + ctx.lineTo(arrivalTimePixel + ARROW_OFFSET_X, OCCUPANCY_ZONE_Y_START + ARROW_OFFSET_Y); + // draw the lower part + ctx.lineTo(arrivalTimePixel + ARROW_WIDTH, OCCUPANCY_ZONE_Y_START + ARROW_BOTTOM_Y); + ctx.lineTo(arrivalTimePixel - ARROW_WIDTH, OCCUPANCY_ZONE_Y_START + ARROW_BOTTOM_Y); + ctx.lineTo(arrivalTimePixel - ARROW_OFFSET_X, OCCUPANCY_ZONE_Y_START + ARROW_OFFSET_Y); ctx.fill(); - ctx.moveTo(arrivalTime - ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); - ctx.lineTo(arrivalTime + ARROW_OFFSET_X, OCCUPANCY_ZONE_START + ARROW_OFFSET_Y); + // draw the white separator in the middle + ctx.moveTo(arrivalTimePixel - ARROW_OFFSET_X, OCCUPANCY_ZONE_Y_START + ARROW_OFFSET_Y); + ctx.lineTo(arrivalTimePixel + ARROW_OFFSET_X, OCCUPANCY_ZONE_Y_START + ARROW_OFFSET_Y); ctx.stroke(); }; type DrawRemainingTrainsBox = { ctx: CanvasRenderingContext2D; - text: string; - textX: number; - textY: number; + remainingTrainsNb: number; + xPosition: number; }; -const drawRemainingTrainsBox = ({ ctx, text, textX, textY }: DrawRemainingTrainsBox) => { +const drawRemainingTrainsBox = ({ ctx, remainingTrainsNb, xPosition }: DrawRemainingTrainsBox) => { + const textY = OCCUPANCY_ZONE_Y_START - REMAINING_TEXT_OFFSET; + ctx.fillStyle = REMAINING_TRAINS_BACKGROUND; ctx.beginPath(); - ctx.rect(textX, textY, REMAINING_TRAINS_WIDTH, REMAINING_TRAINS_HEIGHT); + ctx.rect(xPosition, textY, REMAINING_TRAINS_WIDTH, REMAINING_TRAINS_HEIGHT); ctx.fill(); ctx.stroke(); ctx.fillStyle = WHITE_100; - ctx.font = '400 12px IBM Plex Sans'; + ctx.font = SANS; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText(text, textX + REMAINING_TRAINS_WIDTH / 2, textY + REMAINING_TRAINS_HEIGHT / 2); + ctx.fillText( + `+${remainingTrainsNb} trains`, + xPosition + REMAINING_TRAINS_WIDTH / 2, + textY + REMAINING_TRAINS_HEIGHT / 2 + ); +}; + +const drawOccupationZone = ({ + ctx, + zone, + arrivalTimePixel, + departureTimePixel, + yPosition, + isThroughTrain, + selectedTrainId, +}: { + ctx: CanvasRenderingContext2D; + zone: OccupancyZone; + arrivalTimePixel: number; + departureTimePixel: number; + yPosition: number; + isThroughTrain: boolean; + selectedTrainId: string; +}) => { + ctx.fillStyle = zone.color; + ctx.strokeStyle = WHITE_100; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + ctx.font = '400 10px IBM Plex Mono'; + + if (selectedTrainId === zone.id) { + const extraWidth = isThroughTrain ? X_TROUGHTRAIN_BACKGROUND_PADDING : X_BACKGROUND_PADDING; + const originTextLength = ctx.measureText(zone.originStation || '--').width; + const destinationTextLength = ctx.measureText(zone.destinationStation || '--').width; + + ctx.save(); + ctx.fillStyle = SELECTION_20; + ctx.beginPath(); + ctx.roundRect( + arrivalTimePixel - originTextLength - extraWidth, + yPosition - BACKGROUND_HEIGHT / 2, + departureTimePixel - + arrivalTimePixel + + originTextLength + + destinationTextLength + + extraWidth * 2, + BACKGROUND_HEIGHT, + SELECTED_TRAIN_ID_GRADIANT + ); + ctx.fill(); + ctx.restore(); + } + + if (isThroughTrain) { + drawThroughTrain({ ctx, arrivalTimePixel }); + } else { + drawDefaultZone({ ctx, arrivalTimePixel, departureTimePixel, yPosition }); + } + + drawOccupancyZonesTexts({ + ctx, + zone, + arrivalTimePixel, + departureTimePixel, + yPosition, + isThroughTrain, + selectedTrainId, + }); }; export const drawOccupancyZones = ({ @@ -77,6 +167,7 @@ export const drawOccupancyZones = ({ tracks, occupancyZones, getTimePixel, + selectedTrainId, }: { ctx: CanvasRenderingContext2D; width: number; @@ -84,98 +175,116 @@ export const drawOccupancyZones = ({ tracks: Track[] | undefined; occupancyZones: OccupancyZone[] | undefined; getTimePixel: (time: number) => number; + selectedTrainId: string; }) => { ctx.clearRect(0, 0, width, height); - tracks?.forEach((track, index) => { + if (!tracks || !occupancyZones || occupancyZones.length === 0) return; + + const sortedOccupancyZones = occupancyZones.sort( + (a, b) => a.arrivalTime.getTime() - b.arrivalTime.getTime() + ); + + tracks.forEach((track, index) => { const trackTranslate = index === 0 ? CANVAS_PADDING : TRACK_HEIGHT_CONTAINER; ctx.translate(0, trackTranslate); - const filteredOccupancyZone = occupancyZones?.filter((zone) => zone.trackId === track.id); - const sortedOccupancyZone = filteredOccupancyZone?.sort( - (a, b) => a.arrivalTime.getTime() - b.arrivalTime.getTime() - ); - - if (!sortedOccupancyZone) return; + const filteredOccupancyZones = sortedOccupancyZones.filter((zone) => zone.trackId === track.id); let primaryArrivalTimePixel = 0; let primaryDepartureTimePixel = 0; - let lastDepartureTimePixel = 0; - let yPosition = OCCUPANCY_ZONE_START; + let lastDepartureTimePixel = primaryDepartureTimePixel; + let yPosition = OCCUPANCY_ZONE_Y_START; let yOffset = 0; - let counter = 0; + let zoneCounter = 0; + let zoneIndex = 0; - if (sortedOccupancyZone && sortedOccupancyZone.length > 1) { - primaryArrivalTimePixel = getTimePixel(sortedOccupancyZone[0].arrivalTime.getTime()); - primaryDepartureTimePixel = getTimePixel(sortedOccupancyZone[0].departureTime.getTime()); - lastDepartureTimePixel = primaryDepartureTimePixel; - } + while (zoneIndex < filteredOccupancyZones.length) { + const zone = filteredOccupancyZones[zoneIndex]; + const arrivalTimePixel = getTimePixel(zone.arrivalTime.getTime()); + const departureTimePixel = getTimePixel(zone.departureTime.getTime()); + const isThroughTrain = arrivalTimePixel === departureTimePixel; - // use a for of loop to be able to break the loop if there are more than 9 zones - for (const [sortedIndex, zone] of sortedOccupancyZone.entries()) { - const arrivalTime = getTimePixel(zone.arrivalTime.getTime()); - const departureTime = getTimePixel(zone.departureTime.getTime()); - const isThroughTrain = arrivalTime === departureTime; - - // reset the overlap check if the zone is not overlapping - if (arrivalTime > lastDepartureTimePixel) { - yPosition = OCCUPANCY_ZONE_START; - primaryArrivalTimePixel = arrivalTime; - primaryDepartureTimePixel = departureTime; - lastDepartureTimePixel = departureTime; - yOffset = 0; - counter = 0; - } + // * if the zone is not overlapping with any previous one, draw it in the center of the track + // * and reset the primary values + // * + // * if the zone is overlapping with the previous one, draw it below or above the previous one + // * depending on the overlapping counter + // * + // * if the zone is overlapping with the previous one and the counter is higher than the max zones + // * draw the remaining trains box + // * + if (arrivalTimePixel > lastDepartureTimePixel) { + // reset to initial value if the zone is not overlapping + yPosition = OCCUPANCY_ZONE_Y_START; + primaryArrivalTimePixel = arrivalTimePixel; + primaryDepartureTimePixel = departureTimePixel; + lastDepartureTimePixel = departureTimePixel; + yOffset = Y_OFFSET_INCREMENT; + zoneCounter = 1; - // figure out if the zone is overlapping with any previous one - // if so and it's an even index, move it to the bottom, if it's an odd index, move it to the top - if (arrivalTime >= primaryArrivalTimePixel && arrivalTime < lastDepartureTimePixel) { - if (counter % 2 === 0) { - yPosition -= yOffset; - } else { - yPosition += yOffset; - } - yOffset += Y_OFFSET_INCREMENT; - counter += 1; + drawOccupationZone({ + ctx, + zone, + arrivalTimePixel, + departureTimePixel, + yPosition, + isThroughTrain, + selectedTrainId, + }); + + zoneIndex++; + + continue; } - if (departureTime >= lastDepartureTimePixel) lastDepartureTimePixel = departureTime; - - // draw at least 9 zones and zones texts - // if there are more than 9 zones, draw a box with the remaining trains - if (counter <= MAX_ZONES) { - ctx.fillStyle = zone.color; - ctx.strokeStyle = WHITE_100; - ctx.lineWidth = 1; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - - if (isThroughTrain) { - drawThroughTrain({ ctx, arrivalTime }); - } else { - drawDefaultZone({ ctx, arrivalTime, departureTime, yPosition }); + if (zoneCounter < MAX_ZONES) { + // if so and it's an even index, move it to the bottom, if it's an odd index, move it to the top + if (arrivalTimePixel >= primaryArrivalTimePixel) { + if (zoneCounter % 2 === 0) { + yPosition -= yOffset; + } else { + yPosition += yOffset; + } } - drawOccupancyZonesTexts({ + // update the last departure time if the current zone is longer + if (departureTimePixel >= lastDepartureTimePixel) + lastDepartureTimePixel = departureTimePixel; + + drawOccupationZone({ ctx, zone, - arrivalTime, - departureTime, + arrivalTimePixel, + departureTimePixel, yPosition, isThroughTrain, + selectedTrainId, }); - } else if (counter === MAX_ZONES + 1) { - const textX = - primaryArrivalTimePixel + - (lastDepartureTimePixel - primaryArrivalTimePixel) / 2 - - REMAINING_TRAINS_WIDTH / 2; - const textY = OCCUPANCY_ZONE_START - REMAINING_TEXT_OFFSET; - const text = `+${sortedOccupancyZone.length - sortedIndex} trains`; - drawRemainingTrainsBox({ ctx, text, textX, textY }); + zoneCounter++; + yOffset += Y_OFFSET_INCREMENT; + zoneIndex++; - break; + continue; } + + const nextIndex = filteredOccupancyZones.findIndex( + (filteredZone, i) => + i > zoneIndex && + getTimePixel(filteredZone.arrivalTime.getTime()) >= lastDepartureTimePixel + ); + + const remainingTrainsNb = nextIndex - zoneIndex; + + const xPosition = + primaryArrivalTimePixel + + (lastDepartureTimePixel - primaryArrivalTimePixel) / 2 - + REMAINING_TRAINS_WIDTH / 2; + + drawRemainingTrainsBox({ ctx, remainingTrainsNb, xPosition }); + + zoneIndex += remainingTrainsNb; } }); }; diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts index 0f4e6171a..72e6eac45 100755 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawOccupancyZonesTexts.ts @@ -1,10 +1,5 @@ -import { - OCCUPANCY_ZONE_START, - MINUTES_TEXT_OFFSET, - STATION_TEXT_OFFSET, - FONTS, - COLORS, -} from '../../consts'; +import { MINUTES_TEXT_OFFSET, STATION_TEXT_OFFSET, FONTS, COLORS } from '../../consts'; +import type { OccupancyZone } from '../../types'; import { drawText } from '../../utils'; const BREAKPOINTS = { @@ -12,74 +7,116 @@ const BREAKPOINTS = { small: 4, }; const STROKE_WIDTH = 4; +const X_BACKGROUND_PADDING = 4; const X_INITIAL_POSITION_OFFSET = 8; +const X_MEDIUM_POSITION_OFFSET_BACKGROUND = 12; const Y_INITIAL_POSITION_OFFSET = 5; +const Y_INITIAL_POSITION_OFFSET_BACKGROUND = 18; +const X_SELECTED_MEDIUM_PADDING = 8; +const X_THROUGHTRAIN_OFFSET = 4; +const Y_MEDIUM_POSITION_OFFSET_BACKGROUND = 28; const Y_MEDIUM_POSITION_OFFSET = 14; +const ROTATE_VALUE = (-30 * Math.PI) / 180; const { SANS, MONO } = FONTS; -const { WHITE_100, GREY_50, GREY_60, GREY_80 } = COLORS; +const { WHITE_100, GREY_50, GREY_60, GREY_80, SELECTION_20 } = COLORS; export const drawOccupancyZonesTexts = ({ ctx, zone, - arrivalTime, - departureTime, + arrivalTimePixel, + departureTimePixel, yPosition, isThroughTrain, + selectedTrainId, }: { ctx: CanvasRenderingContext2D; - zone: { - id: string; - arrivalTrainName: string; - arrivalTime: Date; - departureTime: Date; - originStation?: string; - destinationStation?: string; - }; - arrivalTime: number; - departureTime: number; + zone: OccupancyZone; + arrivalTimePixel: number; + departureTimePixel: number; yPosition: number; isThroughTrain: boolean; + selectedTrainId: string; }) => { - const zoneOccupancyLength = departureTime - arrivalTime - STROKE_WIDTH; + const zoneOccupancyLength = departureTimePixel - arrivalTimePixel - STROKE_WIDTH; const isBelowBreakpoint = (breakpoint: keyof typeof BREAKPOINTS) => zoneOccupancyLength < BREAKPOINTS[breakpoint]; - const textLength = ctx.measureText(zone.originStation!).width; + ctx.font = '400 10px IBM Plex Mono'; + const originTextLength = ctx.measureText(zone.originStation || '--').width; + ctx.font = '400 12px IBM Plex Mono'; + const nameTextLength = ctx.measureText(zone.arrivalTrainName).width; + + const { xOriginTrainName, yOriginTrainName } = isBelowBreakpoint('medium') + ? { + xOriginTrainName: + arrivalTimePixel - + originTextLength + + STROKE_WIDTH - + (isThroughTrain ? X_MEDIUM_POSITION_OFFSET_BACKGROUND / 2 : 0), + yOriginTrainName: yPosition - Y_MEDIUM_POSITION_OFFSET, + } + : { + xOriginTrainName: arrivalTimePixel + X_INITIAL_POSITION_OFFSET, + yOriginTrainName: yPosition - Y_INITIAL_POSITION_OFFSET, + }; - const { xName, yName } = { - xName: isBelowBreakpoint('medium') - ? arrivalTime - textLength + STROKE_WIDTH - : arrivalTime + X_INITIAL_POSITION_OFFSET, - yName: isBelowBreakpoint('medium') - ? OCCUPANCY_ZONE_START - Y_MEDIUM_POSITION_OFFSET - : OCCUPANCY_ZONE_START - Y_INITIAL_POSITION_OFFSET, - }; const xArrivalPosition = isBelowBreakpoint('small') ? 'right' : 'center'; const xDeparturePosition = isBelowBreakpoint('small') ? 'left' : 'center'; const textStroke = { - color: WHITE_100, + color: selectedTrainId === zone.id ? 'transparent' : WHITE_100, width: STROKE_WIDTH, }; // train name + if (selectedTrainId === zone.id) { + const { xSelectedTrainNameBackground, ySelectedTrainNameBackground } = isBelowBreakpoint( + 'medium' + ) + ? { + xSelectedTrainNameBackground: xOriginTrainName - X_SELECTED_MEDIUM_PADDING, + ySelectedTrainNameBackground: yPosition - Y_MEDIUM_POSITION_OFFSET_BACKGROUND, + } + : { + xSelectedTrainNameBackground: arrivalTimePixel, + ySelectedTrainNameBackground: yPosition - Y_INITIAL_POSITION_OFFSET_BACKGROUND, + }; + + ctx.save(); + ctx.translate(xSelectedTrainNameBackground, ySelectedTrainNameBackground); + ctx.rotate(ROTATE_VALUE); + ctx.fillStyle = SELECTION_20; + ctx.beginPath(); + ctx.roundRect( + -X_BACKGROUND_PADDING, + 0, + nameTextLength + X_BACKGROUND_PADDING * 2, + Y_INITIAL_POSITION_OFFSET_BACKGROUND + ); + ctx.fill(); + ctx.restore(); + } + drawText({ ctx, text: zone.arrivalTrainName, - x: isThroughTrain ? xName - 4 : xName, - y: yName, + x: xOriginTrainName, + y: yOriginTrainName, color: GREY_50, - rotateAngle: -30, - stroke: textStroke, + rotateAngle: ROTATE_VALUE, + stroke: { + color: WHITE_100, + width: STROKE_WIDTH, + }, }); // arrival minutes & departure minutes drawText({ ctx, text: zone.arrivalTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), - x: isThroughTrain ? arrivalTime - 4 : arrivalTime, + x: isThroughTrain ? arrivalTimePixel - X_THROUGHTRAIN_OFFSET : arrivalTimePixel, y: yPosition + MINUTES_TEXT_OFFSET, color: GREY_80, xPosition: xArrivalPosition, @@ -92,20 +129,20 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, text: zone.departureTime.getMinutes().toLocaleString('fr-FR', { minimumIntegerDigits: 2 }), - x: departureTime, + x: departureTimePixel, y: yPosition + MINUTES_TEXT_OFFSET, color: GREY_80, xPosition: xDeparturePosition, yPosition: 'top', - font: '400 12px IBM Plex Sans', + font: SANS, stroke: textStroke, }); // origin & destination drawText({ ctx, - text: zone.originStation!, - x: isThroughTrain ? arrivalTime - 4 : arrivalTime, + text: zone.originStation || '--', + x: isThroughTrain ? arrivalTimePixel - X_THROUGHTRAIN_OFFSET : arrivalTimePixel, y: yPosition - STATION_TEXT_OFFSET, color: GREY_60, xPosition: 'right', @@ -116,8 +153,8 @@ export const drawOccupancyZonesTexts = ({ drawText({ ctx, - text: zone.destinationStation!, - x: isThroughTrain ? departureTime + 4 : departureTime, + text: zone.destinationStation || '--', + x: isThroughTrain ? departureTimePixel + X_THROUGHTRAIN_OFFSET : departureTimePixel, y: yPosition - STATION_TEXT_OFFSET, color: GREY_60, xPosition: 'left', diff --git a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts index 434faa545..633d8a7ba 100644 --- a/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts +++ b/ui-trackoccupancydiagram/src/components/helpers/drawElements/drawTrack.ts @@ -19,7 +19,7 @@ const drawRails = ({ ctx: CanvasRenderingContext2D; }) => { ctx.clearRect(xStart, yStart, width, 9); - ctx.beginPath(); + ctx.fillStyle = WHITE_50; ctx.strokeStyle = stroke; ctx.beginPath(); diff --git a/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx b/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx index 1f04b414b..9f4787904 100755 --- a/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx +++ b/ui-trackoccupancydiagram/src/components/layers/OccupancyZonesLayer.tsx @@ -9,8 +9,10 @@ import { drawOccupancyZones } from '../helpers/drawElements/drawOccupancyZones'; const OccupancyZonesLayer = ({ useDraw, + selectedTrainId, }: { useDraw: (layer: LayerType, fn: DrawingFunction) => void; + selectedTrainId: string; }) => { const drawingFunction = useCallback( (ctx, { getTimePixel, tracks, occupancyZones, trackOccupancyWidth, trackOccupancyHeight }) => { @@ -22,9 +24,10 @@ const OccupancyZonesLayer = ({ tracks, occupancyZones, getTimePixel, + selectedTrainId, }); }, - [] + [selectedTrainId] ); useDraw('paths', drawingFunction); diff --git a/ui-trackoccupancydiagram/src/components/types.ts b/ui-trackoccupancydiagram/src/components/types.ts index ed63fcee2..2339abf3f 100755 --- a/ui-trackoccupancydiagram/src/components/types.ts +++ b/ui-trackoccupancydiagram/src/components/types.ts @@ -27,6 +27,7 @@ export type TrackOccupancyCanvasProps = { opId: string; useDraw: (layer: LayerType, fn: DrawingFunction) => void; setCanvasesRoot: (root: HTMLDivElement | null) => void; + selectedTrainId: string; }; export type TrackOccupancyManchetteProps = { diff --git a/ui-trackoccupancydiagram/src/components/utils.ts b/ui-trackoccupancydiagram/src/components/utils.ts index a559163e5..f5fe1f284 100644 --- a/ui-trackoccupancydiagram/src/components/utils.ts +++ b/ui-trackoccupancydiagram/src/components/utils.ts @@ -30,7 +30,7 @@ export const drawText = ({ }: DrawTextType) => { ctx.save(); ctx.translate(x, y); - ctx.rotate((rotateAngle * Math.PI) / 180); + ctx.rotate(rotateAngle); ctx.font = font; ctx.textAlign = xPosition;