# PostGIS Integration - Dual-Mode Setup

## Overview

ThrillWiki Django backend uses a **conditional PostGIS setup** that allows geographic data to work in both local development (SQLite) and production (PostgreSQL with PostGIS).

## How It Works

### Database Backends

- **Local Development**: Uses regular SQLite without GIS extensions
  - Geographic coordinates stored in `latitude` and `longitude` DecimalFields
  - No spatial query capabilities
  - Simpler setup, easier for local development

- **Production**: Uses PostgreSQL with PostGIS extension
  - Geographic coordinates stored in `location_point` PointField (PostGIS)
  - Full spatial query capabilities (distance calculations, geographic searches, etc.)
  - Automatically syncs with legacy `latitude`/`longitude` fields

### Model Implementation

The `Park` model uses conditional field definition:

```python
# Conditionally import GIS models only if using PostGIS backend
_using_postgis = (
    'postgis' in settings.DATABASES['default']['ENGINE']
)

if _using_postgis:
    from django.contrib.gis.db import models as gis_models
    from django.contrib.gis.geos import Point
```

**Fields in SQLite mode:**
- `latitude` (DecimalField) - Primary coordinate storage
- `longitude` (DecimalField) - Primary coordinate storage

**Fields in PostGIS mode:**
- `location_point` (PointField) - Primary coordinate storage with GIS capabilities
- `latitude` (DecimalField) - Deprecated, kept for backward compatibility
- `longitude` (DecimalField) - Deprecated, kept for backward compatibility

### Helper Methods

The Park model provides methods that work in both modes:

#### `set_location(longitude, latitude)`
Sets park location from coordinates. Works in both modes:
- SQLite: Updates latitude/longitude fields
- PostGIS: Updates location_point and syncs to latitude/longitude

```python
park.set_location(-118.2437, 34.0522)
```

#### `coordinates` property
Returns coordinates as `(longitude, latitude)` tuple:
- SQLite: Returns from latitude/longitude fields
- PostGIS: Returns from location_point (falls back to lat/lng if not set)

```python
coords = park.coordinates  # (-118.2437, 34.0522)
```

#### `latitude_value` property
Returns latitude value:
- SQLite: Returns from latitude field
- PostGIS: Returns from location_point.y

#### `longitude_value` property
Returns longitude value:
- SQLite: Returns from longitude field
- PostGIS: Returns from location_point.x

## Setup Instructions

### Local Development (SQLite)

1. **No special setup required!** Just use the standard SQLite database:
   ```python
   # django/config/settings/local.py
   DATABASES = {
       'default': {
           'ENGINE': 'django.db.backends.sqlite3',
           'NAME': BASE_DIR / 'db.sqlite3',
       }
   }
   ```

2. Run migrations as normal:
   ```bash
   python manage.py migrate
   ```

3. Use latitude/longitude fields for coordinates:
   ```python
   park = Park.objects.create(
       name="Test Park",
       latitude=40.7128,
       longitude=-74.0060
   )
   ```

### Production (PostgreSQL with PostGIS)

1. **Install PostGIS extension in PostgreSQL:**
   ```sql
   CREATE EXTENSION postgis;
   ```

2. **Configure production settings:**
   ```python
   # django/config/settings/production.py
   DATABASES = {
       'default': {
           'ENGINE': 'django.contrib.gis.db.backends.postgis',
           'NAME': 'thrillwiki',
           'USER': 'your_user',
           'PASSWORD': 'your_password',
           'HOST': 'your_host',
           'PORT': '5432',
       }
   }
   ```

3. **Run migrations:**
   ```bash
   python manage.py migrate
   ```
   
   This will create the `location_point` PointField in addition to the latitude/longitude fields.

4. **Use location_point for geographic queries:**
   ```python
   from django.contrib.gis.geos import Point
   from django.contrib.gis.measure import D
   
   # Create park with PostGIS Point
   park = Park.objects.create(
       name="Test Park",
       location_point=Point(-118.2437, 34.0522, srid=4326)
   )
   
   # Geographic queries (only in PostGIS mode)
   nearby_parks = Park.objects.filter(
       location_point__distance_lte=(
           Point(-118.2500, 34.0500, srid=4326),
           D(km=10)
       )
   )
   ```

## Migration Strategy

