{"id":10827,"date":"2026-09-18T08:05:48","date_gmt":"2026-09-18T07:05:48","guid":{"rendered":"https:\/\/letsrideportugal.com\/?page_id=10827"},"modified":"2026-09-18T19:42:26","modified_gmt":"2026-09-18T18:42:26","slug":"flight-planner","status":"publish","type":"page","link":"https:\/\/letsrideportugal.com\/de\/flight-planner\/","title":{"rendered":"Fl\u00fcge nach Faro finden"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"10827\" class=\"elementor elementor-10827 elementor-10805\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f1170010 e-con-full e-flex e-con e-parent\" data-id=\"f1170010\" data-element_type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f1170011 elementor-widget elementor-widget-shortcode\" data-id=\"f1170011\" data-element_type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><div data-compact=\"0\" data-lang=\"de\" lang=\"de-DE\" class=\"lrp-fp\" data-mode=\"planner\" data-api=\"https:\/\/letsrideportugal.com\/de\/wp-json\/lrp-flights\/v1\/\" data-nonce=\"\" data-preview=\"0\" data-initial-airport=\"\" data-initial-date=\"\" data-planner-url=\"https:\/\/letsrideportugal.com\/de\/flight-planner\/\" data-tours-url=\"https:\/\/letsrideportugal.com\/de\/tourtermine\/\"><p>Wir suchen deinen Weg nach Faro\u2026<\/p><\/div><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-f1170920 elementor-widget elementor-widget-html\" data-id=\"f1170920\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<style>\n\/* Flight Planner draft enhancement; no global map or page-scroll overrides. *\/\n.lrp-fp .lrp-map-zoom-enabled .fp-map-svg{touch-action:pan-y pinch-zoom}\n.lrp-fp .lrp-map-is-zoomed .fp-map-svg{cursor:grab;touch-action:none}\n.lrp-fp .lrp-map-is-dragging .fp-map-svg,.lrp-fp .lrp-map-is-dragging .fp-airport-point{cursor:grabbing}\n.lrp-fp .lrp-map-is-dragging{user-select:none}\n.lrp-fp .lrp-map-zoom-enabled :is(.fp-map-land path,.fp-route-arc){vector-effect:non-scaling-stroke}\n.lrp-fp .lrp-map-zoom-dock{position:absolute;right:20px;top:94px;z-index:2;display:flex;flex-direction:column;align-items:flex-end;gap:7px;pointer-events:none}\n.lrp-fp .lrp-map-zoom-dock[hidden]{display:none}\n.lrp-fp .lrp-map-zoom-controls{display:flex;align-items:center;gap:0;pointer-events:auto;border:1px solid #687253;border-radius:11px;background:#17221eeb;box-shadow:0 4px 18px #0004;overflow:visible}\n.lrp-fp .lrp-map-zoom-button{display:grid;place-items:center;min-width:40px;width:40px;height:42px;min-height:42px;padding:0;border:0;border-right:1px solid #59654c;border-radius:0;background:transparent;color:#f5f2e9;font-size:23px;font-weight:400;line-height:1;transition:background .15s,color .15s}\n.lrp-fp .lrp-map-zoom-button:first-child{border-radius:10px 0 0 10px}\n.lrp-fp .lrp-map-zoom-button:hover:not(:disabled){background:#344331;color:#ffb57f}\n.lrp-fp .lrp-map-zoom-button:disabled{opacity:.32;cursor:default}\n.lrp-fp .lrp-map-zoom-button:focus-visible{position:relative;z-index:1;outline:3px solid #ffb57f;outline-offset:2px}\n.lrp-fp .lrp-map-zoom-level{min-width:48px;padding:0 9px;color:#c9d1bc;font-size:10px;line-height:1;white-space:nowrap}\n.lrp-fp .lrp-map-zoom-hint{margin:0!important;padding:3px 7px;border-radius:5px;background:#152323d9;color:#c0c9b8!important;font-size:9px!important;line-height:1.5!important;text-shadow:0 1px 3px #000}\n@media(max-width:760px){.lrp-fp .lrp-map-zoom-dock{right:12px;top:84px}.lrp-fp .lrp-map-zoom-button{width:42px;min-width:42px;height:44px;min-height:44px}.lrp-fp .lrp-map-zoom-hint{font-size:9px!important}}\n@media(prefers-reduced-motion:reduce){.lrp-fp .lrp-map-zoom-button{transition:none}}\n\n<\/style>\n<script>\n\/* Progressive map navigation for the private Flight Planner drafts. *\/\n(() => {\n  'use strict';\n  if (window.LRPFlightMapZoom) { window.LRPFlightMapZoom.boot(); return; }\n  const tr=(key,vars={})=>window.LRPFlightText?.tr(key,vars)||String({\"load\": \"Finding your way to Faro\u2026\", \"auth\": \"Please sign in to view this preview.\", \"loadError\": \"The flight guide could not be loaded. Please try again.\", \"prepared\": \"The flight guide is being prepared. Please check back soon.\", \"kicker\": \"THE ALGARVE IS CLOSER THAN YOU THINK\", \"title\": \"Find your flight.\", \"subtitle\": \"We\u2019ll find the trails.\", \"intro1\": \"One direct flight. A whole different world.\", \"intro2\": \"Explore routes to Faro and start planning your ride.\", \"directOnly\": \"Direct flights only\", \"trip\": \"Your tour travel dates:\", \"flyTo\": \"fly to Faro\", \"flyHome\": \"fly home\", \"returnFlights\": \"See return flights\", \"exploreAria\": \"Explore direct flights to Faro\", \"mapKicker\": \"YOUR ROUTE TO THE RIDE\", \"europe\": \"Europe\", \"allRoutes\": \"All routes\", \"allRoutesAria\": \"Show all routes\", \"mapAria\": \"Airport route map. Select an airport or use the airport selector beside the map.\", \"loadingMap\": \"Loading the route map\u2026\", \"selectAirport\": \"Select a departure airport\", \"airportCount\": \"airports \u00b7 one great destination\", \"departQuestion\": \"Where are you flying from?\", \"searchAirport\": \"Search city or airport\", \"searchAirportAria\": \"Filter airports by city or airport code\", \"departureAirport\": \"Departure airport\", \"monthQuestion\": \"When do you want to fly?\", \"direction\": \"Flight direction\", \"toFaro\": \"To Faro\", \"fromFaro\": \"From Faro\", \"exploreDates\": \"Explore flight dates\", \"airlineBooking\": \"Your flight is booked directly with the airline.\", \"scheduleKicker\": \"MAKE IT A DATE\", \"scheduleTitle\": \"The way to your next ride.\", \"airline\": \"Airline\", \"allAirlines\": \"All airlines\", \"previous\": \"Previous month\", \"next\": \"Next month\", \"flightDates\": \"Flight dates\", \"timesListed\": \"Times listed\", \"dateListed\": \"Date listed\", \"wholeMonth\": \"Show the whole month\", \"moreDates\": \"Show more flight dates\", \"disclaimer\": \"All times are local to each airport. Schedules may change. Check flight details and prices with the airline before booking.\", \"ctaKicker\": \"FROM ARRIVALS TO ADVENTURE\", \"ctaTitle\": \"We\u2019ll take it from Faro.\", \"ctaText\": \"Airport transfers, good food and great trails. Explore the ride that fits your trip.\", \"ctaButton\": \"Find your riding dates\", \"updated\": \"Flight guide updated\", \"guide\": \"Direct flight schedule guide\", \"noAirport\": \"No matching airports\", \"chooseAirport\": \"Choose an airport below\", \"homeQuestion\": \"Where are you flying home to?\", \"homeAirport\": \"Return destination airport\", \"departureKicker\": \"YOUR DEPARTURE\", \"leavingKicker\": \"LEAVING THE ALGARVE\", \"routeAirlines\": \"AIRLINES ON THIS ROUTE\", \"loadingDates\": \"Loading flight dates\u2026\", \"listedTimes\": \"flight times listed\", \"listedDate\": \"flight date listed, times to be added\", \"noFlights\": \"no flights listed\", \"unchecked\": \"schedule not yet checked\", \"flightSingle\": \"flight\", \"flightPlural\": \"flights\", \"datePlural\": \"dates\", \"noSelection\": \"No listed flights for this selection.\", \"noDates\": \"No flights listed for these dates.\", \"inProgress\": \"We\u2019re still filling in these dates.\", \"otherAirline\": \"Try another airline or show all airlines for this route.\", \"otherDate\": \"Try another date or departure airport. Please also check the airline\u2019s latest schedule.\", \"incomplete\": \"This part of the flight guide is not complete yet. Try another month or airport, or check directly with the airline.\", \"listedDirect\": \"A direct flight is listed\", \"timesFollow\": \"Flight times to follow\", \"checkAirline\": \"Check the airline for flight times.\", \"timeTBC\": \"Time TBC\", \"airlineTBC\": \"Airline to be confirmed\", \"direct\": \"direct\", \"arrivalDay\": \"Arrival day needs confirmation with the airline.\", \"resultsFor\": \"{n} results for {airport}, {date}.\", \"detailsPending\": \"Some flight details for this selection are still being added.\", \"mapFallback\": \"Your next ride starts in Faro.\", \"mapChoose\": \"Choose your departure airport to explore direct flights.\", \"mapKeys\": \"Direct flight routes to Faro. Use arrow keys to explore airports and Enter to select.\", \"selectAirportAction\": \"select departure airport\", \"mapHere\": \"YOUR RIDE STARTS HERE\", \"islandAria\": \"Atlantic island airports\", \"atlanticAlso\": \"Also in the Atlantic\", \"azores\": \"Azores \u00b7 PDL\", \"iceland\": \"Iceland \u00b7 KEF\", \"directFaro\": \"Direct to Faro\", \"daySingle\": \"day\", \"dayPlural\": \"days\", \"fullPlanner\": \"Open the full Flight Planner\", \"findFlights\": \"Find flights\", \"flightForTour\": \"Find flights for this tour\", \"zoomIn\": \"Zoom in on map\", \"zoomOut\": \"Zoom out on map\", \"zoomReset\": \"Reset map zoom\", \"zoomLabel\": \"Map zoom\", \"zoomHint\": \"Scroll to zoom \u00b7 Drag to explore\", \"halfDayText\": \"Your last ride is a half day on {date}. Please confirm return-flight timing with us.\", \"travelText\": \"Suggested travel days: arrive the day before riding and fly home the day after your last ride.\", \"duration1\": \"1 Day\", \"duration2\": \"2 Days\", \"duration3\": \"3 Days\", \"duration4\": \"4 Days\", \"dateLabel\": \"Preferred Start Date (dd\/mm\/yyyy)\", \"dateSingle\": \"date\", \"flightFew\": \"flights\", \"dateFew\": \"dates\", \"dayFew\": \"days\", \"locale\": \"en-GB\", \"atlantic\": \"Atlantic\"}[key]||key).replace(\/\\{(\\w+)\\}\/g,(_,k)=>String(vars[k]??''));\n  const instances = new WeakMap();\n  const MIN = 1, MAX = 8, STEP = 1.5;\n  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));\n  const pointPosition = node => {\n    const match = (node?.getAttribute('transform') || '').match(\/translate\\(\\s*([-\\d.e+]+)[,\\s]+([-\\d.e+]+)\\s*\\)\/i);\n    return match ? {x:Number(match[1]), y:Number(match[2])} : null;\n  };\n  let instanceNumber = 0;\n  const enhance = map => {\n    if (instances.has(map)) { instances.get(map).adopt(); return; }\n    const panel = map.closest('.fp-map-panel');\n    if (!panel) return;\n    let svg = null, width = 920, height = 650;\n    let view = {x:0, y:0, k:1}, markers = [], countryLabels = [], drag = null, suppressUntil = 0;\n    const controls = document.createElement('div');\n    controls.className = 'lrp-map-zoom-controls';\n    controls.setAttribute('role', 'group');\n    controls.setAttribute('aria-label', tr('zoomLabel'));\n    const makeButton = (label, text, action) => {\n      const button = document.createElement('button');\n      button.type = 'button'; button.className = 'lrp-map-zoom-button';\n      button.setAttribute('aria-label', label); button.title = label;\n      button.textContent = text; button.addEventListener('click', action);\n      controls.append(button); return button;\n    };\n    const inside = point => point && point.x >= view.x && point.x <= view.x + width\/view.k && point.y >= view.y && point.y <= view.y + height\/view.k;\n    const selectedPosition = () => pointPosition(svg?.querySelector('.fp-point-selected'));\n    const apply = () => {\n      if (!svg) return;\n      view.k = clamp(view.k, MIN, MAX);\n      view.x = clamp(view.x, 0, width - width\/view.k);\n      view.y = clamp(view.y, 0, height - height\/view.k);\n      svg.setAttribute('viewBox', [view.x, view.y, width\/view.k, height\/view.k].map(n => Number(n.toFixed(6))).join(' '));\n      markers.forEach(({node, original}) => node.setAttribute('transform', original + ' scale(' + (1\/view.k) + ')'));\n      countryLabels.forEach(({node, x, y, original}) => node.setAttribute('transform', original + ' translate(' + x + ' ' + y + ') scale(' + (1\/view.k) + ') translate(' + (-x) + ' ' + (-y) + ')'));\n      map.classList.toggle('lrp-map-is-zoomed', view.k > MIN + .001);\n      zoomIn.disabled = view.k >= MAX - .001;\n      zoomOut.disabled = resetButton.disabled = view.k <= MIN + .001;\n      status.textContent = Math.round(view.k*100) + '%';\n    };\n    const center = point => {\n      if (!point) return;\n      view.x = point.x - width\/view.k\/2;\n      view.y = point.y - height\/view.k\/2;\n    };\n    const zoom = (next, point, fraction = {x:.5, y:.5}) => {\n      next = clamp(next, MIN, MAX);\n      if (!svg || Math.abs(next - view.k) < .00001) return false;\n      point ||= {x:view.x + width\/view.k\/2, y:view.y + height\/view.k\/2};\n      view.k = next;\n      view.x = point.x - fraction.x*width\/view.k;\n      view.y = point.y - fraction.y*height\/view.k;\n      apply(); return true;\n    };\n    const reset = () => {view = {x:0, y:0, k:1}; apply();};\n    const zoomByButton = direction => {\n      const selected = selectedPosition();\n      zoom(direction > 0 ? view.k*STEP : view.k\/STEP, direction > 0 && inside(selected) ? selected : null);\n    };\n    const zoomIn = makeButton(tr('zoomIn'), '+', () => zoomByButton(1));\n    const zoomOut = makeButton(tr('zoomOut'), '\u2212', () => zoomByButton(-1));\n    const resetButton = makeButton(tr('zoomReset'), '\u21ba', reset);\n    const status = document.createElement('output');\n    status.className = 'lrp-map-zoom-level'; status.setAttribute('aria-label', tr('zoomLabel'));\n    controls.append(status);\n    const hint = document.createElement('p');\n    hint.className = 'lrp-map-zoom-hint'; hint.id = 'lrp-map-zoom-hint-' + (++instanceNumber);\n    hint.textContent = tr('zoomHint');\n    const dock = document.createElement('div');\n    dock.className = 'lrp-map-zoom-dock'; dock.append(controls, hint);\n    panel.append(dock);\n    map.classList.add('lrp-map-zoom-enabled');\n    const adopt = () => {\n      const next = map.querySelector('svg.fp-map-svg');\n      if (next === svg) return;\n      svg = next; drag = null; map.classList.remove('lrp-map-is-dragging');\n      dock.hidden = !svg;\n      if (!svg) return;\n      const box = (svg.getAttribute('viewBox') || '').trim().split(\/[ ,]+\/).map(Number);\n      if (box.length === 4 && box[2] > 0 && box[3] > 0) {width = box[2]; height = box[3];}\n      markers = [...svg.querySelectorAll('.fp-airport-point,.fp-faro-point')].map(node => ({node, original:node.getAttribute('transform') || ''}));\n      countryLabels = [...svg.querySelectorAll('.fp-map-country-labels text')].map(node => ({node, x:Number(node.getAttribute('x')) || 0, y:Number(node.getAttribute('y')) || 0, original:node.getAttribute('transform') || ''}));\n      svg.setAttribute('tabindex', '0');\n      svg.setAttribute('aria-describedby', hint.id);\n      const selected = selectedPosition();\n      if (view.k > 1 && selected && !inside(selected)) center(selected);\n      apply();\n    };\n    \/\/ SVG uses xMidYMid\/meet: account for its letterboxed area, not just the element box.\n    const screen = event => {\n      const rect = svg.getBoundingClientRect();\n      const scale = Math.min(rect.width\/width, rect.height\/height);\n      if (!(scale > 0)) return null;\n      const drawnWidth = width*scale, drawnHeight = height*scale;\n      const fx = (event.clientX - rect.left - (rect.width-drawnWidth)\/2)\/drawnWidth;\n      const fy = (event.clientY - rect.top - (rect.height-drawnHeight)\/2)\/drawnHeight;\n      return {x:clamp(fx, 0, 1), y:clamp(fy, 0, 1), scale:scale*view.k, onMap:fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1};\n    };\n    map.addEventListener('wheel', event => {\n      if (!svg || !svg.contains(event.target) || event.ctrlKey || event.metaKey || !event.deltaY) return;\n      const cursor = screen(event); if (!cursor?.onMap) return;\n      const dy = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 300 : 1);\n      const factor = Math.exp(-clamp(dy, -120, 120)*.003);\n      const point = {x:view.x + cursor.x*width\/view.k, y:view.y + cursor.y*height\/view.k};\n      \/\/ At either limit, let normal page scrolling continue.\n      if (zoom(view.k*factor, point, cursor)) event.preventDefault();\n    }, {passive:false});\n    map.addEventListener('pointerdown', event => {\n      if (!svg || !svg.contains(event.target) || event.button !== 0) return;\n      suppressUntil = 0;\n      if (view.k <= 1 || drag) return;\n      const cursor = screen(event); if (!cursor?.onMap) return;\n      drag = {id:event.pointerId, x:event.clientX, y:event.clientY, originX:view.x, originY:view.y, scale:cursor.scale, moved:false};\n    });\n    map.addEventListener('pointermove', event => {\n      if (!drag || event.pointerId !== drag.id) return;\n      const dx = event.clientX - drag.x, dy = event.clientY - drag.y;\n      if (!drag.moved && Math.hypot(dx, dy) < 5) return;\n      if (!drag.moved) {\n        drag.moved = true; map.classList.add('lrp-map-is-dragging');\n        try {map.setPointerCapture(event.pointerId);} catch (_) {}\n      }\n      event.preventDefault();\n      view.x = drag.originX - dx\/drag.scale; view.y = drag.originY - dy\/drag.scale;\n      apply();\n    });\n    const finishDrag = event => {\n      if (!drag || event.pointerId !== drag.id) return;\n      if (drag.moved) suppressUntil = Date.now() + 600;\n      try {if (map.hasPointerCapture?.(event.pointerId)) map.releasePointerCapture(event.pointerId);} catch (_) {}\n      drag = null; map.classList.remove('lrp-map-is-dragging');\n    };\n    map.addEventListener('pointerup', finishDrag);\n    map.addEventListener('pointercancel', finishDrag);\n    map.addEventListener('lostpointercapture', finishDrag);\n    map.addEventListener('pointerleave', event => {if (drag && !drag.moved) finishDrag(event);});\n    map.addEventListener('click', event => {\n      if (event.detail !== 0 && Date.now() < suppressUntil) {\n        event.preventDefault(); event.stopImmediatePropagation(); suppressUntil = 0;\n      }\n    }, true);\n    map.addEventListener('focusin', event => {\n      const point = event.target.closest?.('.fp-airport-point');\n      const position = pointPosition(point);\n      if (view.k > 1 && position && !inside(position)) {center(position); apply();}\n    });\n    map.addEventListener('keydown', event => {\n      if (!svg || !svg.contains(event.target) || event.ctrlKey || event.metaKey || event.altKey) return;\n      if (event.key === '+' || event.key === '=') {event.preventDefault(); zoomByButton(1);}\n      else if (event.key === '-') {event.preventDefault(); zoomByButton(-1);}\n      else if (event.key === '0' || event.key === 'Escape') {event.preventDefault(); reset();}\n      \/\/ Airport groups already own their arrows\/Home\/End; only pan from SVG background focus.\n      else if (event.target === svg && view.k > 1 && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key)) {\n        event.preventDefault();\n        if (event.key === 'ArrowLeft') view.x -= width\/view.k*.15;\n        if (event.key === 'ArrowRight') view.x += width\/view.k*.15;\n        if (event.key === 'ArrowUp') view.y -= height\/view.k*.15;\n        if (event.key === 'ArrowDown') view.y += height\/view.k*.15;\n        apply();\n      }\n    });\n    panel.addEventListener('click', event => {\n      if (event.target.closest?.('[data-action=\"reset-map\"]')) reset();\n    }, true);\n    instances.set(map, {adopt});\n    new MutationObserver(adopt).observe(map, {childList:true});\n    adopt();\n  };\n  const boot = () => document.querySelectorAll('.lrp-fp[data-mode] .fp-map').forEach(enhance);\n  window.LRPFlightMapZoom = {boot};\n  const start = () => {\n    boot();\n    \/\/ Tour selection can replace the whole planner root; attach to new maps as they appear.\n    new MutationObserver(records => {\n      if (records.some(record => [...record.addedNodes].some(node => node.nodeType === 1 && (node.matches?.('.lrp-fp[data-mode],.fp-map') || node.querySelector?.('.lrp-fp[data-mode],.fp-map'))))) boot();\n    }).observe(document.body, {childList:true, subtree:true});\n  };\n  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, {once:true}); else start();\n})();\n\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"footnotes":""},"class_list":["post-10827","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/pages\/10827","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/comments?post=10827"}],"version-history":[{"count":2,"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/pages\/10827\/revisions"}],"predecessor-version":[{"id":10852,"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/pages\/10827\/revisions\/10852"}],"wp:attachment":[{"href":"https:\/\/letsrideportugal.com\/de\/wp-json\/wp\/v2\/media?parent=10827"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}