@geops/rvf-mobility-web-component 0.1.30 → 0.1.31

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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@geops/rvf-mobility-web-component",
3
3
  "license": "UNLICENSED",
4
4
  "description": "Web components for rvf in the domains of mobility and logistics.",
5
- "version": "0.1.30",
5
+ "version": "0.1.31",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
@@ -21,6 +21,7 @@
21
21
  "react-icons": "^5.4.0",
22
22
  "react-spatial": "^1.12.2",
23
23
  "rosetta": "^1.1.0",
24
+ "showdown": "^2.1.0",
24
25
  "tailwind-merge": "^2.6.0"
25
26
  },
26
27
  "devDependencies": {
@@ -1,3 +1,5 @@
1
+ import { MaplibreStyleLayer } from "mobility-toolbox-js/ol";
2
+ import { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreStyleLayer";
1
3
  import { useEffect, useMemo, useState } from "preact/hooks";
2
4
 
3
5
  import useMapContext from "../utils/hooks/useMapContext";
@@ -86,7 +88,8 @@ const useNotifications = () => {
86
88
  abortCtrl = new AbortController();
87
89
  const response = await fetch(url, { signal: abortCtrl.signal });
88
90
  const data = await response.json();
89
- setNotifications(getNotificationsWithStatus(data, now));
91
+ const notifications = getNotificationsWithStatus(data, now);
92
+ setNotifications(notifications);
90
93
  setShouldAddPreviewNotifications(true);
91
94
  };
92
95
 
@@ -150,7 +153,34 @@ const useNotifications = () => {
150
153
  return notifications;
151
154
  };
152
155
 
153
- export default function NotificationLayer() {
156
+ export default function NotificationLayer(props: MaplibreStyleLayerOptions) {
154
157
  useNotifications();
158
+ const { baseLayer, map } = useMapContext();
159
+
160
+ const layer = useMemo(() => {
161
+ if (!baseLayer) {
162
+ return null;
163
+ }
164
+ return new MaplibreStyleLayer({
165
+ isQueryable: true,
166
+ layersFilter: ({ metadata }) => {
167
+ return metadata?.["general.filter"] === "notifications";
168
+ },
169
+ maplibreLayer: baseLayer,
170
+ ...(props || {}),
171
+ });
172
+ }, [baseLayer, props]);
173
+
174
+ useEffect(() => {
175
+ if (!map || !layer) {
176
+ return;
177
+ }
178
+ map.addLayer(layer);
179
+
180
+ return () => {
181
+ map.removeLayer(layer);
182
+ };
183
+ }, [map, layer]);
184
+
155
185
  return null;
156
186
  }
@@ -142,6 +142,7 @@ const addNotificationsLayers = (
142
142
  features,
143
143
  type: "FeatureCollection",
144
144
  },
145
+ generateId: true,
145
146
  type: "geojson",
146
147
  },
147
148
  [
@@ -154,6 +155,9 @@ const addNotificationsLayers = (
154
155
  ],
155
156
  id: "notificationsActive",
156
157
  layout: { visibility: "visible" },
158
+ metadata: {
159
+ "general.filter": "notifications",
160
+ },
157
161
  paint: {
158
162
  "line-color": "rgba(255,0,0,1)",
159
163
  "line-dasharray": [2, 2],
@@ -3,7 +3,9 @@ import type { JSX, PreactDOMAttributes } from "preact";
3
3
  import { memo } from "preact/compat";
4
4
 
5
5
  import useRvfContext from "../utils/hooks/useRvfContext";
6
- import RVFSellingPointDetails from "./RVFSellingPointDetails/RVFSellingPointDetails";
6
+ import RvfLineNetworkDetails from "./RvfLineNetworkDetails/RvfLineNetworkDetails";
7
+ import RvfNotificationDetails from "./RvfNotificationDetails";
8
+ import RvfSellingPointDetails from "./RvfSellingPointDetails";
7
9
  import RvfSharedMobilityDetails from "./RvfSharedMobilityDetail";
8
10
 
9
11
  export type RvfFeatureDetailsProps = JSX.HTMLAttributes<HTMLDivElement> &
@@ -20,9 +22,11 @@ const getIsSharedMobility = (selectedFeature): boolean => {
20
22
  };
21
23
 
22
24
  function RvfFeatureDetails(props: RvfFeatureDetailsProps) {
23
- const { selectedFeature } = useRvfContext();
25
+ const { selectedFeature, selectedFeatures } = useRvfContext();
24
26
  const isSharedMobility = getIsSharedMobility(selectedFeature);
25
27
  const isSellingPoint = !!selectedFeature.get("tickets");
28
+ const isNotification = !!selectedFeature.get("disruption_type");
29
+ const isLineNetwork = !!selectedFeature.get("original_line_id");
26
30
 
27
31
  const showDefaultData = () => {
28
32
  return Object.entries(selectedFeature.getProperties()).map(
@@ -41,11 +45,22 @@ function RvfFeatureDetails(props: RvfFeatureDetailsProps) {
41
45
 
42
46
  return (
43
47
  <div {...props}>
44
- {isSellingPoint && <RVFSellingPointDetails feature={selectedFeature} />}
48
+ {isSellingPoint && <RvfSellingPointDetails feature={selectedFeature} />}
45
49
  {isSharedMobility && (
46
50
  <RvfSharedMobilityDetails selectedFeature={selectedFeature} />
47
51
  )}
48
- {!isSharedMobility && !isSellingPoint && showDefaultData()}
52
+ {isNotification && <RvfNotificationDetails feature={selectedFeature} />}
53
+ {isLineNetwork && (
54
+ <RvfLineNetworkDetails
55
+ feature={selectedFeature}
56
+ features={selectedFeatures}
57
+ />
58
+ )}
59
+ {!isLineNetwork &&
60
+ !isNotification &&
61
+ !isSharedMobility &&
62
+ !isSellingPoint &&
63
+ showDefaultData()}
49
64
  </div>
50
65
  );
51
66
  }
@@ -0,0 +1,98 @@
1
+ import { Feature } from "ol";
2
+ import { useEffect, useMemo, useState } from "preact/hooks";
3
+
4
+ import useMapContext from "../../utils/hooks/useMapContext";
5
+
6
+ let cacheLineInfosById = null;
7
+
8
+ function RvfLineNetworkDetails({
9
+ feature,
10
+ features,
11
+ }: {
12
+ feature: Feature;
13
+ features: Feature[];
14
+ }) {
15
+ const { apikey, mapsurl } = useMapContext();
16
+ const [lineInfos, setLineInfos] = useState(null);
17
+ useEffect(() => {
18
+ const fetchInfos = async () => {
19
+ if (!cacheLineInfosById) {
20
+ const response = await fetch(
21
+ `${mapsurl}/data/network_plans_rvf_prototype.json?key=${apikey}`,
22
+ );
23
+ const data = await response.json();
24
+ cacheLineInfosById = data["geops.lnp.lines"];
25
+ }
26
+ setLineInfos(cacheLineInfosById);
27
+ };
28
+ fetchInfos();
29
+ }, [apikey, mapsurl]);
30
+
31
+ const lineNetworkIdsByOperator = useMemo(() => {
32
+ const byOperators = {};
33
+
34
+ [
35
+ ...new Set(
36
+ features.map((f) => {
37
+ return f.get("original_line_id");
38
+ }),
39
+ ),
40
+ ]
41
+ .filter((id) => {
42
+ return !!id && !!lineInfos?.[id];
43
+ })
44
+ .map((id) => {
45
+ const { operator_name: operatorName } = lineInfos[id];
46
+ if (!byOperators[operatorName]) {
47
+ byOperators[operatorName] = [];
48
+ }
49
+ byOperators[operatorName].push(lineInfos[id]);
50
+ });
51
+
52
+ return byOperators;
53
+ }, [features, lineInfos]);
54
+
55
+ if (!feature || !lineInfos) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <div className="flex flex-col gap-4">
61
+ {Object.entries(lineNetworkIdsByOperator).map(
62
+ ([operatorName, linesInfos]) => {
63
+ return (
64
+ <div className={"flex flex-col gap-2"} key={operatorName}>
65
+ <div>{operatorName}</div>
66
+ {linesInfos.map((lineInfo) => {
67
+ const {
68
+ // color,
69
+ // external_id,
70
+ long_name: longName,
71
+ short_name: shortName,
72
+ // text_color,
73
+ } = lineInfo;
74
+
75
+ return (
76
+ <div className={"flex items-center gap-2"} key={shortName}>
77
+ <div>
78
+ <div
79
+ className={
80
+ "rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
81
+ }
82
+ >
83
+ {shortName}
84
+ </div>
85
+ </div>
86
+ <div>{longName}</div>
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ );
92
+ },
93
+ )}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export default RvfLineNetworkDetails;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfLineNetworkDetails";
@@ -0,0 +1,77 @@
1
+ import { Feature } from "ol";
2
+ import showdown from "showdown";
3
+
4
+ import RvfLink from "../../RvfLink";
5
+ const converter = new showdown.Converter();
6
+ // text = "# hello, markdown!",
7
+ // html = converter.makeHtml(text);
8
+ function RvfNotificationDetails({ feature }: { feature: Feature }) {
9
+ const {
10
+ affected_products: affectedProducts,
11
+ affected_time_intervals: timeIntervals,
12
+ links,
13
+ long_description: description,
14
+ title,
15
+ } = feature.getProperties();
16
+
17
+ let end = "",
18
+ start = "";
19
+ let products = [];
20
+ let externalLinks = [];
21
+ try {
22
+ const timeInterval = JSON.parse(timeIntervals)?.[0] || {};
23
+ const dateStart = new Date(timeInterval.start);
24
+ start =
25
+ dateStart.toLocaleDateString() + " " + dateStart.toLocaleTimeString();
26
+ const dateEnd = new Date(timeInterval.end);
27
+ end = dateEnd.toLocaleDateString() + " " + dateEnd.toLocaleTimeString();
28
+
29
+ products = JSON.parse(affectedProducts);
30
+ products.sort((a, b) => {
31
+ return a.name.localeCompare(b.name);
32
+ });
33
+ externalLinks = JSON.parse(links);
34
+ } catch (e) {
35
+ console.error(e);
36
+ }
37
+ return (
38
+ <div className={"flex flex-col gap-4 text-sm"}>
39
+ <div>
40
+ <div className="text-base font-bold">{title}</div>
41
+ <div className="text-xs">
42
+ {start} - {end}
43
+ </div>
44
+ </div>
45
+ <div
46
+ className={"flex flex-col gap-2"}
47
+ dangerouslySetInnerHTML={{
48
+ __html: converter.makeHtml(description).replace("<hr />", "<br />"),
49
+ }}
50
+ ></div>
51
+ {externalLinks?.map(({ label, uri }) => {
52
+ return (
53
+ <RvfLink href={uri} key={uri}>
54
+ {label}
55
+ </RvfLink>
56
+ );
57
+ })}
58
+ <div className={"font-bold"}>Betroffened Lines:</div>
59
+ <div className={"flex flex-wrap gap-1"}>
60
+ {products?.map(({ name }) => {
61
+ return (
62
+ <div
63
+ className={
64
+ "rounded-md bg-red px-[12px] py-[9px] font-bold leading-none text-white"
65
+ }
66
+ key={name}
67
+ >
68
+ {name}
69
+ </div>
70
+ );
71
+ })}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export default RvfNotificationDetails;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfNotificationDetails";
@@ -10,7 +10,7 @@ const ICON_BY_OPERATED_BY = {
10
10
  Videozentrum: <Video />,
11
11
  };
12
12
 
13
- function RVFSellingPointDetails({ feature }: { feature: Feature }) {
13
+ function RvfSellingPointDetails({ feature }: { feature: Feature }) {
14
14
  if (!feature) {
15
15
  return null;
16
16
  }
@@ -44,4 +44,4 @@ function RVFSellingPointDetails({ feature }: { feature: Feature }) {
44
44
  );
45
45
  }
46
46
 
47
- export default RVFSellingPointDetails;
47
+ export default RvfSellingPointDetails;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfSellingPointDetails";
@@ -16,7 +16,6 @@ function RvfLineNetworkPlanLayer(props: MaplibreStyleLayerOptions) {
16
16
  return null;
17
17
  }
18
18
  return new MaplibreStyleLayer({
19
- isQueryable: true,
20
19
  layersFilter: ({ metadata }) => {
21
20
  return (
22
21
  metadata?.["rvf.filter"] === "netzplan_lines" ||
@@ -24,8 +23,10 @@ function RvfLineNetworkPlanLayer(props: MaplibreStyleLayerOptions) {
24
23
  );
25
24
  },
26
25
  maplibreLayer: baseLayer,
27
- minZoom: 10,
28
26
  name: RVF_LAYERS_NAMES.liniennetz,
27
+ queryRenderedLayersFilter: ({ metadata }) => {
28
+ return metadata?.["rvf.filter"] === "netzplan_trips_info";
29
+ },
29
30
  ...(props || {}),
30
31
  });
31
32
  }, [baseLayer, props]);
@@ -117,9 +117,7 @@ const realtimeLayerProps = {
117
117
  { properties: { delay: delayA, type: typeA } },
118
118
  { properties: { delay: delayB, type: typeB } },
119
119
  ) => {
120
- // console.log(trajA, trajB);
121
120
  if (typeA !== typeB) {
122
- // console.log(trajA.properties.type, trajB.properties.type);
123
121
  if (PRIORITY_FROM_TYPE[typeA] < PRIORITY_FROM_TYPE[typeB]) {
124
122
  return -1;
125
123
  }
@@ -520,7 +518,7 @@ function RvfMobilityMap({
520
518
  <MapContext.Provider value={mapContextValue}>
521
519
  <RvfContext.Provider value={rvfContextValue}>
522
520
  <div
523
- className="relative size-full border font-sans @container/main"
521
+ className="relative size-full overflow-hidden rounded-[16px] border font-sans @container/main"
524
522
  ref={eventNodeRef}
525
523
  style={styleProps}
526
524
  >
@@ -549,7 +547,7 @@ function RvfMobilityMap({
549
547
  title={RVF_LAYERS_TITLES.verkaufsstellen}
550
548
  />
551
549
  <RvfLineNetworkPlanLayer
552
- isQueryable={false}
550
+ isQueryable={true}
553
551
  minZoom={10}
554
552
  title={RVF_LAYERS_TITLES.liniennetz}
555
553
  />
@@ -666,7 +664,7 @@ function RvfMobilityMap({
666
664
  {hasToolbar && (
667
665
  <div
668
666
  className={
669
- "z-[100] flex justify-around overflow-x-hidden border-t bg-white p-1 @lg/main:block @lg/main:border-r @lg/main:p-0 "
667
+ "z-[100] flex justify-around overflow-x-hidden border-t bg-white p-1 @lg/main:block @lg/main:border-r @lg/main:border-t-0 @lg/main:p-0 "
670
668
  }
671
669
  >
672
670
  {hasLayerTree && (
@@ -61,7 +61,15 @@ function SingleClickListener() {
61
61
  feature.set("num_vehicles_available", feature.get("point_count"));
62
62
  }
63
63
 
64
- source?.setData(geojson.writeFeatureObject(feature));
64
+ if (!feature.get("disruption_type")) {
65
+ try {
66
+ const wrote = geojson.writeFeatureObject(feature);
67
+ source?.setData(wrote);
68
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
69
+ } catch (error) {
70
+ // console.error(e);
71
+ }
72
+ }
65
73
  }
66
74
  }
67
75
 
@@ -13,11 +13,15 @@ const getFeatureInformationTitle = (feature: Feature) => {
13
13
  const selectedFeature = features?.[0] || feature;
14
14
  const {
15
15
  category,
16
+ disruption_type: disruptionType,
16
17
  feed_id: feedId,
17
18
  long_name: longName,
18
19
  tickets,
19
20
  } = selectedFeature.getProperties();
20
21
 
22
+ if (disruptionType) {
23
+ return "MOCO Meldung";
24
+ }
21
25
  if (category) {
22
26
  return TITLE_BY_CATEGORY[category] || defaultTitle;
23
27
  }
@@ -86,7 +86,7 @@ export default {
86
86
  lg: getClamp(19, 25), // H4
87
87
  sm: getClamp(13, 15), // small
88
88
  xl: getClamp(24, 31), // h3
89
- xs: getClamp(13, 15), // small
89
+ xs: getClamp(12, 13), // xtra small
90
90
  },
91
91
  },
92
92
  };
@@ -1 +0,0 @@
1
- export { default } from "./RVFSellingPointDetails";