Created
April 7, 2026 06:34
-
-
Save karpada/5a15d1028222cb618db2f7cf6bb81297 to your computer and use it in GitHub Desktop.
Portual Road Trip
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Portugal Places Map</title> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f4f0; } | |
| h1 { font-size: 18px; font-weight: 500; padding: 14px 20px; background: #fff; border-bottom: 0.5px solid #ddd; color: #1a1a1a; } | |
| #map-container { position: relative; width: 100%; height: calc(100vh - 53px); } | |
| #map { width: 100%; height: 100%; } | |
| .legend { position: absolute; bottom: 24px; left: 16px; background: #fff; border: 0.5px solid #ccc; border-radius: 10px; padding: 12px 16px; font-size: 12px; z-index: 1000; } | |
| .legend-title { font-size: 11px; color: #888; font-weight: 500; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.04em; } | |
| .legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; color: #1a1a1a; } | |
| .legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } | |
| .legend-size { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; color: #666; font-size: 11px; } | |
| .size-dot { border-radius: 50%; background: #888; flex-shrink: 0; } | |
| .tooltip { position: absolute; background: #fff; border: 0.5px solid #bbb; border-radius: 8px; padding: 10px 13px; font-size: 13px; pointer-events: none; z-index: 2000; max-width: 220px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: none; } | |
| .tooltip-name { font-weight: 500; color: #1a1a1a; margin-bottom: 4px; } | |
| .tooltip-cat { font-size: 11px; color: #888; } | |
| .tooltip-dur { font-size: 11px; color: #888; margin-top: 3px; } | |
| .filter-bar { position: absolute; top: 12px; left: 12px; right: 12px; display: flex; gap: 6px; flex-wrap: wrap; z-index: 1000; } | |
| .filter-btn { font-size: 11px; padding: 4px 10px; border-radius: 20px; border: 0.5px solid #bbb; background: #fff; cursor: pointer; color: #666; transition: all 0.15s; white-space: nowrap; } | |
| .filter-btn.active { color: #1a1a1a; font-weight: 500; border-color: #555; } | |
| .filter-btn:hover { background: #f5f5f5; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Portugal — Places to Visit</h1> | |
| <div id="map-container"> | |
| <div id="map"></div> | |
| <div class="filter-bar" id="filter-bar"></div> | |
| <div class="legend"> | |
| <div class="legend-title">Category</div> | |
| <div id="legend-cats"></div> | |
| <div class="legend-title" style="margin-top:10px;">Visit duration</div> | |
| <div class="legend-size"><div class="size-dot" style="width:8px;height:8px;"></div> < 1.5h</div> | |
| <div class="legend-size"><div class="size-dot" style="width:13px;height:13px;"></div> 1.5 – 3h</div> | |
| <div class="legend-size"><div class="size-dot" style="width:19px;height:19px;"></div> 3 – 5h</div> | |
| <div class="legend-size"><div class="size-dot" style="width:25px;height:25px;"></div> 5h+</div> | |
| </div> | |
| <div class="tooltip" id="tooltip"></div> | |
| </div> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script> | |
| const places = [ | |
| {name:"Sortelha",lat:40.2985,lng:-7.1670,cat:"Historic Village",dur:1.75,region:"Beira Alta"}, | |
| {name:"Trancoso Walled Town",lat:40.7784,lng:-7.3520,cat:"Medieval Castle",dur:1.75,region:"Beira Alta"}, | |
| {name:"Marialva",lat:40.9226,lng:-7.2042,cat:"Historic Village",dur:1.25,region:"Beira Alta"}, | |
| {name:"Castelo Rodrigo",lat:40.8786,lng:-6.9700,cat:"Historic Village",dur:1.25,region:"Beira Alta"}, | |
| {name:"Penedono Castle",lat:40.9985,lng:-7.3942,cat:"Medieval Castle",dur:1.0,region:"Beira Alta"}, | |
| {name:"Monsanto",lat:40.0440,lng:-7.1140,cat:"Historic Village",dur:2.5,region:"Beira Baixa"}, | |
| {name:"Serra da Estrela — Torre",lat:40.3215,lng:-7.6130,cat:"Scenic Drive",dur:2.5,region:"Beira Interior"}, | |
| {name:"Leiria Castle",lat:39.7471,lng:-8.8089,cat:"Medieval Castle",dur:1.25,region:"Beira Litoral"}, | |
| {name:"Piódão",lat:40.1430,lng:-7.9530,cat:"Historic Village",dur:1.75,region:"Beira Serra"}, | |
| {name:"Rota do Poço Azul",lat:41.7820,lng:-8.1960,cat:"Hiking Trail",dur:3.5,region:"Braga"}, | |
| {name:"University of Coimbra",lat:40.2063,lng:-8.4234,cat:"Historic Village",dur:2.25,region:"Coimbra"}, | |
| {name:"Lamego Castle & Sanctuary",lat:41.0964,lng:-7.8085,cat:"Medieval Castle",dur:2.5,region:"Douro"}, | |
| {name:"Douro Valley — Peso da Régua",lat:41.1600,lng:-7.7900,cat:"Scenic Drive",dur:3.5,region:"Douro"}, | |
| {name:"Amarante & Ponte de São Gonçalo",lat:41.2690,lng:-8.0729,cat:"Historic Village",dur:2.5,region:"Douro"}, | |
| {name:"Passadiços do Paiva & 516 Bridge",lat:40.9380,lng:-8.2850,cat:"Hiking Trail",dur:3.5,region:"Douro/Aveiro"}, | |
| {name:"Arouca Geopark",lat:40.9250,lng:-8.2480,cat:"Wildlife / Park",dur:3.5,region:"Douro/Aveiro"}, | |
| {name:"Santa Maria da Feira",lat:40.9240,lng:-8.5460,cat:"Medieval Castle",dur:1.25,region:"Douro Litoral"}, | |
| {name:"Miradouro de Galafura",lat:41.1710,lng:-7.7620,cat:"Scenic Viewpoint",dur:0.5,region:"Douro Valley"}, | |
| {name:"Miradouro Serra do Pilar",lat:41.1380,lng:-8.6120,cat:"Scenic Viewpoint",dur:0.5,region:"Gaia"}, | |
| {name:"Cerdeira",lat:40.0920,lng:-8.2430,cat:"Historic Village",dur:1.75,region:"Lousã"}, | |
| {name:"Aldeia Mata Pequena",lat:38.9740,lng:-9.2740,cat:"Historic Village",dur:1.25,region:"Mafra"}, | |
| {name:"Guimarães Castle",lat:41.4428,lng:-8.2924,cat:"Medieval Castle",dur:1.75,region:"Minho"}, | |
| {name:"Peneda-Gerês National Park",lat:41.7910,lng:-8.1670,cat:"Wildlife / Park",dur:5.0,region:"Minho"}, | |
| {name:"Lindoso Castle & Espigueiros",lat:41.8730,lng:-8.1920,cat:"Medieval Castle",dur:1.25,region:"Minho"}, | |
| {name:"Miradouro Pedra Bela",lat:41.7350,lng:-8.1840,cat:"Scenic Viewpoint",dur:0.5,region:"Minho"}, | |
| {name:"Gerês — Mirante Velho",lat:41.7250,lng:-8.1580,cat:"Scenic Viewpoint",dur:0.5,region:"Minho"}, | |
| {name:"Citânia de Briteiros",lat:41.5260,lng:-8.2960,cat:"Historic Village",dur:1.75,region:"Minho"}, | |
| {name:"Ponte de Lima",lat:41.7676,lng:-8.5826,cat:"Historic Village",dur:2.5,region:"Minho"}, | |
| {name:"Mercado de Braga",lat:41.5513,lng:-8.4283,cat:"Local Market",dur:1.0,region:"Minho"}, | |
| {name:"Bom Jesus do Monte",lat:41.5540,lng:-8.3830,cat:"Scenic Viewpoint",dur:1.75,region:"Minho"}, | |
| {name:"Viana do Castelo",lat:41.6930,lng:-8.8330,cat:"Historic Village",dur:2.5,region:"Minho"}, | |
| {name:"Óbidos Castle",lat:39.3609,lng:-9.1564,cat:"Medieval Castle",dur:2.5,region:"Oeste"}, | |
| {name:"Berlenga Grande island",lat:39.4080,lng:-9.5090,cat:"Wildlife / Park",dur:5.0,region:"Oeste"}, | |
| {name:"Almourol Castle",lat:39.4740,lng:-8.1700,cat:"Medieval Castle",dur:1.25,region:"Ribatejo"}, | |
| {name:"Tomar (Convent of Christ)",lat:39.6021,lng:-8.4129,cat:"Medieval Castle",dur:2.5,region:"Ribatejo"}, | |
| {name:"Serra da Arrábida",lat:38.4850,lng:-8.9870,cat:"Scenic Drive",dur:3.0,region:"Setúbal"}, | |
| {name:"Douro Internacional Natural Park",lat:41.2500,lng:-6.6000,cat:"Wildlife / Park",dur:3.5,region:"Trás-os-Montes"}, | |
| {name:"Miranda do Douro Castle",lat:41.4970,lng:-6.2770,cat:"Medieval Castle",dur:1.25,region:"Trás-os-Montes"}, | |
| {name:"Montesinho Natural Park",lat:41.9500,lng:-6.7500,cat:"Wildlife / Park",dur:5.0,region:"Trás-os-Montes"}, | |
| {name:"Bragança Castle",lat:41.8062,lng:-6.7575,cat:"Medieval Castle",dur:1.75,region:"Trás-os-Montes"}, | |
| {name:"Chaves — Roman Town & Termas",lat:41.7381,lng:-7.4705,cat:"Local Market",dur:2.5,region:"Trás-os-Montes"}, | |
| {name:"Montalegre Castle",lat:41.8270,lng:-7.7910,cat:"Medieval Castle",dur:1.25,region:"Trás-os-Montes"}, | |
| {name:"Rota de Xertelo e as 7 Lagoas",lat:41.5800,lng:-7.8500,cat:"Hiking Trail",dur:5.5,region:"Vila Real"}, | |
| {name:"Trilho Levada da Víbora",lat:41.6500,lng:-8.2500,cat:"Hiking Trail",dur:3.5,region:"Braga"}, | |
| {name:"Muralha de Óbidos",lat:39.3620,lng:-9.1580,cat:"Medieval Castle",dur:1.0,region:"Oeste"}, | |
| {name:"Marvão",lat:39.3960,lng:-7.3710,cat:"Historic Village",dur:1.5,region:"Portalegre"}, | |
| {name:"PR13 MTG — Rota das Faias",lat:40.4800,lng:-7.3200,cat:"Hiking Trail",dur:2.5,region:"Guarda"}, | |
| {name:"Serra de Arga Trails",lat:41.8200,lng:-8.7200,cat:"Hiking Trail",dur:4.0,region:"Viana do Castelo"}, | |
| {name:"Penela",lat:40.0270,lng:-8.3890,cat:"Historic Village",dur:1.0,region:"Coimbra"}, | |
| {name:"PR3 — Levadas de Jugueiros",lat:41.1950,lng:-8.2100,cat:"Hiking Trail",dur:2.5,region:"Porto"}, | |
| ]; | |
| const catColors = { | |
| "Historic Village": "#378ADD", | |
| "Medieval Castle": "#D4537E", | |
| "Hiking Trail": "#639922", | |
| "Wildlife / Park": "#1D9E75", | |
| "Scenic Drive": "#EF9F27", | |
| "Scenic Viewpoint": "#7F77DD", | |
| "Local Market": "#D85A30", | |
| }; | |
| function durToRadius(d) { | |
| if (d <= 1) return 7; | |
| if (d <= 2) return 10; | |
| if (d <= 3.5) return 14; | |
| if (d <= 5) return 18; | |
| return 22; | |
| } | |
| const map = L.map('map', {zoomControl: true}).setView([40.2, -7.9], 7); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { | |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>', | |
| maxZoom: 18 | |
| }).addTo(map); | |
| const tooltip = document.getElementById('tooltip'); | |
| let activeFilter = 'All'; | |
| const markers = []; | |
| places.forEach(p => { | |
| const r = durToRadius(p.dur); | |
| const color = catColors[p.cat] || '#888'; | |
| const circle = L.circleMarker([p.lat, p.lng], { | |
| radius: r, fillColor: color, color: '#fff', | |
| weight: 1.5, opacity: 1, fillOpacity: 0.85 | |
| }).addTo(map); | |
| circle._cat = p.cat; | |
| circle._data = p; | |
| circle.on('mouseover', function() { | |
| tooltip.style.display = 'block'; | |
| tooltip.innerHTML = `<div class="tooltip-name">${p.name}</div><div class="tooltip-cat">${p.cat} · ${p.region}</div><div class="tooltip-dur">${p.dur}h visit</div>`; | |
| }); | |
| circle.on('mousemove', function(e) { | |
| const c = document.getElementById('map-container').getBoundingClientRect(); | |
| const mx = e.originalEvent.clientX - c.left; | |
| const my = e.originalEvent.clientY - c.top; | |
| tooltip.style.left = (mx + 14) + 'px'; | |
| tooltip.style.top = (my - 10) + 'px'; | |
| }); | |
| circle.on('mouseout', function() { tooltip.style.display = 'none'; }); | |
| circle.on('click', function() { | |
| window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(p.name + ' Portugal')}`, '_blank'); | |
| }); | |
| markers.push(circle); | |
| }); | |
| const cats = Object.keys(catColors); | |
| const filterBar = document.getElementById('filter-bar'); | |
| ['All', ...cats].forEach(cat => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'filter-btn' + (cat === 'All' ? ' active' : ''); | |
| btn.textContent = cat; | |
| if (cat !== 'All') btn.style.borderColor = catColors[cat] + '99'; | |
| btn.onclick = () => { | |
| activeFilter = cat; | |
| document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| markers.forEach(m => { | |
| if (cat === 'All' || m._cat === cat) { | |
| m.setStyle({fillOpacity: 0.85, opacity: 1}); | |
| m.setRadius(durToRadius(m._data.dur)); | |
| } else { | |
| m.setStyle({fillOpacity: 0.07, opacity: 0.2}); | |
| } | |
| }); | |
| }; | |
| filterBar.appendChild(btn); | |
| }); | |
| const legendCats = document.getElementById('legend-cats'); | |
| cats.forEach(c => { | |
| legendCats.innerHTML += `<div class="legend-item"><div class="legend-dot" style="background:${catColors[c]}"></div>${c}</div>`; | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment