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.
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:
- Enter personal preferences and activity interests via forms
- Use their current location to find nearby opportunities
- Drag and drop activities into a personalized itinerary
- Visualize routes and locations with Canvas and SVG
- Save their plans locally for future sessions
This project is significant for several reasons:
- It combines multiple HTML5 APIs into one cohesive application
- It demonstrates practical uses of advanced web technologies
- It follows a structured problem-solving approach applicable to any development challenge
- It creates a genuinely useful application that can be extended further
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.
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:
- Defining requirements - What features must the application include?
- Identifying constraints - What limitations are we working with?
- Clarifying objectives - What does success look like for this project?
- Research needed information - What do we need to know before starting?
Step 2: Devise a Plan
With a clear understanding of the problem, we can strategize our approach:
- Breaking down the problem into manageable components
- Organizing tasks in a logical sequence
- Selecting appropriate technologies for each component
- Defining interfaces between components
- Creating milestones and checkpoints
Step 3: Execute the Plan
This is where we implement our solution:
- Following the plan methodically
- Coding incrementally - one feature at a time
- Testing as we build to catch issues early
- Adapting our approach when faced with obstacles
- Documenting our progress and decisions
Step 4: Review and Extend
After building the solution, we critically evaluate our work:
- Verifying the solution against original requirements
- Testing thoroughly across different scenarios
- Reflecting on the process - what worked, what didn't
- Considering alternative approaches for future reference
- Extending and enhancing the solution with additional features
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 Input Forms: Collect user preferences and interests using HTML5 form features
- Geolocation Integration: Use the user's position to provide location-relevant suggestions
- Drag and Drop Interface: Allow users to organize activities by dragging them into an itinerary
- Data Visualization: Display locations and routes using Canvas and/or SVG
- Persistent Storage: Save user preferences and plans using localStorage
- Responsive Design: Work across different devices and screen sizes
User Scenarios
Let's envision how users would interact with our application:
Technical Constraints
It's important to identify limitations and constraints:
- Browser Support: All features must work on modern browsers (Chrome, Firefox, Safari, Edge)
- Performance: Application should remain responsive even with larger datasets
- Security: User data should be stored securely, with appropriate permissions
- Offline Capability: Basic functionality should work without an internet connection
- No backend services: For this project, we'll use client-side storage only
Key Questions
Following Polya's recommendation to formulate questions, let's identify things we need to clarify:
- What types of outdoor activities should we include?
- How will we generate or simulate activity data based on location?
- How precise should the location mapping be?
- What level of customization should users have for their profile?
- How can we make the drag and drop interface intuitive?
- What visualizations would be most valuable for planning activities?
Analogies and Inspiration
Polya suggests finding analogies to similar problems. We can draw inspiration from:
- Calendar Applications: For scheduling and organizing events
- Travel Planning Tools: For suggesting activities based on location
- Fitness Apps: For tracking and visualizing outdoor activities
- Task Management Tools: For drag-and-drop prioritization
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:
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:
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:
- 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
- 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
- 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
- Phase 4: Visualization
- Implement Canvas-based map
- Create SVG-based activity visualizations
- Develop interactive itinerary summary
- Integrate visualizations with drag and drop
- 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:
- User Forms: HTML5 forms with built-in validation attributes
- Storage: localStorage API for persistence
- Location: Geolocation API for user position
- Activity Cards: Draggable HTML5 elements with Drag and Drop API
- Map Visualization: Canvas API for dynamic rendering
- Activity Icons: SVG for scalable, interactive graphics
- Layout: CSS Grid and Flexbox for responsive design
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:
- The basic HTML structure and CSS layout
- Local storage functionality for user profiles and itineraries
- Form handling with HTML5 validation
- A modular JavaScript architecture with separation of concerns
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}
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:
- Geolocation services to obtain the user's position
- Distance calculation between the user and activities
- Activity data structure with sample activities
- Filtering mechanisms based on user preferences
- Dynamic activity card rendering
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:
- The ability to drag activity cards to the itinerary planner
- Visual feedback during dragging
- Drop handling that adds activities to the itinerary
- Duplicate prevention to avoid adding the same activity twice
- Removal capabilities for activities in the itinerary
- Integration with the map visualization system
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:
- A Canvas-based map visualization system
- Support for both geolocation-based and abstract visualizations
- Dynamic rendering that updates when activities are added or removed
- Visual elements like connections, activity markers, and a compass
- Responsive canvas sizing that adapts to the container
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:
- Improved error handling for all asynchronous operations
- Loading indicators for a better user experience
- Feature detection for browser compatibility
- A notification system for user feedback
- Enhanced responsiveness for mobile devices
- Visual polish through additional styling
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:
- Modular Architecture - The application is divided into logical components with clear responsibilities
- Progressive Enhancement - Core functionality works even when some features aren't available
- Error Handling - Comprehensive error catching and user feedback
- Visual Feedback - Clear indicators for states like loading, drag operations, and success/error
- Performance Considerations - Efficient Canvas rendering with optimizations like throttling
Areas for Improvement:
- SVG Integration - Could make more use of SVG for interactive elements
- Accessibility - Additional work needed for screen reader support and keyboard navigation
- Testing - Would benefit from automated testing for different scenarios and edge cases
- Data Source - Currently using mock data; would need API integration in production
- Performance Optimization - Further optimizations for larger datasets
Extended Features
Building on our solid foundation, here are potential extensions to enhance the application:
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
- Integration of multiple HTML5 APIs in a single application
- Building interactive interfaces with drag and drop functionality
- Creating dynamic visualizations with Canvas and SVG
- Implementing persistent storage for user data
- Handling asynchronous operations like geolocation
- Creating responsive, accessible user interfaces
Problem-Solving Approach
- Systematically breaking down complex problems into manageable pieces
- Planning before implementation to create a coherent architecture
- Adapting to technical constraints and limitations
- Building iteratively with constant testing and refinement
- Evaluating solutions against requirements
- Identifying opportunities for improvement and extension
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:
- A complete user interface for discovering and planning outdoor activities
- Integration of geolocation for personalized, location-based suggestions
- An intuitive drag-and-drop interface for itinerary planning
- Dynamic visualizations that update based on user actions
- Persistent storage to save user preferences and plans
- Robust error handling and fallbacks for a reliable user experience
More importantly, this project illustrates the value of applying a structured problem-solving methodology to web development:
- Understanding the problem through requirements analysis and user scenarios
- Devising a plan with clear architecture and implementation phases
- Executing the plan methodically with continuous testing
- 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
- George Polya's "How to Solve It" - The original book on his problem-solving methodology
- MDN Web Docs: Geolocation API
- MDN Web Docs: Drag and Drop API
- MDN Web Docs: Canvas API
- MDN Web Docs: Web Storage API
- MDN Web Docs: SVG
- Leaflet.js - A lightweight mapping library for real-world implementations
- Progressive Web Apps - Building apps that work offline and load quickly