@geops/rvf-mobility-web-component 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/.fixpackrc +21 -0
  2. package/.husky/commit-msg +1 -0
  3. package/.husky/post-checkout +1 -0
  4. package/.husky/post-merge +1 -0
  5. package/.husky/post-rebase +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.lintstagedrc.js +12 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierrc.js +1 -0
  10. package/CHANGELOG.md +183 -0
  11. package/Logo.svg +10 -0
  12. package/README.md +179 -0
  13. package/__mocks__/dataurl.js +11 -0
  14. package/__mocks__/mapbox-gl.js +19 -0
  15. package/commitlint.config.cjs +1 -0
  16. package/doc/.eslintrc.json +3 -0
  17. package/doc/.fixpackrc +21 -0
  18. package/doc/.prettierrc +1 -0
  19. package/doc/README.md +14 -0
  20. package/doc/declarations.d.ts +6 -0
  21. package/doc/next.config.mjs +4 -0
  22. package/doc/package.json +43 -0
  23. package/doc/postcss.config.mjs +8 -0
  24. package/doc/public/README.md +1 -0
  25. package/doc/src/app/components/GeopsAPIKeyLink.tsx +6 -0
  26. package/doc/src/app/components/GeopsAPIsLink.tsx +6 -0
  27. package/doc/src/app/components/GeopsMapsAPILink.tsx +8 -0
  28. package/doc/src/app/components/GeopsMobility.tsx +9 -0
  29. package/doc/src/app/components/GeopsMobilityDoc.tsx +231 -0
  30. package/doc/src/app/components/GeopsMobilitySearch.tsx +10 -0
  31. package/doc/src/app/components/GeopsMobilitySearchDoc.tsx +129 -0
  32. package/doc/src/app/components/GeopsRealtimeAPILink.tsx +10 -0
  33. package/doc/src/app/components/GeopsStopsAPILink.tsx +8 -0
  34. package/doc/src/app/components/Link.tsx +9 -0
  35. package/doc/src/app/components/WebComponentDoc.tsx +296 -0
  36. package/doc/src/app/favicon.ico +0 -0
  37. package/doc/src/app/geops-mobility/page.tsx +6 -0
  38. package/doc/src/app/geops-mobility-search/page.tsx +7 -0
  39. package/doc/src/app/globals.css +38 -0
  40. package/doc/src/app/hooks/useAttrFromUrlParams.ts +21 -0
  41. package/doc/src/app/hooks/useIsFullScreen.ts +14 -0
  42. package/doc/src/app/hooks/usePublicKey.ts +21 -0
  43. package/doc/src/app/layout.tsx +51 -0
  44. package/doc/src/app/page.tsx +86 -0
  45. package/doc/src/geops-ui.ts +3 -0
  46. package/doc/tailwind.config.ts +20 -0
  47. package/doc/tsconfig.json +40 -0
  48. package/eslint.config.mjs +40 -0
  49. package/favicon.ico +0 -0
  50. package/global.d.ts +4 -0
  51. package/iframe.html +34 -0
  52. package/index.html +276 -0
  53. package/index.js +2162 -0
  54. package/input.css +34 -0
  55. package/jest-setup.js +4 -0
  56. package/jest.config.js +17 -0
  57. package/package.json +80 -0
  58. package/scripts/build.mjs +16 -0
  59. package/scripts/dev.mjs +26 -0
  60. package/search.html +144 -0
  61. package/src/BaseLayer/BaseLayer.tsx +36 -0
  62. package/src/BaseLayer/index.tsx +1 -0
  63. package/src/Copyright/Copyright.tsx +54 -0
  64. package/src/Copyright/index.css +3 -0
  65. package/src/Copyright/index.tsx +1 -0
  66. package/src/DebugDeparture/DebugDeparture.tsx +116 -0
  67. package/src/DebugDeparture/index.tsx +1 -0
  68. package/src/DebugStop/DebugStop.tsx +47 -0
  69. package/src/DebugStop/index.tsx +1 -0
  70. package/src/Departure/Departure.tsx +55 -0
  71. package/src/Departure/index.tsx +1 -0
  72. package/src/GeolocationButton/GeolocationButton.tsx +81 -0
  73. package/src/GeolocationButton/index.tsx +1 -0
  74. package/src/Map/Map.tsx +89 -0
  75. package/src/Map/index.tsx +1 -0
  76. package/src/MobilityMap/MobilityMap.tsx +259 -0
  77. package/src/MobilityMap/index.css +13 -0
  78. package/src/MobilityMap/index.tsx +1 -0
  79. package/src/NotificationLayer/NotificationLayer.tsx +156 -0
  80. package/src/NotificationLayer/index.tsx +1 -0
  81. package/src/NotificationLayer/notificationUtils.ts +191 -0
  82. package/src/Overlay/Overlay.tsx +57 -0
  83. package/src/Overlay/index.tsx +1 -0
  84. package/src/RealtimeLayer/RealtimeLayer.tsx +230 -0
  85. package/src/RealtimeLayer/index.tsx +1 -0
  86. package/src/RouteDestination/RouteDestination.test.tsx +13 -0
  87. package/src/RouteDestination/RouteDestination.tsx +15 -0
  88. package/src/RouteDestination/index.tsx +1 -0
  89. package/src/RouteIcon/RouteIcon.tsx +66 -0
  90. package/src/RouteIcon/index.tsx +1 -0
  91. package/src/RouteIdentifier/RouteIdentifer.tsx +35 -0
  92. package/src/RouteIdentifier/index.tsx +1 -0
  93. package/src/RouteInfos/RouteInfos.tsx +22 -0
  94. package/src/RouteInfos/index.tsx +1 -0
  95. package/src/RouteSchedule/RouteSchedule.tsx +69 -0
  96. package/src/RouteSchedule/firstStation.png +0 -0
  97. package/src/RouteSchedule/index.tsx +1 -0
  98. package/src/RouteSchedule/lastStation.png +0 -0
  99. package/src/RouteSchedule/line.png +0 -0
  100. package/src/RouteSchedule/station.png +0 -0
  101. package/src/RouteScheduleFooter/RouteScheduleFooter.tsx +44 -0
  102. package/src/RouteScheduleFooter/index.tsx +1 -0
  103. package/src/RouteScheduleHeader/RouteScheduleHeader.tsx +58 -0
  104. package/src/RouteScheduleHeader/index.tsx +1 -0
  105. package/src/RouteStop/RouteStop.tsx +121 -0
  106. package/src/RouteStop/index.tsx +1 -0
  107. package/src/RouteStopDelay/RouteStopDelay.tsx +36 -0
  108. package/src/RouteStopDelay/index.tsx +1 -0
  109. package/src/RouteStopName/RouteStopName.tsx +24 -0
  110. package/src/RouteStopName/index.tsx +1 -0
  111. package/src/RouteStopPlatform/RouteStopPlatform.tsx +29 -0
  112. package/src/RouteStopPlatform/index.tsx +1 -0
  113. package/src/RouteStopProgress/RouteStopProgress.tsx +101 -0
  114. package/src/RouteStopProgress/index.tsx +1 -0
  115. package/src/RouteStopServices/RouteStopServices.tsx +26 -0
  116. package/src/RouteStopServices/index.tsx +1 -0
  117. package/src/RouteStopStation/RouteStopStation.tsx +32 -0
  118. package/src/RouteStopStation/index.tsx +1 -0
  119. package/src/RouteStopTime/RouteStopTime.tsx +34 -0
  120. package/src/RouteStopTime/index.tsx +1 -0
  121. package/src/RvfMobilityMap/RvfMobilityMap.tsx +245 -0
  122. package/src/RvfMobilityMap/index.css +13 -0
  123. package/src/RvfMobilityMap/index.tsx +1 -0
  124. package/src/ScaleLine/ScaleLine.tsx +51 -0
  125. package/src/ScaleLine/index.css +6 -0
  126. package/src/ScaleLine/index.tsx +1 -0
  127. package/src/ScrollableHandler/ScrollableHandler.tsx +65 -0
  128. package/src/ScrollableHandler/index.tsx +1 -0
  129. package/src/Search/Search.tsx +18 -0
  130. package/src/Search/index.tsx +1 -0
  131. package/src/SingleClickListener/SingleClickListener.tsx +103 -0
  132. package/src/SingleClickListener/index.tsx +1 -0
  133. package/src/Station/Station.tsx +68 -0
  134. package/src/Station/index.tsx +1 -0
  135. package/src/StationHeader/StationHeader.tsx +32 -0
  136. package/src/StationHeader/index.tsx +1 -0
  137. package/src/StationName/StationName.tsx +21 -0
  138. package/src/StationName/index.tsx +1 -0
  139. package/src/StationServices/StationServices.tsx +80 -0
  140. package/src/StationServices/index.tsx +1 -0
  141. package/src/StationsLayer/StationsLayer.tsx +41 -0
  142. package/src/StationsLayer/index.tsx +1 -0
  143. package/src/StopsSearch/StopsSearch.tsx +254 -0
  144. package/src/StopsSearch/index.tsx +1 -0
  145. package/src/icons/Airport/Airport.tsx +17 -0
  146. package/src/icons/Airport/airport-14-svgrepo-com.svg +41 -0
  147. package/src/icons/Airport/index.tsx +1 -0
  148. package/src/icons/BarAndRestaurants/BarAndRestaurants.tsx +17 -0
  149. package/src/icons/BarAndRestaurants/food-restaurant-svgrepo-com.svg +12 -0
  150. package/src/icons/BarAndRestaurants/index.tsx +1 -0
  151. package/src/icons/Bathroom/Bathroom.tsx +59 -0
  152. package/src/icons/Bathroom/bathroom-restroom-svgrepo-com.svg +38 -0
  153. package/src/icons/Bathroom/index.tsx +1 -0
  154. package/src/icons/BikeStorage/BikeStorage.tsx +17 -0
  155. package/src/icons/BikeStorage/index.tsx +1 -0
  156. package/src/icons/BikeStorage/parking-bicycle-14-svgrepo-com.svg +41 -0
  157. package/src/icons/Elevator/Elevator.tsx +16 -0
  158. package/src/icons/Elevator/elevator-svgrepo-com.svg +2 -0
  159. package/src/icons/Elevator/index.tsx +1 -0
  160. package/src/icons/Police/Police.tsx +20 -0
  161. package/src/icons/Police/index.tsx +1 -0
  162. package/src/icons/Police/polizia.png +0 -0
  163. package/src/icons/README.md +52 -0
  164. package/src/icons/WaitingAreas/WaitingAreas.tsx +16 -0
  165. package/src/icons/WaitingAreas/highway-rest-area-svgrepo-com.svg +5 -0
  166. package/src/icons/WaitingAreas/index.tsx +1 -0
  167. package/src/icons/WaitingAreas/wheelchair-svgrepo-com.svg +2 -0
  168. package/src/icons/WheelChair/WheelChair.tsx +16 -0
  169. package/src/icons/WheelChair/disabili.png +0 -0
  170. package/src/icons/WheelChair/index.tsx +1 -0
  171. package/src/icons/WheelChair/wheelchair-svgrepo-com.svg +2 -0
  172. package/src/index.tsx +50 -0
  173. package/src/utils/MobilityEvent.ts +21 -0
  174. package/src/utils/addSourceAndLayers.ts +62 -0
  175. package/src/utils/centerOnStation.ts +17 -0
  176. package/src/utils/centerOnVehicle.ts +50 -0
  177. package/src/utils/getBgColor.ts +3 -0
  178. package/src/utils/getDelayColor.test.ts +20 -0
  179. package/src/utils/getDelayColor.ts +23 -0
  180. package/src/utils/getDelayColorForVehicle.test.ts +28 -0
  181. package/src/utils/getDelayColorForVehicle.ts +25 -0
  182. package/src/utils/getDelayFontForVehicle.test.ts +7 -0
  183. package/src/utils/getDelayFontForVehicle.tsx +8 -0
  184. package/src/utils/getDelayString.test.ts +22 -0
  185. package/src/utils/getDelayString.ts +28 -0
  186. package/src/utils/getDelayTextForVehicle.test.ts +28 -0
  187. package/src/utils/getDelayTextForVehicle.ts +21 -0
  188. package/src/utils/getFullTrajectoryAndFit.ts +40 -0
  189. package/src/utils/getHoursAndMinutes.test.ts +14 -0
  190. package/src/utils/getHoursAndMinutes.ts +22 -0
  191. package/src/utils/getMainColorForVehicle.test.ts +27 -0
  192. package/src/utils/getMainColorForVehicle.ts +46 -0
  193. package/src/utils/getStopStatus.test.ts +104 -0
  194. package/src/utils/getStopStatus.ts +171 -0
  195. package/src/utils/getTextFontForVehicle.test.ts +7 -0
  196. package/src/utils/getTextFontForVehicle.tsx +9 -0
  197. package/src/utils/getTextForVehicle.test.ts +17 -0
  198. package/src/utils/getTextForVehicle.ts +19 -0
  199. package/src/utils/hooks/useDebug.tsx +11 -0
  200. package/src/utils/hooks/useDeparture.tsx +23 -0
  201. package/src/utils/hooks/useI18n.tsx +20 -0
  202. package/src/utils/hooks/useMapContext.tsx +74 -0
  203. package/src/utils/hooks/useParams.ts +5 -0
  204. package/src/utils/hooks/useRouteStop.tsx +33 -0
  205. package/src/utils/hooks/useStation.tsx +21 -0
  206. package/src/utils/hooks/useUpdatePermalink.tsx +33 -0
  207. package/src/utils/hooks/useZoom.tsx +32 -0
  208. package/src/utils/i18n.ts +16 -0
  209. package/src/utils/translations.ts +31 -0
  210. package/tailwind.config.mjs +56 -0
  211. package/testNotification.json +50653 -0
  212. package/tsconfig.json +12 -0