### From SQLite to PostgreSQL

When migrating from local development (SQLite) to production (PostgreSQL):

1. Export your data from SQLite
2. Set up PostgreSQL with PostGIS
3. Run migrations (will create location_point field)
4. Import your data (latitude/longitude fields will be populated)
5. Run a data migration to populate location_point from lat/lng:

```python
# Example data migration
from django.contrib.gis.geos import Point

for park in Park.objects.filter(latitude__isnull=False, longitude__isnull=False):
    if not park.location_point:
        park.location_point = Point(
            float(park.longitude),
            float(park.latitude),
            srid=4326
        )
        park.save(update_fields=['location_point'])
```

## Benefits

1. **Easy Local Development**: No need to install PostGIS or SpatiaLite for local development
2. **Production Power**: Full GIS capabilities in production with PostGIS
3. **Backward Compatible**: Keeps latitude/longitude fields for compatibility
4. **Unified API**: Helper methods work the same in both modes
5. **Gradual Migration**: Can migrate from SQLite to PostGIS without data loss

## Limitations

### In SQLite Mode (Local Development)

- **No spatial queries**: Cannot use PostGIS query features like:
  - `distance_lte`, `distance_gte` (distance-based searches)
  - `dwithin` (within distance)
  - `contains`, `intersects` (geometric operations)
  - Geographic indexing for performance

- **Workarounds for local development:**
  - Use simple filters on latitude/longitude ranges
  - Implement basic distance calculations in Python if needed
  - Most development work doesn't require spatial queries

### In PostGIS Mode (Production)

- **Use location_point for queries**: Always use the `location_point` field for geographic queries, not lat/lng
- **Sync fields**: If updating location_point directly, remember to sync to lat/lng if needed for compatibility

## Testing

### Test in SQLite (Local)
```bash
cd django
python manage.py shell

# Test basic CRUD
from apps.entities.models import Park
from decimal import Decimal

park = Park.objects.create(
    name="Test Park",
    park_type="theme_park",
    latitude=Decimal("40.7128"),
    longitude=Decimal("-74.0060")
)

print(park.coordinates)  # Should work
print(park.latitude_value)  # Should work
```

### Test in PostGIS (Production)
```bash
cd django
python manage.py shell

# Test GIS features
from apps.entities.models import Park
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D

park = Park.objects.create(
    name="Test Park",
    park_type="theme_park",
    location_point=Point(-118.2437, 34.0522, srid=4326)
)

# Test distance query
nearby = Park.objects.filter(
    location_point__distance_lte=(
        Point(-118.2500, 34.0500, srid=4326),
        D(km=10)
    )
)
```

## Future Considerations

1. **Remove Legacy Fields**: Once fully migrated to PostGIS in production and all code uses location_point, the latitude/longitude fields can be deprecated and eventually removed

2. **Add Spatial Indexes**: In production, add spatial indexes for better query performance:
   ```python
   class Meta:
       indexes = [
           models.Index(fields=['location_point']),  # Spatial index
       ]
   ```

3. **Geographic Search API**: Build geographic search endpoints that work differently based on backend:
   - SQLite: Simple bounding box searches
   - PostGIS: Advanced spatial queries with distance calculations

## Troubleshooting

### "AttributeError: 'DatabaseOperations' object has no attribute 'geo_db_type'"

This error occurs when trying to use PostGIS PointField with regular SQLite. Solution:
- Ensure you're using the local.py settings which uses regular SQLite
- Make sure migrations were created with SQLite active (no location_point field)

### "No such column: location_point"

This occurs when:
- Code tries to access location_point in SQLite mode
- Solution: Use the helper methods (coordinates, latitude_value, longitude_value) instead

### "GDAL library not found"

This occurs when django.contrib.gis is loaded but GDAL is not installed:
- Even with SQLite, GDAL libraries must be available because django.contrib.gis is in INSTALLED_APPS
- Install GDAL via Homebrew: `brew install gdal geos`
- Configure paths in settings if needed

## References

- [Django GIS Documentation](https://docs.djangoproject.com/en/stable/ref/contrib/gis/)
- [PostGIS Documentation](https://postgis.net/documentation/)
- [GeoDjango Tutorial](https://docs.djangoproject.com/en/stable/ref/contrib/gis/tutorial/)
