Apr 18, 2026 23.0 KB javascript of v17
1import { files } from './sdk.js'
2
3const AIRPORTS = {
4 ARN: [17.9186, 59.6519],
5 ATH: [23.9445, 37.9364],
6 BCN: [2.0785, 41.2971],
7 BEG: [20.3091, 44.8184],
8 BER: [13.5033, 52.3667],
9 BOG: [-74.1469, 4.7016],
10 BUD: [19.2556, 47.4298],
11 CAG: [9.0543, 39.2515],
12 CDG: [2.5479, 49.0097],
13 CGH: [-46.6564, -23.6261],
14 CPH: [12.6561, 55.6181],
15 CUN: [-86.8770, 21.0365],
16 CWL: [-3.3433, 51.3967],
17 DEN: [-104.6732, 39.8561],
18 DPS: [115.1671, -8.7482],
19 DSS: [-17.4902, 14.7397],
20 DUB: [-6.2701, 53.4264],
21 EDI: [-3.3725, 55.9500],
22 EPA: [-58.6126, -34.6099],
23 EWR: [-74.1687, 40.6895],
24 EZE: [-58.5356, -34.8222],
25 FCO: [12.2389, 41.8003],
26 FMM: [10.2395, 47.9888],
27 FRA: [8.5622, 50.0333],
28 GIG: [-43.2436, -22.8099],
29 GRU: [-46.4730, -23.4356],
30 GUA: [-90.5275, 14.5833],
31 GYE: [-79.8836, -2.1574],
32 HEL: [24.9633, 60.3172],
33 HKG: [113.9145, 22.3089],
34 HNL: [-157.9224, 21.3187],
35 HRK: [36.2860, 49.9248],
36 IAH: [-95.3414, 29.9844],
37 IGR: [-54.4734, -25.7373],
38 IST: [28.8141, 40.9769],
39 JFK: [-73.7781, 40.6413],
40 KBP: [30.8947, 50.3450],
41 KEF: [-22.6056, 63.9850],
42 KRK: [19.7836, 50.0777],
43 LAS: [-115.1523, 36.0840],
44 LAX: [-118.4085, 33.9425],
45 LHR: [-0.4614, 51.4700],
46 LIS: [-9.1354, 38.7742],
47 MAD: [-3.5673, 40.4936],
48 MEX: [-99.0721, 19.4363],
49 MIA: [-80.2906, 25.7959],
50 MID: [-89.6577, 20.9370],
51 MUC: [11.7861, 48.3538],
52 MUN: [11.7861, 48.3538],
53 NYO: [16.9122, 58.7886],
54 OAK: [-122.2208, 37.7213],
55 OGG: [-156.4305, 20.8986],
56 ORY: [2.3594, 48.7233],
57 OZH: [35.3157, 47.8671],
58 PDX: [-122.5975, 45.5898],
59 PHX: [-112.0116, 33.4373],
60 PRG: [14.2632, 50.1008],
61 PTY: [-79.3835, 9.0714],
62 PVR: [-105.2551, 20.6801],
63 PXM: [-97.0898, 15.8769],
64 SAN: [-117.1896, 32.7336],
65 SAW: [29.3092, 40.8986],
66 SCL: [-70.7858, -33.3930],
67 SDU: [-43.1631, -22.9105],
68 SFO: [-122.3790, 37.6213],
69 SIN: [103.9894, 1.3502],
70 SJU: [-66.0018, 18.4394],
71 SKP: [21.6214, 41.9617],
72 SLC: [-111.9791, 40.7884],
73 STL: [-90.3700, 38.7487],
74 TGD: [19.2519, 42.3593],
75 TIJ: [-116.9700, 32.5411],
76 TIV: [18.7233, 42.4047],
77 TLL: [24.8328, 59.4133],
78 TXL: [13.2877, 52.5597],
79 UIO: [-78.3575, -0.1292],
80 VIE: [16.5697, 48.1103],
81 WAW: [20.9671, 52.1657],
82}
83
84const CITIES = {
85 'Agoura Hills, US': [-118.7609, 34.1531],
86 'Athens, GR': [23.7275, 37.9838],
87 'Bacalar, MX': [-88.3918, 18.6813],
88 'Badajoz, ES': [-6.9706, 38.8794],
89 'Banos, EC': [-78.4247, -1.3928],
90 'Barcelona, ES': [2.1734, 41.3851],
91 'Becici, ME': [18.8567, 42.2886],
92 'Belgrade, RS': [20.4568, 44.7866],
93 'Berlin, DE': [13.4050, 52.5200],
94 'Bologna, IT': [11.3426, 44.4949],
95 'Brookfield, CT, US': [-73.4068, 41.4645],
96 'Buenos Aires, AR': [-58.3816, -34.6037],
97 'Cadiz, ES': [-6.2926, 36.5271],
98 'Cancun, MX': [-86.8515, 21.1619],
99 'Cardiff, GB': [-3.1791, 51.4816],
100 'Carmel-by-the-Sea, US': [-121.9233, 36.5552],
101 'Castelldefels, ES': [1.9767, 41.2800],
102 'Ciudad de Mexico, MX': [-99.1332, 19.4326],
103 'Denpasar, ID': [115.2126, -8.6500],
104 'Denver, US': [-104.9903, 39.7392],
105 'Dresden, DE': [13.7373, 51.0504],
106 'Drogobych, UA': [23.5064, 49.3497],
107 'Dubrovnik, HR': [18.0944, 42.6507],
108 'Edinburgh, GB': [-3.1883, 55.9533],
109 'El Palomar, AR': [-58.6126, -34.6099],
110 'Ellenville, NY, US': [-74.3968, 41.7170],
111 'Encinitas, US': [-117.2928, 33.0370],
112 'Florence, IT': [11.2558, 43.7696],
113 'Fremont, CA, US': [-121.9886, 37.5485],
114 'Gili Trawangan, ID': [116.0345, -8.3520],
115 'Glasgow, GB': [-4.2518, 55.8642],
116 'Guadalajara, MX': [-103.3496, 20.6597],
117 'Guatemala City, GT': [-90.5069, 14.6349],
118 'Hani i Hotit, AL': [19.5603, 42.5336],
119 'Helsinki, FI': [24.9384, 60.1699],
120 'Ivano-Frankivsk, UA': [24.7111, 48.9226],
121 'Kahului, HI, US': [-156.4729, 20.8893],
122 'Karlovy Vary, CZ': [12.8714, 50.2306],
123 'Kharkiv, UA': [36.2304, 49.9935],
124 'Kihei, HI, US': [-156.4659, 20.7644],
125 'Kornati, HR': [15.3000, 43.8000],
126 'Krakow, PL': [19.9450, 50.0647],
127 'Kramatorsk, UA': [37.5558, 48.7362],
128 'Kyiv, UA': [30.5234, 50.4501],
129 'Larkspur, CA, US': [-122.5352, 37.9341],
130 'Leon, NI': [-86.8780, 12.4346],
131 'Lisbon, PT': [-9.1393, 38.7223],
132 'Los Angeles, US': [-118.2437, 34.0522],
133 'Lviv, UA': [24.0297, 49.8397],
134 'Madrid, ES': [-3.7038, 40.4168],
135 'Mahahual, MX': [-87.7112, 18.7142],
136 'Mammoth, CA, US': [-118.9721, 37.6485],
137 'Managua, NI': [-86.2504, 12.1150],
138 'Manchester, GB': [-2.2426, 53.4808],
139 'Memmingen, DE': [10.1811, 47.9837],
140 'Merida, MX': [-89.5926, 20.9674],
141 'Milan, IT': [9.1900, 45.4642],
142 'Morelia, MX': [-101.1950, 19.7060],
143 'Munich, DE': [11.5820, 48.1351],
144 'Napa, US': [-122.2869, 38.2975],
145 'New York, US': [-74.0060, 40.7128],
146 'Newark, NJ, US': [-74.1724, 40.7357],
147 'Ohrid, MK': [20.8019, 41.1231],
148 'Panama City, PA': [-79.5197, 8.9824],
149 'Paris, FR': [2.3522, 48.8566],
150 'Playa del Carmen, MX': [-87.0739, 20.6296],
151 'Podgorica, ME': [19.2636, 42.4304],
152 'Prague, CZ': [14.4378, 50.0755],
153 'Provo, US': [-111.6585, 40.2338],
154 'Puerto Vallarta, MX': [-105.2253, 20.6534],
155 'Pylypets, UA': [23.3311, 48.6614],
156 'Quito, EC': [-78.4678, -0.1807],
157 'Rio de Janeiro, BR': [-43.1729, -22.9068],
158 'Rome, IT': [12.4964, 41.9028],
159 'Sacramento, US': [-121.4944, 38.5816],
160 'Salt Lake City, US': [-111.8910, 40.7608],
161 'San Clemente, US': [-117.6120, 33.4270],
162 'San Diego, US': [-117.1611, 32.7157],
163 'San Francisco, US': [-122.4194, 37.7749],
164 'San Jose, CR': [-84.0907, 9.9281],
165 'San Luis Obispo, US': [-120.6596, 35.2828],
166 'San Salvador, SV': [-89.1872, 13.6929],
167 'Santa Rosa, US': [-122.7141, 38.4405],
168 'Sao Paulo, BR': [-46.6333, -23.5505],
169 'Sarajevo, BA': [18.4131, 43.8563],
170 'Sayulita, MX': [-105.4413, 20.8692],
171 'Sevilla, ES': [-5.9845, 37.3891],
172 'Shkoder, AL': [19.5126, 42.0693],
173 'Skopje, MK': [21.4254, 41.9973],
174 'Split, HR': [16.4402, 43.5081],
175 'Stockton, US': [-121.2908, 37.9577],
176 'Tallinn, EE': [24.7536, 59.4370],
177 'Tegucigalpa, HN': [-87.2068, 14.0723],
178 'Temecula, US': [-117.1484, 33.4936],
179 'Thira, GR': [25.4615, 36.3932],
180 'Tijuana, MX': [-117.0382, 32.5149],
181 'Tirana, AL': [19.8187, 41.3275],
182 'Tivat, ME': [18.7233, 42.4047],
183 'Tulum, MX': [-87.4654, 20.2115],
184 'Vail, CO, US': [-106.3742, 39.6403],
185 'Vallejo, CA, US': [-122.2566, 38.1041],
186 'Venice, IT': [12.3155, 45.4408],
187 'Vorokhta, UA': [24.5417, 48.2747],
188 'Warsaw, PL': [21.0122, 52.2297],
189 'Washington, US': [-77.0369, 38.9072],
190 'Wroclaw, PL': [17.0385, 51.1079],
191 'Yosemite Village, CA, US': [-119.5936, 37.7490],
192 'Zadar, HR': [15.2314, 44.1194],
193 'Zagreb, HR': [15.9819, 45.8150],
194 'Zaporizhzhia, UA': [35.1396, 47.8388],
195 'Zurich, CH': [8.5417, 47.3769],
196}
197
198function resolveLocation(loc) {
199 var trimmed = loc.trim()
200 if (AIRPORTS[trimmed]) return AIRPORTS[trimmed]
201 if (CITIES[trimmed]) return CITIES[trimmed]
202 return null
203}
204
205function parseTrip(filename, content) {
206 var segments = []
207 var lines = content.split('\n')
208 var inTable = false
209
210 for (var i = 0; i < lines.length; i++) {
211 var line = lines[i]
212 if (line.match(/^\|\s*from\s*\|/i)) {
213 inTable = true
214 continue
215 }
216 if (inTable && line.match(/^\|\s*-/)) continue
217 if (inTable && line.startsWith('|')) {
218 var parts = line.split('|').map(function(p) { return p.trim() }).filter(Boolean)
219 if (parts.length >= 4) {
220 var fromCoord = resolveLocation(parts[0])
221 var toCoord = resolveLocation(parts[1])
222 if (fromCoord && toCoord) {
223 segments.push({
224 from: parts[0], to: parts[1], date: parts[2], mode: parts[3],
225 trip: filename, fromCoord: fromCoord, toCoord: toCoord,
226 })
227 }
228 }
229 } else if (inTable && !line.startsWith('|')) {
230 inTable = false
231 }
232 }
233 return segments
234}
235
236function isFlightMode(mode) {
237 return mode.toLowerCase().startsWith('flight')
238}
239
240// great circle interpolation for antimeridian-crossing flights
241// returns a point at the given fraction (0-1) along the great circle from→to
242function interpolateGreatCircle(from, to, fraction) {
243 var toRad = Math.PI / 180
244 var toDeg = 180 / Math.PI
245 var lat1 = from[1] * toRad, lon1 = from[0] * toRad
246 var lat2 = to[1] * toRad, lon2 = to[0] * toRad
247
248 var d = 2 * Math.asin(Math.sqrt(
249 Math.pow(Math.sin((lat1 - lat2) / 2), 2) +
250 Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lon1 - lon2) / 2), 2)
251 ))
252 if (d < 1e-10) return from.slice()
253
254 var A = Math.sin((1 - fraction) * d) / Math.sin(d)
255 var B = Math.sin(fraction * d) / Math.sin(d)
256 var x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2)
257 var y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2)
258 var z = A * Math.sin(lat1) + B * Math.sin(lat2)
259
260 return [
261 Math.atan2(y, x) * toDeg,
262 Math.atan2(z, Math.sqrt(x * x + y * y)) * toDeg,
263 ]
264}
265
266// for flights crossing the antimeridian (|lon diff| > 180), compute waypoints
267// so each sub-arc stays under 180° longitude — renderer draws them correctly
268function flightCoordinates(fromCoord, toCoord) {
269 if (Math.abs(fromCoord[0] - toCoord[0]) <= 180) {
270 return [fromCoord, toCoord]
271 }
272 // 3 intermediate points at 0.25, 0.5, 0.75 → 4 sub-arcs, each ~30-45° max
273 return [
274 fromCoord,
275 interpolateGreatCircle(fromCoord, toCoord, 0.25),
276 interpolateGreatCircle(fromCoord, toCoord, 0.5),
277 interpolateGreatCircle(fromCoord, toCoord, 0.75),
278 toCoord,
279 ]
280}
281
282// country tag → center coordinates + display name
283const COUNTRY_CENTERS = {
284 'albania': { name: 'Albania', iso: 'AL', center: [20.17, 41.15] },
285 'argentina': { name: 'Argentina', iso: 'AR', center: [-63.62, -38.42] },
286 'austria': { name: 'Austria', iso: 'AT', center: [14.55, 47.52] },
287 'bosnia': { name: 'Bosnia and Herzegovina', iso: 'BA', center: [17.68, 43.92] },
288 'bosnia-and-herzegovina': { name: 'Bosnia and Herzegovina', iso: 'BA', center: [17.68, 43.92] },
289 'brazil': { name: 'Brazil', iso: 'BR', center: [-51.93, -14.24] },
290 'canada': { name: 'Canada', iso: 'CA', center: [-106.35, 56.13] },
291 'chile': { name: 'Chile', iso: 'CL', center: [-71.54, -35.68] },
292 'colombia': { name: 'Colombia', iso: 'CO', center: [-74.30, 4.57] },
293 'costa-rica': { name: 'Costa Rica', iso: 'CR', center: [-83.75, 9.75] },
294 'croatia': { name: 'Croatia', iso: 'HR', center: [15.20, 45.10] },
295 'czechia': { name: 'Czechia', iso: 'CZ', center: [15.47, 49.82] },
296 'denmark': { name: 'Denmark', iso: 'DK', center: [9.50, 56.26] },
297 'ecuador': { name: 'Ecuador', iso: 'EC', center: [-78.18, -1.83] },
298 'el-salvador': { name: 'El Salvador', iso: 'SV', center: [-88.90, 13.79] },
299 'estonia': { name: 'Estonia', iso: 'EE', center: [25.01, 58.60] },
300 'finland': { name: 'Finland', iso: 'FI', center: [25.75, 61.92] },
301 'france': { name: 'France', iso: 'FR', center: [2.21, 46.23] },
302 'germany': { name: 'Germany', iso: 'DE', center: [10.45, 51.17] },
303 'greece': { name: 'Greece', iso: 'GR', center: [21.82, 39.07] },
304 'guatemala': { name: 'Guatemala', iso: 'GT', center: [-90.23, 15.78] },
305 'honduras': { name: 'Honduras', iso: 'HN', center: [-86.24, 15.20] },
306 'hong-kong': { name: 'Hong Kong', iso: 'HK', center: [114.17, 22.32] },
307 'hungary': { name: 'Hungary', iso: 'HU', center: [19.50, 47.16] },
308 'iceland': { name: 'Iceland', iso: 'IS', center: [-19.02, 64.96] },
309 'indonesia': { name: 'Indonesia', iso: 'ID', center: [113.92, -0.79] },
310 'ireland': { name: 'Ireland', iso: 'IE', center: [-8.24, 53.41] },
311 'italy': { name: 'Italy', iso: 'IT', center: [12.57, 41.87] },
312 'mexico': { name: 'Mexico', iso: 'MX', center: [-102.55, 23.63] },
313 'montenegro': { name: 'Montenegro', iso: 'ME', center: [19.37, 42.71] },
314 'nicaragua': { name: 'Nicaragua', iso: 'NI', center: [-85.21, 12.87] },
315 'north-macedonia': { name: 'North Macedonia', iso: 'MK', center: [21.75, 41.51] },
316 'panama': { name: 'Panama', iso: 'PA', center: [-80.78, 8.54] },
317 'poland': { name: 'Poland', iso: 'PL', center: [19.15, 51.92] },
318 'portugal': { name: 'Portugal', iso: 'PT', center: [-8.22, 39.40] },
319 'puerto-rico': { name: 'Puerto Rico', iso: 'PR', center: [-66.59, 18.22] },
320 'senegal': { name: 'Senegal', iso: 'SN', center: [-14.45, 14.50] },
321 'serbia': { name: 'Serbia', iso: 'RS', center: [21.01, 44.02] },
322 'singapore': { name: 'Singapore', iso: 'SG', center: [103.82, 1.35] },
323 'slovenia': { name: 'Slovenia', iso: 'SI', center: [14.99, 46.15] },
324 'spain': { name: 'Spain', iso: 'ES', center: [-3.75, 40.46] },
325 'sweden': { name: 'Sweden', iso: 'SE', center: [18.64, 60.13] },
326 'switzerland': { name: 'Switzerland', iso: 'CH', center: [8.23, 46.82] },
327 'turkey': { name: 'Turkey', iso: 'TR', center: [35.24, 38.96] },
328 'uk': { name: 'United Kingdom', iso: 'GB', center: [-3.44, 55.38] },
329 'ukraine': { name: 'Ukraine', iso: 'UA', center: [31.17, 48.38] },
330 'usa': { name: 'United States', iso: 'US', center: [-95.71, 37.09] },
331}
332
333// city tag → key in CITIES lookup (for tags that don't match directly)
334const CITY_TAG_MAP = {
335 'bogota': 'Buenos Aires, AR', // bogota not in CITIES, skip — handled by auto-match
336 'mexico-city': 'Ciudad de Mexico, MX',
337 'new-york': 'New York, US',
338 'buenos-aires': 'Buenos Aires, AR',
339 'rio-de-janeiro': 'Rio de Janeiro, BR',
340 'sao-paulo': 'Sao Paulo, BR',
341 'salt-lake-city': 'Salt Lake City, US',
342 'san-francisco': 'San Francisco, US',
343 'san-diego': 'San Diego, US',
344 'los-angeles': 'Los Angeles, US',
345 'san-clemente': 'San Clemente, US',
346 'san-luis-obispo': 'San Luis Obispo, US',
347 'puerto-vallarta': 'Puerto Vallarta, MX',
348 'playa-del-carmen': 'Playa del Carmen, MX',
349 'ivano-frankivsk': 'Ivano-Frankivsk, UA',
350 'gili-trawangan': 'Gili Trawangan, ID',
351 'guatemala-city': 'Guatemala City, GT',
352 'panama-city': 'Panama City, PA',
353 'karlovy-vary': 'Karlovy Vary, CZ',
354 'hani-i-hotit': 'Hani i Hotit, AL',
355 'el-palomar': 'El Palomar, AR',
356 'carmel-by-the-sea': 'Carmel-by-the-Sea, US',
357 'agoura-hills': 'Agoura Hills, US',
358 'yosemite-village': 'Yosemite Village, CA, US',
359 'las-vegas': null, // not in CITIES
360 'st-louis': null,
361 'hong-kong': null,
362 'san-jose': 'San Jose, CR',
363 'san-salvador': 'San Salvador, SV',
364 'santa-rosa': 'Santa Rosa, US',
365 'san-juan': null,
366}
367
368// extra city coordinates not in CITIES
369const EXTRA_CITIES = {
370 'bogota': [-74.0721, 4.7110],
371 'brooklyn': [-73.9442, 40.6782],
372 'budapest': [19.0402, 47.4979],
373 'cagliari': [9.1217, 39.2238],
374 'copenhagen': [12.5683, 55.6761],
375 'dakar': [-17.4677, 14.7167],
376 'detroit': [-83.0458, 42.3314],
377 'ensenada': [-116.5964, 31.8667],
378 'faro': [-7.9304, 37.0194],
379 'frankfurt': [8.6821, 50.1109],
380 'gdynia': [18.5305, 54.5189],
381 'geneva': [6.1432, 46.2044],
382 'guayaquil': [-79.8868, -2.1710],
383 'hong-kong': [114.1694, 22.3193],
384 'honolulu': [-157.8583, 21.3069],
385 'houston': [-95.3698, 29.7604],
386 'iguazu': [-54.5786, -25.5972],
387 'istanbul': [28.9784, 41.0082],
388 'kolobrzeg': [15.5761, 54.1758],
389 'koper': [13.7301, 45.5469],
390 'kotor': [18.7712, 42.4247],
391 'las-vegas': [-115.1398, 36.1699],
392 'ljubljana': [14.5058, 46.0569],
393 'london': [-0.1278, 51.5074],
394 'malaga': [-4.4214, 36.7213],
395 'miami': [-80.1918, 25.7617],
396 'phoenix': [-112.0740, 33.4484],
397 'portland': [-122.6750, 45.5152],
398 'poznan': [16.9252, 52.4064],
399 'puerto-escondido': [-97.0722, 15.8720],
400 'reykjavik': [-21.8174, 64.1466],
401 'sacramento': [-121.4944, 38.5816],
402 'san-juan': [-66.1057, 18.4655],
403 'santiago': [-70.6693, -33.4489],
404 'singapore': [103.8198, 1.3521],
405 'st-louis': [-90.1994, 38.6270],
406 'stockholm': [18.0686, 59.3293],
407 'stuttgart': [9.1829, 48.7758],
408 'swinoujscie': [14.2471, 53.9101],
409 'tegucigalpa': [-87.2068, 14.0723],
410 'toronto': [-79.3832, 43.6532],
411 'trieste': [13.7768, 45.6495],
412 'vienna': [16.3738, 48.2082],
413}
414
415function resolveCityCoord(tag) {
416 // check explicit mapping first
417 if (CITY_TAG_MAP[tag] !== undefined) {
418 if (CITY_TAG_MAP[tag] === null) {
419 // mapped but no CITIES entry — check EXTRA_CITIES
420 return EXTRA_CITIES[tag] || null
421 }
422 return CITIES[CITY_TAG_MAP[tag]] || null
423 }
424 // check EXTRA_CITIES
425 if (EXTRA_CITIES[tag]) return EXTRA_CITIES[tag]
426 // auto-match: find CITIES key that starts with the tag name (case-insensitive)
427 var tagLower = tag.replace(/-/g, ' ').toLowerCase()
428 var keys = Object.keys(CITIES)
429 for (var i = 0; i < keys.length; i++) {
430 if (keys[i].toLowerCase().startsWith(tagLower + ',') || keys[i].toLowerCase() === tagLower) {
431 return CITIES[keys[i]]
432 }
433 }
434 return null
435}
436
437function parseFrontmatter(content) {
438 var lines = content.split('\n')
439 var inFrontmatter = false
440 var tags = []
441 var title = ''
442 var date = ''
443 var inTags = false
444
445 for (var i = 0; i < lines.length; i++) {
446 var line = lines[i]
447 if (line.trim() === '---') {
448 if (inFrontmatter) break
449 inFrontmatter = true
450 continue
451 }
452 if (!inFrontmatter) continue
453 var titleMatch = line.match(/^title:\s*(.+)/)
454 if (titleMatch) title = titleMatch[1].trim()
455 var dateMatch = line.match(/^date:\s*(.+)/)
456 if (dateMatch) date = dateMatch[1].trim()
457 if (line.match(/^tags:\s*$/)) { inTags = true; continue }
458 if (inTags && line.match(/^\s+-\s+/)) {
459 tags.push(line.replace(/^\s+-\s+/, '').trim())
460 } else if (inTags && !line.match(/^\s/)) {
461 inTags = false
462 }
463 }
464
465 return { title: title, date: date, tags: tags }
466}
467
468export default async function () {
469 var allFiles = await files.list()
470 var mdFiles = allFiles.filter(function(f) { return f.endsWith('.md') })
471
472 var allSegments = []
473 var allTripMeta = [] // { filename, title, date, tags }
474
475 for (var i = 0; i < mdFiles.length; i++) {
476 var content = await files.read(mdFiles[i])
477 if (!content) continue
478 var segments = parseTrip(mdFiles[i], content)
479 allSegments.push.apply(allSegments, segments)
480 var meta = parseFrontmatter(content)
481 meta.filename = mdFiles[i]
482 allTripMeta.push(meta)
483 }
484
485 // per-trip geojson files (all segments — flights + routes + stops)
486 var segmentsByTrip = {}
487 for (var i = 0; i < allSegments.length; i++) {
488 var seg = allSegments[i]
489 if (!segmentsByTrip[seg.trip]) segmentsByTrip[seg.trip] = []
490 segmentsByTrip[seg.trip].push(seg)
491 }
492
493 var errors = []
494 var tripNames = Object.keys(segmentsByTrip)
495 for (var t = 0; t < tripNames.length; t++) {
496 var trip = tripNames[t]
497 var segs = segmentsByTrip[trip]
498 var features = []
499 var stops = {}
500 for (var j = 0; j < segs.length; j++) {
501 var s = segs[j]
502 features.push({
503 type: 'Feature',
504 properties: {
505 layer: isFlightMode(s.mode) ? 'flight' : 'route',
506 from: s.from, to: s.to, date: s.date, mode: s.mode,
507 },
508 geometry: { type: 'LineString', coordinates: [s.fromCoord, s.toCoord] },
509 })
510 stops[s.from] = s.fromCoord
511 stops[s.to] = s.toCoord
512 }
513 var stopNames = Object.keys(stops)
514 for (var k = 0; k < stopNames.length; k++) {
515 features.push({
516 type: 'Feature',
517 properties: { layer: 'stop', name: stopNames[k] },
518 geometry: { type: 'Point', coordinates: stops[stopNames[k]] },
519 })
520 }
521 var geoName = trip.replace(/\.md$/, '') + '.geojson'
522 try {
523 await files.write(geoName, JSON.stringify({ type: 'FeatureCollection', features: features }, null, 2))
524 } catch (e) {
525 errors.push(geoName + ': ' + e.message)
526 }
527 }
528
529 // --- flights.geojson: flights only (no trains, buses, cars) ---
530 var flightSegments = allSegments.filter(function(s) { return isFlightMode(s.mode) })
531 var flightFeatures = []
532 var flightStops = {}
533 for (var i = 0; i < flightSegments.length; i++) {
534 var seg = flightSegments[i]
535 flightFeatures.push({
536 type: 'Feature',
537 properties: {
538 layer: 'flight',
539 from: seg.from, to: seg.to, date: seg.date, mode: seg.mode, trip: seg.trip,
540 },
541 geometry: { type: 'LineString', coordinates: [seg.fromCoord, seg.toCoord] },
542 })
543 flightStops[seg.from] = seg.fromCoord
544 flightStops[seg.to] = seg.toCoord
545 }
546 var flightStopNames = Object.keys(flightStops)
547 for (var i = 0; i < flightStopNames.length; i++) {
548 flightFeatures.push({
549 type: 'Feature',
550 properties: { layer: 'stop', name: flightStopNames[i] },
551 geometry: { type: 'Point', coordinates: flightStops[flightStopNames[i]] },
552 })
553 }
554 try {
555 await files.write('flights.geojson', JSON.stringify({ type: 'FeatureCollection', features: flightFeatures }, null, 2))
556 } catch (e) {
557 errors.push('flights.geojson: ' + e.message)
558 }
559
560 // --- cities.geojson: city points from frontmatter tags ---
561 var cityVisits = {} // tag → { coord, trips: [{ filename, title, date }] }
562 for (var i = 0; i < allTripMeta.length; i++) {
563 var trip = allTripMeta[i]
564 for (var j = 0; j < trip.tags.length; j++) {
565 var tag = trip.tags[j]
566 if (!tag.startsWith('city/')) continue
567 var cityTag = tag.slice(5)
568 var coord = resolveCityCoord(cityTag)
569 if (!coord) continue
570 if (!cityVisits[cityTag]) cityVisits[cityTag] = { coord: coord, trips: [] }
571 cityVisits[cityTag].trips.push({ filename: trip.filename, title: trip.title, date: trip.date })
572 }
573 }
574 var cityFeatures = []
575 var cityTags = Object.keys(cityVisits)
576 for (var i = 0; i < cityTags.length; i++) {
577 var ct = cityTags[i]
578 var cv = cityVisits[ct]
579 var displayName = ct.replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase() })
580 cityFeatures.push({
581 type: 'Feature',
582 properties: {
583 layer: 'city',
584 name: displayName,
585 visitCount: cv.trips.length,
586 trips: cv.trips.map(function(t) { return { file: t.filename, title: t.title, date: t.date } }),
587 },
588 geometry: { type: 'Point', coordinates: cv.coord },
589 })
590 }
591 try {
592 await files.write('cities.geojson', JSON.stringify({ type: 'FeatureCollection', features: cityFeatures }, null, 2))
593 } catch (e) {
594 errors.push('cities.geojson: ' + e.message)
595 }
596
597 // --- countries.geojson: country points from frontmatter tags ---
598 var countryVisits = {} // tag → { info, trips: [{ filename, title, date }] }
599 for (var i = 0; i < allTripMeta.length; i++) {
600 var trip = allTripMeta[i]
601 for (var j = 0; j < trip.tags.length; j++) {
602 var tag = trip.tags[j]
603 if (!tag.startsWith('country/')) continue
604 var countryTag = tag.slice(8)
605 var info = COUNTRY_CENTERS[countryTag]
606 if (!info) continue
607 if (!countryVisits[countryTag]) countryVisits[countryTag] = { info: info, trips: [] }
608 countryVisits[countryTag].trips.push({ filename: trip.filename, title: trip.title, date: trip.date })
609 }
610 }
611 var countryFeatures = []
612 var countryTags = Object.keys(countryVisits)
613 for (var i = 0; i < countryTags.length; i++) {
614 var ctag = countryTags[i]
615 var cv = countryVisits[ctag]
616 countryFeatures.push({
617 type: 'Feature',
618 properties: {
619 layer: 'country',
620 name: cv.info.name,
621 iso: cv.info.iso,
622 visitCount: cv.trips.length,
623 trips: cv.trips.map(function(t) { return { file: t.filename, title: t.title, date: t.date } }),
624 },
625 geometry: { type: 'Point', coordinates: cv.info.center },
626 })
627 }
628 try {
629 await files.write('countries.geojson', JSON.stringify({ type: 'FeatureCollection', features: countryFeatures }, null, 2))
630 } catch (e) {
631 errors.push('countries.geojson: ' + e.message)
632 }
633
634 return new Response(JSON.stringify({
635 ok: true,
636 trips: tripNames.length,
637 segments: allSegments.length,
638 flights: flightSegments.length,
639 cities: cityTags.length,
640 countries: countryTags.length,
641 errors: errors,
642 }))
643}