Skip to content

Instantly share code, notes, and snippets.

@karpada
Created April 7, 2026 06:34
Show Gist options
  • Select an option

  • Save karpada/5a15d1028222cb618db2f7cf6bb81297 to your computer and use it in GitHub Desktop.

Select an option

Save karpada/5a15d1028222cb618db2f7cf6bb81297 to your computer and use it in GitHub Desktop.
Portual Road Trip
<!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> &lt; 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <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