@@ -0,0 +1,191 @@
1
+ import { FeatureCollection } from "geojson";
2
+ import { Feature } from "ol";
3
+ import { getCenter } from "ol/extent";
4
+ import GeoJSON from "ol/format/GeoJSON";
5
+
6
+ import addSourceAndLayers from "../utils/addSourceAndLayers";
7
+
8
+ const format = new GeoJSON();
9
+
10
+ export const getTime = (str) => {
11
+ return parseInt(str?.substr(0, 8).replace(/:/g, ""), 10);
12
+ };
13
+
14
+ /**
15
+ *
16
+ * @param {Array.<Object>} notifications Raw notifications
17
+ * @param {Date} now The date to compare with the affected_time_intervals
18
+ * @returns {Array.<Object>}
19
+ */
20
+ const getNotificationsWithStatus = (notifications, now) => {
21
+ return notifications
22
+ .filter((n) => {
23
+ // TODO: The backend should be responsible to returns only good notifications.
24
+ const notOutOfDate = n.properties.affected_time_intervals.some((ati) => {
25
+ return now < new Date(ati.end);
26
+ });
27
+ return notOutOfDate;
28
+ })
29
+ .map((n) => {
30
+ const isActive = n.properties.affected_time_intervals.some((ati) => {
31
+ const {
32
+ end,
33
+ start,
34
+ time_of_day_end: dayTimeEnd,
35
+ time_of_day_start: dayTimeStart,
36
+ } = ati;
37
+ const nowTime = getTime(now.toTimeString());
38
+ const startTime = getTime(dayTimeStart);
39
+ const endTime = getTime(dayTimeEnd);
40
+ const inRange = new Date(start) <= now && now <= new Date(end);
41
+ return startTime && endTime
42
+ ? inRange && startTime <= nowTime && nowTime <= endTime
43
+ : inRange;
44
+ });
45
+
46
+ const next = n.properties.affected_time_intervals.reduce((a, b) => {
47
+ const aEnd = new Date(a.end);
48
+ const aStart = new Date(a.start);
49
+ const bStart = new Date(b.start);
50
+ return now < aEnd && aStart < bStart ? a : b;
51
+ }, []);
52
+ const nextStartDate = new Date(next.start);
53
+ let starts;
54
+ if (
55
+ now.toDateString() === nextStartDate.toDateString() ||
56
+ now.getTime() - nextStartDate.getTime() > 0
57
+ ) {
58
+ if (next.time_of_day_start) {
59
+ starts = `ab ${next.time_of_day_start.substr(0, 5)}`;
60
+ } else {
61
+ starts = `ab ${nextStartDate.toLocaleTimeString(["de"], {
62
+ hour: "2-digit",
63
+ hour12: false,
64
+ minute: "2-digit",
65
+ })}`;
66
+ }
67
+ } else {
68
+ starts = `ab ${nextStartDate.toLocaleDateString(["de-DE"], {
69
+ day: "numeric",
70
+ month: "short",
71
+ })}`;
72
+ }
73
+
74
+ let iconRefPoint;
75
+ const iconRef = n.features.find((f) => {
76
+ return f.properties.is_icon_ref;
77
+ });
78
+ if (iconRef) {
79
+ const iconRefFeature = format.readFeature(iconRef, {
80
+ dataProjection: "EPSG:4326",
81
+ featureProjection: "EPSG:3857",
82
+ }) as Feature;
83
+ const center = getCenter(iconRefFeature.getGeometry().getExtent());
84
+ iconRefPoint = iconRefFeature.getGeometry().getClosestPoint(center);
85
+ }
86
+
87
+ const properties = {
88
+ ...n.properties,
89
+ iconRefPoint,
90
+ isActive,
91
+ starts,
92
+ };
93
+
94
+ const features = n.features.map((f) => {
95
+ return {
96
+ ...f,
97
+ properties: { ...f.properties, ...properties },
98
+ };
99
+ });
100
+
101
+ return {
102
+ ...n,
103
+ features,
104
+ properties,
105
+ };
106
+ });
107
+ };
108
+
109
+ const getCurrentGraph = (mapping: object, zoom: number) => {
110
+ const breakPoints = Object.keys(mapping).map((k) => {
111
+ return parseFloat(k);
112
+ });
113
+ const closest = breakPoints.reverse().find((bp) => {
114
+ return bp <= Math.floor(zoom) - 1;
115
+ }); // - 1 due to ol zoom !== mapbox zoom
116
+ return mapping[closest || Math.min(...breakPoints)];
117
+ };
118
+
119
+ /**
120
+ * This function add layers in the mapbox style to show notifications lines.
121
+ */
122
+ const addNotificationsLayers = (
123
+ mapboxLayer: object,
124
+ notifications: FeatureCollection[],
125
+ beforeLayerId: string,
126
+ zoom: number,
127
+ graphMapping: object,
128
+ ) => {
129
+ if (!mapboxLayer) {
130
+ return;
131
+ }
132
+ const features = notifications
133
+ .map((n) => {
134
+ return n.features;
135
+ })
136
+ .flat();
137
+ addSourceAndLayers(
138
+ mapboxLayer,
139
+ "notifications",
140
+ {
141
+ data: {
142
+ features,
143
+ type: "FeatureCollection",
144
+ },
145
+ type: "geojson",
146
+ },
147
+ [
148
+ {
149
+ filter: [
150
+ "all",
151
+ ["==", ["get", "isActive"], true],
152
+ ["==", ["get", "graph"], getCurrentGraph(graphMapping, zoom)],
153
+ ["==", ["get", "disruption_type"], "DISRUPTION"],
154
+ ],
155
+ id: "notificationsActive",
156
+ layout: { visibility: "visible" },
157
+ paint: {
158
+ "line-color": "rgba(255,0,0,1)",
159
+ "line-dasharray": [2, 2],
160
+ "line-width": 5,
161
+ },
162
+ source: "notifications",
163
+ type: "line",
164
+ },
165
+ ],
166
+ beforeLayerId,
167
+ );
168
+ };
169
+
170
+ const parsePreviewNotification = (mocoPreviewObject: {
171
+ graphs: object;
172
+ id: number;
173
+ }) => {
174
+ let properties = {};
175
+ const features = Object.keys(mocoPreviewObject.graphs).map((graph) => {
176
+ const feature = mocoPreviewObject.graphs[graph].features[0];
177
+ properties = mocoPreviewObject.graphs[graph].properties;
178
+ return { ...feature, properties: { ...feature.properties, graph } };
179
+ });
180
+ return {
181
+ features,
182
+ properties,
183
+ type: "FeatureCollection",
184
+ };
185
+ };
186
+
187
+ export {
188
+ addNotificationsLayers,
189
+ getNotificationsWithStatus,
190
+ parsePreviewNotification,
191
+ };
@@ -0,0 +1,57 @@
1
+ import { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import type { ScrollableHandlerProps } from "../ScrollableHandler";
4
+
5
+ import ScrollableHandler from "../ScrollableHandler";
6
+
7
+ export type OverlayProps = {
8
+ ScrollableHandlerProps?: ScrollableHandlerProps;
9
+ } & JSX.HTMLAttributes<HTMLDivElement> &
10
+ PreactDOMAttributes;
11
+
12
+ function Overlay({ children, ScrollableHandlerProps = {} }: OverlayProps) {
13
+ let hasChildren = !!children;
14
+ if (Array.isArray(children)) {
15
+ hasChildren =
16
+ children?.length &&
17
+ (children || []).find((c) => {
18
+ return !!c;
19
+ });
20
+ }
21
+
22
+ if (!hasChildren) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <div
28
+ className={`relative z-50 flex flex-col overflow-hidden transition-[min-height,max-height] @lg:transition-[width] ${
29
+ children
30
+ ? "max-h-[70%] min-h-[75px] w-full border-t @lg:h-[100%!important] @lg:max-h-full @lg:w-[350px] @lg:border-r @lg:border-t-0"
31
+ : "max-h-0 min-h-0 @lg:w-0"
32
+ }`}
33
+ >
34
+ {hasChildren && (
35
+ <>
36
+ <ScrollableHandler
37
+ className="absolute inset-0 flex h-[65px] touch-none justify-center @lg:hidden"
38
+ style={{ width: "100%" }}
39
+ {...ScrollableHandlerProps}
40
+ >
41
+ <div
42
+ className="m-2 mr-[-60px] bg-gray-300"
43
+ style={{
44
+ borderRadius: 2,
45
+ height: 4,
46
+ width: 32,
47
+ }}
48
+ />
49
+ </ScrollableHandler>
50
+ {children}
51
+ </>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export default Overlay;
@@ -0,0 +1 @@
1
+ export { default } from "./Overlay";
@@ -0,0 +1,230 @@
1
+ import type { RealtimeMot, RealtimeTrainId } from "mobility-toolbox-js/types";
2
+
3
+ import { RealtimeLayer as MtbRealtimeLayer } from "mobility-toolbox-js/ol";
4
+ import { RealtimeLayerOptions } from "mobility-toolbox-js/ol/layers/RealtimeLayer";
5
+ import { unByKey } from "ol/Observable";
6
+ import { memo } from "preact/compat";
7
+ import { useEffect, useMemo } from "preact/hooks";
8
+
9
+ import centerOnVehicle from "../utils/centerOnVehicle";
10
+ import getDelayColorForVehicle from "../utils/getDelayColorForVehicle";
11
+ import getDelayFontForVehicle from "../utils/getDelayFontForVehicle";
12
+ import getDelayTextForVehicle from "../utils/getDelayTextForVehicle";
13
+ import getTextFontForVehicle from "../utils/getTextFontForVehicle";
14
+ import getTextForVehicle from "../utils/getTextForVehicle";
15
+ import useMapContext from "../utils/hooks/useMapContext";
16
+
17
+ const TRACKING_ZOOM = 16;
18
+
19
+ export type RealtimeLayerProps = RealtimeLayerOptions;
20
+
21
+ function RealtimeLayer(props: RealtimeLayerProps) {
22
+ const {
23
+ apikey,
24
+ isFollowing,
25
+ isTracking,
26
+ map,
27
+ mots,
28
+ realtimeurl,
29
+ setIsFollowing,
30
+ setIsTracking,
31
+ setRealtimeLayer,
32
+ setStation,
33
+ setStopSequence,
34
+ stationId,
35
+ stopSequence,
36
+ tenant,
37
+ trainId,
38
+ } = useMapContext();
39
+
40
+ const layer = useMemo(() => {
41
+ if (!apikey || !realtimeurl) {
42
+ return null;
43
+ }
44
+ const layer = new MtbRealtimeLayer({
45
+ apiKey: apikey,
46
+ getMotsByZoom: mots
47
+ ? () => {
48
+ return mots.split(",") as RealtimeMot[];
49
+ }
50
+ : undefined,
51
+ tenant,
52
+ url: realtimeurl,
53
+ ...props,
54
+ styleOptions: {
55
+ getDelayColor: getDelayColorForVehicle,
56
+ getDelayFont: getDelayFontForVehicle,
57
+ getDelayText: getDelayTextForVehicle,
58
+ getText: getTextForVehicle,
59
+ getTextFont: getTextFontForVehicle,
60
+ ...(props?.styleOptions || {}),
61
+ },
62
+ });
63
+
64
+ layer.setZIndex(1);
65
+
66
+ return layer;
67
+ }, [apikey, mots, realtimeurl, tenant, props]);
68
+
69
+ useEffect(() => {
70
+ if (!map || !layer) {
71
+ return;
72
+ }
73
+ if (map.getView()?.getCenter()) {
74
+ map.addLayer(layer);
75
+ } else {
76
+ map.once("moveend", () => {
77
+ map.addLayer(layer);
78
+ });
79
+ }
80
+
81
+ setRealtimeLayer(layer);
82
+
83
+ return () => {
84
+ map.removeLayer(layer);
85
+ setRealtimeLayer(null);
86
+ };
87
+ }, [map, setRealtimeLayer, layer]);
88
+
89
+ // Behavior when vehicle is selected or not.
90
+ useEffect(() => {
91
+ if (!stopSequence) {
92
+ setIsFollowing(false);
93
+ }
94
+ }, [stopSequence, setIsFollowing]);
95
+
96
+ // Behavior when user tracking is activated or not.
97
+ useEffect(() => {
98
+ const olKeys = [];
99
+ if (isTracking) {
100
+ setIsFollowing(false);
101
+ }
102
+ return () => {
103
+ unByKey(olKeys);
104
+ };
105
+ }, [isTracking, setIsFollowing]);
106
+
107
+ // Deactive auto zooming when the user pans the map
108
+ useEffect(() => {
109
+ if (!map) {
110
+ return;
111
+ }
112
+ let onMovestartKey = null;
113
+ onMovestartKey = map.getView().on("change:center", (evt) => {
114
+ if (evt.target.getInteracting()) {
115
+ setIsFollowing(false);
116
+ setIsTracking(false);
117
+ }
118
+ });
119
+ return () => {
120
+ unByKey(onMovestartKey);
121
+ };
122
+ }, [map, setIsFollowing, setIsTracking]);
123
+
124
+ useEffect(() => {
125
+ let interval = null;
126
+
127
+ if (layer) {
128
+ layer.engine.useThrottle = !isFollowing;
129
+ layer.engine.isUpdateBboxOnMoveEnd = !isFollowing;
130
+ // layer.useRequestAnimationFrame = isFollowing;
131
+ layer.allowRenderWhenAnimating = !!isFollowing;
132
+ }
133
+ if (!isFollowing || !stopSequence || !map || !layer) {
134
+ return;
135
+ }
136
+
137
+ setIsTracking(false);
138
+
139
+ const followVehicle = async (id: RealtimeTrainId) => {
140
+ let vehicle = id && layer?.trajectories?.[id];
141
+
142
+ if (!vehicle) {
143
+ const message = await layer.api.getTrajectory(
144
+ stopSequence.id,
145
+ layer.mode,
146
+ );
147
+ vehicle = message?.content;
148
+ }
149
+
150
+ const success = await centerOnVehicle(vehicle, map, TRACKING_ZOOM);
151
+
152
+ // Once the map is zoomed on the vehicle we follow him, only recenter , no zoom changes.
153
+ if (success === true) {
154
+ interval = setInterval(() => {
155
+ centerOnVehicle(layer?.trajectories?.[stopSequence.id], map);
156
+ }, 1000);
157
+ }
158
+ };
159
+ followVehicle(stopSequence.id);
160
+
161
+ return () => {
162
+ clearInterval(interval);
163
+ };
164
+ }, [isFollowing, map, layer, stopSequence, setIsTracking]);
165
+
166
+ useEffect(() => {
167
+ if (trainId) {
168
+ // No animation, it's nicer for the user.
169
+ const center = layer?.trajectories?.[trainId]?.properties?.coordinate;
170
+ if (center) {
171
+ map.getView().setCenter(center);
172
+ }
173
+ }
174
+ }, [map, trainId, layer]);
175
+
176
+ // Ask the station using the stationId to the Realtime API.
177
+ useEffect(() => {
178
+ if (!stationId || !layer?.api) {
179
+ return;
180
+ }
181
+ const subscribe = async () => {
182
+ layer?.api?.subscribe(`station ${stationId}`, ({ content }) => {
183
+ if (content) {
184
+ setStation(content);
185
+ }
186
+ });
187
+ };
188
+ subscribe();
189
+
190
+ return () => {
191
+ setStation(null);
192
+ if (stationId) {
193
+ layer?.api?.unsubscribe(`station ${stationId}`);
194
+ }
195
+ };
196
+ }, [stationId, layer?.api, setStation]);
197
+
198
+ // Subscribe to the stop sequence of the selected vehicle.
199
+ useEffect(() => {
200
+ if (!trainId || !layer?.api) {
201
+ return;
202
+ }
203
+ layer.selectedVehicleId = trainId;
204
+ layer.highlightTrajectory(trainId);
205
+ const subscribe = async () => {
206
+ layer?.api?.subscribeStopSequence(trainId, ({ content }) => {
207
+ if (content) {
208
+ const [firstStopSequence] = content;
209
+ if (firstStopSequence) {
210
+ setStopSequence(firstStopSequence);
211
+ }
212
+ }
213
+ });
214
+ };
215
+ subscribe();
216
+
217
+ return () => {
218
+ setStopSequence(null);
219
+ if (trainId && layer) {
220
+ layer.api?.unsubscribeStopSequence(trainId);
221
+ layer.selectedVehicleId = null;
222
+ layer.vectorLayer.getSource().clear();
223
+ }
224
+ };
225
+ }, [trainId, layer, layer?.api, setStopSequence]);
226
+
227
+ return null;
228
+ }
229
+
230
+ export default memo(RealtimeLayer);
@@ -0,0 +1 @@
1
+ export { default } from "./RealtimeLayer";
@@ -0,0 +1,13 @@
1
+ import { render } from "@testing-library/preact";
2
+
3
+ import RouteDestination from "./RouteDestination";
4
+
5
+ describe("RouteDestination", () => {
6
+ test("should display destination", () => {
7
+ const { container } = render(
8
+ // @ts-expect-error bad type definition
9
+ <RouteDestination stopSequence={{ destination: "foo" }} />,
10
+ );
11
+ expect(container.textContent).toMatch("foo");
12
+ });
13
+ });
@@ -0,0 +1,15 @@
1
+ import type { RealtimeStopSequence } from "mobility-toolbox-js/types";
2
+
3
+ import { JSX, PreactDOMAttributes } from "preact";
4
+ import { memo } from "preact/compat";
5
+
6
+ export type RouteDestinationProps = {
7
+ stopSequence?: RealtimeStopSequence;
8
+ } & JSX.HTMLAttributes<HTMLSpanElement> &
9
+ PreactDOMAttributes;
10
+
11
+ function RouteDestination({ stopSequence, ...props }: RouteDestinationProps) {
12
+ const { destination } = stopSequence || {};
13
+ return <span {...props}>{destination}</span>;
14
+ }
15
+ export default memo(RouteDestination);
@@ -0,0 +1 @@
1
+ export { default } from "./RouteDestination";
@@ -0,0 +1,66 @@
1
+ import {
2
+ RealtimeDeparture,
3
+ RealtimeLine,
4
+ RealtimeStopSequence,
5
+ RealtimeTrajectory,
6
+ } from "mobility-toolbox-js/types";
7
+ import { JSX, PreactDOMAttributes } from "preact";
8
+
9
+ import getMainColorForVehicle from "../utils/getMainColorForVehicle";
10
+ import getTextFontForVehicle from "../utils/getTextFontForVehicle";
11
+ import getTextForVehicle from "../utils/getTextForVehicle";
12
+
13
+ export type RouteIconProps = {
14
+ departure?: RealtimeDeparture;
15
+ line?: RealtimeLine;
16
+ stopSequence?: RealtimeStopSequence;
17
+ trajectory?: RealtimeTrajectory;
18
+ } & JSX.HTMLAttributes<HTMLSpanElement> &
19
+ PreactDOMAttributes;
20
+ const fontSizesByNbLetters = [16, 16, 16, 14, 12];
21
+
22
+ function RouteIcon({
23
+ children,
24
+ departure,
25
+ line,
26
+ stopSequence,
27
+ trajectory,
28
+ ...props
29
+ }: RouteIconProps) {
30
+ const lineToUse =
31
+ line ||
32
+ departure?.line ||
33
+ stopSequence?.line ||
34
+ trajectory?.properties?.line;
35
+ const backgroundColor = getMainColorForVehicle(
36
+ line || departure || stopSequence || trajectory,
37
+ );
38
+ const color = lineToUse?.text_color || "black";
39
+ let borderColor = lineToUse?.stroke || "black";
40
+ const text = getTextForVehicle(
41
+ line || departure || stopSequence || trajectory,
42
+ );
43
+
44
+ const fontSize = fontSizesByNbLetters[text.length] || 12;
45
+ const font = getTextFontForVehicle(fontSize, text);
46
+
47
+ if (borderColor === backgroundColor) {
48
+ borderColor = "black";
49
+ }
50
+
51
+ return (
52
+ <span
53
+ className="flex h-[40px] min-w-[40px] items-center justify-center rounded-full border-2 px-1"
54
+ style={{
55
+ backgroundColor,
56
+ borderColor,
57
+ color,
58
+ font,
59
+ }}
60
+ {...props}
61
+ >
62
+ {children || text}
63
+ </span>
64
+ );
65
+ }
66
+ export default RouteIcon;
@@ -0,0 +1 @@
1
+ export { default } from "./RouteIcon";
@@ -0,0 +1,35 @@
1
+ import { RealtimeStopSequence } from "mobility-toolbox-js/types";
2
+ import { JSX, PreactDOMAttributes } from "preact";
3
+ import { memo } from "preact/compat";
4
+
5
+ export type RouteIdentifierProps = {
6
+ stopSequence?: RealtimeStopSequence;
7
+ } & JSX.HTMLAttributes<HTMLSpanElement> &
8
+ PreactDOMAttributes;
9
+
10
+ function RouteIdentifier({ stopSequence, ...props }: RouteIdentifierProps) {
11
+ const { longName, routeIdentifier } = stopSequence || {};
12
+ let text = longName;
13
+ if (routeIdentifier) {
14
+ // first part of the id, without leading zeros.
15
+ let id = routeIdentifier;
16
+
17
+ if (/\./.test(routeIdentifier)) {
18
+ [id] = routeIdentifier.split(".");
19
+ } else if (/_/.test(routeIdentifier)) {
20
+ [id] = routeIdentifier.split("_");
21
+ } else if (/:/.test(routeIdentifier)) {
22
+ [id] = routeIdentifier.split(":");
23
+ }
24
+
25
+ if (/^\d*$/.test(id)) {
26
+ id = `${parseInt(id, 10)}`;
27
+ }
28
+
29
+ if (!longName.includes(id)) {
30
+ text = `${longName} (${id})`;
31
+ }
32
+ }
33
+ return <span {...props}>{text}</span>;
34
+ }
35
+ export default memo(RouteIdentifier);
@@ -0,0 +1 @@
1
+ export { default } from "./RouteIdentifer";
@@ -0,0 +1,22 @@
1
+ import { RealtimeStopSequence } from "mobility-toolbox-js/types";
2
+ import { JSX, PreactDOMAttributes } from "preact";
3
+ import { memo } from "preact/compat";
4
+
5
+ import RouteDestination from "../RouteDestination";
6
+ import RouteIdentifier from "../RouteIdentifier";
7
+
8
+ export type RouteInfosProps = {
9
+ stopSequence?: RealtimeStopSequence;
10
+ } & JSX.HTMLAttributes<HTMLDivElement> &
11
+ PreactDOMAttributes;
12
+
13
+ function RouteInfos({ stopSequence, ...props }: RouteInfosProps) {
14
+ return (
15
+ <div {...props}>
16
+ <RouteDestination className="font-bold" stopSequence={stopSequence} />
17
+ <RouteIdentifier className="text-sm" stopSequence={stopSequence} />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ export default memo(RouteInfos);
@@ -0,0 +1 @@
1
+ export { default } from "./RouteInfos";