@geops/rvf-mobility-web-component 0.1.63 → 0.1.65

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 (39) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/docutils.js +29 -19
  3. package/index.html +0 -1
  4. package/index.js +278 -237
  5. package/package.json +12 -12
  6. package/src/BaseLayer/BaseLayer.tsx +18 -2
  7. package/src/Copyright/Copyright.tsx +2 -2
  8. package/src/Copyright/index.tsx +1 -1
  9. package/src/LinesNetworkPlanLayerHighlight/LinesNetworkPlanLayerHighlight.tsx +1 -0
  10. package/src/Map/Map.tsx +27 -1
  11. package/src/MapLayout/MapLayout.tsx +1 -1
  12. package/src/MapLayout/index.tsx +1 -1
  13. package/src/MobilityMap/MobilityMap.tsx +5 -11
  14. package/src/MobilityMap/MobilityMapAttributes.ts +8 -5
  15. package/src/NotificationDetails/NotificationDetails.tsx +75 -57
  16. package/src/Permalink/Permalink.tsx +17 -6
  17. package/src/PermalinkInput/PermalinkInput.tsx +4 -1
  18. package/src/RealtimeLayer/index.tsx +1 -1
  19. package/src/RvfCopyright/RvfCopyright.tsx +32 -0
  20. package/src/RvfCopyright/index.tsx +1 -0
  21. package/src/RvfInputCopy/RvfInputCopy.tsx +18 -8
  22. package/src/RvfMapLayout/RvfMapLayout.tsx +198 -0
  23. package/src/RvfMapLayout/index.tsx +1 -0
  24. package/src/RvfMobilityMap/RvfMobilityMap.tsx +10 -580
  25. package/src/RvfRealtimeLayer/RvfRealtimeLayer.tsx +64 -0
  26. package/src/RvfRealtimeLayer/index.tsx +1 -0
  27. package/src/RvfStationsLayer/RvfStationsLayer.tsx +19 -0
  28. package/src/RvfStationsLayer/index.tsx +1 -0
  29. package/src/ShareMenu/ShareMenu.tsx +3 -1
  30. package/src/StationsLayer/index.tsx +1 -1
  31. package/src/ui/InputCopy/InputCopy.tsx +21 -10
  32. package/src/utils/constants.ts +1 -1
  33. package/src/utils/getUrlFromTemplate.test.ts +23 -0
  34. package/src/utils/getUrlFromTemplate.ts +47 -0
  35. package/src/utils/hooks/useI18n.tsx +2 -4
  36. package/src/utils/hooks/useInitialLayersVisiblity.tsx +27 -4
  37. package/src/utils/hooks/useInitialPermalink.tsx +31 -21
  38. package/src/utils/hooks/usePermalink.tsx +25 -0
  39. package/src/utils/translations.ts +4 -0
@@ -0,0 +1,19 @@
1
+ import { memo } from "preact/compat";
2
+
3
+ import StationsLayer from "../StationsLayer/StationsLayer";
4
+
5
+ import type { MaplibreStyleLayerOptions } from "mobility-toolbox-js/ol";
6
+
7
+ const stationsLayerProps = () => {
8
+ return {
9
+ layersFilter: ({ metadata }) => {
10
+ return metadata?.["general.filter"] === "stations";
11
+ },
12
+ minZoom: 10,
13
+ };
14
+ };
15
+ function RvfStationsLayer(props: Partial<MaplibreStyleLayerOptions>) {
16
+ return <StationsLayer {...stationsLayerProps} {...props} />;
17
+ }
18
+
19
+ export default memo(RvfStationsLayer);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfStationsLayer";
@@ -7,6 +7,7 @@ import PermalinkInput from "../PermalinkInput";
7
7
  import Button from "../ui/Button";
8
8
  import useI18n from "../utils/hooks/useI18n";
9
9
  import useMapContext from "../utils/hooks/useMapContext";
10
+ import usePermalink from "../utils/hooks/usePermalink";
10
11
 
