Add waypoints to printed PDF (#307)

* Print waypoints

* add waypoint titles and format nicely with description and first page

* add translations

* handle creation of second page a bit better

* adds padding between waypoint name and description

---------

Co-authored-by: Christian Beutel <>
This commit is contained in:
Robert Clarke
2025-06-26 11:45:07 +01:00
committed by GitHub
parent 7293017c6d
commit 9a724a5534
11 changed files with 112 additions and 12 deletions

View File

@@ -164,6 +164,7 @@
"import": "Importieren", "import": "Importieren",
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...", "import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
"include-description": "Beschreibung übernehmen", "include-description": "Beschreibung übernehmen",
"include-waypoints": "Wegpunkte einbeziehen",
"integration-description-komoot": "Synchronisiert Deine komoot Touren regelmäßig mit wanderer.", "integration-description-komoot": "Synchronisiert Deine komoot Touren regelmäßig mit wanderer.",
"integration-description-strava": "Synchronisiert Deine strava Routen und Aktivitäten regelmäßig mit wanderer.", "integration-description-strava": "Synchronisiert Deine strava Routen und Aktivitäten regelmäßig mit wanderer.",
"integration-disabled": "Integration deaktiviert", "integration-disabled": "Integration deaktiviert",

View File

@@ -164,6 +164,7 @@
"import": "Import", "import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...", "import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description", "include-description": "Include description",
"include-waypoints": "Include waypoints",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.", "integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.", "integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integration-disabled": "integration disabled", "integration-disabled": "integration disabled",

View File

@@ -164,6 +164,7 @@
"import": "Importar", "import": "Importar",
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...", "import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
"include-description": "Incluir descripción", "include-description": "Incluir descripción",
"include-waypoints": "Incluir puntos de interés",
"integration-description-komoot": "Sincroniza tus recorridos de Komoot con Wanderer en intervalos regulares.", "integration-description-komoot": "Sincroniza tus recorridos de Komoot con Wanderer en intervalos regulares.",
"integration-description-strava": "Sincroniza tus recorridos de Strava con Wanderer en intervalos regulares.", "integration-description-strava": "Sincroniza tus recorridos de Strava con Wanderer en intervalos regulares.",
"integration-disabled": "integración deshabilitada", "integration-disabled": "integración deshabilitada",

View File

@@ -164,6 +164,7 @@
"import": "Importer", "import": "Importer",
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...", "import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
"include-description": "Inclure la description", "include-description": "Inclure la description",
"include-waypoints": "Inclure les points de passage",
"integration-description-komoot": "Synchronisez vos Tours Komoot avec wanderer à intervalles réguliers.", "integration-description-komoot": "Synchronisez vos Tours Komoot avec wanderer à intervalles réguliers.",
"integration-description-strava": "Synchronisez vos itinéraires et vos activités Strava avec wanderer à intervalles réguliers.", "integration-description-strava": "Synchronisez vos itinéraires et vos activités Strava avec wanderer à intervalles réguliers.",
"integration-disabled": "Intégration désactivée", "integration-disabled": "Intégration désactivée",

View File

@@ -164,6 +164,7 @@
"import": "Import", "import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...", "import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description", "include-description": "Include description",
"include-waypoints": "Útpontok hozzáadása",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.", "integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.", "integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integration-disabled": "integration disabled", "integration-disabled": "integration disabled",

View File

@@ -164,6 +164,7 @@
"import": "Importa", "import": "Importa",
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...", "import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
"include-description": "Adotta descrizione", "include-description": "Adotta descrizione",
"include-waypoints": "Includi waypoint",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.", "integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.", "integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integration-disabled": "integration disabled", "integration-disabled": "integration disabled",

View File

@@ -164,6 +164,7 @@
"import": "Importeer", "import": "Importeer",
"import-hint": "Selecteer of sleep GPX-, FIT-, KML- of TCX-bestanden hierheen...", "import-hint": "Selecteer of sleep GPX-, FIT-, KML- of TCX-bestanden hierheen...",
"include-description": "Inclusief beschrijving", "include-description": "Inclusief beschrijving",
"include-waypoints": "Waypoints toevoegen",
"integration-description-komoot": "Synchroniseert je Komoot-tochten met Wanderer op regelmatige tijdstippen.", "integration-description-komoot": "Synchroniseert je Komoot-tochten met Wanderer op regelmatige tijdstippen.",
"integration-description-strava": "Synchroniseert je Strava-routes en -activiteiten met Wanderer op regelmatige tijdstippen.", "integration-description-strava": "Synchroniseert je Strava-routes en -activiteiten met Wanderer op regelmatige tijdstippen.",
"integration-disabled": "Integratie uitgeschakeld", "integration-disabled": "Integratie uitgeschakeld",

View File

@@ -164,6 +164,7 @@
"import": "Importuj", "import": "Importuj",
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...", "import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
"include-description": "Dołącz opis", "include-description": "Dołącz opis",
"include-waypoints": "Uwzględnij punkty trasy",
"integration-description-komoot": "Synchronizuje trasy kamoot z wanderer w równych odstępach.", "integration-description-komoot": "Synchronizuje trasy kamoot z wanderer w równych odstępach.",
"integration-description-strava": "Synchronizuje trasy i aktywność z wanderer w równych odstępach.", "integration-description-strava": "Synchronizuje trasy i aktywność z wanderer w równych odstępach.",
"integration-disabled": "integracja wyłączona", "integration-disabled": "integracja wyłączona",

View File

@@ -164,6 +164,7 @@
"import": "Importar", "import": "Importar",
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...", "import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
"include-description": "Incluir descrição", "include-description": "Incluir descrição",
"include-waypoints": "Incluir pontos de passagem",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.", "integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.", "integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integration-disabled": "integration disabled", "integration-disabled": "integration disabled",

View File

@@ -164,6 +164,7 @@
"import": "导入", "import": "导入",
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...", "import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
"include-description": "包含描述", "include-description": "包含描述",
"include-waypoints": "包括航点",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.", "integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.", "integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integration-disabled": "integration disabled", "integration-disabled": "integration disabled",

View File

@@ -81,6 +81,7 @@
let printLoading: boolean = $state(false); let printLoading: boolean = $state(false);
let includeDescription: boolean = $state(false); let includeDescription: boolean = $state(false);
let includeWaypoints: boolean = $state(false);
onMount(() => { onMount(() => {
let qrcode = new QrCodeWithLogo({ let qrcode = new QrCodeWithLogo({
@@ -92,6 +93,30 @@
}); });
}); });
function faUnicode(name: string) {
var testI = document.createElement('i');
var char;
testI.className = 'fa fa-' + name;
document.body.appendChild(testI);
char = window.getComputedStyle( testI, ':before' )
.content.replace(/'|"/g, '');
testI.remove();
return char.charCodeAt(0);
}
function getTextHeight(text: string, doc: jsPDF, width: number) {
let numLines = doc.splitTextToSize(text, width).length;
return (numLines * doc.getFontSize() * doc.getLineHeightFactor() -
doc.getFontSize() * (doc.getLineHeightFactor() - 1)) /
doc.internal.scaleFactor;
}
async function print() { async function print() {
if (!map) { if (!map) {
return; return;
@@ -328,25 +353,17 @@
doc.setFont("IBMPlexSans-Regular", "normal"); doc.setFont("IBMPlexSans-Regular", "normal");
doc.text($trail.location || "-", 12, currentHeight); doc.text($trail.location || "-", 12, currentHeight);
// End of page
currentHeight = height;
// Logo // Logo
const logo = new Image(); const logo = new Image();
logo.src = "/imgs/logo_text_dark.png"; logo.src = "/imgs/logo_text_dark.png";
const logoRatio = 64 / 212; const logoRatio = 64 / 212;
const logoWidth = 32; const logoWidth = 32;
const logoHeight = logoWidth * logoRatio; const logoHeight = logoWidth * logoRatio;
doc.addImage(
logo,
width - 8 - logoWidth,
height - 4 - logoHeight,
logoWidth,
logoHeight,
);
if (includeDescription && $trail.description) { function addLogo() {
doc.addPage();
doc.text(formatHTMLAsText($trail.description), 16, 16, {
maxWidth: width - 32,
});
doc.addImage( doc.addImage(
logo, logo,
width - 8 - logoWidth, width - 8 - logoWidth,
@@ -356,6 +373,68 @@
); );
} }
function newPage() {
addLogo();
doc.addPage();
currentHeight = 16;
}
if (includeDescription && $trail.description) {
let textHeight = getTextHeight($trail.description, doc, width - 32)
if (currentHeight + textHeight + 8 > height) {
newPage();
}
doc.text(formatHTMLAsText($trail.description), 16, currentHeight, {
maxWidth: width - 32,
});
currentHeight += getTextHeight($trail.description, doc, width - 32) + 8;
}
if (includeWaypoints && $trail.expand?.waypoints) {
const header = $_("waypoints", { values: { n: 2 } })
let textHeight = getTextHeight(header, doc, width - 32)
if (currentHeight + textHeight + 8 > height) {
newPage();
}
doc.setFont("IBMPlexSans-SemiBold", "bold");
doc.text(header, 16, currentHeight);
currentHeight += textHeight + 8;
doc.setFont("IBMPlexSans-Regular", "normal");
($trail.expand.waypoints || []).forEach(waypoint => {
let description = waypoint.description || "";
let name = waypoint.name || "";
let content = description;
if (name) {
content = `${name}\n${description}`;
}
let textHeight = getTextHeight(content, doc, width - 32)
if (currentHeight + textHeight + 16 > height) {
newPage();
}
doc.setFont("fa-solid-900", "normal");
doc.text(String.fromCharCode(faUnicode(waypoint.icon || "circle")), 16, currentHeight);
doc.setFont("IBMPlexSans-Regular", "normal");
if (name) {
doc.setFont("IBMPlexSans-SemiBold", "bold");
doc.text(name, 24, currentHeight);
currentHeight += getTextHeight(name, doc, width - 32) + 2;
doc.setFont("IBMPlexSans-Regular", "normal");
}
doc.text(description, 24, currentHeight, {
maxWidth: width - 32 - 8
});
currentHeight += textHeight + 8;
});
}
addLogo();
doc.save($trail.name + ".pdf"); doc.save($trail.name + ".pdf");
printLoading = false; printLoading = false;
} catch (e) { } catch (e) {
@@ -504,6 +583,17 @@
>{$_("include-description")}</label >{$_("include-description")}</label
> >
</div> </div>
<div>
<input
id="waypoints-checkbox"
type="checkbox"
bind:checked={includeWaypoints}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
/>
<label for="waypoints-checkbox" class="ms-2 text-sm"
>{$_("include-waypoints")}</label
>
</div>
<Button <Button
loading={printLoading} loading={printLoading}