File size: 4,066 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
 * URL state serialization for the Route Explorer modal.
 *
 * Format: `?explorer=from:CN,to:DE,hs:85,cargo:container,tab:1`
 *
 * Invalid values fall back to defaults silently — never throw — so the modal
 * always opens to a usable state regardless of where the URL came from.
 */

export type ExplorerCargo = 'container' | 'tanker' | 'bulk' | 'roro';
export type ExplorerTab = 1 | 2 | 3 | 4;

export interface ExplorerUrlState {
  fromIso2: string | null;
  toIso2: string | null;
  hs2: string | null;
  cargo: ExplorerCargo | null;
  tab: ExplorerTab;
}

export const DEFAULT_EXPLORER_STATE: ExplorerUrlState = {
  fromIso2: null,
  toIso2: null,
  hs2: null,
  cargo: null,
  tab: 1,
};

const EXPLORER_QUERY_KEY = 'explorer';
const VALID_CARGO: ReadonlySet<ExplorerCargo> = new Set([
  'container',
  'tanker',
  'bulk',
  'roro',
]);
const VALID_TABS: ReadonlySet<ExplorerTab> = new Set([1, 2, 3, 4]);
const ISO2_RE = /^[A-Z]{2}$/;
const HS2_RE = /^\d{1,2}$/;

/**
 * Parse the `explorer=...` query parameter into a typed state object.
 * Unknown or malformed fields fall back to defaults silently.
 */
export function parseExplorerUrl(search: string): ExplorerUrlState {
  let raw: string | null;
  try {
    raw = new URLSearchParams(search).get(EXPLORER_QUERY_KEY);
  } catch {
    return { ...DEFAULT_EXPLORER_STATE };
  }
  if (!raw) return { ...DEFAULT_EXPLORER_STATE };

  const out: ExplorerUrlState = { ...DEFAULT_EXPLORER_STATE };
  const parts = raw.split(',');
  for (const part of parts) {
    const [key, value] = part.split(':');
    if (!key || value === undefined) continue;
    switch (key.trim().toLowerCase()) {
      case 'from': {
        const v = value.trim().toUpperCase();
        if (ISO2_RE.test(v)) out.fromIso2 = v;
        break;
      }
      case 'to': {
        const v = value.trim().toUpperCase();
        if (ISO2_RE.test(v)) out.toIso2 = v;
        break;
      }
      case 'hs': {
        const v = value.trim();
        if (HS2_RE.test(v)) out.hs2 = v;
        break;
      }
      case 'cargo': {
        const v = value.trim().toLowerCase() as ExplorerCargo;
        if (VALID_CARGO.has(v)) out.cargo = v;
        break;
      }
      case 'tab': {
        const n = Number.parseInt(value.trim(), 10) as ExplorerTab;
        if (VALID_TABS.has(n)) out.tab = n;
        break;
      }
      default:
        break;
    }
  }
  return out;
}

/**
 * Serialize a state object into the `explorer=...` query value. Returns
 * `null` when no field is set so callers can remove the param entirely.
 */
export function serializeExplorerUrl(state: ExplorerUrlState): string | null {
  const parts: string[] = [];
  if (state.fromIso2 && ISO2_RE.test(state.fromIso2)) parts.push(`from:${state.fromIso2}`);
  if (state.toIso2 && ISO2_RE.test(state.toIso2)) parts.push(`to:${state.toIso2}`);
  if (state.hs2 && HS2_RE.test(state.hs2)) parts.push(`hs:${state.hs2}`);
  if (state.cargo && VALID_CARGO.has(state.cargo)) parts.push(`cargo:${state.cargo}`);
  if (state.tab !== 1 && VALID_TABS.has(state.tab)) parts.push(`tab:${state.tab}`);
  if (parts.length === 0) return null;
  return parts.join(',');
}

/**
 * Apply a state update to `window.location` without triggering a navigation.
 * No-op when running in a non-browser context (test runners, sidecar).
 */
export function writeExplorerUrl(state: ExplorerUrlState): void {
  if (typeof window === 'undefined' || !window.history?.replaceState) return;
  const url = new URL(window.location.href);
  const serialized = serializeExplorerUrl(state);
  if (serialized) {
    url.searchParams.set(EXPLORER_QUERY_KEY, serialized);
  } else {
    url.searchParams.delete(EXPLORER_QUERY_KEY);
  }
  window.history.replaceState(window.history.state, '', url.toString());
}

/**
 * Read state from the current `window.location`. Returns defaults when
 * running in a non-browser context.
 */
export function readExplorerUrl(): ExplorerUrlState {
  if (typeof window === 'undefined') return { ...DEFAULT_EXPLORER_STATE };
  return parseExplorerUrl(window.location.search);
}