Geolocation

The Geolocation component provides an interface to the Geolocation API, enabling your Progressive Web App to access the user's geographic location. This allows you to create location-aware applications that can provide personalized content, navigation, and services based on the user's position.

This component is particularly useful for:

  • Location-based services and recommendations

  • Mapping and navigation applications

  • Delivery and transportation tracking

  • Fitness and activity tracking apps

  • Store locators and proximity searches

  • Emergency services and safety features

  • Weather and local information services

  • Social check-ins and location sharing

Browser Support

The Geolocation API is widely supported across all modern browsers on both desktop and mobile devices. However, for security and privacy reasons, browsers require:

  1. HTTPS: Geolocation only works on secure origins (https:// or localhost)

  2. User Permission: Users must explicitly grant permission to access their location

  3. User Gesture: Initial requests should be triggered by user interaction

Usage

Basic Location Request

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Find Your Location</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'locate', 'click') }}>
        Get My Location
    </button>

    <div id="location-display"></div>
</div>

<script>
    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords, timestamp } = event.detail;

        document.getElementById('location-display').innerHTML = `
            <p>Latitude: ${coords.latitude}</p>
            <p>Longitude: ${coords.longitude}</p>
            <p>Accuracy: ${coords.accuracy} meters</p>
        `;
    });

    document.addEventListener('pwa--geolocation:error', (event) => {
        const error = event.detail;
        alert(`Error: ${error.message}`);
    });
</script>

Continuous Location Tracking

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Live Location Tracking</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'watch', 'click', {
        enableHighAccuracy: true,
        maximumAge: 0,
        timeout: 5000
    }) }}>
        Start Tracking
    </button>

    <button {{ stimulus_action('@pwa/geolocation', 'clearWatch', 'click') }}>
        Stop Tracking
    </button>

    <div id="tracking-display">
        <p>Status: <span id="tracking-status">Not tracking</span></p>
        <p>Position: <span id="current-position">-</span></p>
        <p>Updates: <span id="update-count">0</span></p>
    </div>
</div>

<script>
    let updateCount = 0;

    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords } = event.detail;
        updateCount++;

        document.getElementById('tracking-status').textContent = 'Active';
        document.getElementById('current-position').textContent =
            `${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}`;
        document.getElementById('update-count').textContent = updateCount;
    });

    document.addEventListener('pwa--geolocation:watch:cleared', () => {
        document.getElementById('tracking-status').textContent = 'Stopped';
    });
</script>

Distance Calculator

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Distance to Destination</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'locate', 'click', {
        enableHighAccuracy: true
    }) }}>
        Calculate Distance
    </button>

    <div id="distance-result"></div>
</div>

<script>
    // Destination coordinates (example: Eiffel Tower)
    const destination = {
        lat: 48.8584,
        lng: 2.2945,
        name: 'Eiffel Tower'
    };

    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords } = event.detail;
        const distance = calculateDistance(
            coords.latitude,
            coords.longitude,
            destination.lat,
            destination.lng
        );

        document.getElementById('distance-result').innerHTML = `
            <p>Your location: ${coords.latitude.toFixed(4)}, ${coords.longitude.toFixed(4)}</p>
            <p>Distance to ${destination.name}: <strong>${distance.toFixed(2)} km</strong></p>
        `;
    });

    // Haversine formula for calculating distance between two coordinates
    function calculateDistance(lat1, lon1, lat2, lon2) {
        const R = 6371; // Earth's radius in kilometers
        const dLat = toRadians(lat2 - lat1);
        const dLon = toRadians(lon2 - lon1);

        const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                  Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
                  Math.sin(dLon / 2) * Math.sin(dLon / 2);

        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }

    function toRadians(degrees) {
        return degrees * (Math.PI / 180);
    }
</script>

Location-Based Content

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Local Weather</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'locate', 'click') }}>
        Get Local Weather
    </button>

    <div id="weather-display"></div>
</div>

