Weekend Project: Building an Interactive Web Application

Combining HTML5 Forms, APIs, and Graphics with Problem-Solving Methodology

Project Overview

This weekend project brings together everything we've learned this week about HTML5 forms, local storage, Geolocation, Drag and Drop, Canvas, and SVG to build a complete interactive web application. We'll approach this project systematically using George Polya's renowned 4-step problem-solving procedure, which will help us organize our work and create a polished final product.

flowchart TD A[Weekend Project] --> B[Polya's Problem-Solving Process] A --> C[HTML5 Technologies] B --> B1[1. Understand the Problem] B --> B2[2. Devise a Plan] B --> B3[3. Execute the Plan] B --> B4[4. Review & Extend] C --> C1[HTML5 Forms & Validation] C --> C2[Geolocation API] C --> C3[Drag and Drop API] C --> C4[Canvas for Dynamic Graphics] C --> C5[SVG for Interactive Elements] C --> C6[LocalStorage for Persistence]

Project Goal

We'll build "LocalAdventures" – an interactive application that helps users discover, plan, and visualize outdoor activities in their area. The app will allow users to:

This project is significant for several reasons:

George Polya's Problem-Solving Process

Before we dive into the code, let's understand the framework we'll use to approach this project. George Polya (1887-1985) was a mathematician who developed a four-step approach to problem-solving in his book "How to Solve It." Though originally created for mathematical problems, his methodology is extremely valuable for tackling programming and development challenges.

1. Understand Define requirements Identify constraints Clarify objectives Research needed info Formulate questions Explore analogies 2. Plan Break down problem Identify patterns Consider approaches Select technologies Create milestones Design architecture 3. Execute Follow the plan Code incrementally Test as you build Overcome obstacles Adapt as needed Document progress 4. Review Verify solution Test thoroughly Reflect on process Consider alternatives Refine implementation Extend functionality

Let's explore each step in detail and see how it applies to our web development project:

Step 1: Understand the Problem

Before writing any code, it's essential to fully comprehend what we're trying to accomplish. This involves:

Step 2: Devise a Plan

With a clear understanding of the problem, we can strategize our approach:

Step 3: Execute the Plan

This is where we implement our solution:

Step 4: Review and Extend

After building the solution, we critically evaluate our work:

Throughout our project, we'll explicitly reference these steps to demonstrate how this structured approach leads to better problem-solving and development outcomes.

Step 1: Understanding the Problem - Requirements Analysis

Following Polya's first step, let's thoroughly understand our project requirements before diving into implementation.

Core Requirements

User Scenarios

Let's envision how users would interact with our application:

sequenceDiagram participant User participant App participant Storage participant Geolocation participant ActivityData User->>App: Open application App->>Storage: Check for existing profile Storage-->>App: Return saved preferences (if any) App->>User: Display welcome screen alt New User User->>App: Fill in preferences form App->>Storage: Save user preferences else Returning User App->>User: Load saved preferences end User->>App: Request activity suggestions App->>Geolocation: Request user location Geolocation-->>App: Return coordinates App->>ActivityData: Query with location and preferences ActivityData-->>App: Return activity suggestions App->>User: Display activity options User->>App: Drag activities to itinerary App->>App: Update itinerary visualization User->>App: Save itinerary App->>Storage: Store itinerary data Storage-->>App: Confirm storage App->>User: Display confirmation

Technical Constraints

It's important to identify limitations and constraints:

Key Questions

Following Polya's recommendation to formulate questions, let's identify things we need to clarify:

Analogies and Inspiration

Polya suggests finding analogies to similar problems. We can draw inspiration from:

With this thorough understanding of our problem, we can move confidently to the planning phase.

Step 2: Devising a Plan - Architecture and Components

Now that we understand the problem, we'll create a structured plan following Polya's second step.

Application Architecture

Let's define the high-level architecture of our application:

classDiagram class App { +initializeApp() +loadUserData() +renderInterface() } class UserProfile { -preferences -savedItineraries +getPreferences() +savePreferences() +getSavedItineraries() +saveItinerary() } class FormHandler { -formElements +validateInput() +processForm() +displayFormErrors() } class LocationService { -currentPosition +getUserLocation() +calculateDistance() +showLocationOnMap() } class ActivityFinder { -activityDatabase +filterByPreferences() +filterByLocation() +getSuggestions() } class DragDropManager { -dragElements -dropTargets +initializeDragDrop() +handleDragStart() +handleDrop() } class Visualizer { -canvas -svgContainer +drawActivityMap() +createItinerarySummary() +updateVisualization() } class StorageManager { +saveData() +loadData() +clearData() } App --> UserProfile App --> FormHandler App --> LocationService App --> ActivityFinder App --> DragDropManager App --> Visualizer App --> StorageManager UserProfile --> StorageManager ActivityFinder --> LocationService

This architecture follows the principle of separation of concerns, with each module having a distinct responsibility.

UI Components and Layout

Let's plan the user interface components:

LocalAdventures User Profile Preferences Form Saved Itineraries Activity Suggestions Hiking Trail 5 miles away • Moderate difficulty ⭐⭐⭐⭐☆ (Draggable) Mountain Biking 8 miles away • Advanced difficulty ⭐⭐⭐⭐⭐ (Draggable) Kayaking 3 miles away • Beginner friendly ⭐⭐⭐☆☆ (Draggable) My Itinerary Activity Map Drop Activities Here Hiking Trail Kayaking

Data Model

Let's define the core data structures we'll use:

// User Profile Data Structure
const userProfile = {
    name: "Jane Smith",
    preferences: {
        activityTypes: ["hiking", "cycling", "kayaking"],
        difficultyLevel: "intermediate",
        maxDistance: 15, // miles
        preferredDuration: 3, // hours
        schedule: ["weekend", "evening"]
    },
    savedItineraries: [
        {
            id: "itin-001",
            name: "Saturday Adventure",
            date: "2025-05-10",
            activities: [
                { id: "act-1", type: "hiking", name: "Eagle Peak Trail" },
                { id: "act-2", type: "kayaking", name: "Silver Lake" }
            ]
        }
    ]
};

// Activity Data Structure
const activityData = [
    {
        id: "act-001",
        name: "Eagle Peak Trail",
        type: "hiking",
        location: {
            latitude: 37.7749,
            longitude: -122.4194,
            address: "Eagle Peak, National Park"
        },
        distance: 3.5, // miles from current location
        difficulty: "moderate",
        duration: 2.5, // hours
        description: "Scenic trail with mountain views.",
        rating: 4.5,
        imageUrl: "images/eagle-peak.jpg"
    },
    // More activities...
];

Implementation Plan and Milestones

Breaking down the work into manageable pieces:

  1. Phase 1: Basic Structure and Storage
    • Set up project structure and HTML layout
    • Implement localStorage mechanisms
    • Create user profile form with HTML5 validation
    • Build basic data models
  2. Phase 2: Location and Activities
    • Implement Geolocation API integration
    • Create sample activity database
    • Build filtering mechanisms based on user location and preferences
    • Display activity suggestions
  3. Phase 3: Drag and Drop Interface
    • Make activity cards draggable
    • Create itinerary drop zones
    • Implement drag and drop event handling
    • Update UI based on drag operations
  4. Phase 4: Visualization
    • Implement Canvas-based map
    • Create SVG-based activity visualizations
    • Develop interactive itinerary summary
    • Integrate visualizations with drag and drop
  5. Phase 5: Refinement and Testing
    • Improve responsive design
    • Enhance user experience and feedback
    • Test across browsers and devices
    • Implement error handling and fallbacks

Technology Choices

For each component, we'll select the most appropriate HTML5 technology:

With this detailed plan in place, we're ready to move to the implementation phase.

Step 3: Executing the Plan - Implementation

Now we'll begin implementing our solution following the plan we've created. We'll work through each phase methodically, testing as we go.

Phase 1: Basic Structure and Storage

Let's start with the HTML structure and storage mechanisms:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LocalAdventures - Find Outdoor Activities Nearby</title>
    <style>
        /* Core styles */
        :root {
            --primary-color: #1976d2;
            --primary-light: #e3f2fd;
            --secondary-color: #ff9800;
            --secondary-light: #fff3e0;
            --success-color: #4caf50;
            --success-light: #e8f5e9;
            --info-color: #00bcd4;
            --info-light: #e0f7fa;
            --warning-color: #ff5722;
            --warning-light: #fbe9e7;
            --gray-light: #f5f5f5;
            --gray-medium: #e0e0e0;
            --gray-dark: #9e9e9e;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            color: #333;
            background-color: #f9f9f9;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        
        header {
            background-color: var(--primary-color);
            color: white;
            padding: 1rem;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        header h1 {
            margin: 0;
            font-size: 1.8rem;
        }
        
        main {
            display: grid;
            grid-template-columns: 1fr 3fr;
            grid-template-rows: auto 1fr;
            grid-gap: 20px;
            margin-top: 20px;
        }
        
        /* Responsive layout */
        @media (max-width: 768px) {
            main {
                grid-template-columns: 1fr;
            }
        }
        
        /* User profile section */
        .profile-section {
            grid-column: 1;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            padding: 20px;
        }
        
        .profile-form {
            margin-top: 15px;
        }
        
        .form-group {
            margin-bottom: 15px;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: 500;
        }
        
        input, select {
            width: 100%;
            padding: 8px;
            border: 1px solid var(--gray-medium);
            border-radius: 4px;
            font-size: 1rem;
        }
        
        input:invalid {
            border-color: var(--warning-color);
        }
        
        button {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1rem;
            transition: background-color 0.3s;
        }
        
        button:hover {
            background-color: #0d47a1;
        }
        
        /* Saved itineraries */
        .saved-itineraries {
            margin-top: 30px;
        }
        
        .itinerary-item {
            padding: 10px;
            background-color: var(--primary-light);
            border-radius: 4px;
            margin-bottom: 10px;
            cursor: pointer;
        }
        
        .itinerary-item:hover {
            background-color: var(--gray-light);
        }
        
        /* Content area */
        .content-section {
            grid-column: 2;
            display: grid;
            grid-template-columns: 3fr 2fr;
            grid-gap: 20px;
        }
        
        /* Activities panel */
        .activities-panel {
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            padding: 20px;
        }
        
        .activity-card {
            background-color: var(--secondary-light);
            border-radius: 8px;
            padding: 15px;
            margin-bottom: 15px;
            border-left: 4px solid var(--secondary-color);
            cursor: grab;
        }
        
        .activity-card h3 {
            margin-top: 0;
            margin-bottom: 5px;
        }
        
        .activity-card p {
            margin: 5px 0;
            font-size: 0.9rem;
        }
        
        .activity-card.dragging {
            opacity: 0.5;
        }
        
        /* Itinerary panel */
        .itinerary-panel {
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            padding: 20px;
            display: flex;
            flex-direction: column;
        }
        
        .map-container {
            flex: 1;
            background-color: var(--info-light);
            border-radius: 8px;
            margin-bottom: 15px;
            overflow: hidden;
        }
        
        #activity-map {
            width: 100%;
            height: 100%;
        }
        
        .drop-container {
            min-height: 150px;
            background-color: var(--success-light);
            border: 2px dashed var(--success-color);
            border-radius: 8px;
            padding: 10px;
        }
        
        .drop-container.drag-over {
            background-color: var(--success-color);
            border-style: solid;
        }
        
        .itinerary-activity {
            background-color: white;
            border-radius: 4px;
            padding: 10px;
            margin-bottom: 8px;
            border-left: 3px solid var(--primary-color);
        }
        
        /* Initial welcome screen */
        .welcome-screen {
            grid-column: 1 / -1;
            text-align: center;
            padding: 40px;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .hidden {
            display: none;
        }
    </style>
</head>
<body>
    <header>
        <div class="container">
            <h1>LocalAdventures</h1>
        </div>
    </header>
    
    <div class="container">
        <div id="welcome-screen" class="welcome-screen">
            <h2>Welcome to LocalAdventures!</h2>
            <p>Discover and plan outdoor activities in your area.</p>
            <button id="get-started-btn">Get Started</button>
        </div>
        
        <main id="app-main" class="hidden">
            <section class="profile-section">
                <h2>Your Profile</h2>
                <form id="profile-form" class="profile-form">
                    <div class="form-group">
                        <label for="user-name">Name</label>
                        <input type="text" id="user-name" name="userName" required>
                    </div>
                    
                    <div class="form-group">
                        <label for="activity-types">Activity Interests</label>
                        <select id="activity-types" name="activityTypes" multiple required>
                            <option value="hiking">Hiking</option>
                            <option value="cycling">Cycling</option>
                            <option value="kayaking">Kayaking</option>
                            <option value="camping">Camping</option>
                            <option value="fishing">Fishing</option>
                            <option value="climbing">Rock Climbing</option>
                        </select>
                    </div>
                    
                    <div class="form-group">
                        <label for="difficulty-level">Preferred Difficulty</label>
                        <select id="difficulty-level" name="difficultyLevel" required>
                            <option value="beginner">Beginner</option>
                            <option value="intermediate">Intermediate</option>
                            <option value="advanced">Advanced</option>
                        </select>
                    </div>
                    
                    <div class="form-group">
                        <label for="max-distance">Maximum Distance (miles)</label>
                        <input type="range" id="max-distance" name="maxDistance" min="5" max="50" step="5" value="15">
                        <output for="max-distance">15 miles</output>
                    </div>
                    
                    <div class="form-group">
                        <label for="use-location">Use My Current Location</label>
                        <input type="checkbox" id="use-location" name="useLocation" checked>
                    </div>
                    
                    <button type="submit">Save Preferences</button>
                </form>
                
                <div class="saved-itineraries">
                    <h3>Saved Itineraries</h3>
                    <div id="itineraries-list">
                        <p>No saved itineraries yet.</p>
                    </div>
                </div>
            </section>
            
            <section class="content-section">
                <div class="activities-panel">
                    <h2>Suggested Activities</h2>
                    <p id="location-status">Finding activities near you...</p>
                    <div id="activities-container">
                        <!-- Activities will be dynamically added here -->
                    </div>
                </div>
                
                <div class="itinerary-panel">
                    <h2>My Adventure Plan</h2>
                    <div class="map-container">
                        <canvas id="activity-map" width="400" height="300"></canvas>
                    </div>
                    
                    <h3>Drag Activities Here</h3>
                    <div id="itinerary-drop" class="drop-container">
                        <!-- Dragged activities will be placed here -->
                    </div>
                    
                    <div class="form-group" style="margin-top: 15px;">
                        <input type="text" id="itinerary-name" placeholder="Name your adventure plan">
                        <button id="save-itinerary-btn" style="margin-top: 10px;">Save Plan</button>
                    </div>
                </div>
            </section>
        </main>
    </div>
    
    <script>
        // Storage manager to handle localStorage operations
        const StorageManager = {
            saveData: function(key, data) {
                try {
                    localStorage.setItem(key, JSON.stringify(data));
                    return true;
                } catch (error) {
                    console.error('Error saving data:', error);
                    return false;
                }
            },
            
            loadData: function(key) {
                try {
                    const data = localStorage.getItem(key);
                    return data ? JSON.parse(data) : null;
                } catch (error) {
                    console.error('Error loading data:', error);
                    return null;
                }
            },
            
            clearData: function(key) {
                try {
                    localStorage.removeItem(key);
                    return true;
                } catch (error) {
                    console.error('Error clearing data:', error);
                    return false;
                }
            }
        };
        
        // User profile manager
        const UserProfile = {
            data: {
                name: '',
                preferences: {
                    activityTypes: [],
                    difficultyLevel: 'beginner',
                    maxDistance: 15,
                    useLocation: true
                },
                savedItineraries: []
            },
            
            initialize: function() {
                const savedProfile = StorageManager.loadData('userProfile');
                if (savedProfile) {
                    this.data = savedProfile;
                    this.populateForm();
                    return true;
                }
                return false;
            },
            
            saveProfile: function() {
                return StorageManager.saveData('userProfile', this.data);
            },
            
            populateForm: function() {
                document.getElementById('user-name').value = this.data.name;
                
                const activitySelect = document.getElementById('activity-types');
                Array.from(activitySelect.options).forEach(option => {
                    option.selected = this.data.preferences.activityTypes.includes(option.value);
                });
                
                document.getElementById('difficulty-level').value = this.data.preferences.difficultyLevel;
                
                const distanceSlider = document.getElementById('max-distance');
                distanceSlider.value = this.data.preferences.maxDistance;
                distanceSlider.nextElementSibling.textContent = `${this.data.preferences.maxDistance} miles`;
                
                document.getElementById('use-location').checked = this.data.preferences.useLocation;
            },
            
            updateFromForm: function() {
                this.data.name = document.getElementById('user-name').value;
                
                const activitySelect = document.getElementById('activity-types');
                this.data.preferences.activityTypes = Array.from(activitySelect.selectedOptions).map(option => option.value);
                
                this.data.preferences.difficultyLevel = document.getElementById('difficulty-level').value;
                this.data.preferences.maxDistance = parseInt(document.getElementById('max-distance').value);
                this.data.preferences.useLocation = document.getElementById('use-location').checked;
            },
            
            saveItinerary: function(name, activities) {
                const newItinerary = {
                    id: 'itin-' + Date.now(),
                    name: name,
                    date: new Date().toISOString().slice(0, 10),
                    activities: activities
                };
                
                this.data.savedItineraries.push(newItinerary);
                this.saveProfile();
                this.renderSavedItineraries();
                return newItinerary;
            },
            
            renderSavedItineraries: function() {
                const container = document.getElementById('itineraries-list');
                
                if (this.data.savedItineraries.length === 0) {
                    container.innerHTML = '<p>No saved itineraries yet.</p>';
                    return;
                }
                
                container.innerHTML = '';
                this.data.savedItineraries.forEach(itinerary => {
                    const item = document.createElement('div');
                    item.className = 'itinerary-item';
                    item.dataset.id = itinerary.id;
                    
                    item.innerHTML = `
                        <strong>${itinerary.name}</strong>
                        <p>${itinerary.date} • ${itinerary.activities.length} activities</p>
                    `;
                    
                    item.addEventListener('click', () => {
                        // Load this itinerary
                        App.loadItinerary(itinerary);
                    });
                    
                    container.appendChild(item);
                });
            }
        };
        
        // Form Validation Handler
        const FormHandler = {
            initialize: function() {
                const profileForm = document.getElementById('profile-form');
                const maxDistanceSlider = document.getElementById('max-distance');
                
                // Update distance output value
                maxDistanceSlider.addEventListener('input', function() {
                    this.nextElementSibling.textContent = `${this.value} miles`;
                });
                
                // Form submission
                profileForm.addEventListener('submit', function(e) {
                    e.preventDefault();
                    
                    if (this.checkValidity()) {
                        UserProfile.updateFromForm();
                        UserProfile.saveProfile();
                        App.updateBasedOnPreferences();
                        
                        alert('Your preferences have been saved!');
                    } else {
                        // Form is not valid
                        this.reportValidity();
                    }
                });
            },
            
            validateItineraryForm: function() {
                const name = document.getElementById('itinerary-name').value.trim();
                const dropZone = document.getElementById('itinerary-drop');
                const activities = dropZone.querySelectorAll('.itinerary-activity');
                
                if (name === '') {
                    alert('Please name your adventure plan.');
                    return false;
                }
                
                if (activities.length === 0) {
                    alert('Please add at least one activity to your plan.');
                    return false;
                }
                
                return true;
            }
        };
        
        // Main Application Controller
        const App = {
            initialize: function() {
                // Set up event listeners
                document.getElementById('get-started-btn').addEventListener('click', this.startApp.bind(this));
                document.getElementById('save-itinerary-btn').addEventListener('click', this.saveCurrentItinerary.bind(this));
                
                // Initialize form handler
                FormHandler.initialize();
                
                // Initialize map canvas
                this.initializeMap();
            },
            
            startApp: function() {
                // Hide welcome screen, show main app
                document.getElementById('welcome-screen').classList.add('hidden');
                document.getElementById('app-main').classList.remove('hidden');
                
                // Load user profile if exists
                const profileExists = UserProfile.initialize();
                
                if (profileExists) {
                    // Show saved itineraries
                    UserProfile.renderSavedItineraries();
                    
                    // Update app based on preferences
                    this.updateBasedOnPreferences();
                }
            },
            
            updateBasedOnPreferences: function() {
                // This will be implemented in Phase 2 when we add location and activities
                console.log('Updating based on preferences...');
                
                // For now, we'll initialize with dummy data in the next phase
            },
            
            saveCurrentItinerary: function() {
                if (FormHandler.validateItineraryForm()) {
                    const name = document.getElementById('itinerary-name').value.trim();
                    const dropZone = document.getElementById('itinerary-drop');
                    const activityElements = dropZone.querySelectorAll('.itinerary-activity');
                    
                    // Extract activity data
                    const activities = Array.from(activityElements).map(el => {
                        return {
                            id: el.dataset.id,
                            name: el.querySelector('h4').textContent,
                            type: el.dataset.type
                        };
                    });
                    
                    // Save to user profile
                    UserProfile.saveItinerary(name, activities);
                    
                    // Clear form and drop zone
                    document.getElementById('itinerary-name').value = '';
                    dropZone.innerHTML = '';
                    
                    // Update map
                    this.updateMapVisualization([]);
                    
                    alert('Your adventure plan has been saved!');
                }
            },
            
            loadItinerary: function(itinerary) {
                // This functionality will be implemented in later phases
                alert(`Loading itinerary: ${itinerary.name}`);
            },
            
            initializeMap: function() {
                const canvas = document.getElementById('activity-map');
                const ctx = canvas.getContext('2d');
                
                // Clear the canvas
                ctx.fillStyle = 'white';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                
                // Draw a placeholder
                ctx.font = '14px Arial';
                ctx.fillStyle = '#999';
                ctx.textAlign = 'center';
                ctx.fillText('Map will appear when activities are selected', canvas.width/2, canvas.height/2);
            },
            
            updateMapVisualization: function(activities) {
                // This will be implemented in Phase 4
                console.log('Updating map with activities:', activities);
            }
        };
        
        // Initialize the application when the DOM is loaded
        document.addEventListener('DOMContentLoaded', function() {
            App.initialize();
        });
    </script>
</body>
</html>

In this first phase, we've established:

This gives us a solid foundation to build upon as we implement more features in the subsequent phases.

Phase 2: Location and Activities Integration

Now, let's add geolocation support and activity suggestions:

// Add these new modules to the JavaScript section

// Location Service to handle geolocation
const LocationService = {
    currentPosition: null,
    
    initialize: function() {
        return new Promise((resolve, reject) => {
            if (!navigator.geolocation) {
                reject('Geolocation is not supported by your browser');
                return;
            }
            
            navigator.geolocation.getCurrentPosition(
                position => {
                    this.currentPosition = {
                        latitude: position.coords.latitude,
                        longitude: position.coords.longitude,
                        accuracy: position.coords.accuracy
                    };
                    resolve(this.currentPosition);
                },
                error => {
                    console.error('Error getting location:', error);
                    reject(error.message);
                },
                {
                    enableHighAccuracy: true,
                    timeout: 10000,
                    maximumAge: 0
                }
            );
        });
    },
    
    calculateDistance: function(lat1, lon1, lat2, lon2) {
        // Implementation of the Haversine formula to calculate distance between coordinates
        function toRad(value) {
            return value * Math.PI / 180;
        }
        
        const R = 3958.8; // Earth's radius in miles
        const dLat = toRad(lat2 - lat1);
        const dLon = toRad(lon2 - lon1);
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                  Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * 
                  Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        const distance = R * c;
        
        return distance;
    },
    
    getDistanceFromCurrentPosition: function(lat, lon) {
        if (!this.currentPosition) {
            return null;
        }
        
        return this.calculateDistance(
            this.currentPosition.latitude,
            this.currentPosition.longitude,
            lat,
            lon
        );
    }
};

// Activity finder to handle suggestions
const ActivityFinder = {
    activities: [
        {
            id: "act-001",
            name: "Eagle Peak Trail",
            type: "hiking",
            location: {
                latitude: 37.7749,
                longitude: -122.4194,
                address: "Eagle Peak, National Park"
            },
            difficulty: "intermediate",
            duration: 2.5, // hours
            description: "Scenic trail with mountain views.",
            rating: 4.5,
            imageUrl: "images/eagle-peak.jpg"
        },
        {
            id: "act-002",
            name: "Blue Lake Kayaking",
            type: "kayaking",
            location: {
                latitude: 37.8049,
                longitude: -122.4149,
                address: "Blue Lake Recreation Area"
            },
            difficulty: "beginner",
            duration: 1.5, // hours
            description: "Calm waters perfect for beginners.",
            rating: 4.0,
            imageUrl: "images/blue-lake.jpg"
        },
        {
            id: "act-003",
            name: "Mountain View Cycling",
            type: "cycling",
            location: {
                latitude: 37.7549,
                longitude: -122.4294,
                address: "Mountain View Trail"
            },
            difficulty: "advanced",
            duration: 3.0, // hours
            description: "Challenging trail with steep climbs.",
            rating: 4.8,
            imageUrl: "images/mountain-cycling.jpg"
        },
        {
            id: "act-004",
            name: "Sunset Rock Climbing",
            type: "climbing",
            location: {
                latitude: 37.7649,
                longitude: -122.4394,
                address: "Sunset Cliffs"
            },
            difficulty: "advanced",
            duration: 4.0, // hours
            description: "Multiple routes with varying difficulty.",
            rating: 4.7,
            imageUrl: "images/sunset-climbing.jpg"
        },
        {
            id: "act-005",
            name: "Forest Camping Ground",
            type: "camping",
            location: {
                latitude: 37.7849,
                longitude: -122.4094,
                address: "National Forest Campground"
            },
            difficulty: "beginner",
            duration: 24.0, // hours
            description: "Family-friendly camping with amenities.",
            rating: 4.2,
            imageUrl: "images/forest-camping.jpg"
        },
        {
            id: "act-006",
            name: "Silver River Fishing",
            type: "fishing",
            location: {
                latitude: 37.7349,
                longitude: -122.4494,
                address: "Silver River Access Point"
            },
            difficulty: "intermediate",
            duration: 5.0, // hours
            description: "Great spot for trout fishing.",
            rating: 4.3,
            imageUrl: "images/silver-river.jpg"
        }
    ],
    
    initialize: function() {
        // In a real app, we might load activities from an API
        console.log('Activity data initialized with', this.activities.length, 'activities');
    },
    
    updateDistances: function() {
        if (!LocationService.currentPosition) {
            return;
        }
        
        this.activities.forEach(activity => {
            activity.distance = LocationService.getDistanceFromCurrentPosition(
                activity.location.latitude, 
                activity.location.longitude
            );
        });
    },
    
    getActivitiesByPreferences: function(preferences) {
        let filtered = [...this.activities];
        
        // Filter by activity types if specified
        if (preferences.activityTypes && preferences.activityTypes.length > 0) {
            filtered = filtered.filter(activity => 
                preferences.activityTypes.includes(activity.type)
            );
        }
        
        // Filter by difficulty
        if (preferences.difficultyLevel) {
            filtered = filtered.filter(activity => {
                // Match difficulty levels appropriately
                if (preferences.difficultyLevel === 'beginner') {
                    return activity.difficulty === 'beginner';
                } else if (preferences.difficultyLevel === 'intermediate') {
                    return activity.difficulty === 'beginner' || activity.difficulty === 'intermediate';
                } else {
                    // Advanced users can see all difficulties
                    return true;
                }
            });
        }
        
        // Filter by distance if we have location data
        if (preferences.maxDistance && LocationService.currentPosition) {
            filtered = filtered.filter(activity => 
                activity.distance <= preferences.maxDistance
            );
        }
        
        // Sort by distance if available, otherwise by rating
        if (LocationService.currentPosition) {
            filtered.sort((a, b) => a.distance - b.distance);
        } else {
            filtered.sort((a, b) => b.rating - a.rating);
        }
        
        return filtered;
    },
    
    renderActivityCards: function(container, activities) {
        container.innerHTML = '';
        
        if (activities.length === 0) {
            container.innerHTML = '

No activities match your preferences. Try adjusting your filters.

'; return; } activities.forEach(activity => { const card = document.createElement('div'); card.className = 'activity-card'; card.draggable = true; card.dataset.id = activity.id; card.dataset.type = activity.type; // Calculate distance string let distanceStr = ''; if (activity.distance !== undefined) { distanceStr = `${activity.distance.toFixed(1)} miles away • `; } // Convert rating to stars const stars = '★'.repeat(Math.floor(activity.rating)) + '☆'.repeat(5 - Math.floor(activity.rating)); card.innerHTML = `

${activity.name}

${distanceStr}${activity.difficulty} difficulty

${activity.description}

${stars} (${activity.rating.toFixed(1)})

Duration: ~${activity.duration} hours

`; // Make the card draggable card.addEventListener('dragstart', function(e) { e.dataTransfer.setData('text/plain', activity.id); this.classList.add('dragging'); }); card.addEventListener('dragend', function() { this.classList.remove('dragging'); }); container.appendChild(card); }); } }; // Add code to initialize these services in the App.initialize method: initialize: function() { // Set up event listeners document.getElementById('get-started-btn').addEventListener('click', this.startApp.bind(this)); document.getElementById('save-itinerary-btn').addEventListener('click', this.saveCurrentItinerary.bind(this)); // Initialize form handler FormHandler.initialize(); // Initialize map canvas this.initializeMap(); // Initialize activity data ActivityFinder.initialize(); }, // Update the updateBasedOnPreferences method: updateBasedOnPreferences: function() { const locationStatus = document.getElementById('location-status'); if (UserProfile.data.preferences.useLocation) { locationStatus.textContent = 'Getting your location...'; LocationService.initialize() .then(position => { locationStatus.textContent = 'Found activities near you'; ActivityFinder.updateDistances(); this.showActivitySuggestions(); }) .catch(error => { locationStatus.textContent = `Couldn't access your location: ${error}`; UserProfile.data.preferences.useLocation = false; UserProfile.saveProfile(); this.showActivitySuggestions(); }); } else { locationStatus.textContent = 'Browse activities by interest'; this.showActivitySuggestions(); } }, showActivitySuggestions: function() { const activitiesContainer = document.getElementById('activities-container'); const filteredActivities = ActivityFinder.getActivitiesByPreferences(UserProfile.data.preferences); ActivityFinder.renderActivityCards(activitiesContainer, filteredActivities); },

In this phase, we've added:

Phase 3: Drag and Drop Implementation

Now, let's implement the drag and drop

Phase 3: Drag and Drop Implementation

Now, let's implement the drag and drop functionality for planning activities:

// Add the DragDropManager module
              const DragDropManager = {
                  initialize: function() {
                      const dropZone = document.getElementById('itinerary-drop');
                      
                      // Set up the drop zone event handlers
                      dropZone.addEventListener('dragover', this.handleDragOver);
                      dropZone.addEventListener('dragleave', this.handleDragLeave);
                      dropZone.addEventListener('drop', this.handleDrop);
                      
                      console.log('Drag and drop functionality initialized');
                  },
                  
                  handleDragOver: function(e) {
                      // Prevent default to allow drop
                      e.preventDefault();
                      
                      // Add visual feedback
                      this.classList.add('drag-over');
                      
                      // Set the drop effect to 'move'
                      e.dataTransfer.dropEffect = 'move';
                  },
                  
                  handleDragLeave: function(e) {
                      // Remove visual feedback
                      this.classList.remove('drag-over');
                  },
                  
                  handleDrop: function(e) {
                      // Prevent default behavior
                      e.preventDefault();
                      
                      // Remove visual feedback
                      this.classList.remove('drag-over');
                      
                      // Get the dragged activity ID
                      const activityId = e.dataTransfer.getData('text/plain');
                      
                      // Find the activity data
                      const activity = ActivityFinder.activities.find(a => a.id === activityId);
                      
                      if (activity) {
                          // Check if it's already in the itinerary
                          const existingItem = this.querySelector(`[data-id="${activityId}"]`);
                          if (existingItem) {
                              alert('This activity is already in your itinerary.');
                              return;
                          }
                          
                          // Create a new item in the itinerary
                          const item = document.createElement('div');
                          item.className = 'itinerary-activity';
                          item.dataset.id = activity.id;
                          item.dataset.type = activity.type;
                          
                          item.innerHTML = `
                              

${activity.name}

${activity.difficulty} difficulty • ~${activity.duration} hours

`; // Add a remove button handler const removeBtn = item.querySelector('.remove-btn'); removeBtn.addEventListener('click', function() { item.remove(); // Update the map visualization App.updateItineraryVisualization(); }); // Add to the drop zone this.appendChild(item); // Update the map visualization App.updateItineraryVisualization(); } } }; // Add a new method to App for updating visualization when itinerary changes App.updateItineraryVisualization = function() { const dropZone = document.getElementById('itinerary-drop'); const activityItems = dropZone.querySelectorAll('.itinerary-activity'); // Collect the activities in the itinerary const activities = Array.from(activityItems).map(item => { const id = item.dataset.id; return ActivityFinder.activities.find(a => a.id === id); }); // Update the map visualization this.updateMapVisualization(activities); }; // Update the App.initialize method to include the DragDropManager initialize: function() { // Set up event listeners document.getElementById('get-started-btn').addEventListener('click', this.startApp.bind(this)); document.getElementById('save-itinerary-btn').addEventListener('click', this.saveCurrentItinerary.bind(this)); // Initialize form handler FormHandler.initialize(); // Initialize map canvas this.initializeMap(); // Initialize activity data ActivityFinder.initialize(); // Initialize drag and drop DragDropManager.initialize(); },

In this phase, we've added:

Phase 4: Canvas and SVG Visualization

Now, let's implement the visualization components using Canvas and SVG:

// Add the Visualizer module
              const Visualizer = {
                  initialize: function() {
                      // Get the canvas context
                      this.canvas = document.getElementById('activity-map');
                      this.ctx = this.canvas.getContext('2d');
                      
                      // Set up initial canvas state
                      this.resizeCanvas();
                      
                      // Handle window resize
                      window.addEventListener('resize', this.resizeCanvas.bind(this));
                      
                      console.log('Visualizer initialized');
                  },
                  
                  resizeCanvas: function() {
                      // Adjust canvas size to its container
                      const container = this.canvas.parentElement;
                      
                      // Set canvas dimensions
                      this.canvas.width = container.clientWidth;
                      this.canvas.height = container.clientHeight;
                      
                      // Redraw if we have activities to display
                      if (this.currentActivities) {
                          this.drawActivityMap(this.currentActivities);
                      } else {
                          this.drawEmptyState();
                      }
                  },
                  
                  drawEmptyState: function() {
                      const ctx = this.ctx;
                      const width = this.canvas.width;
                      const height = this.canvas.height;
                      
                      // Clear the canvas
                      ctx.fillStyle = '#e1f5fe';
                      ctx.fillRect(0, 0, width, height);
                      
                      // Draw a message
                      ctx.font = '14px Arial';
                      ctx.fillStyle = '#0277bd';
                      ctx.textAlign = 'center';
                      ctx.fillText('Add activities to your itinerary to see them on the map', width/2, height/2);
                      
                      // Draw compass
                      this.drawCompass(width - 40, 40, 30);
                  },
                  
                  drawCompass: function(x, y, size) {
                      const ctx = this.ctx;
                      
                      // Draw compass circle
                      ctx.beginPath();
                      ctx.arc(x, y, size, 0, Math.PI * 2);
                      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
                      ctx.fill();
                      ctx.strokeStyle = '#0277bd';
                      ctx.lineWidth = 2;
                      ctx.stroke();
                      
                      // Draw N indicator
                      ctx.beginPath();
                      ctx.moveTo(x, y - size + 5);
                      ctx.lineTo(x - 5, y);
                      ctx.lineTo(x + 5, y);
                      ctx.closePath();
                      ctx.fillStyle = '#f44336';
                      ctx.fill();
                      
                      // Draw letters
                      ctx.font = '10px Arial';
                      ctx.textAlign = 'center';
                      ctx.fillStyle = '#0277bd';
                      ctx.fillText('N', x, y - size + 12);
                      ctx.fillText('E', x + size - 8, y + 4);
                      ctx.fillText('S', x, y + size - 4);
                      ctx.fillText('W', x - size + 8, y + 4);
                  },
                  
                  drawActivityMap: function(activities) {
                      this.currentActivities = activities;
                      
                      if (!activities || activities.length === 0) {
                          this.drawEmptyState();
                          return;
                      }
                      
                      const ctx = this.ctx;
                      const width = this.canvas.width;
                      const height = this.canvas.height;
                      
                      // Clear the canvas
                      ctx.fillStyle = '#e1f5fe';
                      ctx.fillRect(0, 0, width, height);
                      
                      // If we don't have location data, draw an abstract visualization
                      if (!LocationService.currentPosition) {
                          this.drawAbstractVisualization(activities);
                          return;
                      }
                      
                      // Draw actual map based on location data
                      this.drawLocationBasedMap(activities);
                  },
                  
                  drawAbstractVisualization: function(activities) {
                      const ctx = this.ctx;
                      const width = this.canvas.width;
                      const height = this.canvas.height;
                      
                      // Draw title
                      ctx.font = 'bold 14px Arial';
                      ctx.fillStyle = '#0277bd';
                      ctx.textAlign = 'center';
                      ctx.fillText('Activity Plan', width/2, 25);
                      
                      // Define activity type colors
                      const typeColors = {
                          hiking: '#4caf50',
                          cycling: '#2196f3',
                          kayaking: '#00bcd4',
                          climbing: '#ff5722',
                          camping: '#8bc34a',
                          fishing: '#03a9f4'
                      };
                      
                      // Calculate positions in a circle
                      const centerX = width / 2;
                      const centerY = height / 2;
                      const radius = Math.min(width, height) * 0.35;
                      
                      // Draw activities
                      activities.forEach((activity, index) => {
                          const angle = (index / activities.length) * Math.PI * 2;
                          const x = centerX + Math.cos(angle) * radius;
                          const y = centerY + Math.sin(angle) * radius;
                          
                          // Draw activity node
                          ctx.beginPath();
                          ctx.arc(x, y, 20, 0, Math.PI * 2);
                          ctx.fillStyle = typeColors[activity.type] || '#9e9e9e';
                          ctx.fill();
                          ctx.strokeStyle = '#fff';
                          ctx.lineWidth = 2;
                          ctx.stroke();
                          
                          // Draw activity icon (simplified as text)
                          ctx.font = '12px Arial';
                          ctx.fillStyle = '#fff';
                          ctx.textAlign = 'center';
                          ctx.textBaseline = 'middle';
                          ctx.fillText(activity.type.charAt(0).toUpperCase(), x, y);
                          
                          // Draw connection to center
                          ctx.beginPath();
                          ctx.moveTo(centerX, centerY);
                          ctx.lineTo(x, y);
                          ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
                          ctx.lineWidth = 1;
                          ctx.stroke();
                          
                          // Draw activity name
                          ctx.font = '10px Arial';
                          ctx.fillStyle = '#333';
                          ctx.textAlign = 'center';
                          ctx.textBaseline = 'middle';
                          
                          // Angle text away from center
                          ctx.save();
                          ctx.translate(x, y);
                          
                          // Position text outside the circle
                          const textRadius = 25;
                          const textX = Math.cos(angle) * textRadius;
                          const textY = Math.sin(angle) * textRadius;
                          
                          // Rotate text to be readable
                          let textAngle = angle;
                          if (textAngle > Math.PI / 2 && textAngle < Math.PI * 3 / 2) {
                              textAngle += Math.PI;
                          }
                          ctx.rotate(textAngle + Math.PI / 2);
                          
                          ctx.fillText(activity.name, 0, textRadius);
                          ctx.restore();
                      });
                      
                      // Draw a user icon in the center
                      ctx.beginPath();
                      ctx.arc(centerX, centerY, 15, 0, Math.PI * 2);
                      ctx.fillStyle = '#e91e63';
                      ctx.fill();
                      ctx.strokeStyle = '#fff';
                      ctx.lineWidth = 2;
                      ctx.stroke();
                      
                      ctx.font = 'bold 10px Arial';
                      ctx.fillStyle = '#fff';
                      ctx.textAlign = 'center';
                      ctx.textBaseline = 'middle';
                      ctx.fillText('YOU', centerX, centerY);
                      
                      // Draw compass
                      this.drawCompass(width - 40, 40, 30);
                  },
                  
                  drawLocationBasedMap: function(activities) {
                      const ctx = this.ctx;
                      const width = this.canvas.width;
                      const height = this.canvas.height;
                      
                      // Get user position
                      const userPos = LocationService.currentPosition;
                      
                      // Find bounding box for user and all activities
                      let minLat = userPos.latitude;
                      let maxLat = userPos.latitude;
                      let minLng = userPos.longitude;
                      let maxLng = userPos.longitude;
                      
                      activities.forEach(activity => {
                          minLat = Math.min(minLat, activity.location.latitude);
                          maxLat = Math.max(maxLat, activity.location.latitude);
                          minLng = Math.min(minLng, activity.location.longitude);
                          maxLng = Math.max(maxLng, activity.location.longitude);
                      });
                      
                      // Add padding to the bounding box
                      const padding = 0.1; // 10% padding
                      const latRange = maxLat - minLat;
                      const lngRange = maxLng - minLng;
                      
                      minLat -= latRange * padding;
                      maxLat += latRange * padding;
                      minLng -= lngRange * padding;
                      maxLng += lngRange * padding;
                      
                      // Function to convert coordinates to canvas positions
                      const coordToPixel = (lat, lng) => {
                          const x = width * (lng - minLng) / (maxLng - minLng);
                          const y = height * (1 - (lat - minLat) / (maxLat - minLat));
                          return { x, y };
                      };
                      
                      // Draw background grid
                      ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
                      ctx.lineWidth = 0.5;
                      
                      for (let i = 0; i <= 10; i++) {
                          // Vertical lines
                          const x = width * (i / 10);
                          ctx.beginPath();
                          ctx.moveTo(x, 0);
                          ctx.lineTo(x, height);
                          ctx.stroke();
                          
                          // Horizontal lines
                          const y = height * (i / 10);
                          ctx.beginPath();
                          ctx.moveTo(0, y);
                          ctx.lineTo(width, y);
                          ctx.stroke();
                      }
                      
                      // Draw activity connections
                      ctx.beginPath();
                      
                      // Start from user position
                      const userPixel = coordToPixel(userPos.latitude, userPos.longitude);
                      ctx.moveTo(userPixel.x, userPixel.y);
                      
                      // Connect to each activity in order
                      activities.forEach(activity => {
                          const pixel = coordToPixel(
                              activity.location.latitude,
                              activity.location.longitude
                          );
                          ctx.lineTo(pixel.x, pixel.y);
                      });
                      
                      // Style for connections
                      ctx.strokeStyle = '#0288d1';
                      ctx.lineWidth = 2;
                      ctx.stroke();
                      
                      // Define activity type colors
                      const typeColors = {
                          hiking: '#4caf50',
                          cycling: '#2196f3',
                          kayaking: '#00bcd4',
                          climbing: '#ff5722',
                          camping: '#8bc34a',
                          fishing: '#03a9f4'
                      };
                      
                      // Draw each activity location
                      activities.forEach((activity, index) => {
                          const pixel = coordToPixel(
                              activity.location.latitude,
                              activity.location.longitude
                          );
                          
                          // Activity circle
                          ctx.beginPath();
                          ctx.arc(pixel.x, pixel.y, 10, 0, Math.PI * 2);
                          ctx.fillStyle = typeColors[activity.type] || '#9e9e9e';
                          ctx.fill();
                          ctx.strokeStyle = '#fff';
                          ctx.lineWidth = 2;
                          ctx.stroke();
                          
                          // Activity number
                          ctx.font = 'bold 10px Arial';
                          ctx.fillStyle = '#fff';
                          ctx.textAlign = 'center';
                          ctx.textBaseline = 'middle';
                          ctx.fillText(index + 1, pixel.x, pixel.y);
                          
                          // Activity name
                          ctx.font = '12px Arial';
                          ctx.fillStyle = '#333';
                          ctx.textAlign = 'center';
                          ctx.textBaseline = 'top';
                          ctx.fillText(activity.name, pixel.x, pixel.y + 15);
                      });
                      
                      // Draw user location
                      ctx.beginPath();
                      ctx.arc(userPixel.x, userPixel.y, 10, 0, Math.PI * 2);
                      ctx.fillStyle = '#e91e63';
                      ctx.fill();
                      ctx.strokeStyle = '#fff';
                      ctx.lineWidth = 2;
                      ctx.stroke();
                      
                      // Draw user marker
                      ctx.font = 'bold 10px Arial';
                      ctx.fillStyle = '#fff';
                      ctx.textAlign = 'center';
                      ctx.textBaseline = 'middle';
                      ctx.fillText('YOU', userPixel.x, userPixel.y);
                      
                      // Draw compass
                      this.drawCompass(width - 40, 40, 30);
                      
                      // Draw scale if we have real distances
                      if (activities.length > 0 && activities[0].distance) {
                          this.drawDistanceScale(width - 100, height - 20);
                      }
                  },
                  
                  drawDistanceScale: function(x, y) {
                      const ctx = this.ctx;
                      
                      // Draw a 5-mile scale bar
                      const scaleWidth = 50; // pixels
                      const scaleMiles = 5;  // miles represented
                      
                      ctx.beginPath();
                      ctx.moveTo(x - scaleWidth, y);
                      ctx.lineTo(x, y);
                      ctx.strokeStyle = '#333';
                      ctx.lineWidth = 2;
                      ctx.stroke();
                      
                      // Draw ticks
                      for (let i = 0; i <= 5; i++) {
                          const tickX = x - scaleWidth + (i * scaleWidth / 5);
                          ctx.beginPath();
                          ctx.moveTo(tickX, y);
                          ctx.lineTo(tickX, y - 5);
                          ctx.strokeStyle = '#333';
                          ctx.lineWidth = 1;
                          ctx.stroke();
                      }
                      
                      // Draw label
                      ctx.font = '10px Arial';
                      ctx.fillStyle = '#333';
                      ctx.textAlign = 'center';
                      ctx.textBaseline = 'bottom';
                      ctx.fillText(`${scaleMiles} miles`, x - scaleWidth/2, y - 8);
                  },
                  
                  createItinerarySummary: function(activities) {
                      // This method would create an SVG-based summary of the itinerary
                      // For simplicity in this project, we'll focus on the Canvas map
                      console.log('Creating itinerary summary for', activities.length, 'activities');
                  }
              };
              
              // Update the App.initialize method to include the Visualizer
              initialize: function() {
                  // Set up event listeners
                  document.getElementById('get-started-btn').addEventListener('click', this.startApp.bind(this));
                  document.getElementById('save-itinerary-btn').addEventListener('click', this.saveCurrentItinerary.bind(this));
                  
                  // Initialize components
                  FormHandler.initialize();
                  ActivityFinder.initialize();
                  DragDropManager.initialize();
                  Visualizer.initialize();
              },
              
              // Update the App.updateMapVisualization method
              updateMapVisualization: function(activities) {
                  Visualizer.drawActivityMap(activities);
              },

In this phase, we've added:

Phase 5: Refinement and Error Handling

To complete our application, let's add error handling, loading states, and final polish:

// Add CSS for loading and error states
              <style>
              /* Add these to your existing CSS */
              .loading-indicator {
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  padding: 20px;
                  color: #666;
              }
              
              .loading-indicator::after {
                  content: '';
                  width: 20px;
                  height: 20px;
                  margin-left: 10px;
                  border: 2px solid #ddd;
                  border-top: 2px solid var(--primary-color);
                  border-radius: 50%;
                  animation: spin 1s linear infinite;
              }
              
              .error-message {
                  padding: 10px;
                  background-color: var(--warning-light);
                  border-left: 4px solid var(--warning-color);
                  margin: 10px 0;
              }
              
              .toast-notification {
                  position: fixed;
                  bottom: 20px;
                  right: 20px;
                  padding: 15px 20px;
                  background-color: #333;
                  color: white;
                  border-radius: 4px;
                  box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                  transition: opacity 0.3s, transform 0.3s;
                  opacity: 0;
                  transform: translateY(20px);
                  z-index: 1000;
              }
              
              .toast-notification.visible {
                  opacity: 1;
                  transform: translateY(0);
              }
              
              @keyframes spin {
                  0% { transform: rotate(0deg); }
                  100% { transform: rotate(360deg); }
              }
              
              /* Improve responsive design */
              @media (max-width: 768px) {
                  .content-section {
                      grid-template-columns: 1fr;
                  }
                  
                  .profile-section {
                      order: -1;
                  }
              }
              
              /* Add a bit more flair to the interface */
              .activity-card {
                  transition: transform 0.2s, box-shadow 0.2s;
              }
              
              .activity-card:hover {
                  transform: translateY(-3px);
                  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
              }
              
              .activity-rating {
                  color: #ffc107;
                  font-weight: bold;
              }
              </style>
              
              // Add a notification module
              const NotificationSystem = {
                  show: function(message, type = 'info', duration = 3000) {
                      // Create or reuse notification element
                      let notification = document.querySelector('.toast-notification');
                      
                      if (!notification) {
                          notification = document.createElement('div');
                          notification.className = 'toast-notification';
                          document.body.appendChild(notification);
                      }
                      
                      // Set message and type
                      notification.textContent = message;
                      notification.classList.remove('success', 'error', 'info', 'warning');
                      
                      if (['success', 'error', 'info', 'warning'].includes(type)) {
                          notification.classList.add(type);
                      }
                      
                      // Show the notification
                      setTimeout(() => {
                          notification.classList.add('visible');
                      }, 10);
                      
                      // Hide after duration
                      setTimeout(() => {
                          notification.classList.remove('visible');
                      }, duration);
                  }
              };
              
              // Add error handling wrapper for API calls
              function handleAsyncError(promise, errorMessage) {
                  return promise.catch(error => {
                      console.error(errorMessage, error);
                      NotificationSystem.show(errorMessage, 'error');
                      throw error;
                  });
              }
              
              // Update LocationService with better error handling
              initialize: function() {
                  return new Promise((resolve, reject) => {
                      if (!navigator.geolocation) {
                          const error = 'Geolocation is not supported by your browser';
                          NotificationSystem.show(error, 'error');
                          reject(error);
                          return;
                      }
                      
                      navigator.geolocation.getCurrentPosition(
                          position => {
                              this.currentPosition = {
                                  latitude: position.coords.latitude,
                                  longitude: position.coords.longitude,
                                  accuracy: position.coords.accuracy
                              };
                              NotificationSystem.show('Location found successfully', 'success');
                              resolve(this.currentPosition);
                          },
                          error => {
                              let errorMessage;
                              
                              switch(error.code) {
                                  case error.PERMISSION_DENIED:
                                      errorMessage = 'Location access denied. Check your browser permissions.';
                                      break;
                                  case error.POSITION_UNAVAILABLE:
                                      errorMessage = 'Location information unavailable. Try again later.';
                                      break;
                                  case error.TIMEOUT:
                                      errorMessage = 'Location request timed out. Try again.';
                                      break;
                                  default:
                                      errorMessage = 'Unknown error getting location.';
                              }
                              
                              console.error('Geolocation error:', errorMessage);
                              NotificationSystem.show(errorMessage, 'error');
                              reject(errorMessage);
                          },
                          {
                              enableHighAccuracy: true,
                              timeout: 10000,
                              maximumAge: 0
                          }
                      );
                  });
              },
              
              // Update App.updateBasedOnPreferences with loading and error states
              updateBasedOnPreferences: function() {
                  const locationStatus = document.getElementById('location-status');
                  const activitiesContainer = document.getElementById('activities-container');
                  
                  // Show loading state
                  locationStatus.className = 'loading-indicator';
                  locationStatus.textContent = 'Loading activities';
                  
                  activitiesContainer.innerHTML = '';
                  
                  if (UserProfile.data.preferences.useLocation) {
                      // Try to get location
                      handleAsyncError(
                          LocationService.initialize(),
                          'Could not access your location'
                      )
                      .then(position => {
                          locationStatus.className = '';
                          locationStatus.textContent = 'Activities near you';
                          
                          ActivityFinder.updateDistances();
                          this.showActivitySuggestions();
                          
                          // Update map with user location
                          Visualizer.drawActivityMap([]);
                      })
                      .catch(error => {
                          // Handle error by falling back to non-location mode
                          locationStatus.className = '';
                          locationStatus.textContent = 'Showing activities by interest (location unavailable)';
                          
                          UserProfile.data.preferences.useLocation = false;
                          UserProfile.saveProfile();
                          this.showActivitySuggestions();
                      });
                  } else {
                      // Just show activities without location
                      setTimeout(() => {
                          locationStatus.className = '';
                          locationStatus.textContent = 'Activities by interest';
                          this.showActivitySuggestions();
                      }, 500);
                  }
              },
              
              // Add fallback for browsers that don't support specific features
              checkBrowserSupport: function() {
                  const supportIssues = [];
                  
                  // Check for localStorage
                  if (!window.localStorage) {
                      supportIssues.push('Local Storage is not supported. Your preferences will not be saved between sessions.');
                  }
                  
                  // Check for Geolocation
                  if (!navigator.geolocation) {
                      supportIssues.push('Geolocation is not supported. Location-based features will not be available.');
                  }
                  
                  // Check for Canvas
                  if (!document.createElement('canvas').getContext) {
                      supportIssues.push('Canvas is not supported. Map visualizations will not be displayed.');
                  }
                  
                  // Check for Drag and Drop
                  const div = document.createElement('div');
                  if (!('draggable' in div) || !('ondragstart' in div && 'ondrop' in div)) {
                      supportIssues.push('Drag and Drop is not supported. You will not be able to drag activities to your itinerary.');
                  }
                  
                  // Display any issues
                  if (supportIssues.length > 0) {
                      const container = document.createElement('div');
                      container.className = 'error-message';
                      
                      const heading = document.createElement('h3');
                      heading.textContent = 'Browser Compatibility Issues';
                      container.appendChild(heading);
                      
                      const list = document.createElement('ul');
                      supportIssues.forEach(issue => {
                          const item = document.createElement('li');
                          item.textContent = issue;
                          list.appendChild(item);
                      });
                      
                      container.appendChild(list);
                      
                      document.querySelector('.container').insertBefore(
                          container, 
                          document.getElementById('welcome-screen')
                      );
                  }
                  
                  return supportIssues.length === 0;
              },

We'll need to update the App.initialize method one more time to include these new features:

initialize: function() {
                  // Check for browser support
                  this.checkBrowserSupport();
                  
                  // Set up event listeners
                  document.getElementById('get-started-btn').addEventListener('click', this.startApp.bind(this));
                  document.getElementById('save-itinerary-btn').addEventListener('click', this.saveCurrentItinerary.bind(this));
                  
                  // Initialize components
                  FormHandler.initialize();
                  ActivityFinder.initialize();
                  DragDropManager.initialize();
                  Visualizer.initialize();
                  
                  // Add responsive handlers
                  window.addEventListener('resize', this.handleResize.bind(this));
                  this.handleResize();
              },
              
              handleResize: function() {
                  // Adjust interface based on screen size
                  const width = window.innerWidth;
                  
                  if (width < 768) {
                      document.body.classList.add('mobile-view');
                  } else {
                      document.body.classList.remove('mobile-view');
                  }
              },
              
              saveCurrentItinerary: function() {
                  if (FormHandler.validateItineraryForm()) {
                      const name = document.getElementById('itinerary-name').value.trim();
                      const dropZone = document.getElementById('itinerary-drop');
                      const activityElements = dropZone.querySelectorAll('.itinerary-activity');
                      
                      // Extract activity data
                      const activities = Array.from(activityElements).map(el => {
                          return {
                              id: el.dataset.id,
                              name: el.querySelector('h4').textContent,
                              type: el.dataset.type
                          };
                      });
                      
                      // Save to user profile
                      UserProfile.saveItinerary(name, activities);
                      
                      // Clear form and drop zone
                      document.getElementById('itinerary-name').value = '';
                      dropZone.innerHTML = '';
                      
                      // Update map
                      this.updateMapVisualization([]);
                      
                      // Show success notification
                      NotificationSystem.show('Your adventure plan has been saved!', 'success');
                  }
              },

In this final phase, we've added:

With these refinements, our application is now complete and ready for user testing.

Step 4: Review and Extend - Evaluation and Next Steps

Following Polya's fourth step, let's review our solution, identify potential improvements, and explore extensions.

Project Review

Let's evaluate our solution against the original requirements:

Requirement Implementation Assessment
HTML5 Form Features User profile form with validation, custom inputs, and data persistence ✅ Complete
Geolocation Integration Location detection, distance calculation, and location-based filtering ✅ Complete
Drag and Drop Interface Draggable activity cards, drop zone for itinerary planning ✅ Complete
Canvas Visualization Interactive map showing activities and routes ✅ Complete
SVG Elements Used within the Canvas visualization for icons (compass) ⚠️ Limited implementation
Local Storage User preferences and itineraries saved between sessions ✅ Complete
Responsive Design Layout adapts to screen size with CSS Grid and media queries ✅ Complete
Error Handling Comprehensive error catching and user feedback ✅ Complete

Most requirements have been fully implemented. The SVG integration could be extended further with more dedicated SVG elements for activity icons and interactive components.

Technical Review

Our implementation has several technical strengths and areas for improvement:

Strengths:

Areas for Improvement:

Extended Features

Building on our solid foundation, here are potential extensions to enhance the application:

graph TD A[LocalAdventures App] --> B[Extended Features] B --> C1[Backend Integration] B --> C2[Social Features] B --> C3[Enhanced Visualization] B --> C4[Advanced Planning] B --> C5[Mobile Enhancements] C1 --> D1[Real activity database] C1 --> D2[User accounts] C1 --> D3[API integration] C2 --> D4[Share itineraries] C2 --> D5[Activity reviews] C2 --> D6[Group planning] C3 --> D7[3D terrain mapping] C3 --> D8[Weather integration] C3 --> D9[Animated routes] C4 --> D10[Time-based scheduling] C4 --> D11[Transportation options] C4 --> D12[Equipment checklists] C5 --> D13[Progressive Web App] C5 --> D14[Offline capability] C5 --> D15[GPS navigation]

Let's explore a few of these extensions in more detail:

1. Backend Integration

// Example of how to integrate with a backend API
              async function fetchActivitiesFromAPI(lat, lng, preferences) {
                  try {
                      const response = await fetch(`https://api.localadventures.com/activities?lat=${lat}&lng=${lng}`, {
                          method: 'POST',
                          headers: {
                              'Content-Type': 'application/json'
                          },
                          body: JSON.stringify(preferences)
                      });
                      
                      if (!response.ok) {
                          throw new Error(`API error: ${response.status}`);
                      }
                      
                      return await response.json();
                  } catch (error) {
                      console.error('Error fetching activities:', error);
                      return []; // Fall back to empty list or default activities
                  }
              }

2. Enhanced Visualization with SVG Overlays

// Example of adding SVG overlays to the map visualization
              function createSVGOverlay(container, activities) {
                  // Create SVG element
                  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                  svg.setAttribute("width", "100%");
                  svg.setAttribute("height", "100%");
                  svg.style.position = "absolute";
                  svg.style.top = "0";
                  svg.style.left = "0";
                  svg.style.pointerEvents = "none";
                  
                  // Add activity icons as SVG elements
                  activities.forEach(activity => {
                      // Create icon based on activity type
                      const icon = document.createElementNS("http://www.w3.org/2000/svg", "g");
                      icon.setAttribute("transform", `translate(${activity.x}, ${activity.y})`);
                      
                      // Different paths for different activity types
                      let path;
                      switch (activity.type) {
                          case "hiking":
                              path = "M10,2 L14,6 L12,10 L8,10 L6,6 Z"; // Mountain icon
                              break;
                          case "kayaking":
                              path = "M5,10 C5,5 15,5 15,10 L12,15 L8,15 Z"; // Water icon
                              break;
                          // Add more types...
                          default:
                              path = "M8,3 L13,8 L8,13 L3,8 Z"; // Default diamond
                      }
                      
                      // Create path element
                      const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path");
                      pathElement.setAttribute("d", path);
                      pathElement.setAttribute("fill", activityColors[activity.type] || "#999");
                      pathElement.setAttribute("stroke", "#fff");
                      pathElement.setAttribute("stroke-width", "1");
                      
                      icon.appendChild(pathElement);
                      svg.appendChild(icon);
                      
                      // Add animations
                      const animate = document.createElementNS("http://www.w3.org/2000/svg", "animate");
                      animate.setAttribute("attributeName", "transform");
                      animate.setAttribute("attributeType", "XML");
                      animate.setAttribute("type", "scale");
                      animate.setAttribute("from", "1");
                      animate.setAttribute("to", "1.2");
                      animate.setAttribute("dur", "1s");
                      animate.setAttribute("repeatCount", "indefinite");
                      
                      icon.appendChild(animate);
                  });
                  
                  container.appendChild(svg);
              }

3. Weather Integration

// Example of integrating weather data with activities
              async function fetchWeatherForActivities(activities) {
                  const weatherPromises = activities.map(activity => {
                      const { latitude, longitude } = activity.location;
                      return fetch(`https://api.weather.com/forecast?lat=${latitude}&lon=${longitude}`)
                          .then(response => response.json())
                          .then(weatherData => {
                              return {
                                  activityId: activity.id,
                                  forecast: weatherData.daily.slice(0, 5) // Next 5 days
                              };
                          })
                          .catch(error => {
                              console.error(`Weather fetch error for ${activity.name}:`, error);
                              return {
                                  activityId: activity.id,
                                  forecast: null
                              };
                          });
                  });
                  
                  return Promise.all(weatherPromises);
              }

4. Progressive Web App Enhancements

// service-worker.js for offline capabilities
              const CACHE_NAME = 'localadventures-v1';
              const URLS_TO_CACHE = [
                  '/',
                  '/index.html',
                  '/styles/main.css',
                  '/scripts/app.js',
                  '/images/icons.svg',
                  // Add other assets
              ];
              
              self.addEventListener('install', event => {
                  event.waitUntil(
                      caches.open(CACHE_NAME)
                          .then(cache => {
                              return cache.addAll(URLS_TO_CACHE);
                          })
                  );
              });
              
              self.addEventListener('fetch', event => {
                  event.respondWith(
                      caches.match(event.request)
                          .then(response => {
                              // Return cached response if found
                              if (response) {
                                  return response;
                              }
                              
                              // Clone the request for fetch and cache
                              const fetchRequest = event.request.clone();
                              
                              return fetch(fetchRequest).then(response => {
                                  // Check if response is valid
                                  if (!response || response.status !== 200 || response.type !== 'basic') {
                                      return response;
                                  }
                                  
                                  // Clone the response for cache and return
                                  const responseToCache = response.clone();
                                  
                                  caches.open(CACHE_NAME)
                                      .then(cache => {
                                          cache.put(event.request, responseToCache);
                                      });
                                  
                                  return response;
                              });
                          })
                  );
              });

Learning Outcomes and Reflections

This project has demonstrated several important concepts and practices:

Technical Skills

Problem-Solving Approach

By applying Polya's 4-step problem-solving process, we've created a robust application that not only meets the technical requirements but also provides a valuable user experience. This structured approach to development can be applied to projects of any size and complexity.

Conclusion

The LocalAdventures project successfully demonstrates how HTML5 technologies can be combined to create interactive, location-aware web applications. By applying George Polya's systematic problem-solving approach, we've built a solution that addresses all requirements while maintaining a clear architecture and robust implementation.

Key achievements include:

More importantly, this project illustrates the value of applying a structured problem-solving methodology to web development:

  1. Understanding the problem through requirements analysis and user scenarios
  2. Devising a plan with clear architecture and implementation phases
  3. Executing the plan methodically with continuous testing
  4. Reviewing and extending through evaluation and enhancement

As you tackle your own projects, remember that the most successful solutions come from careful planning, methodical implementation, and continuous refinement. By following this process, you'll be well-equipped to build sophisticated web applications that provide genuine value to users.

Practice Activities

Activity 1: Feature Extension

Choose one of the proposed extensions (backend integration, social features, enhanced visualization, advanced planning, or mobile enhancements) and implement it in the LocalAdventures application. Document your process using Polya's 4-step approach.

Activity 2: Alternative Visualization

Create an alternative visualization for the activities using SVG instead of Canvas. Implement interactive features like hovering over activities to see details or clicking to highlight routes.

Activity 3: Mobile Optimization

Enhance the application for mobile use with touch-friendly interactions, responsive design improvements, and mobile-specific features like "near me" quick searches.

Activity 4: Problem-Solving Practice

Select a different web development challenge (e.g., building a workout tracker, recipe manager, or travel journal) and apply Polya's 4-step approach to design a solution. Focus on the planning phase, creating detailed architecture and component diagrams.

Activity 5: Collaborative Extension

Work with a partner to add collaborative features to the LocalAdventures app, such as shared itineraries or group planning tools. Define the problem together, create a plan, implement your solution, and review it as a team.

Additional Resources