{"id":109,"date":"2026-04-10T11:20:56","date_gmt":"2026-04-10T11:20:56","guid":{"rendered":"https:\/\/skipperud.dk\/?page_id=109"},"modified":"2026-04-19T12:45:15","modified_gmt":"2026-04-19T12:45:15","slug":"loeberute","status":"publish","type":"page","link":"https:\/\/skipperud.dk\/?page_id=109","title":{"rendered":"L\u00f8berute"},"content":{"rendered":"<div class=\"osm-planner-roende\">\r\n    <style>\r\n        #map-canvas { height: 600px; width: 100%; border-radius: 12px; border: 3px solid #2c3e50; z-index: 1; }\r\n        .planner-ui { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin-bottom: 15px; border: 1px solid #ddd; }\r\n        .search-container { display: flex; gap: 10px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 15px; }\r\n        .search-container input { flex-grow: 1; padding: 10px; border: 2px solid #ddd; border-radius: 6px; }\r\n        .dist-box { font-size: 2rem; font-weight: 800; color: #27ae60; margin: 10px 0; }\r\n        .toolbar { display: flex; gap: 10px; flex-wrap: wrap; }\r\n        .btn-core { padding: 12px 18px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; color: white; font-size: 0.85rem; }\r\n        .btn-search { background: #2c3e50; }\r\n        .btn-mode { background: #3498db; min-width: 150px; }\r\n        .btn-undo { background: #f39c12; }\r\n        .btn-clear { background: #e74c3c; }\r\n        #log-status { font-size: 0.8rem; color: #95a5a6; margin-top: 5px; }\r\n    <\/style>\r\n\r\n    <link rel=\"stylesheet\" href=\"https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.css\" \/>\r\n    <script src=\"https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.js\"><\/script>\r\n\r\n    <div class=\"planner-ui\">\r\n        <div class=\"search-container\">\r\n            <input type=\"text\" id=\"zip-input\" placeholder=\"Indtast postnummer (f.eks. 8410)\">\r\n            <button class=\"btn-core btn-search\" onclick=\"goToZip()\">S\u00f8g<\/button>\r\n        <\/div>\r\n        <div class=\"toolbar\">\r\n            <button class=\"btn-core btn-mode\" id=\"draw-mode\" onclick=\"toggleMode()\">Mode: F\u00f8lg stier<\/button>\r\n            <button class=\"btn-core btn-undo\" onclick=\"popPoint()\">Fortryd<\/button>\r\n            <button class=\"btn-core btn-clear\" onclick=\"clearRoute()\">Ryd rute<\/button>\r\n        <\/div>\r\n        <div class=\"dist-box\"><span id=\"total-km\">0.00<\/span> km<\/div>\r\n        <div id=\"log-status\">Startet ved Hestehave Skov. Klik for at tegne.<\/div>\r\n    <\/div>\r\n\r\n    <div id=\"map-canvas\"><\/div>\r\n\r\n    <script>\r\n        let leafMap, markersList = [], segmentsList = [], totalDistMeters = 0, isSnapping = true;\r\n\r\n        \/\/ \u2705 NY: ORS API-n\u00f8gle \u2014 gratis p\u00e5 openrouteservice.org (5000 req\/dag)\r\n        const ORS_API_KEY = 'DIN_ORS_N\u00d8GLE_HER';\r\n\r\n        function init() {\r\n            leafMap = L.map('map-canvas').setView([56.2890, 10.4850], 14);\r\n            L.tileLayer('https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png', {\r\n                attribution: '&copy; OpenStreetMap'\r\n            }).addTo(leafMap);\r\n            leafMap.on('click', e => handleClick(e.latlng));\r\n            setTimeout(() => { leafMap.invalidateSize(); }, 600);\r\n        }\r\n\r\n        async function goToZip() {\r\n            const zip = document.getElementById('zip-input').value;\r\n            if (zip.length !== 4) return;\r\n            try {\r\n                const r = await fetch(`https:\/\/api.dataforsyningen.dk\/postnumre\/${zip}`);\r\n                const d = await r.json();\r\n                if (d && d.visueltcenter) leafMap.setView([d.visueltcenter[1], d.visueltcenter[0]], 14);\r\n            } catch (e) { alert(\"Postnummer ikke fundet\"); }\r\n        }\r\n\r\n        async function handleClick(latlng) {\r\n            const col = isSnapping ? '#3498db' : '#9b59b6';\r\n            const m = L.circleMarker(latlng, { radius: 6, color: col, fillColor: col, fillOpacity: 1 }).addTo(leafMap);\r\n            markersList.push({ marker: m, latlng: latlng });\r\n\r\n            if (markersList.length > 1) {\r\n                const from = markersList[markersList.length - 2].latlng;\r\n                const to = latlng;\r\n                isSnapping ? await routeWithFallback(from, to) : drawManual(from, to);\r\n            }\r\n        }\r\n\r\n        \/\/ \u2705 NY: Komplet fallback-k\u00e6de: ORS \u2192 OSRM \u2192 GraphHopper \u2192 direkte linje\r\n        async function routeWithFallback(from, to) {\r\n            setStatus(\"S\u00f8ger skovsti...\");\r\n\r\n            \/\/ 1. Fors\u00f8g OpenRouteService (bedst til skovstier)\r\n            const orsResult = await tryORS(from, to);\r\n            if (orsResult) return renderLine(orsResult.coords, orsResult.dist, '#27ae60', false, 'ORS');\r\n\r\n            \/\/ 2. Fors\u00f8g OSRM med \u00f8get radius\r\n            setStatus(\"Fors\u00f8ger OSRM...\");\r\n            const osrmResult = await tryOSRM(from, to);\r\n            if (osrmResult) return renderLine(osrmResult.coords, osrmResult.dist, '#3498db', false, 'OSRM');\r\n\r\n            \/\/ 3. Fors\u00f8g GraphHopper (hiking-profil, ingen n\u00f8gle n\u00f8dvendig)\r\n            setStatus(\"Fors\u00f8ger GraphHopper hiking...\");\r\n            const ghResult = await tryGraphHopper(from, to);\r\n            if (ghResult) return renderLine(ghResult.coords, ghResult.dist, '#e67e22', false, 'GraphHopper');\r\n\r\n            \/\/ 4. Direkte linje som sidste udvej\r\n            setStatus(\"Ingen sti fundet \u2014 tegner direkte linje. Klik t\u00e6ttere p\u00e5 stien.\");\r\n            drawManual(from, to);\r\n        }\r\n\r\n        \/\/ \u2705 NY: OpenRouteService med foot-hiking profil\r\n        async function tryORS(from, to) {\r\n            if (!ORS_API_KEY || ORS_API_KEY === 'DIN_ORS_N\u00d8GLE_HER') return null;\r\n            try {\r\n                const res = await fetch('https:\/\/api.openrouteservice.org\/v2\/directions\/foot-hiking\/geojson', {\r\n                    method: 'POST',\r\n                    headers: {\r\n                        'Authorization': ORS_API_KEY,\r\n                        'Content-Type': 'application\/json'\r\n                    },\r\n                    body: JSON.stringify({\r\n                        coordinates: [[from.lng, from.lat], [to.lng, to.lat]],\r\n                        \/\/ \u2705 Tillad snapping op til 350m fra klik-punkt\r\n                        radiuses: [350, 350],\r\n                        \/\/ \u2705 Inkluder detaljerede skovstier\r\n                        preference: 'recommended',\r\n                        extra_info: ['waytype', 'surface']\r\n                    })\r\n                });\r\n                if (!res.ok) return null;\r\n                const data = await res.json();\r\n                const feature = data.features?.[0];\r\n                if (!feature) return null;\r\n                const coords = feature.geometry.coordinates.map(c => [c[1], c[0]]);\r\n                const dist = feature.properties.summary.distance;\r\n                return validateRoute(from, to, coords, dist) ? { coords, dist } : null;\r\n            } catch { return null; }\r\n        }\r\n\r\n        \/\/ \u2705 FORBEDRET: OSRM med 150m radius (fra 50m) og alternativ rute\r\n        async function tryOSRM(from, to) {\r\n            try {\r\n                const url = `https:\/\/router.project-osrm.org\/route\/v1\/foot\/` +\r\n                    `${from.lng},${from.lat};${to.lng},${to.lat}` +\r\n                    `?overview=full&geometries=geojson&continue_straight=true` +\r\n                    `&radiuses=150;150`; \/\/ \u2705 150m i stedet for 50m\r\n                const res = await fetch(url);\r\n                const data = await res.json();\r\n                if (data.code !== 'Ok') return null;\r\n                const coords = data.routes[0].geometry.coordinates.map(c => [c[1], c[0]]);\r\n                const dist = data.routes[0].distance;\r\n                return validateRoute(from, to, coords, dist) ? { coords, dist } : null;\r\n            } catch { return null; }\r\n        }\r\n\r\n        \/\/ \u2705 NY: GraphHopper med hiking-profil \u2014 ingen n\u00f8gle n\u00f8dvendig (demo)\r\n        async function tryGraphHopper(from, to) {\r\n            try {\r\n                const url = `https:\/\/graphhopper.com\/api\/1\/route` +\r\n                    `?point=${from.lat},${from.lng}&point=${to.lat},${to.lng}` +\r\n                    `&profile=hike&locale=da&calc_points=true&points_encoded=false` +\r\n                    `&key=LijBPDQGfu7Iiq80w3HzwB4RUDJbMbhs6BkJqjtnTC0`; \/\/ GraphHoppers demo-n\u00f8gle\r\n                const res = await fetch(url);\r\n                if (!res.ok) return null;\r\n                const data = await res.json();\r\n                const path = data.paths?.[0];\r\n                if (!path) return null;\r\n                const coords = path.points.coordinates.map(c => [c[1], c[0]]);\r\n                const dist = path.distance;\r\n                return validateRoute(from, to, coords, dist) ? { coords, dist } : null;\r\n            } catch { return null; }\r\n        }\r\n\r\n        \/\/ \u2705 NY: Valider at ruten ikke laver absurd stor omvej (3x direkte distance)\r\n        function validateRoute(from, to, coords, dist) {\r\n            const directDist = from.distanceTo(to);\r\n            if (dist > directDist * 3) {\r\n                setStatus(\"Omvej detekteret \u2014 fors\u00f8ger n\u00e6ste router.\");\r\n                return false;\r\n            }\r\n            return true;\r\n        }\r\n\r\n        function drawManual(from, to) {\r\n            const d = from.distanceTo(to);\r\n            renderLine([from, to], d, '#9b59b6', true, 'Manuel');\r\n        }\r\n\r\n        function renderLine(coords, dist, color, dashed, source) {\r\n            const p = L.polyline(coords, {\r\n                color: color, weight: 6, opacity: 0.75,\r\n                dashArray: dashed ? '10, 10' : null\r\n            }).addTo(leafMap);\r\n\r\n            \/\/ \u2705 NY: Tooltip viser hvilken router der fandt stien\r\n            p.bindTooltip(`${(dist \/ 1000).toFixed(2)} km (${source})`, { sticky: true });\r\n\r\n            segmentsList.push({ line: p, m: dist });\r\n            totalDistMeters += dist;\r\n            updateUI();\r\n            setStatus(`Sti fundet via ${source} \u2014 ${(dist \/ 1000).toFixed(2)} km`);\r\n        }\r\n\r\n        function setStatus(msg) {\r\n            document.getElementById('log-status').innerText = msg;\r\n        }\r\n\r\n        function toggleMode() {\r\n            isSnapping = !isSnapping;\r\n            const b = document.getElementById('draw-mode');\r\n            b.innerText = isSnapping ? \"Mode: F\u00f8lg stier\" : \"Mode: Tegn frit\";\r\n            b.style.background = isSnapping ? \"#3498db\" : \"#9b59b6\";\r\n        }\r\n\r\n        function popPoint() {\r\n            if (markersList.length > 0) leafMap.removeLayer(markersList.pop().marker);\r\n            if (segmentsList.length > 0) {\r\n                const s = segmentsList.pop();\r\n                leafMap.removeLayer(s.line);\r\n                totalDistMeters -= s.m;\r\n            }\r\n            updateUI();\r\n        }\r\n\r\n        function clearRoute() {\r\n            markersList.forEach(x => leafMap.removeLayer(x.marker));\r\n            segmentsList.forEach(x => leafMap.removeLayer(x.line));\r\n            markersList = []; segmentsList = []; totalDistMeters = 0;\r\n            updateUI();\r\n            setStatus(\"Rute ryddet. Klik for at starte forfra.\");\r\n        }\r\n\r\n        function updateUI() {\r\n            document.getElementById('total-km').innerText = (Math.max(0, totalDistMeters) \/ 1000).toFixed(2);\r\n        }\r\n\r\n        window.addEventListener('load', init);\r\n        if (document.readyState === 'complete') init();\r\n    <\/script>\r\n<\/div>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-109","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/pages\/109","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/skipperud.dk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=109"}],"version-history":[{"count":2,"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/pages\/109\/revisions"}],"predecessor-version":[{"id":111,"href":"https:\/\/skipperud.dk\/index.php?rest_route=\/wp\/v2\/pages\/109\/revisions\/111"}],"wp:attachment":[{"href":"https:\/\/skipperud.dk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=109"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}