mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
feat(tours): estimate the walking time of imported tours
A hike imported from GPX, KML/KMZ or wanderer had no time at all, so several hikes on one day could not be planned against each other. It now gets the walking time the planner times a hike by (DIN 33466), worked out from its own distance, ascent and descent; a run gets its distance at the pace entered. Without heights in the track the time is unknown rather than the time on the flat, and types timed by a router, by a speed of their own or by nothing get no estimate. The estimate is derived on every read and never stored, so tours imported before get it too and no stored or entered time is touched. The rule moved to @trek/shared, where the planner, the client and the server read it. The tour list, the day rows on desktop and phone and the tour detail say it as an estimate, apart from a planned total; list_tours and get_tour_plan carry it as time_estimate and name it as derived.
This commit is contained in:
@@ -1364,5 +1364,11 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'الجولة كاملة',
|
||||
'tours.stages.totalPartial': 'معروف لـ {known} من {of} مراحل',
|
||||
'tours.stages.openInPlanner': 'فتح هذه المرحلة في المخطط',
|
||||
'tours.estimate.label': 'الوقت المقدَّر',
|
||||
'tours.estimate.short': 'تقديري',
|
||||
'tours.estimate.unknown': 'غير معروف',
|
||||
'tours.estimate.noHeights': 'لا يحتوي المسار على ارتفاعات، لذا لا يمكن تقدير وقته.',
|
||||
'tours.estimate.basisDin': 'محسوب من المسافة والصعود والنزول وفق قاعدة لافتات المشي (DIN 33466)، دون استراحات.',
|
||||
'tours.estimate.basisPace': 'محسوب من المسافة بالوتيرة المُدخلة.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1304,5 +1304,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Bütün tur',
|
||||
'tours.stages.totalPartial': '{of} mərhələdən {known} üçün məlumdur',
|
||||
'tours.stages.openInPlanner': 'Bu mərhələni planlayıcıda aç',
|
||||
'tours.estimate.label': 'Təxmini vaxt',
|
||||
'tours.estimate.short': 'Təxmini',
|
||||
'tours.estimate.unknown': 'Naməlum',
|
||||
'tours.estimate.noHeights': 'Trekdə hündürlük yoxdur, buna görə vaxtı təxmin etmək olmur.',
|
||||
'tours.estimate.basisDin':
|
||||
'Məsafə, qalxma və enmədən piyada nişanları qaydası ilə (DIN 33466) hesablanıb, fasilələr daxil deyil.',
|
||||
'tours.estimate.basisPace': 'Məsafədən daxil edilmiş templə hesablanıb.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1317,5 +1317,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Roteiro completo',
|
||||
'tours.stages.totalPartial': 'conhecido para {known} de {of} etapas',
|
||||
'tours.stages.openInPlanner': 'Abrir esta etapa no planejador',
|
||||
'tours.estimate.label': 'Tempo estimado',
|
||||
'tours.estimate.short': 'Estimado',
|
||||
'tours.estimate.unknown': 'Desconhecido',
|
||||
'tours.estimate.noHeights': 'A trilha não tem altitudes, então o tempo não pode ser estimado.',
|
||||
'tours.estimate.basisDin':
|
||||
'Calculado pela distância, subida e descida segundo a regra das placas de trilha (DIN 33466), sem pausas.',
|
||||
'tours.estimate.basisPace': 'Calculado pela distância no ritmo informado.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1323,5 +1323,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Ruta sencera',
|
||||
'tours.stages.totalPartial': 'conegut per a {known} de {of} etapes',
|
||||
'tours.stages.openInPlanner': 'Obre aquesta etapa al planificador',
|
||||
'tours.estimate.label': 'Temps estimat',
|
||||
'tours.estimate.short': 'Estimat',
|
||||
'tours.estimate.unknown': 'Desconegut',
|
||||
'tours.estimate.noHeights': 'El track no té altituds, així que no se’n pot estimar el temps.',
|
||||
'tours.estimate.basisDin':
|
||||
'Calculat a partir de la distància, la pujada i la baixada segons la regla dels senyals excursionistes (DIN 33466), sense pauses.',
|
||||
'tours.estimate.basisPace': 'Calculat a partir de la distància al ritme introduït.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1308,5 +1308,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Celá trasa',
|
||||
'tours.stages.totalPartial': 'známo pro {known} z {of} etap',
|
||||
'tours.stages.openInPlanner': 'Otevřít tuto etapu v plánovači',
|
||||
'tours.estimate.label': 'Odhadovaný čas',
|
||||
'tours.estimate.short': 'Odhad',
|
||||
'tours.estimate.unknown': 'Neznámý',
|
||||
'tours.estimate.noHeights': 'Trasa nemá výšky, proto nelze čas odhadnout.',
|
||||
'tours.estimate.basisDin':
|
||||
'Vypočteno ze vzdálenosti, stoupání a klesání podle pravidla turistických rozcestníků (DIN 33466), bez přestávek.',
|
||||
'tours.estimate.basisPace': 'Vypočteno ze vzdálenosti při zadaném tempu.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1335,6 +1335,13 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Ganze Tour',
|
||||
'tours.stages.totalPartial': 'bekannt für {known} von {of} Etappen',
|
||||
'tours.stages.openInPlanner': 'Diese Etappe im Planer öffnen',
|
||||
'tours.estimate.label': 'Geschätzte Zeit',
|
||||
'tours.estimate.short': 'Geschätzt',
|
||||
'tours.estimate.unknown': 'Unbekannt',
|
||||
'tours.estimate.noHeights': 'Der Track hat keine Höhen, daher lässt sich keine Zeit schätzen.',
|
||||
'tours.estimate.basisDin':
|
||||
'Aus Strecke, Auf- und Abstieg nach der Wegweiser-Regel (DIN 33466) berechnet, ohne Pausen.',
|
||||
'tours.estimate.basisPace': 'Aus der Strecke beim eingegebenen Tempo berechnet.',
|
||||
};
|
||||
|
||||
export default tours;
|
||||
|
||||
@@ -1290,5 +1290,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Whole tour',
|
||||
'tours.stages.totalPartial': 'known for {known} of {of} stages',
|
||||
'tours.stages.openInPlanner': 'Open this stage in the planner',
|
||||
'tours.estimate.label': 'Estimated time',
|
||||
'tours.estimate.short': 'Estimated',
|
||||
'tours.estimate.unknown': 'Unknown',
|
||||
'tours.estimate.noHeights': 'The track has no heights, so its time cannot be estimated.',
|
||||
'tours.estimate.basisDin':
|
||||
'Worked out from distance, ascent and descent by the hiking signpost rule (DIN 33466), without breaks.',
|
||||
'tours.estimate.basisPace': 'Worked out from the distance at the pace entered.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1316,5 +1316,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Ruta completa',
|
||||
'tours.stages.totalPartial': 'conocido para {known} de {of} etapas',
|
||||
'tours.stages.openInPlanner': 'Abrir esta etapa en el planificador',
|
||||
'tours.estimate.label': 'Tiempo estimado',
|
||||
'tours.estimate.short': 'Estimado',
|
||||
'tours.estimate.unknown': 'Desconocido',
|
||||
'tours.estimate.noHeights': 'El track no tiene altitudes, así que no se puede estimar su tiempo.',
|
||||
'tours.estimate.basisDin':
|
||||
'Calculado a partir de la distancia, el ascenso y el descenso según la regla de las señales de senderismo (DIN 33466), sin pausas.',
|
||||
'tours.estimate.basisPace': 'Calculado a partir de la distancia al ritmo introducido.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1299,5 +1299,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Kogu matk',
|
||||
'tours.stages.totalPartial': 'teada {known} etapi kohta {of}-st',
|
||||
'tours.stages.openInPlanner': 'Ava see etapp planeerijas',
|
||||
'tours.estimate.label': 'Hinnanguline aeg',
|
||||
'tours.estimate.short': 'Hinnang',
|
||||
'tours.estimate.unknown': 'Teadmata',
|
||||
'tours.estimate.noHeights': 'Rajal pole kõrgusi, seega aega ei saa hinnata.',
|
||||
'tours.estimate.basisDin':
|
||||
'Arvutatud vahemaa, tõusu ja laskumise järgi matkaviitade reegli (DIN 33466) alusel, pausideta.',
|
||||
'tours.estimate.basisPace': 'Arvutatud vahemaast sisestatud tempoga.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1343,5 +1343,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Itinéraire complet',
|
||||
'tours.stages.totalPartial': 'connu pour {known} étapes sur {of}',
|
||||
'tours.stages.openInPlanner': 'Ouvrir cette étape dans le planificateur',
|
||||
'tours.estimate.label': 'Temps estimé',
|
||||
'tours.estimate.short': 'Estimé',
|
||||
'tours.estimate.unknown': 'Inconnu',
|
||||
'tours.estimate.noHeights': 'Le tracé n’a pas d’altitudes, son temps ne peut donc pas être estimé.',
|
||||
'tours.estimate.basisDin':
|
||||
'Calculé à partir de la distance, du dénivelé positif et négatif selon la règle des panneaux de randonnée (DIN 33466), sans pauses.',
|
||||
'tours.estimate.basisPace': 'Calculé à partir de la distance à l’allure saisie.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1345,5 +1345,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Όλη η διαδρομή',
|
||||
'tours.stages.totalPartial': 'γνωστό για {known} από {of} σκέλη',
|
||||
'tours.stages.openInPlanner': 'Άνοιγμα αυτού του σκέλους στον σχεδιαστή',
|
||||
'tours.estimate.label': 'Εκτιμώμενος χρόνος',
|
||||
'tours.estimate.short': 'Εκτίμηση',
|
||||
'tours.estimate.unknown': 'Άγνωστος',
|
||||
'tours.estimate.noHeights': 'Η διαδρομή δεν έχει υψόμετρα, οπότε ο χρόνος δεν μπορεί να εκτιμηθεί.',
|
||||
'tours.estimate.basisDin':
|
||||
'Υπολογισμένος από απόσταση, ανάβαση και κατάβαση με τον κανόνα των πινακίδων πεζοπορίας (DIN 33466), χωρίς διαλείμματα.',
|
||||
'tours.estimate.basisPace': 'Υπολογισμένος από την απόσταση με τον ρυθμό που δόθηκε.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1322,5 +1322,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Teljes túra',
|
||||
'tours.stages.totalPartial': 'ismert {known} szakasznál a {of} közül',
|
||||
'tours.stages.openInPlanner': 'A szakasz megnyitása a tervezőben',
|
||||
'tours.estimate.label': 'Becsült idő',
|
||||
'tours.estimate.short': 'Becsült',
|
||||
'tours.estimate.unknown': 'Ismeretlen',
|
||||
'tours.estimate.noHeights': 'A nyomvonalnak nincsenek magasságai, így az ideje nem becsülhető.',
|
||||
'tours.estimate.basisDin':
|
||||
'A távból, emelkedésből és lejtésből számolva a turistaútjelzők szabálya szerint (DIN 33466), szünetek nélkül.',
|
||||
'tours.estimate.basisPace': 'A távból számolva a megadott tempóval.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1296,5 +1296,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Seluruh tur',
|
||||
'tours.stages.totalPartial': 'diketahui untuk {known} dari {of} etape',
|
||||
'tours.stages.openInPlanner': 'Buka etape ini di perencana',
|
||||
'tours.estimate.label': 'Perkiraan waktu',
|
||||
'tours.estimate.short': 'Perkiraan',
|
||||
'tours.estimate.unknown': 'Tidak diketahui',
|
||||
'tours.estimate.noHeights': 'Jalur tidak memiliki data ketinggian, jadi waktunya tidak dapat diperkirakan.',
|
||||
'tours.estimate.basisDin':
|
||||
'Dihitung dari jarak, tanjakan, dan turunan menurut aturan papan penunjuk pendakian (DIN 33466), tanpa istirahat.',
|
||||
'tours.estimate.basisPace': 'Dihitung dari jarak dengan kecepatan yang dimasukkan.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1330,5 +1330,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Tour completo',
|
||||
'tours.stages.totalPartial': 'noto per {known} tappe su {of}',
|
||||
'tours.stages.openInPlanner': 'Apri questa tappa nel pianificatore',
|
||||
'tours.estimate.label': 'Tempo stimato',
|
||||
'tours.estimate.short': 'Stimato',
|
||||
'tours.estimate.unknown': 'Sconosciuto',
|
||||
'tours.estimate.noHeights': 'La traccia non ha quote, quindi il suo tempo non si può stimare.',
|
||||
'tours.estimate.basisDin':
|
||||
'Calcolato da distanza, salita e discesa secondo la regola dei cartelli escursionistici (DIN 33466), senza pause.',
|
||||
'tours.estimate.basisPace': 'Calcolato dalla distanza al ritmo inserito.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1265,5 +1265,11 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'ツアー全体',
|
||||
'tours.stages.totalPartial': '{of} 件中 {known} 件のステージで判明',
|
||||
'tours.stages.openInPlanner': 'このステージをプランナーで開く',
|
||||
'tours.estimate.label': '推定時間',
|
||||
'tours.estimate.short': '推定',
|
||||
'tours.estimate.unknown': '不明',
|
||||
'tours.estimate.noHeights': 'トラックに標高データがないため、時間を推定できません。',
|
||||
'tours.estimate.basisDin': '距離・登り・下りから登山道標識の基準(DIN 33466)で算出、休憩は含みません。',
|
||||
'tours.estimate.basisPace': '距離と入力したペースから算出。',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1261,5 +1261,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': '전체 투어',
|
||||
'tours.stages.totalPartial': '{of}개 구간 중 {known}개에서 알려짐',
|
||||
'tours.stages.openInPlanner': '이 구간을 플래너에서 열기',
|
||||
'tours.estimate.label': '예상 시간',
|
||||
'tours.estimate.short': '예상',
|
||||
'tours.estimate.unknown': '알 수 없음',
|
||||
'tours.estimate.noHeights': '트랙에 고도 정보가 없어 시간을 추정할 수 없습니다.',
|
||||
'tours.estimate.basisDin':
|
||||
'거리, 오르막, 내리막으로 등산로 표지 규칙(DIN 33466)에 따라 계산했으며 휴식은 포함하지 않습니다.',
|
||||
'tours.estimate.basisPace': '거리와 입력한 페이스로 계산했습니다.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1326,5 +1326,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Hele tocht',
|
||||
'tours.stages.totalPartial': 'bekend voor {known} van {of} etappes',
|
||||
'tours.stages.openInPlanner': 'Deze etappe openen in de planner',
|
||||
'tours.estimate.label': 'Geschatte tijd',
|
||||
'tours.estimate.short': 'Geschat',
|
||||
'tours.estimate.unknown': 'Onbekend',
|
||||
'tours.estimate.noHeights': 'De track heeft geen hoogtes, dus de tijd kan niet worden geschat.',
|
||||
'tours.estimate.basisDin':
|
||||
'Berekend uit afstand, stijging en daling volgens de regel van de wandelwegwijzers (DIN 33466), zonder pauzes.',
|
||||
'tours.estimate.basisPace': 'Berekend uit de afstand bij het ingevoerde tempo.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1359,5 +1359,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Cała trasa',
|
||||
'tours.stages.totalPartial': 'znane dla {known} z {of} etapów',
|
||||
'tours.stages.openInPlanner': 'Otwórz ten etap w planerze',
|
||||
'tours.estimate.label': 'Szacowany czas',
|
||||
'tours.estimate.short': 'Szacunek',
|
||||
'tours.estimate.unknown': 'Nieznany',
|
||||
'tours.estimate.noHeights': 'Ślad nie ma wysokości, więc nie da się oszacować czasu.',
|
||||
'tours.estimate.basisDin':
|
||||
'Wyliczony z dystansu, podejść i zejść według reguły drogowskazów turystycznych (DIN 33466), bez przerw.',
|
||||
'tours.estimate.basisPace': 'Wyliczony z dystansu przy podanym tempie.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1365,5 +1365,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Весь маршрут',
|
||||
'tours.stages.totalPartial': 'известно для {known} из {of} этапов',
|
||||
'tours.stages.openInPlanner': 'Открыть этот этап в планировщике',
|
||||
'tours.estimate.label': 'Расчётное время',
|
||||
'tours.estimate.short': 'Оценка',
|
||||
'tours.estimate.unknown': 'Неизвестно',
|
||||
'tours.estimate.noHeights': 'В треке нет высот, поэтому время оценить нельзя.',
|
||||
'tours.estimate.basisDin':
|
||||
'Рассчитано по расстоянию, набору и сбросу высоты по правилу туристических указателей (DIN 33466), без перерывов.',
|
||||
'tours.estimate.basisPace': 'Рассчитано по расстоянию при введённом темпе.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1312,5 +1312,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Celá trasa',
|
||||
'tours.stages.totalPartial': 'známe pre {known} z {of} etáp',
|
||||
'tours.stages.openInPlanner': 'Otvoriť túto etapu v plánovači',
|
||||
'tours.estimate.label': 'Odhadovaný čas',
|
||||
'tours.estimate.short': 'Odhad',
|
||||
'tours.estimate.unknown': 'Neznámy',
|
||||
'tours.estimate.noHeights': 'Trasa nemá výšky, preto nemožno čas odhadnúť.',
|
||||
'tours.estimate.basisDin':
|
||||
'Vypočítané zo vzdialenosti, stúpania a klesania podľa pravidla turistických smerovníkov (DIN 33466), bez prestávok.',
|
||||
'tours.estimate.basisPace': 'Vypočítané zo vzdialenosti pri zadanom tempe.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1295,5 +1295,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Hela turen',
|
||||
'tours.stages.totalPartial': 'känt för {known} av {of} etapper',
|
||||
'tours.stages.openInPlanner': 'Öppna etappen i planeraren',
|
||||
'tours.estimate.label': 'Uppskattad tid',
|
||||
'tours.estimate.short': 'Uppskattad',
|
||||
'tours.estimate.unknown': 'Okänd',
|
||||
'tours.estimate.noHeights': 'Spåret har inga höjder, så tiden kan inte uppskattas.',
|
||||
'tours.estimate.basisDin':
|
||||
'Beräknad från sträcka, stigning och nedstigning enligt vandringsskyltarnas regel (DIN 33466), utan pauser.',
|
||||
'tours.estimate.basisPace': 'Beräknad från sträckan i angivet tempo.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1240,5 +1240,11 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'ทั้งเส้นทาง',
|
||||
'tours.stages.totalPartial': 'ทราบสำหรับ {known} จาก {of} ช่วง',
|
||||
'tours.stages.openInPlanner': 'เปิดช่วงนี้ในตัววางแผน',
|
||||
'tours.estimate.label': 'เวลาโดยประมาณ',
|
||||
'tours.estimate.short': 'ประมาณ',
|
||||
'tours.estimate.unknown': 'ไม่ทราบ',
|
||||
'tours.estimate.noHeights': 'เส้นทางไม่มีข้อมูลความสูง จึงประมาณเวลาไม่ได้',
|
||||
'tours.estimate.basisDin': 'คำนวณจากระยะทาง ระยะขึ้นและลงตามกฎป้ายบอกทางเดินป่า (DIN 33466) ไม่รวมเวลาพัก',
|
||||
'tours.estimate.basisPace': 'คำนวณจากระยะทางตามเพซที่กรอกไว้',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -20,7 +20,7 @@ const toursModules = import.meta.glob<{ default: TranslationStrings }>('./*/tour
|
||||
const TOUR_DOMAINS: { file: string; generalKeyCount: number; modules: DomainModules }[] = [
|
||||
{
|
||||
file: 'tours',
|
||||
generalKeyCount: 1177,
|
||||
generalKeyCount: 1183,
|
||||
modules: toursModules,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1302,5 +1302,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Tüm tur',
|
||||
'tours.stages.totalPartial': '{of} etabın {known} tanesi için biliniyor',
|
||||
'tours.stages.openInPlanner': 'Bu etabı planlayıcıda aç',
|
||||
'tours.estimate.label': 'Tahmini süre',
|
||||
'tours.estimate.short': 'Tahmini',
|
||||
'tours.estimate.unknown': 'Bilinmiyor',
|
||||
'tours.estimate.noHeights': 'İzde yükseklik yok, bu yüzden süresi tahmin edilemiyor.',
|
||||
'tours.estimate.basisDin':
|
||||
'Mesafe, tırmanış ve inişten yürüyüş tabelası kuralına (DIN 33466) göre hesaplandı, molalar hariç.',
|
||||
'tours.estimate.basisPace': 'Mesafeden, girilen tempoyla hesaplandı.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1351,5 +1351,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Увесь маршрут',
|
||||
'tours.stages.totalPartial': 'відомо для {known} з {of} етапів',
|
||||
'tours.stages.openInPlanner': 'Відкрити цей етап у планувальнику',
|
||||
'tours.estimate.label': 'Орієнтовний час',
|
||||
'tours.estimate.short': 'Оцінка',
|
||||
'tours.estimate.unknown': 'Невідомо',
|
||||
'tours.estimate.noHeights': 'У треку немає висот, тому час оцінити не можна.',
|
||||
'tours.estimate.basisDin':
|
||||
'Розраховано за відстанню, набором і скиданням висоти за правилом туристичних вказівників (DIN 33466), без перерв.',
|
||||
'tours.estimate.basisPace': 'Розраховано за відстанню при введеному темпі.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1284,5 +1284,12 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': 'Cả hành trình',
|
||||
'tours.stages.totalPartial': 'đã biết cho {known} trên {of} chặng',
|
||||
'tours.stages.openInPlanner': 'Mở chặng này trong trình lập kế hoạch',
|
||||
'tours.estimate.label': 'Thời gian ước tính',
|
||||
'tours.estimate.short': 'Ước tính',
|
||||
'tours.estimate.unknown': 'Không rõ',
|
||||
'tours.estimate.noHeights': 'Lộ trình không có độ cao, nên không thể ước tính thời gian.',
|
||||
'tours.estimate.basisDin':
|
||||
'Tính từ quãng đường, độ cao lên và xuống theo quy tắc biển chỉ dẫn leo núi (DIN 33466), không tính nghỉ.',
|
||||
'tours.estimate.basisPace': 'Tính từ quãng đường theo tốc độ đã nhập.',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1203,5 +1203,11 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': '整條路線',
|
||||
'tours.stages.totalPartial': '{of} 個階段中 {known} 個已知',
|
||||
'tours.stages.openInPlanner': '在規劃器中開啟此階段',
|
||||
'tours.estimate.label': '預估時間',
|
||||
'tours.estimate.short': '預估',
|
||||
'tours.estimate.unknown': '未知',
|
||||
'tours.estimate.noHeights': '軌跡沒有海拔資料,無法估算時間。',
|
||||
'tours.estimate.basisDin': '依健行路標規則(DIN 33466)由距離、爬升與下降計算,不含休息。',
|
||||
'tours.estimate.basisPace': '依距離與輸入的配速計算。',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -1204,5 +1204,11 @@ const tours: TranslationStrings = {
|
||||
'tours.stages.wholeTour': '整条路线',
|
||||
'tours.stages.totalPartial': '{of} 个阶段中 {known} 个已知',
|
||||
'tours.stages.openInPlanner': '在规划器中打开此阶段',
|
||||
'tours.estimate.label': '预计用时',
|
||||
'tours.estimate.short': '预计',
|
||||
'tours.estimate.unknown': '未知',
|
||||
'tours.estimate.noHeights': '轨迹没有海拔数据,无法估算用时。',
|
||||
'tours.estimate.basisDin': '按徒步路标规则(DIN 33466)根据距离、爬升和下降计算,不含休息。',
|
||||
'tours.estimate.basisPace': '按距离和输入的配速计算。',
|
||||
};
|
||||
export default tours;
|
||||
|
||||
@@ -57,6 +57,7 @@ export * from './roadtrip/roadtrip.schema';
|
||||
export * from './place/track-colors';
|
||||
export * from './tours/tours.schema';
|
||||
export * from './tours/run-pace';
|
||||
export * from './tours/tour-time-estimate';
|
||||
export * from './tours/ski.schema';
|
||||
export * from './tours/tour-stages';
|
||||
export * from './tours/tour-provenance.schema';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
hikingTimeMinutes,
|
||||
tourTimeEstimate,
|
||||
tourTimeEstimateSchema,
|
||||
type TourTimeEstimateSource,
|
||||
} from './tour-time-estimate';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// FE-TOURS-HIKINGTIME-001 to -003 moved here with the rule; TIME-01-001 on: the estimate of a tour without a planned time.
|
||||
describe('hikingTimeMinutes (DIN 33466)', () => {
|
||||
it('FE-TOURS-HIKINGTIME-001: on the flat it is 4 km an hour', () => {
|
||||
expect(hikingTimeMinutes(8, 0, 0)).toBe(120);
|
||||
});
|
||||
|
||||
it('FE-TOURS-HIKINGTIME-002: a short steep climb is ruled by the height, the distance counts by half', () => {
|
||||
// 2 km and 450 m up: 1.5 h for the height plus half of 0.5 h for the distance.
|
||||
expect(hikingTimeMinutes(2, 450, 0)).toBe(105);
|
||||
});
|
||||
|
||||
it('FE-TOURS-HIKINGTIME-003: a long walk is ruled by the distance, the height counts by half', () => {
|
||||
// 12 km, 300 m up and 500 m down: 3 h for the distance plus half of 2 h for the height.
|
||||
expect(hikingTimeMinutes(12, 300, 500)).toBe(240);
|
||||
});
|
||||
});
|
||||
|
||||
const imported = (over: Partial<TourTimeEstimateSource> = {}): TourTimeEstimateSource => ({
|
||||
tour_type: 'hike',
|
||||
distance: 12,
|
||||
elevation_gain: 300,
|
||||
elevation_loss: 500,
|
||||
duration: null,
|
||||
run_pace_seconds_per_km: null,
|
||||
...over,
|
||||
});
|
||||
|
||||
describe('tourTimeEstimate (TIME-01)', () => {
|
||||
it('TIME-01-001: an imported hike is estimated by the planner rule from its own climb and descent', () => {
|
||||
expect(tourTimeEstimate(imported())).toEqual({ minutes: 240, basis: 'din33466' });
|
||||
});
|
||||
|
||||
it('TIME-01-002: a hike without heights is unknown, never timed as if it were flat', () => {
|
||||
expect(tourTimeEstimate(imported({ elevation_gain: null, elevation_loss: null }))).toEqual({
|
||||
minutes: null,
|
||||
basis: 'din33466',
|
||||
});
|
||||
expect(tourTimeEstimate(imported({ elevation_loss: null }))?.minutes).toBeNull();
|
||||
});
|
||||
|
||||
it('TIME-01-003: a tour the planner timed keeps its own time and is not estimated', () => {
|
||||
expect(tourTimeEstimate(imported({ duration: 200 }))).toBeNull();
|
||||
expect(tourTimeEstimate(imported({ duration: 0 }))).toBeNull();
|
||||
});
|
||||
|
||||
it('TIME-01-004: a run is estimated at the pace its runners entered, and without one not at all', () => {
|
||||
expect(tourTimeEstimate(imported({ tour_type: 'run', distance: 8, run_pace_seconds_per_km: 360 }))).toEqual({
|
||||
minutes: 48,
|
||||
basis: 'run_pace',
|
||||
});
|
||||
expect(tourTimeEstimate(imported({ tour_type: 'run', run_pace_seconds_per_km: null }))).toBeNull();
|
||||
expect(tourTimeEstimate(imported({ tour_type: 'run', run_pace_seconds_per_km: undefined }))).toBeNull();
|
||||
});
|
||||
|
||||
it('TIME-01-005: types timed by a router, a speed of their own or nothing get no estimate', () => {
|
||||
for (const type of ['bike', 'walk', 'kayak', 'ski', 'horse', 'food']) {
|
||||
expect(tourTimeEstimate(imported({ tour_type: type, run_pace_seconds_per_km: 360 }))).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('TIME-01-006: without a distance there is nothing to estimate', () => {
|
||||
expect(tourTimeEstimate(imported({ distance: null }))).toBeNull();
|
||||
});
|
||||
|
||||
it('TIME-01-007: the wire shape takes whole minutes or null and a known basis only', () => {
|
||||
expect(tourTimeEstimateSchema.safeParse({ minutes: 240, basis: 'din33466' }).success).toBe(true);
|
||||
expect(tourTimeEstimateSchema.safeParse({ minutes: null, basis: 'run_pace' }).success).toBe(true);
|
||||
expect(tourTimeEstimateSchema.safeParse({ minutes: 1.5, basis: 'din33466' }).success).toBe(false);
|
||||
expect(tourTimeEstimateSchema.safeParse({ minutes: 10, basis: 'router' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { runMinutesAtPace } from './run-pace';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Walking time the way hiking signposts in the Alps state it (DIN 33466, as the
|
||||
* DAV and the Swiss hiking trails use it): 4 km an hour on the flat, 300 m of
|
||||
* climb and 500 m of descent an hour, and of the time for the distance and the
|
||||
* time for the height only the larger counts in full, the smaller by half.
|
||||
*
|
||||
* The planner times a hike by it, and an imported hike is estimated by it
|
||||
* (TIME-01), so both come from this one rule.
|
||||
*/
|
||||
const FLAT_KM_PER_HOUR = 4;
|
||||
const CLIMB_M_PER_HOUR = 300;
|
||||
const DESCENT_M_PER_HOUR = 500;
|
||||
|
||||
export function hikingTimeMinutes(distanceKm: number, climbM: number, descentM: number): number {
|
||||
const flat = distanceKm / FLAT_KM_PER_HOUR;
|
||||
const height = climbM / CLIMB_M_PER_HOUR + descentM / DESCENT_M_PER_HOUR;
|
||||
const hours = Math.max(flat, height) + Math.min(flat, height) / 2;
|
||||
return Math.round(hours * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME-01: what an estimate is worked out by. `din33466`: a hike's distance,
|
||||
* climb and descent by the signposts' rule; `run_pace`: a run's distance at
|
||||
* the pace its runners entered.
|
||||
*/
|
||||
export const tourTimeEstimateBasisSchema = z.enum(['din33466', 'run_pace']);
|
||||
export type TourTimeEstimateBasis = z.infer<typeof tourTimeEstimateBasisSchema>;
|
||||
|
||||
/**
|
||||
* TIME-01: the time a tour without a planned time is estimated to take, in
|
||||
* minutes and without breaks, worked out whenever it is read and never
|
||||
* stored. `minutes` is null when the rule needs heights the track does not
|
||||
* carry: then the time is unknown, never the time on the flat.
|
||||
*/
|
||||
export const tourTimeEstimateSchema = z.object({
|
||||
minutes: z.number().int().nonnegative().nullable(),
|
||||
basis: tourTimeEstimateBasisSchema,
|
||||
});
|
||||
export type TourTimeEstimate = z.infer<typeof tourTimeEstimateSchema>;
|
||||
|
||||
/** What an estimate is read from: the tour's type, its measures, its planned time and a run's pace. */
|
||||
export interface TourTimeEstimateSource {
|
||||
tour_type: string;
|
||||
distance: number | null;
|
||||
elevation_gain: number | null;
|
||||
elevation_loss: number | null;
|
||||
/** The time the planner worked out, in minutes; a tour that has one is not estimated. */
|
||||
duration: number | null;
|
||||
run_pace_seconds_per_km?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME-01: the estimate of a tour that has no time of its own (an imported
|
||||
* track, which the planner never timed), or null where there is nothing to
|
||||
* estimate it by. Only the rules the planner times these types by are used:
|
||||
* a hike by DIN 33466 from its own climb and descent, a run by the pace its
|
||||
* runners entered. A bike tour, a city walk and a paddle tour are timed by a
|
||||
* router or by a speed of their own, a ride and a ski tour by none, so none
|
||||
* of them is estimated here.
|
||||
*/
|
||||
export function tourTimeEstimate(tour: TourTimeEstimateSource): TourTimeEstimate | null {
|
||||
if (tour.duration != null || tour.distance == null) return null;
|
||||
if (tour.tour_type === 'hike') {
|
||||
const known = tour.elevation_gain != null && tour.elevation_loss != null;
|
||||
return {
|
||||
minutes: known ? hikingTimeMinutes(tour.distance, tour.elevation_gain ?? 0, tour.elevation_loss ?? 0) : null,
|
||||
basis: 'din33466',
|
||||
};
|
||||
}
|
||||
const pace = tour.run_pace_seconds_per_km ?? null;
|
||||
if (tour.tour_type === 'run' && pace !== null) {
|
||||
return { minutes: runMinutesAtPace(tour.distance, pace), basis: 'run_pace' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { tourPackingLinkSchema } from './tour-packing-link.schema';
|
||||
import { tourPoiCategorySchema } from './tour-poi-category';
|
||||
import { tourOriginSchema, tourProvenanceSchema } from './tour-provenance.schema';
|
||||
import { maxTourMinutesFor, TOUR_DAY_MINUTES, tourStageCountOf, tourStageIssues } from './tour-stages';
|
||||
import { tourTimeEstimateSchema } from './tour-time-estimate';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -310,6 +311,14 @@ export const tourListItemSchema = tourSchema.extend({
|
||||
*/
|
||||
track_started_at: z.string().nullable().optional(),
|
||||
track_ended_at: z.string().nullable().optional(),
|
||||
/**
|
||||
* TIME-01: the time a tour without a planned time (an import) is estimated
|
||||
* to take, worked out on reading by `tourTimeEstimate` and never stored;
|
||||
* null where nothing estimates it. Its minutes are null when the track has
|
||||
* no heights. Apart from the times recorded in the file and from a planned
|
||||
* total the travellers enter. Absent in a list cached before it existed.
|
||||
*/
|
||||
time_estimate: tourTimeEstimateSchema.nullable().optional(),
|
||||
/** ACT-01: the recordings assigned to this tour, one per execution. Absent in a list cached before it existed. */
|
||||
activity_tracks: z.array(tourActivityRefSchema).optional(),
|
||||
/** ACT-01: the tour this recording documents, when it is assigned to one; null otherwise. */
|
||||
|
||||
Reference in New Issue
Block a user