Master Travel Designer — triggers when user wants to plan a trip, research a destination, needs a detailed schedule, mentions "Traveler Profile", "Security Briefing", "Geographical Validation", "Google Maps CSV", or "GPX track". Produces a single mobile-ready Trip Hub HTML with embedded GPX tracks, verified coordinates, cultural depth, security briefing, and formal references. All sub-agent logic (Security, Cultural Research, Geographical Validator) is embedded directly in this skill.
Transform a Traveler Profile into a flawless, logically sound, and emotionally engaging itinerary delivered as:
File 1: Places (CSV) — Static POIs, hotels, restaurants with validated coordinates.File 2: Tracks (GeoJSON/GPX Source) — GeoJSON FeatureCollection for map rendering.File 3: GPX Track (XML) — Portable .gpx for hiking/cycling apps (Komoot, Garmin, AllTrails).trip_hub.html — A single, self-contained, mobile-first Trip Hub with map, GPX overlay, all tabs.STRICT RULE: Always produce Files 1, 2, and 3 as separate explicit code blocks BEFORE generating the HTML. Never skip steps.
Before starting, confirm you have:
| Field | Required | Notes |
|---|---|---|
| Destination(s) | ✅ | List all cities/regions |
| Dates / Month | ✅ | For weather + open days |
| Duration | ✅ | Number of days |
| Travelers | ✅ | Count + type (couple, family, solo) |
| Budget (Total) | ✅ | Currency |
| Pace | ✅ | Relaxed / Balanced / Intense |
| Interests | ✅ | History, Food, Nature, Adventure… |
| Track Generation? | ✅ | Yes/No — required for hike/cycle routes |
| Dietary Restrictions | Optional | Vegetarian, Halal, Gluten-free… |
| Must-Haves | Optional | Specific sites/experiences |
| To Avoid | Optional | Crowded spots, steep terrain… |
Source from: official government advisories (US State Dept, UK FCDO, Italy "Viaggiare Sicuri", Australia DFAT).
Pillar 1 — 🛂 Entry Requirements
Pillar 2 — 🏥 Health & Medical
Pillar 3 — 🛡️ Safety & Security
Pillar 4 — ⚖️ Local Laws & Customs
Pillar 5 — 🆘 Emergency Contacts
Output format: Render as a styled HTML string for the
securitytab in the Trip Hub.
For each day, produce:
DAY N — [Title with emotional hook, e.g., "Ancient Kyoto at Dawn"]
🌤️ Weather note | 🚌 Transport method
📖 Narrative (2–3 sentences setting the scene)
📍 Places (ordered chronologically, verified coordinates)
🍽️ Food Edit (1 breakfast + 1 lunch + 1 dinner recommendation)
💎 Hidden Gem (1 non-touristy spot, sourced from district-level research)
🔄 Plan B (if weather is bad or site is closed)
Source from: Wikipedia, Wikivoyage, local tourism boards.
Context Generation Rules:
[Destination] + History / Architecture / Culture for each major site.For each Place Card, include:
Gastronomy Research:
[Destination] cuisine Wikipedia for authentic regional dishes.Recency Check:
Break down per person and total:
| Category | Per Person/Day | Total (N days) | Notes |
|---|---|---|---|
| Flights | — | $ | Return estimate |
| Accommodation | $/night | $ | Category (Budget/Mid/Luxury) |
| Local Transport | $/day | $ | IC Card, JR Pass, etc. |
| Food | $/day | $ | Budget/Mid/Luxury tier |
| Activities & Entry | $/day | $ | Museums, tours |
| Misc / Shopping | — | $ | Buffer 10% |
| TOTAL | $ |
Format:
| Situation | Original Script | Romanization | Pronunciation tip | English Meaning |
Address Verification Rules:
Street Name, Building Number, Postal Code, City, Country35.6586, 139.7454)."Verification Needed" — never fabricate coordinates.Category → Color Mapping (strict):
| Category | Hex Color | Use for |
|---|---|---|
| Food & Drink | #e67e22 | Restaurants, cafes, bars, markets |
| Nature | #27ae60 | Parks, gardens, viewpoints, hikes |
| Shopping | #f1c40f | Boutiques, markets |
| Culture | #2980b9 | Museums, art galleries, theaters |
| History | #8e44ad | Temples, castles, monuments |
| Adventure | #c0392b | Hiking, cycling, sports |
| Relaxation | #1abc9c | Spas, onsen, beaches |
| Nightlife | #2c3e50 | Bars, clubs, izakayas |
| Lodging | #34495e | Hotels, ryokans, hostels |
| Transportation | #7f8c8d | Stations, airports, ports |
| Essentials | #e84393 | Pharmacies, hospitals, embassies |
Pre-output Checklist (run mentally for every row):
Output as a fenced code block labeled File 1: Places (CSV).
Columns:
Day,Name,Address,Lat,Lon,Category,Color,Description,Reference,OpeningHours,PriceRange
Example row:
1,Fushimi Inari Taisha,"68 Fukakusa Yabunouchicho, Fushimi-ku, Kyoto 612-0882",34.9671,135.7727,History,#8e44ad,"Iconic vermillion torii gate tunnel. Arrive before 7am to beat crowds.",https://inari.jp,24h,Free
Output as a fenced code block labeled File 2: Tracks (GeoJSON/GPX Source).
Format: GeoJSON FeatureCollection with LineString features.
Required properties per feature:
{
"type": "Feature",
"properties": {
"day": 5,
"name": "Hakone Hike — Old Tokaido Trail",
"description": "Classic volcanic ridge walk via Owakudani",
"distance_km": 8.5,
"elevation_gain_m": 420,
"duration_h": 3.5,
"difficulty": "Moderate",
"color": "#c0392b"
},
"geometry": {
"type": "LineString",
"coordinates": [
[139.0261, 35.2329],
[139.0280, 35.2350],
...minimum 10 waypoints for any hiking track...
]
}
}
⚠️ Coordinate Order in GeoJSON is [Longitude, Latitude] — never reverse this.
Output as a fenced code block labeled File 3: GPX Track (XML).
Generate a valid .gpx XML file for every hiking or cycling track in the itinerary.
Full GPX template:
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="TravelDesignerPro"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>TRACK_NAME</name>
<desc>TRACK_DESCRIPTION</desc>
<author><name>Travel Designer Pro</name></author>
<time>YYYY-MM-DDTHH:MM:SSZ</time>
<bounds minlat="MIN_LAT" minlon="MIN_LON" maxlat="MAX_LAT" maxlon="MAX_LON"/>
</metadata>
<!-- Named waypoints (trailhead, summits, viewpoints) -->
<wpt lat="35.2329" lon="139.0261">
<name>Trailhead — Hakone-Yumoto Station</name>
<desc>Start of the Old Tokaido Trail hike</desc>
<sym>Trailhead</sym>
</wpt>
<wpt lat="35.2507" lon="139.0213">
<name>Owakudani — Volcanic Valley Viewpoint</name>
<desc>Active volcanic area. Stay on marked paths.</desc>
<sym>Summit</sym>
</wpt>
<!-- Track -->
<trk>
<name>TRACK_NAME</name>
<desc>TRACK_DESCRIPTION — Distance: X km, Elevation gain: Y m</desc>
<type>hiking</type>
<trkseg>
<!-- Minimum 15 trkpt entries for a realistic track -->
<trkpt lat="35.2329" lon="139.0261"><ele>110</ele><time>2025-01-01T08:00:00Z</time></trkpt>
<trkpt lat="35.2341" lon="139.0268"><ele>125</ele><time>2025-01-01T08:10:00Z</time></trkpt>
<!-- ... continue with realistic elevation progression ... -->
</trkseg>
</trk>
</gpx>
GPX Quality Rules:
<ele> (elevation in meters) for every <trkpt>.<bounds> must reflect actual min/max of all coordinates.Label section: REFERENCES & CITATIONS
Organize by category:
### 🏨 Accommodation
- [Hotel Name] — Source: [Booking.com / TripAdvisor / Official Site] | Rating: X.X/5 | Reason chosen: [brief note]
### 🍽️ Restaurants
- [Name] — Source: [Michelin Guide / Tabelog / Google Maps 4.5+] | Cuisine: | Avg price:
### 🏛️ Attractions
- [Name] — Source: [Official tourism site / Wikipedia / Wikivoyage] | URL:
### 🛡️ Security Sources
- Entry requirements: [Japan Tourism Agency / Official Embassy site] | Last checked: [date]
- Safety advisory: [US State Dept / UK FCDO] | Risk level: 🟢/🟡/🔴
### 🌦️ Weather
- Source: [Japan Meteorological Agency / climate-data.org] | Data period: historical average for [month]
### 🗺️ Maps & GPX
- Coordinates verified via: [Google Maps / OpenStreetMap / official trail map]
- GPX elevation data: [OpenTopoData / official trail authority]
Generate a single self-contained trip_hub.html file.
| Tab ID | Content |
|---|---|
itinerary | Day sections with place cards, narrative, hidden gem |
budget | Full budget breakdown table |
phrases | Language guide table |
security | Security briefing (all 5 pillars) |
prep | Packing list + apps + etiquette |
gpx | NEW: GPX download section with track stats |
refs | References & citations |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="page-title">Trip Hub</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet" />
<style>
:root {
--ink: #1a1a2e;
--deep: #16213e;
--mid: #0f3460;
--gold: #e2b96f;
--cream: #fdf6ec;
--muted: #8a8fa8;
--card-bg: #ffffff;
--border: #e8e4dd;
--radius: 12px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DM Sans', system-ui, sans-serif;
background: var(--cream);
color: var(--ink);
display: flex;
height: 100dvh;
overflow: hidden;
}
/* ── SIDEBAR ── */
#sidebar {
width: 460px; min-width: 460px;
height: 100%;
display: flex;
flex-direction: column;
background: var(--card-bg);
border-right: 1px solid var(--border);
box-shadow: 4px 0 20px rgba(0,0,0,0.06);
z-index: 1000;
transition: transform 0.35s cubic-bezier(.4,0,.2,1);
}
/* ── HERO ── */
.hero {
background: linear-gradient(145deg, var(--ink) 0%, var(--mid) 100%);
color: #fff;
padding: 28px 24px 22px;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute; inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.hero-flag { font-size: 2.2rem; margin-bottom: 8px; }
.hero h1 {
font-family: 'Playfair Display', serif;
font-size: 1.55rem; font-weight: 900;
line-height: 1.2; letter-spacing: -0.5px;
position: relative;
}
.hero-meta {
margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap;
position: relative;
}
.hero-tag {
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.2);
color: rgba(255,255,255,0.9);
font-size: 0.72rem; font-weight: 600;
padding: 3px 10px; border-radius: 20px;
letter-spacing: 0.5px; text-transform: uppercase;
}
/* ── TABS ── */
.tabs {
display: flex; gap: 0;
background: var(--deep);
overflow-x: auto; scrollbar-width: none;
flex-shrink: 0;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-btn {
flex: 1; min-width: 52px;
background: none; border: none; border-bottom: 3px solid transparent;
color: rgba(255,255,255,0.45);
padding: 11px 10px; cursor: pointer;
font-family: 'DM Sans', sans-serif;
font-weight: 600; font-size: 0.75rem;
white-space: nowrap; letter-spacing: 0.3px;
transition: all 0.2s;
}
.tab-btn:hover { color: rgba(255,255,255,0.8); }
.tab-btn.active {
color: var(--gold);
border-bottom-color: var(--gold);
background: rgba(255,255,255,0.04);
}
/* ── CONTENT PANES ── */
.content-pane { display: none; padding: 20px; overflow-y: auto; flex: 1; }
.content-pane.active { display: block; }
/* ── DAY SECTIONS ── */
.day-section {
margin-bottom: 28px;
border-left: 3px solid var(--gold);
padding-left: 16px;
}
.day-label {
font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 2px; color: var(--gold); margin-bottom: 4px;
}
.day-title {
font-family: 'Playfair Display', serif;
font-size: 1.25rem; color: var(--ink); margin-bottom: 6px;
}
.day-narrative { font-size: 0.82rem; color: var(--muted); line-height: 1.65; margin-bottom: 12px; }
/* ── PLACE CARDS ── */
.place-card {
background: var(--cream);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 13px 14px;
margin: 8px 0;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
position: relative;
padding-left: 18px;
}
.place-card::before {
content: '';
position: absolute; left: 0; top: 0; bottom: 0;
width: 4px; border-radius: 4px 0 0 4px;
background: var(--cat-color, var(--gold));
}
.place-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
border-color: var(--cat-color, var(--gold));
}
.place-cat {
font-size: 0.6rem; font-weight: 800; text-transform: uppercase;
letter-spacing: 1.2px; color: var(--cat-color, var(--muted));
margin-bottom: 3px;
}
.place-name { font-size: 0.95rem; font-weight: 700; color: var(--ink); margin-bottom: 4px; }
.place-desc { font-size: 0.78rem; color: var(--muted); line-height: 1.5; }
.place-meta {
display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap;
}
.place-badge {
font-size: 0.65rem; background: rgba(0,0,0,0.05);
border-radius: 4px; padding: 2px 7px; color: var(--muted);
}
/* ── TABLES ── */
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 0.85rem; }
th { background: var(--deep); color: var(--gold); font-weight: 700; padding: 10px 12px; text-align: left; font-size: 0.75rem; letter-spacing: 0.5px; text-transform: uppercase; }
td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(0,0,0,0.015); }
/* ── SECTION HEADERS ── */
.section-header {
font-family: 'Playfair Display', serif;
font-size: 1.1rem; font-weight: 700;
color: var(--ink); margin: 20px 0 12px;
padding-bottom: 6px;
border-bottom: 2px solid var(--gold);
display: flex; align-items: center; gap: 8px;
}
/* ── GPX PANEL ── */
.gpx-card {
background: linear-gradient(135deg, var(--ink), var(--mid));
border-radius: var(--radius);
padding: 20px; color: #fff; margin: 12px 0;
}
.gpx-card h3 { font-family: 'Playfair Display', serif; font-size: 1.1rem; margin-bottom: 6px; color: var(--gold); }
.gpx-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 12px 0; }
.gpx-stat { background: rgba(255,255,255,0.08); border-radius: 8px; padding: 10px 12px; }
.gpx-stat-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px; opacity: 0.6; }
.gpx-stat-value { font-size: 1.1rem; font-weight: 700; color: var(--gold); }
.gpx-download-btn {
display: block; width: 100%;
background: var(--gold); color: var(--ink);
border: none; border-radius: 8px;
padding: 12px; font-weight: 700; font-size: 0.9rem;
cursor: pointer; text-align: center; margin-top: 10px;
transition: opacity 0.2s;
}
.gpx-download-btn:hover { opacity: 0.85; }
/* ── SECURITY BADGES ── */
.risk-badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 12px; border-radius: 20px;
font-size: 0.75rem; font-weight: 700;
}
.risk-low { background: #d4edda; color: #155724; }
.risk-mod { background: #fff3cd; color: #856404; }
.risk-high { background: #f8d7da; color: #721c24; }
.pillar {
background: var(--cream); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px; margin: 10px 0;
}
.pillar h4 { font-size: 0.85rem; font-weight: 700; color: var(--mid); margin-bottom: 8px; }
.pillar ul { padding-left: 18px; font-size: 0.82rem; color: #444; line-height: 1.8; }
/* ── REFERENCES ── */
.ref-item {
font-size: 0.78rem; padding: 8px 0;
border-bottom: 1px solid var(--border);
color: #555; line-height: 1.5;
}
.ref-item a { color: var(--mid); text-decoration: none; font-weight: 600; }
.ref-item a:hover { text-decoration: underline; }
/* ── MAP ── */
#map { flex: 1; height: 100%; }
/* ── MOBILE TOGGLE ── */
#mobile-toggle {
display: none;
position: fixed; bottom: 24px; right: 20px; z-index: 2000;
background: var(--ink); color: var(--gold);
border: 2px solid var(--gold);
padding: 12px 24px; border-radius: 30px;
font-family: 'DM Sans', sans-serif;
font-weight: 700; font-size: 0.9rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
cursor: pointer; letter-spacing: 0.5px;
}
/* ── MOBILE ── */
@media (max-width: 768px) {
body { flex-direction: column; }
#sidebar {
width: 100%; min-width: unset;
position: absolute; height: 100%;
transform: translateY(100%);
}
#sidebar.active { transform: translateY(0); }
#map { height: 100dvh; width: 100vw; }
#mobile-toggle { display: block; }
}
/* ── SCROLL ── */
#itinerary, #budget, #phrases, #security, #prep, #gpx, #refs {
scroll-behavior: smooth;
}
</style>
</head>
<body>
<button id="mobile-toggle" onclick="toggleSidebar()">🗺 View List</button>
<div id="sidebar">
<div class="hero">
<div class="hero-flag" id="trip-flag">🌍</div>
<h1 id="trip-title">Trip Hub</h1>
<div class="hero-meta" id="trip-meta"></div>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="showTab('itinerary',this)">🗓 Days</button>
<button class="tab-btn" onclick="showTab('budget',this)">💰 Budget</button>
<button class="tab-btn" onclick="showTab('phrases',this)">🗣 Phrases</button>
<button class="tab-btn" onclick="showTab('security',this)">🛡 Safety</button>
<button class="tab-btn" onclick="showTab('prep',this)">🧳 Prep</button>
<button class="tab-btn" onclick="showTab('gpx',this)">🧭 GPX</button>
<button class="tab-btn" onclick="showTab('refs',this)">📚 Refs</button>
</div>
<div id="itinerary" class="content-pane active"></div>
<div id="budget" class="content-pane"></div>
<div id="phrases" class="content-pane"></div>
<div id="security" class="content-pane"></div>
<div id="prep" class="content-pane"></div>
<div id="gpx" class="content-pane"></div>
<div id="refs" class="content-pane"></div>
</div>
<div id="map"></div>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
// ═══════════════════════════════════════════════════════
// INJECT ALL DATA HERE — replace placeholders below
// ═══════════════════════════════════════════════════════
const tripData = {
flag: "🗾",
title: "Japan: Tokyo & Kyoto",
tags: ["7 Days", "2 Travelers", "$5,000", "Balanced Pace"],
budget: `<div class="section-header">💰 Budget Breakdown</div>
<table>
<tr><th>Category</th><th>Per Person</th><th>Total</th><th>Notes</th></tr>
<!-- FILL IN ROWS -->
</table>`,
phrases: `<div class="section-header">🗣️ Essential Japanese</div>
<table>
<tr><th>Situation</th><th>Japanese</th><th>Romaji</th><th>Pronunciation</th><th>Meaning</th></tr>
<!-- FILL IN ROWS -->
</table>`,
security: `<div class="section-header">🛡️ Security Briefing</div>
<!-- FILL IN SECURITY HTML USING PILLAR CLASSES -->`,
concierge: `<div class="section-header">🧳 Travel Prep</div>
<!-- FILL IN PACKING LIST, APPS, ETIQUETTE -->`,
references: `<div class="section-header">📚 References & Citations</div>
<!-- FILL IN REF ITEMS -->`,
// GPX data for download
gpxFiles: [
{
name: "Hakone Old Tokaido Hike",
day: 5,
distance_km: 8.5,
elevation_gain_m: 420,
duration_h: 3.5,
difficulty: "Moderate",
// Full GPX XML string
xml: `<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="TravelDesignerPro" xmlns="http://www.topografix.com/GPX/1/1">
<metadata><name>Hakone Old Tokaido Trail</name></metadata>
<wpt lat="35.2329" lon="139.0261"><name>Trailhead</name></wpt>
<trk><name>Hakone Hike</name><trkseg>
<trkpt lat="35.2329" lon="139.0261"><ele>110</ele></trkpt>
<!-- ADD MORE TRKPTS -->
</trkseg></trk>
</gpx>`
}
],
days: [
{
dayLabel: "Day 1",
title: "Arrival in Tokyo",
narrative: "Touch down at Narita and ease into the city. Tonight is for noodles and neon.",
places: [
{
name: "Shinjuku Station",
lat: 35.6896, lon: 139.7006,
category: "Transportation", color: "#7f8c8d",
description: "World's busiest station — follow the South Exit signs.",
hours: "24h", price: "Free",
ref: "https://www.jreast.co.jp/e/stations/e1130.html",
didYouKnow: "Shinjuku Station has 200 exits and sees 3.5M passengers daily.",
hiddenGem: "Kabukicho Ichiban-gai alley — ramen stalls open until 4am."
}
]
}
],
tracks: {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Hakone Old Tokaido Trail",
"day": 5, "distance_km": 8.5,
"elevation_gain_m": 420, "difficulty": "Moderate",
"color": "#c0392b"
},
"geometry": {
"type": "LineString",
"coordinates": [
[139.0261, 35.2329],
[139.0275, 35.2345],
[139.0213, 35.2507]
// ADD MORE COORDS
]
}
}
]
}
};
// ═══════════════════════════════════════════════════════
// CATEGORY COLORS
// ═══════════════════════════════════════════════════════
const CAT_COLORS = {
"Food & Drink":"#e67e22","Nature":"#27ae60","Shopping":"#f1c40f",
"Culture":"#2980b9","History":"#8e44ad","Adventure":"#c0392b",
"Relaxation":"#1abc9c","Nightlife":"#2c3e50","Lodging":"#34495e",
"Transportation":"#7f8c8d","Essentials":"#e84393"
};
// ═══════════════════════════════════════════════════════
// UI INIT
// ═══════════════════════════════════════════════════════
document.title = tripData.title + " — Trip Hub";
document.getElementById('page-title').textContent = tripData.title + " — Trip Hub";
document.getElementById('trip-flag').textContent = tripData.flag;
document.getElementById('trip-title').textContent = tripData.title;
const metaEl = document.getElementById('trip-meta');
(tripData.tags || []).forEach(t => {
const s = document.createElement('span');
s.className = 'hero-tag'; s.textContent = t;
metaEl.appendChild(s);
});
document.getElementById('budget').innerHTML = tripData.budget;
document.getElementById('phrases').innerHTML = tripData.phrases;
document.getElementById('security').innerHTML = tripData.security;
document.getElementById('prep').innerHTML = tripData.concierge;
document.getElementById('refs').innerHTML = tripData.references;
// ── GPX PANEL ──
const gpxPane = document.getElementById('gpx');
gpxPane.innerHTML = `<div class="section-header">🧭 GPX Tracks</div>`;
(tripData.gpxFiles || []).forEach((g, i) => {
const card = document.createElement('div');
card.className = 'gpx-card';
card.innerHTML = `
<h3>${g.name}</h3>
<div style="font-size:0.75rem;opacity:0.65;margin-bottom:8px">Day ${g.day} • ${g.difficulty}</div>
<div class="gpx-stats">
<div class="gpx-stat"><div class="gpx-stat-label">Distance</div><div class="gpx-stat-value">${g.distance_km} km</div></div>
<div class="gpx-stat"><div class="gpx-stat-label">Elevation ↑</div><div class="gpx-stat-value">${g.elevation_gain_m} m</div></div>
<div class="gpx-stat"><div class="gpx-stat-label">Duration</div><div class="gpx-stat-value">${g.duration_h} h</div></div>
<div class="gpx-stat"><div class="gpx-stat-label">Difficulty</div><div class="gpx-stat-value">${g.difficulty}</div></div>
</div>
<button class="gpx-download-btn" onclick="downloadGPX(${i})">⬇ Download ${g.name}.gpx</button>`;
gpxPane.appendChild(card);
});
// ── ITINERARY ──
const itinEl = document.getElementById('itinerary');
const allMarkers = L.featureGroup();
tripData.days.forEach(day => {
const sec = document.createElement('div');
sec.className = 'day-section';
sec.innerHTML = `
<div class="day-label">${day.dayLabel}</div>
<div class="day-title">${day.title}</div>
<div class="day-narrative">${day.narrative}</div>
<div class="places-list"></div>`;
const placesList = sec.querySelector('.places-list');
(day.places || []).forEach(p => {
const color = p.color || CAT_COLORS[p.category] || "#888";
// Map marker
const marker = L.circleMarker([p.lat, p.lon], {
radius: 9, fillColor: color, color: "#fff",
weight: 2.5, opacity: 1, fillOpacity: 0.92
}).bindPopup(`
<div style="font-family:'DM Sans',sans-serif;min-width:200px">
<div style="font-size:0.65rem;text-transform:uppercase;letter-spacing:1px;color:${color};font-weight:800">${p.category}</div>
<div style="font-size:1rem;font-weight:700;margin:3px 0">${p.name}</div>
<div style="font-size:0.8rem;color:#555;margin-bottom:6px">${p.description}</div>
${p.hours ? `<div style="font-size:0.75rem">🕐 ${p.hours}</div>` : ''}
${p.price ? `<div style="font-size:0.75rem">💰 ${p.price}</div>` : ''}
${p.didYouKnow ? `<div style="font-size:0.75rem;margin-top:6px;padding:6px;background:#f9f6f0;border-radius:6px">💡 ${p.didYouKnow}</div>` : ''}
${p.ref ? `<a href="${p.ref}" target="_blank" style="font-size:0.75rem;color:#0f3460;font-weight:600;display:block;margin-top:6px">🔗 Source / Reference</a>` : ''}
</div>`);
allMarkers.addLayer(marker);
// Sidebar card
const card = document.createElement('div');
card.className = 'place-card';
card.style.setProperty('--cat-color', color);
card.innerHTML = `
<div class="place-cat">${p.category}</div>
<div class="place-name">${p.name}</div>
<div class="place-desc">${p.description}</div>
<div class="place-meta">
${p.hours ? `<span class="place-badge">🕐 ${p.hours}</span>` : ''}
${p.price ? `<span class="place-badge">💰 ${p.price}</span>` : ''}
${p.hiddenGem ? `<span class="place-badge">💎 Gem nearby</span>` : ''}
</div>`;
card.onclick = () => {
map.flyTo([p.lat, p.lon], 16, { duration: 1.2 });
marker.openPopup();
if (window.innerWidth <= 768) toggleSidebar();
};
placesList.appendChild(card);
});
itinEl.appendChild(sec);
});
// ═══════════════════════════════════════════════════════
// MAP
// ═══════════════════════════════════════════════════════
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' });
const topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { attribution: '© OpenTopoMap' });
const map = L.map('map', {
center: [35.6762, 139.6503], zoom: 10,
layers: [osm], preferCanvas: true
});
L.control.layers({ "🗺 Standard": osm, "⛰ Topographic": topo }, {}, { position: 'topright' }).addTo(map);
allMarkers.addTo(map);
const tracksLayer = L.geoJSON(tripData.tracks, {
style: f => ({ color: f.properties.color || "#c0392b", weight: 4, opacity: 0.8, dashArray: null }),
onEachFeature: (feature, layer) => {
const p = feature.properties;
layer.bindPopup(`
<b style="font-family:'DM Sans',sans-serif">${p.name}</b><br>
<small>📏 ${p.distance_km} km | ↑ ${p.elevation_gain_m} m | ${p.difficulty}</small>`);
}
}).addTo(map);
// Fit all bounds
const combined = L.featureGroup([allMarkers, tracksLayer]);
if (combined.getBounds().isValid()) {
map.fitBounds(combined.getBounds().pad(0.15));
}
// ═══════════════════════════════════════════════════════
// INTERACTIONS
// ═══════════════════════════════════════════════════════
function showTab(id, btn) {
document.querySelectorAll('.content-pane').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(id).classList.add('active');
if (btn) btn.classList.add('active');
}
function toggleSidebar() {
const sb = document.getElementById('sidebar');
const btn = document.getElementById('mobile-toggle');
sb.classList.toggle('active');
btn.textContent = sb.classList.contains('active') ? '🗺 View Map' : '🗺 View List';
}
function downloadGPX(index) {
const g = tripData.gpxFiles[index];
if (!g || !g.xml) return alert('GPX data not available.');
const blob = new Blob([g.xml], { type: 'application/gpx+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = g.name.replace(/\s+/g, '_') + '.gpx';
a.click();
URL.revokeObjectURL(url);
}
</script>
</body>
</html>
| App | Purpose | Platform |
|---|---|---|
| Suica / Pasmo | IC transport card | iOS/Android |
| Google Maps | Navigation + transit | iOS/Android |
| Google Translate | Camera + voice translation | iOS/Android |
| Tabelog | Restaurant discovery & reviews | iOS/Android |
| Japan Official Travel App | Real-time disaster alerts | iOS/Android |
| Komoot / AllTrails | GPX navigation for hikes | iOS/Android |
Before outputting, run this final checklist:
| Gate | Check |
|---|---|
| ✅ File 1 present | CSV block labeled File 1: Places (CSV) exists |
| ✅ File 2 present | GeoJSON block labeled File 2: Tracks (GeoJSON/GPX Source) exists |
| ✅ File 3 present | GPX XML block labeled File 3: GPX Track (XML) exists (if hiking/cycling) |
| ✅ trip_hub.html | HTML file generated with all tabs populated |
| ✅ References | REFERENCES & CITATIONS section present |
| ✅ GPX quality | ≥15 trkpt entries, realistic elevation, wpt for trailhead + summit |
| ✅ GeoJSON order | Coordinates are [Lon, Lat] order (GeoJSON standard) |
| ✅ Leaflet order | L.marker uses [Lat, Lon] order (Leaflet standard) |
| ✅ Security | All 5 pillars addressed with official source cited |
| ✅ Cultural depth | Every place has "Did You Know" + "Hidden Gem" |
| ✅ Mobile ready | toggleSidebar() logic preserved, tab scrolling works |
| ✅ GPX download | downloadGPX() function wired to button in GPX tab |
Coordinate Order Warning (common LLM error):
- GeoJSON geometry:
[longitude, latitude](e.g.,[139.7454, 35.6586])- Leaflet/CSV:
latitude, longitude(e.g.,35.6586, 139.7454) These are OPPOSITE — always double-check before outputting.