feat(tours): paddle obstacle card, admin regions panel and obstacle marks on the route band

PAD-05B: a click on an obstacle on the map or on the route band opens its card in the look of the via ferrata card: the kind with its glyph, the name, how it stands to the route (on it at a distance along it, near it with the link marked unclear, or only in the area), the paddling rule its tags give with canoe read before boat, a condition as written, height, grade, hours and fees, the source with the date of the data and the OpenStreetMap link. A missing rule reads as unknown, never as allowed, and a portage way recorded as private says so. Objects only in the area show from zoom 13 on, on Leaflet and GL alike; on GL the lines only near the route are dashed as on Leaflet, one click handler picks the marker over the line, and a picked marker no longer flickers. The route band marks the obstacles on and near the route by their share of the measured line. The obstacle notice also names an answer cut at its limit.

PAD-05A: the admin area gets a Paddle water data card under the addons, on desktop and phone: a search in Geofabrik's list with each region's path, the regions with their state (waiting, reading with its progress, ready with objects and data date, failed with the reason), reading again and removing after a confirmation. The list follows a read without reloading. Region changes write audit rows.

Map colours of paddle objects and portages come from theme tokens read at draw time, as the via ferratas do; the ferrata layer now shares that reader. The rail takes the planner page's parts as one bag, the map hit tests of the tour layers are one helper, and the unused noWaterData string is gone from every locale.
This commit is contained in:
Maurice
2026-10-16 23:03:09 +00:00
parent 89ad4ec56a
commit 03d6457bde
97 changed files with 1924 additions and 492 deletions
-2
View File
@@ -298,8 +298,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'أدخل سرعة بين 0.5 و20 كم/س.',
'tours.paddle.chooseBoat': 'اختر القارب والمسطح المائي لحفظ جولة التجديف هذه.',
'tours.paddle.chooseBoatImport': 'اختر القارب والمسطح المائي للاستيراد كجولة تجديف.',
'tours.paddle.noWaterData':
'لا يعرض TREK بعد السدود الصغيرة والأهوسة ونقاط حمل القارب. تحقق بنفسك من الماء وقواعده قبل الانطلاق.',
'tours.paddle.drawnByHand':
'تُرسم جولة التجديف يدويًا: يصل TREK نقاطك بخطوط مستقيمة. لا يُحسب أي مسار، ولا شيء يقول إن الماء صالح للتجديف.',
'tours.paddle.timeEstimated': 'وقت التجديف (تقديري)',
-2
View File
@@ -287,8 +287,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '0,5 ilə 20 km/saat arasında sürət daxil et.',
'tours.paddle.chooseBoat': 'Bu avarlı turu yadda saxlamaq üçün qayığı və suyu seç.',
'tours.paddle.chooseBoatImport': 'Avarlı tur kimi idxal etmək üçün qayığı və suyu seç.',
'tours.paddle.noWaterData':
'TREK hələ bəndləri, şlüzləri və daşıma yerlərini göstərmir. Yola çıxmazdan əvvəl suyu və qaydalarını özün yoxla.',
'tours.paddle.drawnByHand':
'Avarlı tur əllə çəkilir: TREK nöqtələrini düz xətlərlə birləşdirir. Heç nə marşrutlaşdırılmır və heç nə suyun keçilə bilən olduğunu demir.',
'tours.paddle.timeEstimated': 'Avar vaxtı (təxmini)',
-2
View File
@@ -293,8 +293,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Informe uma velocidade de 0,5 a 20 km/h.',
'tours.paddle.chooseBoat': 'Escolha a embarcação e a água para salvar este passeio.',
'tours.paddle.chooseBoatImport': 'Escolha a embarcação e a água para importar como passeio de remo.',
'tours.paddle.noWaterData':
'O TREK ainda não mostra barragens, eclusas nem trechos de transbordo. Confira você mesmo a água e as regras antes de sair.',
'tours.paddle.drawnByHand':
'Um passeio de remo é desenhado à mão: o TREK liga seus pontos com linhas retas. Nada é roteado, e nada diz que a água é navegável.',
'tours.paddle.timeEstimated': 'Tempo de remada (estimado)',
-2
View File
@@ -293,8 +293,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Introdueix una velocitat de 0,5 a 20 km/h.',
'tours.paddle.chooseBoat': 'Tria l’embarcació i l’aigua per desar aquesta ruta.',
'tours.paddle.chooseBoatImport': 'Tria l’embarcació i l’aigua per importar com a ruta en piragua.',
'tours.paddle.noWaterData':
'TREK encara no mostra rescloses, preses ni transports per terra. Comprova tu mateix l’aigua i les normes abans de sortir.',
'tours.paddle.drawnByHand':
'Una ruta en piragua es dibuixa a mà: TREK uneix els punts amb línies rectes. No es calcula res i res no diu que l’aigua sigui navegable.',
'tours.paddle.timeEstimated': 'Temps de rem (estimat)',
-2
View File
@@ -287,8 +287,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Zadejte rychlost od 0,5 do 20 km/h.',
'tours.paddle.chooseBoat': 'Vyberte loď a vodu, abyste mohli tuto túru uložit.',
'tours.paddle.chooseBoatImport': 'Vyberte loď a vodu pro import jako vodácké túry.',
'tours.paddle.noWaterData':
'TREK zatím nezobrazuje jezy, zdymadla ani přenášky. Před vyplutím si vodu a pravidla ověřte sami.',
'tours.paddle.drawnByHand':
'Vodácká túra se kreslí ručně: TREK spojí vaše body přímými čarami. Nic se nepočítá a nic neříká, že je voda sjízdná.',
'tours.paddle.timeEstimated': 'Čas na vodě (odhad)',
+29
View File
@@ -862,5 +862,34 @@ const admin: TranslationStrings = {
'admin.amapKeyHintShort':
'Für die Ortssuche in Festlandchina. Braucht einen Web-Service-Schlüssel, keinen JS-API-Schlüssel.',
'admin.collab.links.subtitle': 'Geteilte Links und Lesezeichen',
'admin.paddleRegions.title': 'Paddel-Gewässerdaten',
'admin.paddleRegions.hint':
'Wehre, Schleusen und Befahrungsregeln aus OpenStreetMap, je Region gelesen und jeden Monat neu.',
'admin.paddleRegions.search': 'Region hinzufügen',
'admin.paddleRegions.searchPlaceholder': 'Geofabrik durchsuchen, zum Beispiel Oberpfalz',
'admin.paddleRegions.noHits': 'Keine Region mit diesem Namen',
'admin.paddleRegions.added': 'Hinzugefügt',
'admin.paddleRegions.empty': 'Noch keine Region. Paddeltouren zeigen dann, dass keine Hindernisdaten bekannt sind.',
'admin.paddleRegions.status.queued': 'Wartet',
'admin.paddleRegions.status.importing': 'Liest',
'admin.paddleRegions.status.ready': 'Bereit',
'admin.paddleRegions.status.failed': 'Fehler',
'admin.paddleRegions.phase.download': 'Lädt herunter',
'admin.paddleRegions.phase.read': 'Liest aus',
'admin.paddleRegions.objects': '{count} Objekte',
'admin.paddleRegions.objects.one': '{count} Objekt',
'admin.paddleRegions.asOf': 'Stand {date}',
'admin.paddleRegions.keptData': 'Der zuvor gelesene Stand bleibt in Gebrauch.',
'admin.paddleRegions.refresh': 'Neu lesen',
'admin.paddleRegions.remove': 'Region entfernen',
'admin.paddleRegions.removeMessage':
'{name} und ihre Hindernisdaten werden entfernt. Paddeltouren dort zeigen dann keine Daten.',
'admin.paddleRegions.retry': 'Erneut versuchen',
'admin.paddleRegions.offline': 'Regionen lassen sich nur mit Verbindung verwalten.',
'admin.paddleRegions.catalogUnavailable': 'Geofabrik ist gerade nicht erreichbar. Versuche es später erneut.',
'admin.paddleRegions.attribution':
'Daten © OpenStreetMap-Mitwirkende (ODbL), regionale Auszüge von Geofabrik. Neu gelesen am 3. jedes Monats.',
'admin.paddleRegions.toast.added': 'Region hinzugefügt. TREK liest sie im Hintergrund.',
'admin.paddleRegions.toast.removed': 'Region entfernt',
};
export default admin;
+30 -2
View File
@@ -295,8 +295,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Gib ein Tempo von 0,5 bis 20 km/h ein.',
'tours.paddle.chooseBoat': 'Wähle Boot und Gewässer, um diese Paddeltour zu speichern.',
'tours.paddle.chooseBoatImport': 'Wähle Boot und Gewässer, um als Paddeltour zu importieren.',
'tours.paddle.noWaterData':
'Wehre, Schleusen und Umtragestellen zeigt TREK noch nicht an. Prüfe das Gewässer und seine Regeln selbst, bevor du losfährst.',
'tours.paddle.drawnByHand':
'Eine Paddeltour wird von Hand gezeichnet: TREK verbindet deine Punkte mit geraden Linien. Nichts wird geroutet, und nichts sagt, dass das Gewässer befahrbar ist.',
'tours.paddle.timeEstimated': 'Paddelzeit (geschätzt)',
@@ -448,6 +446,36 @@ const tours: TranslationStrings = {
'tours.paddle.obstacle.kind.slipway': 'Slipanlage',
'tours.paddle.obstacle.kind.portage': 'Umtragestelle',
'tours.paddle.obstacle.kind.access': 'Befahrungsregel',
'tours.paddle.obstacles.truncated':
'TREK zeigt hier nur einen Teil der Objekte, das Gebiet enthält mehr. Prüfe das Gewässer selbst.',
'tours.paddle.obstacles.dateUnknown': 'unbekannt',
'tours.paddle.obstacleCard.onRoute': 'Auf der Route bei',
'tours.paddle.obstacleCard.near': 'In der Nähe',
'tours.paddle.obstacleCard.area': 'Im Gebiet',
'tours.paddle.obstacleCard.uncertain': 'Bezug unsicher',
'tours.paddle.obstacleCard.alongInfo': 'Wie weit es vom Start an der Route liegt',
'tours.paddle.obstacleCard.distanceInfo':
'Wie weit es von der Route entfernt liegt. Es kann an einem anderen Arm des Gewässers liegen.',
'tours.paddle.obstacleCard.privateWay': 'Privater Weg',
'tours.paddle.obstacleCard.noAccessInfo': 'Keine Befahrungsangabe',
'tours.paddle.obstacleCard.condition': 'Bedingung',
'tours.paddle.obstacleCard.access.canoe': 'Kanu',
'tours.paddle.obstacleCard.access.boat': 'Boot',
'tours.paddle.obstacleCard.access.access': 'Zugang',
'tours.paddle.obstacleCard.accessValue.yes': 'ja',
'tours.paddle.obstacleCard.accessValue.no': 'nein',
'tours.paddle.obstacleCard.accessValue.private': 'privat',
'tours.paddle.obstacleCard.accessValue.permissive': 'geduldet',
'tours.paddle.obstacleCard.accessValue.designated': 'ausgewiesen',
'tours.paddle.obstacleCard.fact.height': 'Höhe',
'tours.paddle.obstacleCard.fact.width': 'Breite',
'tours.paddle.obstacleCard.fact.grade': 'Schwierigkeit',
'tours.paddle.obstacleCard.fact.opening_hours': 'Öffnungszeiten',
'tours.paddle.obstacleCard.fact.seasonal': 'Saisonal',
'tours.paddle.obstacleCard.fact.fee': 'Gebühr',
'tours.paddle.obstacleCard.fact.operator': 'Betreiber',
'tours.paddle.obstacleCard.source': 'OpenStreetMap-Daten, Stand {date}, keine Prüfung vor Ort.',
'tours.paddle.obstacleCard.sourceUndated': 'OpenStreetMap-Daten unbekannten Stands, keine Prüfung vor Ort.',
};
export default tours;
+29
View File
@@ -833,5 +833,34 @@ const admin: TranslationStrings = {
'admin.mapsKeyHintShort': 'Adds photos, ratings and opening hours. Every lookup then goes to Google.',
'admin.amapKeyHintShort': 'For place search inside mainland China. Needs a web service key, not a JS API key.',
'admin.collab.links.subtitle': 'Shared links and bookmarks',
'admin.paddleRegions.title': 'Paddle water data',
'admin.paddleRegions.hint':
'Weirs, locks and paddling rules from OpenStreetMap, read per region and again every month.',
'admin.paddleRegions.search': 'Add a region',
'admin.paddleRegions.searchPlaceholder': 'Search Geofabrik, for example Oberpfalz',
'admin.paddleRegions.noHits': 'No region by that name',
'admin.paddleRegions.added': 'Added',
'admin.paddleRegions.empty': 'No region yet. Paddle tours then show that no obstacle data is known.',
'admin.paddleRegions.status.queued': 'Waiting',
'admin.paddleRegions.status.importing': 'Reading',
'admin.paddleRegions.status.ready': 'Ready',
'admin.paddleRegions.status.failed': 'Failed',
'admin.paddleRegions.phase.download': 'Downloading',
'admin.paddleRegions.phase.read': 'Reading',
'admin.paddleRegions.objects': '{count} objects',
'admin.paddleRegions.objects.one': '{count} object',
'admin.paddleRegions.asOf': 'As of {date}',
'admin.paddleRegions.keptData': 'The data read before stays in use.',
'admin.paddleRegions.refresh': 'Read again',
'admin.paddleRegions.remove': 'Remove region',
'admin.paddleRegions.removeMessage':
'{name} and its obstacle data are removed. Paddle tours there then show no data.',
'admin.paddleRegions.retry': 'Try again',
'admin.paddleRegions.offline': 'Regions can only be managed with a connection.',
'admin.paddleRegions.catalogUnavailable': 'Geofabrik cannot be reached right now. Try again later.',
'admin.paddleRegions.attribution':
'Data © OpenStreetMap contributors (ODbL), regional extracts by Geofabrik. Read again on the 3rd of every month.',
'admin.paddleRegions.toast.added': 'Region added. TREK reads it in the background.',
'admin.paddleRegions.toast.removed': 'Region removed',
};
export default admin;
+29 -2
View File
@@ -286,8 +286,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Enter a speed from 0.5 to 20 km/h.',
'tours.paddle.chooseBoat': 'Choose the boat and the water to save this paddle tour.',
'tours.paddle.chooseBoatImport': 'Choose the boat and the water to import as a paddle tour.',
'tours.paddle.noWaterData':
'TREK does not show weirs, locks or portages yet. Check the water and its rules yourself before you set off.',
'tours.paddle.drawnByHand':
'A paddle tour is drawn by hand: TREK joins your points with straight lines. Nothing is routed, and nothing says the water can be paddled.',
'tours.paddle.timeEstimated': 'Paddling time (estimated)',
@@ -437,5 +435,34 @@ const tours: TranslationStrings = {
'tours.paddle.obstacle.kind.slipway': 'Slipway',
'tours.paddle.obstacle.kind.portage': 'Portage',
'tours.paddle.obstacle.kind.access': 'Access rule',
'tours.paddle.obstacles.truncated':
'TREK shows only part of the objects here; the area holds more. Check the water yourself.',
'tours.paddle.obstacles.dateUnknown': 'an unknown date',
'tours.paddle.obstacleCard.onRoute': 'On the route at',
'tours.paddle.obstacleCard.near': 'Near the route',
'tours.paddle.obstacleCard.area': 'In the area',
'tours.paddle.obstacleCard.uncertain': 'Link unclear',
'tours.paddle.obstacleCard.alongInfo': 'How far along the route it lies, from the start',
'tours.paddle.obstacleCard.distanceInfo': 'How far from the route it lies. It may be on another arm of the water.',
'tours.paddle.obstacleCard.privateWay': 'Private way',
'tours.paddle.obstacleCard.noAccessInfo': 'No paddling rule known',
'tours.paddle.obstacleCard.condition': 'Condition',
'tours.paddle.obstacleCard.access.canoe': 'Canoe',
'tours.paddle.obstacleCard.access.boat': 'Boat',
'tours.paddle.obstacleCard.access.access': 'Access',
'tours.paddle.obstacleCard.accessValue.yes': 'yes',
'tours.paddle.obstacleCard.accessValue.no': 'no',
'tours.paddle.obstacleCard.accessValue.private': 'private',
'tours.paddle.obstacleCard.accessValue.permissive': 'tolerated',
'tours.paddle.obstacleCard.accessValue.designated': 'designated',
'tours.paddle.obstacleCard.fact.height': 'Height',
'tours.paddle.obstacleCard.fact.width': 'Width',
'tours.paddle.obstacleCard.fact.grade': 'Grade',
'tours.paddle.obstacleCard.fact.opening_hours': 'Hours',
'tours.paddle.obstacleCard.fact.seasonal': 'Seasonal',
'tours.paddle.obstacleCard.fact.fee': 'Fee',
'tours.paddle.obstacleCard.fact.operator': 'Operator',
'tours.paddle.obstacleCard.source': 'OpenStreetMap data as of {date}, not a check on site.',
'tours.paddle.obstacleCard.sourceUndated': 'OpenStreetMap data of an unknown date, not a check on site.',
};
export default tours;
-2
View File
@@ -292,8 +292,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Introduce una velocidad de 0,5 a 20 km/h.',
'tours.paddle.chooseBoat': 'Elige la embarcación y el agua para guardar esta ruta.',
'tours.paddle.chooseBoatImport': 'Elige la embarcación y el agua para importar como ruta en piragua.',
'tours.paddle.noWaterData':
'TREK aún no muestra presas, esclusas ni porteos. Comprueba tú mismo el agua y sus normas antes de salir.',
'tours.paddle.drawnByHand':
'Una ruta en piragua se dibuja a mano: TREK une tus puntos con líneas rectas. Nada se calcula y nada indica que el agua sea navegable.',
'tours.paddle.timeEstimated': 'Tiempo de remo (estimado)',
-2
View File
@@ -289,8 +289,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Sisesta kiirus vahemikus 0,5 kuni 20 km/h.',
'tours.paddle.chooseBoat': 'Vali paat ja veekogu, et see matk salvestada.',
'tours.paddle.chooseBoatImport': 'Vali paat ja veekogu, et importida aerutamismatkana.',
'tours.paddle.noWaterData':
'TREK ei näita veel paise, lüüse ega ülekandekohti. Kontrolli enne teele asumist veekogu ja selle reegleid ise.',
'tours.paddle.drawnByHand':
'Aerutamismatk joonistatakse käsitsi: TREK ühendab sinu punktid sirgjoontega. Midagi ei marsruudita ja miski ei ütle, et vesi on läbitav.',
'tours.paddle.timeEstimated': 'Aerutamisaeg (hinnanguline)',
-2
View File
@@ -298,8 +298,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Saisissez une vitesse de 0,5 à 20 km/h.',
'tours.paddle.chooseBoat': 'Choisissez l’embarcation et le plan d’eau pour enregistrer cette sortie.',
'tours.paddle.chooseBoatImport': 'Choisissez l’embarcation et le plan d’eau pour importer en sortie en pagaie.',
'tours.paddle.noWaterData':
'TREK n’affiche pas encore les barrages, écluses ni portages. Vérifiez vous-même le plan d’eau et ses règles avant de partir.',
'tours.paddle.drawnByHand':
'Une sortie en pagaie se trace à la main : TREK relie vos points en lignes droites. Rien n’est calculé, et rien ne dit que l’eau est navigable.',
'tours.paddle.timeEstimated': 'Temps de pagaie (estimé)',
-2
View File
@@ -299,8 +299,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Δώστε ταχύτητα από 0,5 έως 20 km/h.',
'tours.paddle.chooseBoat': 'Επιλέξτε σκάφος και νερό για να αποθηκεύσετε τη διαδρομή.',
'tours.paddle.chooseBoatImport': 'Επιλέξτε σκάφος και νερό για εισαγωγή ως διαδρομή με κουπί.',
'tours.paddle.noWaterData':
'Το TREK δεν δείχνει ακόμη φράγματα, υδατοφράκτες ή σημεία μεταφοράς. Ελέγξτε μόνοι σας το νερό και τους κανόνες πριν ξεκινήσετε.',
'tours.paddle.drawnByHand':
'Μια διαδρομή με κουπί σχεδιάζεται με το χέρι: το TREK ενώνει τα σημεία σας με ευθείες. Τίποτα δεν δρομολογείται και τίποτα δεν λέει ότι το νερό είναι πλωτό.',
'tours.paddle.timeEstimated': 'Χρόνος κωπηλασίας (εκτίμηση)',
-2
View File
@@ -293,8 +293,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Adj meg egy tempót 0,5 és 20 km/h között.',
'tours.paddle.chooseBoat': 'Válaszd ki a hajót és a vizet a túra mentéséhez.',
'tours.paddle.chooseBoatImport': 'Válaszd ki a hajót és a vizet az evezős túraként való importáláshoz.',
'tours.paddle.noWaterData':
'A TREK még nem mutatja a gátakat, zsilipeket és átemelőket. Indulás előtt magad ellenőrizd a vizet és a szabályokat.',
'tours.paddle.drawnByHand':
'Az evezős túrát kézzel rajzolod: a TREK egyenes vonalakkal köti össze a pontjaidat. Semmi sincs útvonalra tervezve, és semmi sem mondja, hogy a víz járható.',
'tours.paddle.timeEstimated': 'Evezési idő (becslés)',
-2
View File
@@ -286,8 +286,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Masukkan kecepatan dari 0,5 hingga 20 km/jam.',
'tours.paddle.chooseBoat': 'Pilih perahu dan perairan untuk menyimpan tur dayung ini.',
'tours.paddle.chooseBoatImport': 'Pilih perahu dan perairan untuk mengimpor sebagai tur dayung.',
'tours.paddle.noWaterData':
'TREK belum menampilkan bendung, pintu air, atau titik angkut. Periksa sendiri perairan dan aturannya sebelum berangkat.',
'tours.paddle.drawnByHand':
'Tur dayung digambar dengan tangan: TREK menghubungkan titik-titikmu dengan garis lurus. Tidak ada yang dirutekan, dan tidak ada yang menyatakan perairan bisa didayung.',
'tours.paddle.timeEstimated': 'Waktu mendayung (perkiraan)',
-2
View File
@@ -293,8 +293,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Inserisci una velocità da 0,5 a 20 km/h.',
'tours.paddle.chooseBoat': 'Scegli imbarcazione e acqua per salvare questa uscita.',
'tours.paddle.chooseBoatImport': 'Scegli imbarcazione e acqua per importare come uscita in pagaia.',
'tours.paddle.noWaterData':
'TREK non mostra ancora sbarramenti, chiuse e trasbordi. Controlla tu stesso l’acqua e le sue regole prima di partire.',
'tours.paddle.drawnByHand':
'Un’uscita in pagaia si disegna a mano: TREK collega i tuoi punti con linee rette. Niente viene calcolato e niente dice che l’acqua sia navigabile.',
'tours.paddle.timeEstimated': 'Tempo di pagaiata (stimato)',
-2
View File
@@ -280,8 +280,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '0.5〜20 km/h の速度を入力してください。',
'tours.paddle.chooseBoat': 'このパドルツアーを保存するには、ボートと水域を選んでください。',
'tours.paddle.chooseBoatImport': 'パドルツアーとして取り込むには、ボートと水域を選んでください。',
'tours.paddle.noWaterData':
'TREK はまだ堰、閘門、陸上運搬区間を表示しません。出発前に水域とそのルールをご自身で確認してください。',
'tours.paddle.drawnByHand':
'パドルツアーは手で描きます。TREK はポイントを直線で結ぶだけで、ルート計算はせず、その水域を漕げることも示しません。',
'tours.paddle.timeEstimated': 'パドル時間(推定)',
-2
View File
@@ -278,8 +278,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '0.5~20 km/h 사이의 속도를 입력하세요.',
'tours.paddle.chooseBoat': '이 패들 투어를 저장하려면 보트와 수역을 선택하세요.',
'tours.paddle.chooseBoatImport': '패들 투어로 가져오려면 보트와 수역을 선택하세요.',
'tours.paddle.noWaterData':
'TREK은 아직 보, 갑문, 육상 운반 구간을 표시하지 않습니다. 출발 전에 수역과 규칙을 직접 확인하세요.',
'tours.paddle.drawnByHand':
'패들 투어는 손으로 그립니다. TREK은 지점들을 직선으로 이을 뿐 경로를 계산하지 않으며, 그 물길을 지날 수 있다고 알려주지도 않습니다.',
'tours.paddle.timeEstimated': '패들 시간(추정)',
-2
View File
@@ -294,8 +294,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Vul een tempo van 0,5 tot 20 km/h in.',
'tours.paddle.chooseBoat': 'Kies boot en water om deze peddeltocht op te slaan.',
'tours.paddle.chooseBoatImport': 'Kies boot en water om als peddeltocht te importeren.',
'tours.paddle.noWaterData':
'TREK toont nog geen stuwen, sluizen of overdraagplaatsen. Controleer het water en de regels zelf voor je vertrekt.',
'tours.paddle.drawnByHand':
'Een peddeltocht teken je met de hand: TREK verbindt je punten met rechte lijnen. Er wordt niets gerouteerd en niets zegt dat het water bevaarbaar is.',
'tours.paddle.timeEstimated': 'Peddeltijd (geschat)',
-2
View File
@@ -300,8 +300,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Podaj tempo od 0,5 do 20 km/h.',
'tours.paddle.chooseBoat': 'Wybierz łódź i wodę, aby zapisać ten spływ.',
'tours.paddle.chooseBoatImport': 'Wybierz łódź i wodę, aby zaimportować jako spływ.',
'tours.paddle.noWaterData':
'TREK nie pokazuje jeszcze jazów, śluz ani przenosek. Przed wyruszeniem sprawdź sam wodę i obowiązujące zasady.',
'tours.paddle.drawnByHand':
'Spływ rysuje się ręcznie: TREK łączy Twoje punkty prostymi liniami. Nic nie jest wyznaczane i nic nie mówi, że woda jest spławna.',
'tours.paddle.timeEstimated': 'Czas na wodzie (szacunkowo)',
-2
View File
@@ -303,8 +303,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Введите скорость от 0,5 до 20 км/ч.',
'tours.paddle.chooseBoat': 'Выберите лодку и водоём, чтобы сохранить этот сплав.',
'tours.paddle.chooseBoatImport': 'Выберите лодку и водоём, чтобы импортировать как сплав.',
'tours.paddle.noWaterData':
'TREK пока не показывает плотины, шлюзы и обносы. Перед выходом сами проверьте водоём и правила на нём.',
'tours.paddle.drawnByHand':
'Сплав рисуется вручную: TREK соединяет ваши точки прямыми линиями. Ничего не прокладывается, и ничто не говорит, что по воде можно пройти.',
'tours.paddle.timeEstimated': 'Время на воде (оценка)',
-2
View File
@@ -288,8 +288,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Zadajte rýchlosť od 0,5 do 20 km/h.',
'tours.paddle.chooseBoat': 'Vyberte loď a vodu, aby ste mohli túto túru uložiť.',
'tours.paddle.chooseBoatImport': 'Vyberte loď a vodu na import ako vodáckej túry.',
'tours.paddle.noWaterData':
'TREK zatiaľ nezobrazuje hate, plavebné komory ani prenášky. Pred vyplávaním si vodu a pravidlá overte sami.',
'tours.paddle.drawnByHand':
'Vodácka túra sa kreslí ručne: TREK spojí vaše body priamymi čiarami. Nič sa nepočíta a nič nehovorí, že je voda splavná.',
'tours.paddle.timeEstimated': 'Čas na vode (odhad)',
-2
View File
@@ -285,8 +285,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Ange ett tempo från 0,5 till 20 km/h.',
'tours.paddle.chooseBoat': 'Välj båt och vatten för att spara paddelturen.',
'tours.paddle.chooseBoatImport': 'Välj båt och vatten för att importera som paddeltur.',
'tours.paddle.noWaterData':
'TREK visar ännu inte dammar, slussar eller lyft. Kontrollera vattnet och dess regler själv innan du ger dig av.',
'tours.paddle.drawnByHand':
'En paddeltur ritas för hand: TREK förbinder dina punkter med raka linjer. Inget ruttas, och inget säger att vattnet går att paddla.',
'tours.paddle.timeEstimated': 'Paddeltid (uppskattad)',
-2
View File
@@ -274,8 +274,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'ใส่ความเร็วระหว่าง 0.5 ถึง 20 กม./ชม.',
'tours.paddle.chooseBoat': 'เลือกเรือและแหล่งน้ำเพื่อบันทึกทริปพายเรือนี้',
'tours.paddle.chooseBoatImport': 'เลือกเรือและแหล่งน้ำเพื่อนำเข้าเป็นทริปพายเรือ',
'tours.paddle.noWaterData':
'TREK ยังไม่แสดงฝาย ประตูน้ำ หรือจุดแบกเรือ โปรดตรวจสอบแหล่งน้ำและกฎด้วยตัวเองก่อนออกเดินทาง',
'tours.paddle.drawnByHand':
'ทริปพายเรือวาดด้วยมือ: TREK เชื่อมจุดของคุณด้วยเส้นตรง ไม่มีการคำนวณเส้นทาง และไม่มีอะไรบอกว่าแหล่งน้ำพายผ่านได้',
'tours.paddle.timeEstimated': 'เวลาพาย (โดยประมาณ)',
-2
View File
@@ -287,8 +287,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '0,5 ile 20 km/sa arasında bir hız gir.',
'tours.paddle.chooseBoat': 'Bu kürek turunu kaydetmek için tekneyi ve suyu seç.',
'tours.paddle.chooseBoatImport': 'Kürek turu olarak içe aktarmak için tekneyi ve suyu seç.',
'tours.paddle.noWaterData':
'TREK henüz savakları, kilitleri ve taşıma noktalarını göstermiyor. Yola çıkmadan önce suyu ve kurallarını kendin kontrol et.',
'tours.paddle.drawnByHand':
'Kürek turu elle çizilir: TREK noktalarını düz çizgilerle birleştirir. Hiçbir şey rotalanmaz ve hiçbir şey suyun geçilebilir olduğunu söylemez.',
'tours.paddle.timeEstimated': 'Kürek süresi (tahmini)',
-2
View File
@@ -300,8 +300,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Введіть швидкість від 0,5 до 20 км/год.',
'tours.paddle.chooseBoat': 'Виберіть човен і водойму, щоб зберегти цей сплав.',
'tours.paddle.chooseBoatImport': 'Виберіть човен і водойму, щоб імпортувати як сплав.',
'tours.paddle.noWaterData':
'TREK поки не показує греблі, шлюзи й обноси. Перед виходом самі перевірте водойму та правила на ній.',
'tours.paddle.drawnByHand':
'Сплав малюється вручну: TREK з’єднує ваші точки прямими лініями. Нічого не прокладається, і ніщо не каже, що водою можна пройти.',
'tours.paddle.timeEstimated': 'Час на воді (оцінка)',
-2
View File
@@ -287,8 +287,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': 'Nhập tốc độ từ 0,5 đến 20 km/h.',
'tours.paddle.chooseBoat': 'Chọn thuyền và mặt nước để lưu chuyến chèo này.',
'tours.paddle.chooseBoatImport': 'Chọn thuyền và mặt nước để nhập thành chuyến chèo thuyền.',
'tours.paddle.noWaterData':
'TREK chưa hiển thị đập tràn, âu thuyền hay điểm khiêng thuyền. Hãy tự kiểm tra mặt nước và quy định trước khi xuất phát.',
'tours.paddle.drawnByHand':
'Chuyến chèo thuyền được vẽ bằng tay: TREK nối các điểm của bạn bằng đường thẳng. Không có gì được định tuyến, và không gì cho biết mặt nước có chèo được hay không.',
'tours.paddle.timeEstimated': 'Thời gian chèo (ước tính)',
-1
View File
@@ -264,7 +264,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '請輸入 0.5 至 20 km/h 之間的速度。',
'tours.paddle.chooseBoat': '請選擇船隻和水域以儲存此划槳行程。',
'tours.paddle.chooseBoatImport': '請選擇船隻和水域以匯入為划槳行程。',
'tours.paddle.noWaterData': 'TREK 尚未顯示堰壩、船閘或陸上搬運點。出發前請自行確認水域及其規定。',
'tours.paddle.drawnByHand': '划槳行程需手動繪製:TREK 以直線連接你的點,不進行路線規劃,也不表示該水域可以通行。',
'tours.paddle.timeEstimated': '划槳時間(估算)',
'tours.paddle.timeEstimatedShort': '划槳',
-1
View File
@@ -265,7 +265,6 @@ const tours: TranslationStrings = {
'tours.paddle.speedInvalid': '请输入 0.5 至 20 km/h 之间的速度。',
'tours.paddle.chooseBoat': '请选择船只和水域以保存此划桨路线。',
'tours.paddle.chooseBoatImport': '请选择船只和水域以导入为划桨路线。',
'tours.paddle.noWaterData': 'TREK 暂不显示堰坝、船闸或陆上搬运点。出发前请自行确认水域及其规定。',
'tours.paddle.drawnByHand': '划桨路线需手动绘制:TREK 用直线连接你的点,不做路线规划,也不表示该水域可以通行。',
'tours.paddle.timeEstimated': '划桨用时(估算)',
'tours.paddle.timeEstimatedShort': '划桨',
@@ -1,5 +1,5 @@
import type { PaddleFeature, PaddleGeometry } from './paddle-obstacles.schema';
import { NEAR_M, ON_ROUTE_M, relateObstaclesToRoute } from './paddle-route-relation';
import { measuredRouteLengthM, NEAR_M, ON_ROUTE_M, relateObstaclesToRoute } from './paddle-route-relation';
import { describe, expect, it } from 'vitest';
@@ -92,3 +92,11 @@ describe('relateObstaclesToRoute', () => {
expect(relateObstaclesToRoute(route, [otherArm])[0]).toMatchObject({ relation: 'near', distanceM: 100 });
});
});
describe('measuredRouteLengthM', () => {
it('measures the route the way its relations do', () => {
expect(measuredRouteLengthM(route)).toBeCloseTo(2000, 0);
expect(measuredRouteLengthM([at(0, 0)])).toBe(0);
expect(measuredRouteLengthM([])).toBe(0);
});
});
+34 -12
View File
@@ -54,11 +54,16 @@ function crosses(a: Local, b: Local, c: Local, d: Local): number | null {
return t >= 0 && t <= 1 && u >= 0 && u <= 1 ? t : null;
}
/** The rings of an object: a point is one ring of one point, a line one open ring, an area its outline and holes. */
function ringsOf(geometry: PaddleGeometry): ReadonlyArray<ReadonlyArray<readonly [number, number]>> {
if (geometry.type === 'Point') return [[geometry.coordinates, geometry.coordinates]];
if (geometry.type === 'LineString') return [geometry.coordinates];
return geometry.coordinates;
/**
* The rings of an object: a point is one ring of one point, a line one open
* ring, an area its outline and holes. Read as plain numbers, since the client
* compiles this without strict mode, where the schema's pairs are "maybe two".
*/
function ringsOf(geometry: PaddleGeometry): Array<Array<readonly [number, number]>> {
const pair = (position: readonly unknown[]): readonly [number, number] => [Number(position[0]), Number(position[1])];
if (geometry.type === 'Point') return [[pair(geometry.coordinates), pair(geometry.coordinates)]];
if (geometry.type === 'LineString') return [geometry.coordinates.map(pair)];
return geometry.coordinates.map((ring) => ring.map(pair));
}
function segmentsOf(rings: ReadonlyArray<readonly Local[]>): Array<[Local, Local]> {
@@ -154,6 +159,29 @@ function closestApproach(
return best;
}
/** The flat view of a route and the distance along it at each point, as the relations measure it. */
function measured(route: readonly LatLng[]): { path: Local[]; along: number[] } {
const project = projector(route[0]?.[0] ?? 0);
const path = route.map(([lat, lng]) => project(lat, lng));
const along: number[] = [0];
for (let i = 1; i < path.length; i++) {
const a = path[i - 1] as Local;
const b = path[i] as Local;
along.push((along[i - 1] ?? 0) + Math.hypot(b.x - a.x, b.y - a.y));
}
return { path, along };
}
/**
* The length of a route as the relations measure along it, in metres: what
* their `alongM` is a part of, so a view that draws the route at another
* scale places an obstacle by its share of this length.
*/
export function measuredRouteLengthM(route: readonly LatLng[]): number {
const { along } = measured(route);
return along[along.length - 1] ?? 0;
}
/**
* The obstacles on or near a route, in the order the route reaches them. The
* route is `[lat, lng]` pairs as the planner draws it; an empty or one-point
@@ -167,13 +195,7 @@ export function relateObstaclesToRoute(
): RelatedObstacle[] {
if (route.length < 2) return [];
const project = projector(route[0]?.[0] ?? 0);
const path = route.map(([lat, lng]) => project(lat, lng));
const along: number[] = [0];
for (let i = 1; i < path.length; i++) {
const a = path[i - 1] as Local;
const b = path[i] as Local;
along.push((along[i - 1] ?? 0) + Math.hypot(b.x - a.x, b.y - a.y));
}
const { path, along } = measured(route);
const routeBounds = boundsOf(path, NEAR_M);
const related: RelatedObstacle[] = [];
for (const feature of features) {