<script>
    document.addEventListener('pwa--geolocation:position', async (event) => {
        const { coords } = event.detail;

        // Show loading state
        document.getElementById('weather-display').innerHTML =
            '<p>Loading weather data...</p>';

        try {
            // Fetch weather based on coordinates
            const response = await fetch(
                `/api/weather?lat=${coords.latitude}&lng=${coords.longitude}`
            );
            const weather = await response.json();

            document.getElementById('weather-display').innerHTML = `
                <h3>${weather.location}</h3>
                <p>Temperature: ${weather.temperature}°C</p>
                <p>Condition: ${weather.condition}</p>
                <p>Humidity: ${weather.humidity}%</p>
            `;
        } catch (error) {
            document.getElementById('weather-display').innerHTML =
                '<p>Failed to load weather data</p>';
        }
    });
</script>

Geofencing

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Location Alerts</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'watch', 'click', {
        enableHighAccuracy: true,
        maximumAge: 0
    }) }}>
        Enable Location Alerts
    </button>

    <button {{ stimulus_action('@pwa/geolocation', 'clearWatch', 'click') }}>
        Disable Alerts
    </button>

    <div id="alert-display"></div>
</div>

<script>
    // Define geofence areas (example: important locations)
    const geofences = [
        {
            name: 'Home',
            lat: 48.8566,
            lng: 2.3522,
            radius: 100 // meters
        },
        {
            name: 'Office',
            lat: 48.8738,
            lng: 2.2950,
            radius: 50
        }
    ];

    let lastNotifiedZone = null;

    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords } = event.detail;

        geofences.forEach(fence => {
            const distance = calculateDistance(
                coords.latitude,
                coords.longitude,
                fence.lat,
                fence.lng
            ) * 1000; // Convert to meters

            if (distance <= fence.radius && lastNotifiedZone !== fence.name) {
                // Entered geofence
                lastNotifiedZone = fence.name;
                showAlert(`You've arrived at ${fence.name}`);
            } else if (distance > fence.radius && lastNotifiedZone === fence.name) {
                // Left geofence
                lastNotifiedZone = null;
                showAlert(`You've left ${fence.name}`);
            }
        });
    });

    function showAlert(message) {
        const alertDiv = document.getElementById('alert-display');
        alertDiv.innerHTML = `<div class="alert">${message}</div>`;
        setTimeout(() => alertDiv.innerHTML = '', 5000);
    }

    function calculateDistance(lat1, lon1, lat2, lon2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLon = (lon2 - lon1) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                  Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
                  Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }
</script>

Fitness Tracker with Route Recording

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <div class="fitness-tracker">
        <h2>Run Tracker</h2>

        <div class="stats">
            <div>Distance: <span id="distance">0.00</span> km</div>
            <div>Duration: <span id="duration">00:00</span></div>
            <div>Speed: <span id="speed">0.0</span> km/h</div>
        </div>

        <button id="start-run" {{ stimulus_action('@pwa/geolocation', 'watch', 'click', {
            enableHighAccuracy: true,
            maximumAge: 0
        }) }}>
            Start Run
        </button>

        <button id="stop-run" {{ stimulus_action('@pwa/geolocation', 'clearWatch', 'click') }} disabled>
            Stop Run
        </button>

        <canvas id="route-map" width="400" height="300"></canvas>
    </div>
</div>