11
12
  import type { HTMLAttributes, PreactDOMAttributes } from "preact";
12
13
 
@@ -16,13 +17,14 @@ function ShareMenu({
16
17
  }: HTMLAttributes<HTMLDivElement> & PreactDOMAttributes) {
17
18
  const { map } = useMapContext();
18
19
  const { t } = useI18n();
20
+ const permalink = usePermalink();
19
21
 
20
22
  return (
21
23
  // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
22
24
  <div className={twMerge(`flex flex-col gap-4 ${className}`)} {...props}>
23
25
  <Button
24
26
  className="w-fit"
25
- href={`mailto:?subject=Karte&body=${window?.location.href}`}
27
+ href={`mailto:?subject=Karte&body=${permalink}`}
26
28
  >
27
29
  <Email />
28
30
  <span>{t("share_email_send")}</span>
@@ -1 +1 @@
1
- export { default } from "./StationsLayer";
1
+ export { default } from "../RvfStationsLayer";
@@ -2,6 +2,7 @@ import { useId, useState } from "preact/hooks";
2
2
  import { twMerge } from "tailwind-merge";
3
3
 
4
4
  import Copy from "../../icons/Copy";
5
+ import useI18n from "../../utils/hooks/useI18n";
5
6
  import IconButton from "../IconButton";
6
7
  import Input from "../Input";
7
8
 
@@ -24,31 +25,38 @@ function InputCopy({
24
25
  tooltipProps = emptyProps,
25
26
  ...props
26
27
  }: InputCopyProps) {
28
+ const { t } = useI18n();
27
29
  const [positionTooltip, setPositionTooltip] = useState<DOMRect>();
28
30
  const [isTooptipShowed, setIsTooltipShowed] = useState(false);
29
31
  const inputId = useId();
32
+ const [node, setNode] = useState<HTMLDivElement | null>(null);
30
33
 
31
34
  const handleCopyClick = (event) => {
32
35
  setPositionTooltip(event.currentTarget.getBoundingClientRect());
33
- void navigator.clipboard.writeText(window?.location.href).then(() => {
36
+ const input: HTMLInputElement | null = node.querySelector(`#${inputId}`);
37
+ void navigator.clipboard.writeText(input?.value).then(() => {
34
38
  setIsTooltipShowed(true);
35
39
  setTimeout(() => {
36
40
  setIsTooltipShowed(false);
37
41
  }, 1000);
38
42
  });
39
- (document.getElementById(inputId) as HTMLInputElement | null)?.select();
43
+ input?.select();
40
44
  };
41
45
 
42
46
  const handleInputFocus = () => {
43
- (document.getElementById(inputId) as HTMLInputElement | null)?.select();
47
+ const input: HTMLInputElement | null = node.querySelector(`#${inputId}`);
48
+ input?.select();
44
49
  };
45
50
 
46
51
  return (
47
52
  <div
53
+ ref={(elt) => {
54
+ setNode(elt);
55
+ }}
48
56
  {...containerProps}
49
57
  className={twMerge(
50
- "relative flex items-center",
51
- containerProps?.className,
58
+ "relative flex h-7 items-center",
59
+ containerProps?.className as string,
52
60
  )}
53
61
  >
54
62
  <Input
@@ -57,27 +65,30 @@ function InputCopy({
57
65
  readOnly
58
66
  type="text"
59
67
  {...props}
60
- className={twMerge("h-full flex-1 border border-r-0", props?.className)}
68
+ className={twMerge(
69
+ "h-7 flex-1 border border-r-0",
70
+ props?.className as string,
71
+ )}
61
72
  />
62
73
  <IconButton
63
- className="h-full rounded-none border p-2 shadow-none"
74
+ className="size-7 rounded-none border p-2 shadow-none"
64
75
  onClick={handleCopyClick}
65
76
  >
66
- <Copy />
77
+ <Copy size={20} />
67
78
  </IconButton>
68
79
  <div
69
80
  {...tooltipProps}
70
81
  className={twMerge(
71
82
  `fixed hidden rounded bg-gray-600 p-1 text-sm text-white`,
72
83
  isTooptipShowed && "block",
73
- tooltipProps?.className,
84
+ tooltipProps?.className as string,
74
85
  )}
75
86
  style={{
76
87
  left: positionTooltip?.left - 30,
77
88
  top: positionTooltip?.top - 30,
78
89
  }}
79
90
  >
80
- Kopiert!
91
+ {t("input_copy_success")}!
81
92
  </div>
82
93
  </div>
83
94
  );
@@ -71,6 +71,7 @@ export const DEFAULT_QUERYABLE_LAYERS = Object.values(LAYERS_NAMES).filter(
71
71
  // Order of the first level
72
72
  export const LAYER_TREE_ORDER = [
73
73
  LAYER_NAME_NOTIFICATIONS,
74
+ LAYER_NAME_MAPSET,
74
75
  LAYER_NAME_REALTIME,
75
76
  LAYER_NAME_STATIONS,
76
77
  LAYER_NAME_LINESNETWORKPLAN,
@@ -78,7 +79,6 @@ export const LAYER_TREE_ORDER = [
78
79
  LAYERS_NAMES.verkaufsstellen,
79
80
  LAYERS_NAMES.pois,
80
81
  LAYERS_NAMES.sharedMobility,
81
- LAYER_NAME_MAPSET,
82
82
  ];
83
83
 
84
84
  export const LAYERS_WITH_LINK = Object.values(LAYERS_NAMES).filter((name) => {
@@ -0,0 +1,23 @@
1
+ import getUrlFromTemplate from "./getUrlFromTemplate";
2
+
3
+ describe("getUrlFromTemplate", () => {
4
+ it("should replace parameters in the template", () => {
5
+ const template = "?z={{z}}&x={{x}}&y={{y}}";
6
+ const params = new URLSearchParams({ x: "512", y: "200", z: "10" });
7
+ const url = getUrlFromTemplate(template, params);
8
+ expect(url).toBe("http://localhost/?z=10&x=512&y=200");
9
+ });
10
+
11
+ it("should handle hash templates", () => {
12
+ const template = "#/map/{{z}}/{{x}}/{{y}}";
13
+ const params = new URLSearchParams({ x: "512", y: "200", z: "10" });
14
+ const url = getUrlFromTemplate(template, params);
15
+ expect(url).toBe("http://localhost/#/map/10/512/200");
16
+ });
17
+
18
+ it("should handle null templates returning the window location", () => {
19
+ const params = new URLSearchParams({ x: "512", y: "200", z: "10" });
20
+ const url = getUrlFromTemplate(null, params);
21
+ expect(url).toBe("http://localhost/");
22
+ });
23
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Return an url as a string from a template and a list of parameters.
3
+ * Example:
4
+ * template: "https://example.com/{{z}}/{{x}}/{{y}}.png"
5
+ * params: {z: "10", x: "512", y: "512"}
6
+ * return: "https://example.com/10/512/512.png"
7
+ *
8
+ * If a parameter is missing, the template is not replaced.
9
+ *
10
+ * @param template - The url template containing parameters in the form {{param}}
11
+ * @param params - An object containing the parameters to replace in the template
12
+ * @returns The url with the parameters replaced
13
+ */
14
+ function getUrlFromTemplate(template: string, params: URLSearchParams): string {
15
+ let tpl = template || "";
16
+ params?.forEach((value, key) => {
17
+ tpl = tpl.replace(`{{${key}}}`, value);
18
+ });
19
+ if (tpl.startsWith("#")) {
20
+ tpl = `${window.location.href.split("#")[0]}${tpl}`;
21
+ } else if (tpl.startsWith("?") || !tpl) {
22
+ const existingParams = new URLSearchParams(window.location.search);
23
+
24
+ // Set current values of the template parameters
25
+ const tplParams = new URLSearchParams(tpl);
26
+ tplParams.forEach((value, key) => {
27
+ existingParams.set(key, value);
28
+ });
29
+
30
+ // Remove duplicated parameters if the template already includes them
31
+ if (template?.includes("{{x}}") && template?.includes("{{y}}")) {
32
+ existingParams.delete("center");
33
+ }
34
+ if (template?.includes("{{z}}")) {
35
+ existingParams.delete("zoom");
36
+ }
37
+
38
+ let str = existingParams.toString();
39
+ if (!str.startsWith("?") && tpl.length) {
40
+ str = `?${str}`;
41
+ }
42
+ tpl = `${window.location.href.split("?")[0]}${str}${window.location.hash}`;
43
+ }
44
+ return tpl;
45
+ }
46
+
47
+ export default getUrlFromTemplate;
@@ -3,11 +3,9 @@ import { useContext } from "preact/hooks";
3
3
 
4
4
  import type { Rosetta } from "rosetta";
5
5
 
6
- import type { Translations } from "../translations";
6
+ export type I18NContextType = Rosetta<Record<string, string>>;
7
7
 
8
- export type I18NContextType = Rosetta<Translations>;
9
-
10
- export const I18nContext = createContext({
8
+ export const I18nContext = createContext<I18NContextType>({
11
9
  t: (id: string, templateValues?: Record<string, string>) => {
12
10
  return `${id} ${JSON.stringify(templateValues)}`;
13
11
  },
@@ -1,28 +1,51 @@
1
1
  import { getLayersAsFlatArray } from "mobility-toolbox-js/ol";
2
2
  import { unByKey } from "ol/Observable";
3
- import { useEffect } from "preact/hooks";
3
+ import { useEffect, useRef } from "preact/hooks";
4
4
 
5
5
  import applyInitialLayerVisibility from "../applyInitialLayerVisibility";
6
6
 
7
+ import useInitialPermalink from "./useInitialPermalink";
8
+
7
9
  import type { Map } from "ol";
10
+ const useInitialLayersVisiblity = (
11
+ map: Map,
12
+ layers: string,
13
+ permalinkTemplate: string,
14
+ ) => {
15
+ const isPermalinkAlreadyUsed = useRef(false);
16
+ const permalinkLayersRef = useRef(
17
+ useInitialPermalink(permalinkTemplate)?.layers,
18
+ );
8
19
 
9
- const useInitialLayersVisiblity = (map: Map, layers: string) => {
10
20
  // Apply initial visibility of layers from layers attribute
11
21
  useEffect(() => {
12
22
  if (!map) {
13
23
  return;
14
24
  }
25
+ let layersToUse = layers;
26
+
27
+ // We use the permalink param only once, at the first render
28
+ if (
29
+ permalinkLayersRef.current !== null &&
30
+ permalinkLayersRef.current !== undefined &&
31
+ !isPermalinkAlreadyUsed.current
32
+ ) {
33
+ layersToUse = permalinkLayersRef.current;
34
+ isPermalinkAlreadyUsed.current = true;
35
+ }
36
+
15
37
  getLayersAsFlatArray(map.getLayers().getArray()).forEach((layer) => {
16
- applyInitialLayerVisibility(layers, layer);
38
+ applyInitialLayerVisibility(layersToUse, layer);
17
39
  });
18
40
 
19
41
  const key = map.getLayers().on("add", (event) => {
20
- applyInitialLayerVisibility(layers, event.element);
42
+ applyInitialLayerVisibility(layersToUse, event.element);
21
43
  });
22
44
  return () => {
23
45
  unByKey(key);
24
46
  };
25
47
  }, [map, layers]);
48
+
26
49
  return null;
27
50
  };
28
51
 
@@ -1,27 +1,31 @@
1
- import { useMemo, useRef } from "preact/hooks";
1
+ import { useMemo } from "preact/hooks";
2
+
3
+ import useMapContext from "./useMapContext";
2
4
 
3
5
  import type { MobilityMapProps } from "../../MobilityMap/MobilityMap";
4
6
 
5
7
  /**
6
- * Return x,y and z values from the url. This hook should not managed more usecases than that.
8
+ * Return x,y,z and layers values from the url. This hook should not managed more usecases than that.
7
9
  * The application should be responsible to read url parameters then provides these parameters as attributes to the web-component.
8
10
  */
9
11
  const useInitialPermalink = (
10
- permalinktemplate: string,
12
+ permalinkTemplate?: string,
11
13
  ): null | Partial<MobilityMapProps> => {
12
- const prevProps = useRef<Partial<MobilityMapProps>>(null);
13
-
14
+ const { permalinktemplate } = useMapContext();
14
15
  const props = useMemo(() => {
15
- if (!permalinktemplate) {
16
+ const template = permalinkTemplate || permalinktemplate;
17
+ console.log("template", template);
18
+ if (!template) {
16
19
  return null;
17
20
  }
18
21
  try {
19
- let x: null | string,
22
+ let layers: null | string,
23
+ x: null | string,
20
24
  y: null | string,
21
- z: null | string = null;
25
+ z: null | string;
22
26
 
23
- if (permalinktemplate?.startsWith("?")) {
24
- const urlSearchParams = new URLSearchParams(permalinktemplate);
27
+ if (template?.startsWith("?")) {
28
+ const urlSearchParams = new URLSearchParams(template);
25
29
  const names = [...urlSearchParams.keys()];
26
30
  const nameX = names.find((name) => {
27
31
  return urlSearchParams.get(name).includes("{{x}}");
@@ -32,12 +36,16 @@ const useInitialPermalink = (
32
36
  const nameZ = names.find((name) => {
33
37
  return urlSearchParams.get(name).includes("{{z}}");
34
38
  });
39
+ const nameLayers = names.find((name) => {
40
+ return urlSearchParams.get(name).includes("{{layers}}");
41
+ });
35
42
  const currSearchParams = new URLSearchParams(window.location.search);
36
43
  x = currSearchParams.get(nameX);
37
44
  y = currSearchParams.get(nameY);
38
45
  z = currSearchParams.get(nameZ);
39
- } else if (permalinktemplate?.startsWith("#")) {
40
- const values = permalinktemplate.substring(1).split("/");
46
+ layers = currSearchParams.get(nameLayers);
47
+ } else if (template?.startsWith("#")) {
48
+ const values = template.substring(1).split("/");
41
49
  const currHash = window.location.hash;
42
50
  const currIndexes = currHash.substring(1).split("/");
43
51
  const indexX = values.findIndex((name) => {
@@ -49,9 +57,13 @@ const useInitialPermalink = (
49
57
  const indexZ = values.findIndex((name) => {
50
58
  return name.includes("{{z}}");
51
59
  });
60
+ const indexLayers = values.findIndex((name) => {
61
+ return name.includes("{{layers}}");
62
+ });
52
63
  x = indexX > -1 ? currIndexes[indexX] : null;
53
64
  y = indexY > -1 ? currIndexes[indexY] : null;
54
65
  z = indexZ > -1 ? currIndexes[indexZ] : null;
66
+ layers = indexLayers > -1 ? currIndexes[indexLayers] : null;
55
67
  }
56
68
  const propsFromPermalink: Partial<MobilityMapProps> = {};
57
69
  if (x && y) {
@@ -60,24 +72,22 @@ const useInitialPermalink = (
60
72
  if (z) {
61
73
  propsFromPermalink.zoom = z;
62
74
  }
75
+ if (layers !== null && layers !== undefined) {
76
+ propsFromPermalink.layers = layers;
77
+ }
63
78
  return propsFromPermalink;
64
79
  } catch (error) {
65
80
  // eslint-disable-next-line no-console
66
81
  console.warn(
67
- "Impossible to read x,y,z from the url with permalinktemplate",
68
- permalinktemplate,
82
+ "Impossible to read x,y,z and layers from the url with template",
83
+ template,
69
84
  error,
70
85
  );
71
86
  }
72
87
  return null;
73
- }, [permalinktemplate]);
88
+ }, [permalinktemplate, permalinkTemplate]);
74
89
 
75
- // We want to apply the value from the url only once
76
- if (!prevProps.current && props) {
77
- prevProps.current = props;
78
- return props;
79
- }
80
- return {};
90
+ return props;
81
91
  };
82
92
 
83
93
  export default useInitialPermalink;
@@ -0,0 +1,25 @@
1
+ // import debounce from "lodash.debounce";
2
+ // import { getLayersAsFlatArray } from "mobility-toolbox-js/ol";
3
+ // import { unByKey } from "ol/Observable";
4
+ // import { useCallback, useEffect } from "preact/hooks";
5
+
6
+ import getUrlFromTemplate from "../getUrlFromTemplate";
7
+
8
+ import useMapContext from "./useMapContext";
9
+
10
+ // import { LAYER_PROP_IS_EXPORTING } from "../constants";
11
+ // import getPermalinkParameters from "../getPermalinkParameters";
12
+ // // import getLayersAsFlatArray from "../getLayersAsFlatArray";
13
+
14
+ // import useMapContext from "./useMapContext";
15
+
16
+ // import type { Map } from "ol";
17
+ // import type { EventsKey } from "ol/events";
18
+
19
+ function usePermalink() {
20
+ const { permalinktemplate, permalinkUrlSearchParams } = useMapContext();
21
+ const url = getUrlFromTemplate(permalinktemplate, permalinkUrlSearchParams);
22
+ return url;
23
+ }
24
+
25
+ export default usePermalink;
@@ -20,6 +20,7 @@ const translations: Translations = {
20
20
  from_to: "von {{from}} bis {{to}}",
21
21
  geolocation_button_title_off: "Geolokalisierung deaktivieren",
22
22
  geolocation_button_title_on: "Geolokalisierung aktivieren",
23
+ input_copy_success: "Kopiert!",
23
24
  [LAYERS_NAMES.bikeFrelo]: "Frelo",
24
25
  [LAYERS_NAMES.bikeOthers]: "Fahrrad, andere Anbieter",
25
26
  [LAYERS_NAMES.cargobikeFrelo]: "Lastenfrelo",
@@ -76,6 +77,7 @@ const translations: Translations = {
76
77
  from_to: "from {{from}} to {{to}}",
77
78
  geolocation_button_title_off: "Disable geolocation",
78
79
  geolocation_button_title_on: "Enable geolocation",
80
+ input_copy_success: "Copied!",
79
81
  [LAYERS_NAMES.bikeFrelo]: "Frelo",
80
82
  [LAYERS_NAMES.bikeOthers]: "Bicycle, other providers",
81
83
  [LAYERS_NAMES.cargobikeFrelo]: "Cargo bike Frelo",
@@ -129,6 +131,7 @@ const translations: Translations = {
129
131
  from_to: "de {{from}} à {{to}}",
130
132
  geolocation_button_title_off: "Désactiver la géolocalisation",
131
133
  geolocation_button_title_on: "Activer la géolocalisation",
134
+ input_copy_success: "Copié!",
132
135
  [LAYERS_NAMES.bikeFrelo]: "Frelo",
133
136
  [LAYERS_NAMES.bikeOthers]: "Vélo, autres fournisseurs",
134
137
  [LAYERS_NAMES.cargobikeFrelo]: "Cargobike Frelo",
@@ -182,6 +185,7 @@ const translations: Translations = {
182
185
  from_to: "da {{from}} a {{to}}",
183
186
  geolocation_button_title_off: "Disattiva geolocalizzazione",
184
187
  geolocation_button_title_on: "Attiva geolocalizzazione",
188
+ input_copy_success: "Copiato!",
185
189
  [LAYERS_NAMES.bikeFrelo]: "Frelo",
186
190
  [LAYERS_NAMES.bikeOthers]: "Bicicletta, altri fornitori",
187
191
  [LAYERS_NAMES.cargobikeFrelo]: "Cargobike Frelo",