# 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

{% hint style="warning" %}
Always handle permission denials gracefully and provide clear explanations of why your app needs location access.
{% endhint %}

## Usage

### Basic Location Request

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Continuous Location Tracking

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Distance Calculator

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Location-Based Content

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Geofencing

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Fitness Tracker with Route Recording

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

### Store Locator

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

## 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`

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

{% hint style="info" %}
`enableHighAccuracy: true` provides better accuracy but uses more battery and may take longer. Use it only when precision is important.
{% endhint %}

### `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`

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

{% hint style="warning" %}
Continuous tracking consumes significant battery power. Always provide a way to stop tracking when it's no longer needed.
{% endhint %}

### `clearWatch`

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

```twig
{{ 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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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)

```javascript
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

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

### Convert Coordinates to Compass Direction

```javascript
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

```javascript
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

{% code lineNumbers="true" %}

```twig
<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>
```

{% endcode %}

## 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pwa.spomky-labs.com/symfony-ux/geolocation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
