add Google Maps route import and plugin corridor search

This commit is contained in:
Maurice
2026-09-12 12:24:33 +02:00
parent af45b0ca74
commit ff5fa55fad
59 changed files with 849 additions and 36 deletions
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'استيراد مسار Google Maps',
'roadtrip.import.note': 'تُضاف المحطات إلى اليوم المحدد. يعيد TREK حساب المسار.',
'roadtrip.import.unresolved': 'تعذر تحديد الموقع؛ سيتم تخطي هذه المحطة.',
'roadtrip.import.day': 'اليوم',
'roadtrip.hazards.show': 'عرض مناطق الخطر',
'roadtrip.hazards.current': 'التحذيرات الحالية',
'roadtrip.hazards.note': 'التحذيرات لا تعني إغلاق الطرق. يبقى المسار دون تغيير.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importar rota do Google Maps',
'roadtrip.import.note': 'As paradas são adicionadas ao dia escolhido. O TREK recalcula a rota.',
'roadtrip.import.unresolved': 'Local não encontrado; esta parada será ignorada.',
'roadtrip.import.day': 'Dia',
'roadtrip.hazards.show': 'Mostrar áreas de risco',
'roadtrip.hazards.current': 'Alertas atuais',
'roadtrip.hazards.note': 'Alertas não são interdições de vias. A rota não muda.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importa una ruta de Google Maps',
'roadtrip.import.note': 'Les parades s’afegeixen al dia triat. TREK recalcula la ruta.',
'roadtrip.import.unresolved': 'Ubicació no trobada; aquesta parada s’ometrà.',
'roadtrip.import.day': 'Dia',
'roadtrip.hazards.show': 'Mostra zones de perill',
'roadtrip.hazards.current': 'Avisos actuals',
'roadtrip.hazards.note': 'Els avisos no són talls de carretera. La ruta no canvia.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importovat trasu z Google Maps',
'roadtrip.import.note': 'Zastávky se přidají ke zvolenému dni. TREK trasu přepočítá.',
'roadtrip.import.unresolved': 'Místo nenalezeno; tato zastávka bude přeskočena.',
'roadtrip.import.day': 'Den',
'roadtrip.hazards.show': 'Zobrazit nebezpečné oblasti',
'roadtrip.hazards.current': 'Aktuální výstrahy',
'roadtrip.hazards.note': 'Výstrahy nejsou uzavírky silnic. Trasa se nemění.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google-Maps-Route importieren',
'roadtrip.import.note': 'Stopps werden an den gewählten Tag angehängt. TREK berechnet die Route neu.',
'roadtrip.import.unresolved': 'Ort nicht gefunden; dieser Stopp wird übersprungen.',
'roadtrip.import.day': 'Tag',
'roadtrip.hazards.show': 'Gefahrengebiete anzeigen',
'roadtrip.hazards.current': 'Aktuelle Warnungen',
'roadtrip.hazards.note': 'Warnungen sind keine Straßensperrungen. Die Route bleibt unverändert.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Import Google Maps route',
'roadtrip.import.note': 'Stops are appended to the selected day. TREK recalculates the route.',
'roadtrip.import.unresolved': 'Location unresolved; this stop will be skipped.',
'roadtrip.import.day': 'Day',
'roadtrip.hazards.show': 'Show hazard areas',
'roadtrip.hazards.current': 'Current warnings',
'roadtrip.hazards.note': 'Warnings are not road closures. Routes stay unchanged.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importar ruta de Google Maps',
'roadtrip.import.note': 'Las paradas se añaden al día elegido. TREK recalcula la ruta.',
'roadtrip.import.unresolved': 'Ubicación no encontrada; se omitirá esta parada.',
'roadtrip.import.day': 'Día',
'roadtrip.hazards.show': 'Mostrar zonas de peligro',
'roadtrip.hazards.current': 'Alertas actuales',
'roadtrip.hazards.note': 'Las alertas no son cierres de carreteras. La ruta no cambia.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importer un itinéraire Google Maps',
'roadtrip.import.note': 'Les étapes sont ajoutées au jour choisi. TREK recalcule le trajet.',
'roadtrip.import.unresolved': 'Lieu introuvable ; cette étape sera ignorée.',
'roadtrip.import.day': 'Jour',
'roadtrip.hazards.show': 'Afficher les zones à risque',
'roadtrip.hazards.current': 'Alertes actuelles',
'roadtrip.hazards.note': 'Les alertes ne sont pas des fermetures de routes. Le trajet reste inchangé.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Εισαγωγή διαδρομής Google Maps',
'roadtrip.import.note': 'Οι στάσεις προστίθενται στην επιλεγμένη ημέρα. Το TREK υπολογίζει ξανά τη διαδρομή.',
'roadtrip.import.unresolved': 'Η τοποθεσία δεν βρέθηκε· αυτή η στάση θα παραλειφθεί.',
'roadtrip.import.day': 'Ημέρα',
'roadtrip.hazards.show': 'Εμφάνιση επικίνδυνων περιοχών',
'roadtrip.hazards.current': 'Τρέχουσες προειδοποιήσεις',
'roadtrip.hazards.note': 'Οι προειδοποιήσεις δεν σημαίνουν κλειστούς δρόμους. Η διαδρομή δεν αλλάζει.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google Maps-útvonal importálása',
'roadtrip.import.note': 'A megállók a kiválasztott naphoz kerülnek. A TREK újratervezi az útvonalat.',
'roadtrip.import.unresolved': 'A hely nem található; ez a megálló kimarad.',
'roadtrip.import.day': 'Nap',
'roadtrip.hazards.show': 'Veszélyes területek megjelenítése',
'roadtrip.hazards.current': 'Aktuális figyelmeztetések',
'roadtrip.hazards.note': 'A figyelmeztetések nem útlezárások. Az útvonal változatlan marad.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Impor rute Google Maps',
'roadtrip.import.note': 'Perhentian ditambahkan ke hari yang dipilih. TREK menghitung ulang rute.',
'roadtrip.import.unresolved': 'Lokasi tidak ditemukan; perhentian ini akan dilewati.',
'roadtrip.import.day': 'Hari',
'roadtrip.hazards.show': 'Tampilkan area bahaya',
'roadtrip.hazards.current': 'Peringatan terkini',
'roadtrip.hazards.note': 'Peringatan bukan penutupan jalan. Rute tidak berubah.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importa percorso Google Maps',
'roadtrip.import.note': 'Le tappe vengono aggiunte al giorno scelto. TREK ricalcola il percorso.',
'roadtrip.import.unresolved': 'Posizione non trovata; questa tappa verrà saltata.',
'roadtrip.import.day': 'Giorno',
'roadtrip.hazards.show': 'Mostra zone di pericolo',
'roadtrip.hazards.current': 'Avvisi attuali',
'roadtrip.hazards.note': 'Gli avvisi non indicano strade chiuse. Il percorso non cambia.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google Mapsのルートをインポート',
'roadtrip.import.note': '立ち寄り先を選択した日に追加し、TREKでルートを再計算します。',
'roadtrip.import.unresolved': '場所が見つからないため、この立ち寄り先はスキップされます。',
'roadtrip.import.day': '日',
'roadtrip.hazards.show': '危険地域を表示',
'roadtrip.hazards.current': '現在の警報',
'roadtrip.hazards.note': '警報は通行止めを意味しません。ルートは変更されません。',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google Maps 경로 가져오기',
'roadtrip.import.note': '경유지를 선택한 날짜에 추가합니다. TREK이 경로를 다시 계산합니다.',
'roadtrip.import.unresolved': '위치를 찾을 수 없어 이 경유지를 건너뜁니다.',
'roadtrip.import.day': '일',
'roadtrip.hazards.show': '위험 지역 표시',
'roadtrip.hazards.current': '현재 경보',
'roadtrip.hazards.note': '경보는 도로 폐쇄를 의미하지 않습니다. 경로는 변경되지 않습니다.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google Maps-route importeren',
'roadtrip.import.note': 'Stops worden aan de gekozen dag toegevoegd. TREK berekent de route opnieuw.',
'roadtrip.import.unresolved': 'Locatie niet gevonden; deze stop wordt overgeslagen.',
'roadtrip.import.day': 'Dag',
'roadtrip.hazards.show': 'Gevarengebieden tonen',
'roadtrip.hazards.current': 'Actuele waarschuwingen',
'roadtrip.hazards.note': 'Waarschuwingen zijn geen wegafsluitingen. De route blijft ongewijzigd.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importuj trasę z Google Maps',
'roadtrip.import.note': 'Przystanki zostaną dodane do wybranego dnia. TREK przeliczy trasę.',
'roadtrip.import.unresolved': 'Nie znaleziono miejsca; ten przystanek zostanie pominięty.',
'roadtrip.import.day': 'Dzień',
'roadtrip.hazards.show': 'Pokaż obszary zagrożeń',
'roadtrip.hazards.current': 'Aktualne ostrzeżenia',
'roadtrip.hazards.note': 'Ostrzeżenia nie oznaczają zamknięcia dróg. Trasa pozostaje bez zmian.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Импорт маршрута Google Maps',
'roadtrip.import.note': 'Остановки добавляются в выбранный день. TREK пересчитает маршрут.',
'roadtrip.import.unresolved': 'Место не найдено; эта остановка будет пропущена.',
'roadtrip.import.day': 'День',
'roadtrip.hazards.show': 'Показывать опасные зоны',
'roadtrip.hazards.current': 'Текущие предупреждения',
'roadtrip.hazards.note': 'Предупреждения не означают закрытие дорог. Маршрут не меняется.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Importera Google Maps-rutt',
'roadtrip.import.note': 'Stoppen läggs till på vald dag. TREK beräknar rutten på nytt.',
'roadtrip.import.unresolved': 'Platsen hittades inte; stoppet hoppas över.',
'roadtrip.import.day': 'Dag',
'roadtrip.hazards.show': 'Visa riskområden',
'roadtrip.hazards.current': 'Aktuella varningar',
'roadtrip.hazards.note': 'Varningar är inte vägavstängningar. Rutten ändras inte.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Google Maps rotasını içe aktar',
'roadtrip.import.note': 'Duraklar seçilen güne eklenir. TREK rotayı yeniden hesaplar.',
'roadtrip.import.unresolved': 'Konum bulunamadı; bu durak atlanacak.',
'roadtrip.import.day': 'Gün',
'roadtrip.hazards.show': 'Tehlike bölgelerini göster',
'roadtrip.hazards.current': 'Güncel uyarılar',
'roadtrip.hazards.note': 'Uyarılar yol kapatmaları değildir. Rota değişmez.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Імпорт маршруту Google Maps',
'roadtrip.import.note': 'Зупинки додаються до вибраного дня. TREK перерахує маршрут.',
'roadtrip.import.unresolved': 'Місце не знайдено; цю зупинку буде пропущено.',
'roadtrip.import.day': 'День',
'roadtrip.hazards.show': 'Показувати небезпечні зони',
'roadtrip.hazards.current': 'Поточні попередження',
'roadtrip.hazards.note': 'Попередження не означають закриття доріг. Маршрут не змінюється.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': 'Nhập tuyến đường Google Maps',
'roadtrip.import.note': 'Các điểm dừng được thêm vào ngày đã chọn. TREK tính lại tuyến đường.',
'roadtrip.import.unresolved': 'Không tìm thấy vị trí; điểm dừng này sẽ bị bỏ qua.',
'roadtrip.import.day': 'Ngày',
'roadtrip.hazards.show': 'Hiển thị vùng nguy hiểm',
'roadtrip.hazards.current': 'Cảnh báo hiện tại',
'roadtrip.hazards.note': 'Cảnh báo không có nghĩa là đường bị đóng. Tuyến đường không thay đổi.',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': '匯入 Google Maps 路線',
'roadtrip.import.note': '停靠點將新增至所選日期。TREK 將重新計算路線。',
'roadtrip.import.unresolved': '找不到位置;將略過此停靠點。',
'roadtrip.import.day': '天',
'roadtrip.hazards.show': '顯示危險區域',
'roadtrip.hazards.current': '目前警報',
'roadtrip.hazards.note': '警報不代表道路封閉。路線保持不變。',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const roadtrip: TranslationStrings = {
'roadtrip.import.title': '导入 Google Maps 路线',
'roadtrip.import.note': '停靠点将添加到所选日期。TREK 将重新计算路线。',
'roadtrip.import.unresolved': '未找到位置;将跳过此停靠点。',
'roadtrip.import.day': '天',
'roadtrip.hazards.show': '显示危险区域',
'roadtrip.hazards.current': '当前警报',
'roadtrip.hazards.note': '警报不代表道路封闭。路线保持不变。',
+2
View File
@@ -79,3 +79,5 @@ export * from './plugins/plugins.schema';
export * from './roadtrip/preferences.schema';
export * from './roadtrip/hazards.schema';
export * from './roadtrip/planning.schema';
export * from './roadtrip/search.schema';
export * from './roadtrip/google-import.schema';
@@ -0,0 +1,27 @@
import { idSchema } from '../common/primitives.schema';
import { z } from 'zod';
export const googleRoutePreviewRequestSchema = z.object({ url: z.string().url().max(12000) });
export const googleRouteStopSchema = z.object({
name: z.string().min(1).max(200),
lat: z.number().min(-90).max(90),
lng: z.number().min(-180).max(180),
});
export const googleRoutePreviewSchema = z.object({
stops: z
.array(
z.object({
name: z.string(),
lat: z.number().nullable(),
lng: z.number().nullable(),
}),
)
.max(30),
});
export const googleRouteImportSchema = z.object({
dayId: idSchema,
stops: z.array(googleRouteStopSchema).min(2).max(30),
});
export type GoogleRouteImport = z.infer<typeof googleRouteImportSchema>;
export type GoogleRoutePreview = z.infer<typeof googleRoutePreviewSchema>;
+14
View File
@@ -0,0 +1,14 @@
import { roadtripSearchAreaSchema } from './search.schema';
import { describe, expect, it } from 'vitest';
describe('Roadtrip search area', () => {
const request = { categories: ['fuel'], bbox: { south: 48, north: 49, west: 10, east: 11 } };
it('validates categories and ordered geographic bounds', () => {
expect(roadtripSearchAreaSchema.safeParse(request).success).toBe(true);
expect(roadtripSearchAreaSchema.safeParse({ ...request, categories: ['unknown'] }).success).toBe(false);
expect(roadtripSearchAreaSchema.safeParse({ ...request, bbox: { ...request.bbox, south: 50 } }).success).toBe(
false,
);
});
});
+52
View File
@@ -0,0 +1,52 @@
import { roadtripCorridorRequestSchema } from './planning.schema';
import { z } from 'zod';
export const roadtripSearchAreaSchema = z.object({
categories: z.array(roadtripCorridorRequestSchema.shape.category).min(1).max(7),
bbox: z
.object({
south: z.number().min(-90).max(90),
north: z.number().min(-90).max(90),
west: z.number().min(-180).max(180),
east: z.number().min(-180).max(180),
})
.refine((b) => b.south < b.north && b.west < b.east, 'Invalid search area'),
lang: z.string().max(20).optional(),
});
export type RoadtripSearchArea = z.infer<typeof roadtripSearchAreaSchema>;
export const roadtripSearchPoiSchema = z.object({
osm_id: z.string(),
name: z.string(),
lat: z.number(),
lng: z.number(),
category: z.string(),
poi_type: z.string(),
source: z.string(),
address: z.string().nullable(),
website: z.string().nullable(),
phone: z.string().nullable(),
opening_hours: z.string().nullable(),
cuisine: z.string().nullable(),
brand: z.string().nullable().optional(),
brand_wikidata: z.string().nullable().optional(),
charging: z
.object({
sockets: z.array(z.object({ type: z.string(), count: z.number().nullable(), kw: z.number().nullable() })),
capacity: z.number().nullable(),
fee: z.boolean().nullable(),
})
.nullable()
.optional(),
rating: z.number().nullable().optional(),
pluginId: z.string().optional(),
});
export const roadtripSearchAreaResponseSchema = z.object({
pois: z.array(roadtripSearchPoiSchema),
sources: z.array(z.string()),
failedSources: z.array(z.string()),
truncated: z.boolean(),
clamped: z.boolean(),
});
export type RoadtripSearchAreaResponse = z.infer<typeof roadtripSearchAreaResponseSchema>;