<script>
    let routePoints = [];
    let totalDistance = 0;
    let startTime = null;
    let durationInterval = null;

    document.getElementById('start-run').addEventListener('click', () => {
        routePoints = [];
        totalDistance = 0;
        startTime = Date.now();

        document.getElementById('start-run').disabled = true;
        document.getElementById('stop-run').disabled = false;

        durationInterval = setInterval(updateDuration, 1000);
    });

    document.getElementById('stop-run').addEventListener('click', () => {
        document.getElementById('start-run').disabled = false;
        document.getElementById('stop-run').disabled = true;

        clearInterval(durationInterval);
        saveRoute();
    });

    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords } = event.detail;
        const point = {
            lat: coords.latitude,
            lng: coords.longitude,
            timestamp: Date.now()
        };

        if (routePoints.length > 0) {
            const lastPoint = routePoints[routePoints.length - 1];
            const distance = calculateDistance(
                lastPoint.lat,
                lastPoint.lng,
                point.lat,
                point.lng
            );
            totalDistance += distance;

            // Update stats
            document.getElementById('distance').textContent = totalDistance.toFixed(2);

            // Calculate speed (km/h)
            if (coords.speed !== null) {
                document.getElementById('speed').textContent =
                    (coords.speed * 3.6).toFixed(1);
            }
        }

        routePoints.push(point);
        drawRoute();
    });

    function updateDuration() {
        const elapsed = Math.floor((Date.now() - startTime) / 1000);
        const minutes = Math.floor(elapsed / 60);
        const seconds = elapsed % 60;
        document.getElementById('duration').textContent =
            `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    }

    function drawRoute() {
        const canvas = document.getElementById('route-map');
        const ctx = canvas.getContext('2d');

        if (routePoints.length === 0) return;

        // Clear canvas
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // Find bounds
        const lats = routePoints.map(p => p.lat);
        const lngs = routePoints.map(p => p.lng);
        const minLat = Math.min(...lats);
        const maxLat = Math.max(...lats);
        const minLng = Math.min(...lngs);
        const maxLng = Math.max(...lngs);

        // Draw route
        ctx.strokeStyle = '#3b82f6';
        ctx.lineWidth = 3;
        ctx.beginPath();

        routePoints.forEach((point, i) => {
            const x = ((point.lng - minLng) / (maxLng - minLng)) * (canvas.width - 20) + 10;
            const y = canvas.height - (((point.lat - minLat) / (maxLat - minLat)) * (canvas.height - 20) + 10);

            if (i === 0) {
                ctx.moveTo(x, y);
            } else {
                ctx.lineTo(x, y);
            }
        });

        ctx.stroke();

        // Draw start point
        const start = routePoints[0];
        const startX = ((start.lng - minLng) / (maxLng - minLng)) * (canvas.width - 20) + 10;
        const startY = canvas.height - (((start.lat - minLat) / (maxLat - minLat)) * (canvas.height - 20) + 10);
        ctx.fillStyle = '#10b981';
        ctx.beginPath();
        ctx.arc(startX, startY, 5, 0, 2 * Math.PI);
        ctx.fill();
    }

    function calculateDistance(lat1, lon1, lat2, lon2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLon = (lon2 - lon1) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                  Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
                  Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }

    function saveRoute() {
        const route = {
            points: routePoints,
            distance: totalDistance,
            duration: Date.now() - startTime,
            timestamp: new Date().toISOString()
        };

        // Save to localStorage or send to server
        localStorage.setItem('lastRoute', JSON.stringify(route));
        console.log('Route saved:', route);
    }
</script>

<style>
    .stats {
        display: flex;
        gap: 20px;
        margin: 20px 0;
        font-size: 18px;
    }

    .stats span {
        font-weight: bold;
        color: #3b82f6;
    }

    #route-map {
        border: 2px solid #e5e7eb;
        border-radius: 8px;
        margin-top: 20px;
    }
</style>

Store Locator

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <h2>Find Nearest Store</h2>

    <button {{ stimulus_action('@pwa/geolocation', 'locate', 'click', {
        enableHighAccuracy: true
    }) }}>
        Find Stores Near Me
    </button>

    <div id="stores-list"></div>
</div>

<script>
    // Store locations
    const stores = [
        { name: 'Downtown Store', lat: 48.8566, lng: 2.3522, address: '123 Main St' },
        { name: 'North Branch', lat: 48.8738, lng: 2.2950, address: '456 North Ave' },
        { name: 'East Location', lat: 48.8606, lng: 2.3376, address: '789 East Blvd' },
        { name: 'West Shop', lat: 48.8584, lng: 2.2945, address: '321 West St' }
    ];

    document.addEventListener('pwa--geolocation:position', (event) => {
        const { coords } = event.detail;

        // Calculate distances and sort
        const storesWithDistance = stores.map(store => ({
            ...store,
            distance: calculateDistance(
                coords.latitude,
                coords.longitude,
                store.lat,
                store.lng
            )
        })).sort((a, b) => a.distance - b.distance);

        // Display stores
        const listHtml = storesWithDistance.map((store, index) => `
            <div class="store-item">
                <h3>${index + 1}. ${store.name}</h3>
                <p>${store.address}</p>
                <p><strong>${store.distance.toFixed(2)} km away</strong></p>
                <a href="https://www.google.com/maps/dir/?api=1&origin=${coords.latitude},${coords.longitude}&destination=${store.lat},${store.lng}" target="_blank">
                    Get Directions →
                </a>
            </div>
        `).join('');

        document.getElementById('stores-list').innerHTML = listHtml;
    });

    function calculateDistance(lat1, lon1, lat2, lon2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLon = (lon2 - lon1) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                  Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
                  Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }
</script>

<style>
    .store-item {
        padding: 15px;
        margin: 10px 0;
        border: 1px solid #e5e7eb;
        border-radius: 8px;
    }

    .store-item h3 {
        margin: 0 0 5px 0;
    }

    .store-item p {
        margin: 5px 0;
    }

    .store-item a {
        color: #3b82f6;
        text-decoration: none;
    }
</style>

Parameters

None

Actions

locate

Retrieves the user's current position once. This is ideal for one-time location requests like "find stores near me" or "show local weather".

Options:

  • enableHighAccuracy (boolean, optional): If true, requests the most accurate location available (may use GPS). Default: false

  • timeout (number, optional): Maximum time (in milliseconds) to wait for a position. Default: Infinity

  • maximumAge (number, optional): Maximum age (in milliseconds) of a cached position that is acceptable to return. Default: 0

{{ stimulus_action('@pwa/geolocation', 'locate', 'click', {
    enableHighAccuracy: true,
    timeout: 10000,
    maximumAge: 0
}) }}

enableHighAccuracy: true provides better accuracy but uses more battery and may take longer. Use it only when precision is important.

watch

Starts continuous position tracking. The component will repeatedly update the position as the user moves. This is ideal for navigation, fitness tracking, or real-time location sharing.

Options:

  • enableHighAccuracy (boolean, optional): If true, requests high-accuracy position updates. Default: false

  • timeout (number, optional): Maximum time to wait for each position update. Default: Infinity

  • maximumAge (number, optional): Maximum age of cached positions. Default: 0

{{ stimulus_action('@pwa/geolocation', 'watch', 'click', {
    enableHighAccuracy: true,
    timeout: 5000,
    maximumAge: 0
}) }}

clearWatch

Stops continuous position tracking that was started with the watch action.

{{ stimulus_action('@pwa/geolocation', 'clearWatch', 'click') }}

Targets

None

Events

pwa--geolocation:position

Dispatched when a position update is received (from either locate or watch actions).

Payload: {coords, timestamp}

  • coords.latitude (number): Latitude in decimal degrees

  • coords.longitude (number): Longitude in decimal degrees

  • coords.accuracy (number): Accuracy of the position in meters

  • coords.altitude (number|null): Altitude in meters above sea level (if available)

  • coords.altitudeAccuracy (number|null): Accuracy of altitude in meters (if available)

  • coords.heading (number|null): Direction of travel in degrees (0-360) (if available)

  • coords.speed (number|null): Speed in meters per second (if available)

  • timestamp (number): Timestamp when the position was acquired

Example:

document.addEventListener('pwa--geolocation:position', (event) => {
    const { coords, timestamp } = event.detail;

    console.log('Position:', coords.latitude, coords.longitude);
    console.log('Accuracy:', coords.accuracy, 'meters');

    if (coords.speed !== null) {
        console.log('Speed:', (coords.speed * 3.6).toFixed(1), 'km/h');
    }

    if (coords.heading !== null) {
        console.log('Heading:', coords.heading, 'degrees');
    }
});

pwa--geolocation:error

Dispatched when an error occurs while retrieving the position (permission denied, timeout, position unavailable).

Payload: Error object with code and message

  • code (number): Error code (1: PERMISSION_DENIED, 2: POSITION_UNAVAILABLE, 3: TIMEOUT)

  • message (string): Human-readable error description

Example:

document.addEventListener('pwa--geolocation:error', (event) => {
    const error = event.detail;

    switch (error.code) {
        case 1: // PERMISSION_DENIED
            alert('Please allow location access to use this feature');
            break;
        case 2: // POSITION_UNAVAILABLE
            alert('Location information is unavailable');
            break;
        case 3: // TIMEOUT
            alert('Location request timed out. Please try again');
            break;
    }

    console.error('Geolocation error:', error.message);
});

pwa--geolocation:unsupported

Dispatched when the browser does not support the Geolocation API.

No payload

Example:

document.addEventListener('pwa--geolocation:unsupported', () => {
    alert('Your browser does not support geolocation');
    // Show alternative content or functionality
});

pwa--geolocation:watch:cleared

Dispatched when continuous position tracking is stopped (via clearWatch action).

No payload

Example:

document.addEventListener('pwa--geolocation:watch:cleared', () => {
    console.log('Location tracking stopped');
    // Update UI to reflect tracking stopped
    document.getElementById('tracking-status').textContent = 'Inactive';
});

Best Practices

  1. Request permission contextually: Ask for location access when the user interacts with a location-based feature

  2. Explain why you need it: Clearly communicate why your app needs location access

  3. Handle errors gracefully: Provide fallback options when location access is denied or unavailable

  4. Use appropriate accuracy: Don't request high accuracy unless you really need it

  5. Stop tracking when done: Always clear watches to save battery

  6. Cache positions wisely: Use maximumAge to avoid unnecessary GPS usage

  7. Respect privacy: Don't share or store location data without explicit consent

  8. Provide visual feedback: Show loading states while waiting for position

  9. Set reasonable timeouts: Don't let requests hang indefinitely

  10. Test offline behavior: Ensure your app handles lack of GPS signal gracefully

Coordinate Calculations

Distance Between Two Points (Haversine Formula)

function calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371; // Earth's radius in kilometers
    const dLat = toRadians(lat2 - lat1);
    const dLon = toRadians(lon2 - lon1);

    const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
              Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) *
              Math.sin(dLon / 2) * Math.sin(dLon / 2);

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c; // Distance in kilometers
}

function toRadians(degrees) {
    return degrees * (Math.PI / 180);
}

Check if Point is Within Radius

function isWithinRadius(centerLat, centerLon, pointLat, pointLon, radiusKm) {
    const distance = calculateDistance(centerLat, centerLon, pointLat, pointLon);
    return distance <= radiusKm;
}

Convert Coordinates to Compass Direction

function getCompassDirection(heading) {
    const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
    const index = Math.round(heading / 45) % 8;
    return directions[index];
}

Error Handling

function handleLocationError(error) {
    const errorMessages = {
        1: {
            title: 'Permission Denied',
            message: 'Please enable location access in your browser settings.',
            action: 'Show how to enable location access'
        },
        2: {
            title: 'Position Unavailable',
            message: 'Your location could not be determined. Please check your GPS and internet connection.',
            action: 'Retry'
        },
        3: {
            title: 'Timeout',
            message: 'Location request took too long. Please try again.',
            action: 'Retry'
        }
    };

    const errorInfo = errorMessages[error.code] || {
        title: 'Unknown Error',
        message: 'An error occurred while getting your location.',
        action: 'Try Again'
    };

    showErrorDialog(errorInfo);
}

Complete Example: Real-Time Delivery Tracker

<div {{ stimulus_controller('@pwa/geolocation') }}>
    <div class="delivery-tracker">
        <h1>Delivery Tracking</h1>

        <div class="order-info">
            <h2>Order #12345</h2>
            <p>Status: <span id="delivery-status">Preparing</span></p>
        </div>

        <div class="location-display">
            <h3>Your Location</h3>
            <p id="user-location">Waiting for location...</p>
            <p id="accuracy-info"></p>
        </div>

        <div class="delivery-info">
            <h3>Delivery Details</h3>
            <p>Distance to destination: <span id="distance-to-home">-</span></p>
            <p>Estimated arrival: <span id="eta">-</span></p>
        </div>

        <button id="start-tracking" {{ stimulus_action('@pwa/geolocation', 'watch', 'click', {
            enableHighAccuracy: true,
            maximumAge: 0,
            timeout: 10000
        }) }}>
            Start Tracking
        </button>

        <button id="stop-tracking" {{ stimulus_action('@pwa/geolocation', 'clearWatch', 'click') }} disabled>
            Stop Tracking
        </button>

        <div class="map-container">
            <canvas id="delivery-map" width="600" height="400"></canvas>
        </div>

        <div id="notifications"></div>
    </div>
</div>

<script>
    // Delivery destination (home address)
    const destination = {
        lat: 48.8566,
        lng: 2.3522,
        address: '123 Main Street, Paris'
    };

    let isTracking = false;
    let currentPosition = null;
    let locationHistory = [];
    let lastDistanceToHome = null;

    // UI Elements
    const startBtn = document.getElementById('start-tracking');
    const stopBtn = document.getElementById('stop-tracking');
    const statusEl = document.getElementById('delivery-status');
    const locationEl = document.getElementById('user-location');
    const accuracyEl = document.getElementById('accuracy-info');
    const distanceEl = document.getElementById('distance-to-home');
    const etaEl = document.getElementById('eta');
    const notificationsEl = document.getElementById('notifications');

    startBtn.addEventListener('click', () => {
        isTracking = true;
        startBtn.disabled = true;
        stopBtn.disabled = false;
        statusEl.textContent = 'Out for Delivery';
        showNotification('Tracking started', 'info');
    });

    stopBtn.addEventListener('click', () => {
        isTracking = false;
        startBtn.disabled = false;
        stopBtn.disabled = true;
        statusEl.textContent = 'Tracking Paused';
        showNotification('Tracking stopped', 'info');
    });

    document.addEventListener('pwa--geolocation:position', (event) => {
        if (!isTracking) return;

        const { coords, timestamp } = event.detail;
        currentPosition = coords;

        // Update location display
        locationEl.textContent =
            `${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}`;
        accuracyEl.textContent =
            `Accuracy: ±${Math.round(coords.accuracy)}m`;

        // Calculate distance to destination
        const distanceToHome = calculateDistance(
            coords.latitude,
            coords.longitude,
            destination.lat,
            destination.lng
        );

        distanceEl.textContent = `${distanceToHome.toFixed(2)} km`;

        // Calculate ETA (assuming average speed from GPS)
        if (coords.speed && coords.speed > 0.5) { // Moving faster than 0.5 m/s
            const speedKmh = coords.speed * 3.6;
            const etaMinutes = (distanceToHome / speedKmh) * 60;
            etaEl.textContent = `${Math.round(etaMinutes)} minutes`;
        } else {
            etaEl.textContent = 'Calculating...';
        }

        // Check proximity alerts
        if (distanceToHome < 0.5 && (!lastDistanceToHome || lastDistanceToHome >= 0.5)) {
            showNotification('Almost home! Less than 500m away', 'success');
        }

        if (distanceToHome < 0.1) {
            showNotification('You have arrived!', 'success');
            statusEl.textContent = 'Delivered';
            stopBtn.click();
        }

        lastDistanceToHome = distanceToHome;

        // Add to history
        locationHistory.push({
            lat: coords.latitude,
            lng: coords.longitude,
            timestamp
        });

        // Keep only last 50 points
        if (locationHistory.length > 50) {
            locationHistory.shift();
        }

        // Draw map
        drawMap();
    });

    document.addEventListener('pwa--geolocation:error', (event) => {
        const error = event.detail;

        let message = 'Location error occurred';
        switch (error.code) {
            case 1:
                message = 'Location access denied. Please enable location permissions.';
                break;
            case 2:
                message = 'Location unavailable. Check your GPS and internet connection.';
                break;
            case 3:
                message = 'Location request timed out. Retrying...';
                break;
        }

        showNotification(message, 'error');
    });

    function drawMap() {
        const canvas = document.getElementById('delivery-map');
        const ctx = canvas.getContext('2d');

        // Clear canvas
        ctx.fillStyle = '#f3f4f6';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        if (!currentPosition) return;

        // Calculate bounds
        const allPoints = [
            ...locationHistory.map(p => ({ lat: p.lat, lng: p.lng })),
            { lat: destination.lat, lng: destination.lng },
            { lat: currentPosition.latitude, lng: currentPosition.longitude }
        ];

        const lats = allPoints.map(p => p.lat);
        const lngs = allPoints.map(p => p.lng);
        const minLat = Math.min(...lats) - 0.001;
        const maxLat = Math.max(...lats) + 0.001;
        const minLng = Math.min(...lngs) - 0.001;
        const maxLng = Math.max(...lngs) + 0.001;

        const latRange = maxLat - minLat;
        const lngRange = maxLng - minLng;

        function projectPoint(lat, lng) {
            const x = ((lng - minLng) / lngRange) * (canvas.width - 40) + 20;
            const y = canvas.height - (((lat - minLat) / latRange) * (canvas.height - 40) + 20);
            return { x, y };
        }

        // Draw path
        if (locationHistory.length > 1) {
            ctx.strokeStyle = '#3b82f6';
            ctx.lineWidth = 3;
            ctx.beginPath();

            locationHistory.forEach((point, i) => {
                const { x, y } = projectPoint(point.lat, point.lng);
                if (i === 0) {
                    ctx.moveTo(x, y);
                } else {
                    ctx.lineTo(x, y);
                }
            });

            ctx.stroke();
        }

        // Draw destination
        const destPos = projectPoint(destination.lat, destination.lng);
        ctx.fillStyle = '#ef4444';
        ctx.beginPath();
        ctx.arc(destPos.x, destPos.y, 8, 0, 2 * Math.PI);
        ctx.fill();
        ctx.fillStyle = '#000';
        ctx.font = '12px sans-serif';
        ctx.fillText('Home', destPos.x + 12, destPos.y + 4);

        // Draw current position
        const currPos = projectPoint(currentPosition.latitude, currentPosition.longitude);
        ctx.fillStyle = '#10b981';
        ctx.beginPath();
        ctx.arc(currPos.x, currPos.y, 10, 0, 2 * Math.PI);
        ctx.fill();

        // Draw accuracy circle
        const metersPerPixel = (latRange * 111000) / (canvas.height - 40);
        const accuracyRadius = currentPosition.accuracy / metersPerPixel;
        ctx.strokeStyle = 'rgba(16, 185, 129, 0.3)';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(currPos.x, currPos.y, accuracyRadius, 0, 2 * Math.PI);
        ctx.stroke();
    }

    function calculateDistance(lat1, lon1, lat2, lon2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * Math.PI / 180;
        const dLon = (lon2 - lon1) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                  Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
                  Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }

    function showNotification(message, type) {
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;
        notification.textContent = message;
        notificationsEl.appendChild(notification);

        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => notification.remove(), 300);
        }, 5000);
    }
</script>

<style>
    .delivery-tracker {
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
    }

    .order-info, .location-display, .delivery-info {
        background: #f9fafb;
        padding: 15px;
        margin: 15px 0;
        border-radius: 8px;
    }

    .order-info h2 {
        margin: 0 0 10px 0;
    }

    #delivery-status {
        font-weight: bold;
        color: #3b82f6;
    }

    button {
        padding: 12px 24px;
        margin: 10px 10px 10px 0;
        border: none;
        border-radius: 6px;
        font-size: 16px;
        cursor: pointer;
        transition: all 0.2s;
    }

    #start-tracking {
        background: #10b981;
        color: white;
    }

    #start-tracking:hover:not(:disabled) {
        background: #059669;
    }

    #stop-tracking {
        background: #ef4444;
        color: white;
    }

    #stop-tracking:hover:not(:disabled) {
        background: #dc2626;
    }

    button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }

    .map-container {
        margin: 20px 0;
        border: 2px solid #e5e7eb;
        border-radius: 8px;
        overflow: hidden;
    }

    #delivery-map {
        display: block;
        width: 100%;
    }

    #notifications {
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 1000;
    }

    .notification {
        padding: 15px 20px;
        margin-bottom: 10px;
        border-radius: 6px;
        color: white;
        font-weight: 500;
        transition: opacity 0.3s;
        min-width: 250px;
    }

    .notification.info {
        background: #3b82f6;
    }

    .notification.success {
        background: #10b981;
    }

    .notification.error {
        background: #ef4444;
    }
</style>

Troubleshooting

Location access denied

Solution: Provide clear instructions on how to enable location permissions in browser settings. Consider showing a help modal with browser-specific instructions.

Low accuracy

Common causes:

  1. Indoor location requests (GPS signal blocked)

  2. enableHighAccuracy set to false

  3. Device limitations

Solutions:

  • Use enableHighAccuracy: true for better precision

  • Request location outdoors when possible

  • Display accuracy information to users

Timeout errors

Solutions:

  • Increase timeout value: timeout: 15000 (15 seconds)

  • Show loading state to users

  • Provide retry option

  • Consider using cached positions with maximumAge

Battery drain

Solutions:

  • Use watch only when necessary

  • Always call clearWatch when done

  • Set reasonable timeout values

  • Use enableHighAccuracy: false when precision isn't critical

  • Consider updating less frequently

HTTPS requirement

Geolocation only works on secure contexts (HTTPS or localhost). If testing on a local network, use:

  • localhost instead of 127.0.0.1

  • Set up HTTPS for local development

  • Use tools like ngrok for testing on mobile devices

Last updated

Was this helpful?