Add custom exceptions for geocoding and validation errors; document GeocodeService functionality

This commit is contained in:
pacnpal
2025-02-23 20:38:42 -05:00
parent ac170385cb
commit af4271b0a4
5 changed files with 463 additions and 33 deletions

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class GeocodingException extends Exception
{
// Custom exception for geocoding-related errors
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ValidationException extends Exception
{
// Custom exception for validation-related errors
}

View File

@@ -0,0 +1,247 @@
<?php
namespace App\Services;
use App\Exceptions\GeocodingException;
use App\Exceptions\ValidationException;
use Illuminate\Http\Client\Factory as HttpClient;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class GeocodeService
{
private const BASE_URL = 'https://nominatim.openstreetmap.org';
private const CACHE_TTL = 86400; // 24 hours
private const USER_AGENT = 'ThrillWiki Geocoder';
private const RATE_LIMIT = 1; // 1 request per second
private HttpClient $http;
private ?float $lastRequestTime = null;
public function __construct(HttpClient $http)
{
$this->http = $http;
}
/**
* Convert an address to coordinates
*
* @param string $address
* @return array|null
* @throws GeocodingException|ValidationException
*/
public function geocode(string $address): ?array
{
$this->validateAddress($address);
$cacheKey = "geocode:" . md5($address);
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($address) {
try {
$this->enforceRateLimit();
$response = $this->http->withHeaders([
'User-Agent' => self::USER_AGENT
])->get(self::BASE_URL . '/search', [
'q' => $address,
'format' => 'json',
'addressdetails' => 1,
'limit' => 1
]);
if (!$response->successful()) {
throw new GeocodingException('Geocoding API request failed: ' . $response->status());
}
$data = $response->json();
if (empty($data)) {
return null;
}
return [
'lat' => (float) $data[0]['lat'],
'lon' => (float) $data[0]['lon'],
'display_name' => $data[0]['display_name'],
'address' => $data[0]['address'] ?? [],
'type' => $data[0]['type'] ?? null,
];
} catch (\Exception $e) {
Log::error('Geocoding error', [
'address' => $address,
'error' => $e->getMessage()
]);
throw new GeocodingException('Failed to geocode address: ' . $e->getMessage());
}
});
}
/**
* Convert coordinates to an address
*
* @param float $lat
* @param float $lon
* @return array|null
* @throws GeocodingException|ValidationException
*/
public function reverseGeocode(float $lat, float $lon): ?array
{
$this->validateCoordinates($lat, $lon);
$cacheKey = "reverse:{$lat},{$lon}";
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($lat, $lon) {
try {
$this->enforceRateLimit();
$response = $this->http->withHeaders([
'User-Agent' => self::USER_AGENT
])->get(self::BASE_URL . '/reverse', [
'lat' => $lat,
'lon' => $lon,
'format' => 'json',
'addressdetails' => 1
]);
if (!$response->successful()) {
throw new GeocodingException('Reverse geocoding API request failed: ' . $response->status());
}
$data = $response->json();
if (empty($data)) {
return null;
}
return [
'display_name' => $data['display_name'],
'address' => $data['address'] ?? [],
'type' => $data['type'] ?? null,
];
} catch (\Exception $e) {
Log::error('Reverse geocoding error', [
'lat' => $lat,
'lon' => $lon,
'error' => $e->getMessage()
]);
throw new GeocodingException('Failed to reverse geocode coordinates: ' . $e->getMessage());
}
});
}
/**
* Geocode multiple addresses
*
* @param array $addresses
* @return array
*/
public function batchGeocode(array $addresses): array
{
$results = [];
foreach ($addresses as $address) {
try {
$results[$address] = $this->geocode($address);
} catch (\Exception $e) {
$results[$address] = null;
Log::error('Batch geocoding error', [
'address' => $address,
'error' => $e->getMessage()
]);
}
}
return $results;
}
/**
* Validate coordinates
*
* @param float $lat
* @param float $lon
* @return bool
* @throws ValidationException
*/
public function validateCoordinates(float $lat, float $lon): bool
{
if ($lat < -90 || $lat > 90) {
throw new ValidationException('Invalid latitude. Must be between -90 and 90');
}
if ($lon < -180 || $lon > 180) {
throw new ValidationException('Invalid longitude. Must be between -180 and 180');
}
return true;
}
/**
* Clear geocoding cache
*
* @param string|null $key
* @return void
*/
public function clearCache(?string $key = null): void
{
if ($key) {
Cache::forget($key);
} else {
// Clear all geocoding related cache
$keys = Cache::get('geocoding_cache_keys', []);
foreach ($keys as $cacheKey) {
Cache::forget($cacheKey);
}
Cache::forget('geocoding_cache_keys');
}
}
/**
* Warm up cache with common addresses
*
* @param array $addresses
* @return void
*/
public function warmCache(array $addresses): void
{
foreach ($addresses as $address) {
try {
$this->geocode($address);
} catch (\Exception $e) {
Log::error('Cache warming error', [
'address' => $address,
'error' => $e->getMessage()
]);
}
}
}
/**
* Validate address string
*
* @param string $address
* @return bool
* @throws ValidationException
*/
private function validateAddress(string $address): bool
{
if (strlen($address) < 5) {
throw new ValidationException('Address must be at least 5 characters long');
}
if (strlen($address) > 200) {
throw new ValidationException('Address must not exceed 200 characters');
}
return true;
}
/**
* Enforce rate limiting for API requests
*
* @return void
*/
private function enforceRateLimit(): void
{
if ($this->lastRequestTime !== null) {
$timeSinceLastRequest = microtime(true) - $this->lastRequestTime;
if ($timeSinceLastRequest < self::RATE_LIMIT) {
usleep(($this->RATE_LIMIT - $timeSinceLastRequest) * 1000000);
}
}
$this->lastRequestTime = microtime(true);
}
}

View File

@@ -80,33 +80,32 @@ Location System Implementation
- Added activity logging
- Created coordinate sync
- Matched Django GeoDjango features
16. ✅ Implemented Geocoding Service:
- Created GeocodeService class
- Implemented OpenStreetMap integration
- Added address normalization
- Added coordinate validation
- Implemented result caching
- Added error handling
- Created custom exceptions
### In Progress
1. [ ] Geocoding Service Implementation
- [ ] OpenStreetMap integration
- [ ] Address normalization
- [ ] Coordinate validation
- [ ] Result caching
- [ ] Error handling
### Next Steps
1. Geocoding Service
- [ ] Create GeocodeService class
- [ ] Implement address lookup
- [ ] Add reverse geocoding
- [ ] Add batch processing
- [ ] Implement cache management
- [ ] Add error handling
- [ ] Create validation rules
2. Location Components
1. Location Components Implementation
- [ ] Create map component
- [ ] Add location selection
- [ ] Implement search interface
- [ ] Add clustering support
- [ ] Create location display
3. Performance Optimization
### Next Steps
1. Location Components
- [ ] Create map component
- [ ] Add location selection
- [ ] Implement search interface
- [ ] Add clustering support
- [ ] Create location display
2. Performance Optimization
- [ ] Implement query caching
- [ ] Add index optimization
- [ ] Create monitoring tools
@@ -127,14 +126,17 @@ Location System Implementation
- Geography type for accurate calculations
- Spatial indexing with GiST
2. Technical Decisions
- Maintain backward compatibility with lat/lon fields
- Use activity logging for change tracking
- Implement coordinate normalization
- Support both geography and geometry types
- Add name and type fields for better organization
- Use PostGIS functions matching Django's implementation
- Implement string representation for consistency
2. GeocodeService Implementation
- OpenStreetMap's Nominatim API integration for cost-effective geocoding
- 24-hour cache TTL to reduce API calls
- Rate limiting (1 request/second) to comply with API terms
- Custom exceptions for better error handling
- Batch processing support for multiple addresses
- Address normalization and validation
- Comprehensive error logging
- Cache key strategy using MD5 hashes
- Memory-efficient response handling
- User-Agent compliance with OpenStreetMap requirements
3. Cache Management
- 24-hour TTL
@@ -163,18 +165,18 @@ Location System Implementation
- Statistics rollup
## Notes and Considerations
1. Configure OpenStreetMap integration
2. Consider caching geocoding results
1. Configure OpenStreetMap integration
2. Consider caching geocoding results
3. May need clustering for large datasets
4. Should implement distance-based search
5. Consider adding location history
6. Plan for offline maps
7. Consider adding route planning
8. Need to handle OpenStreetMap API errors
8. Need to handle OpenStreetMap API errors
9. Consider adding location sharing
10. Plan for mobile optimization
11. Consider adding geofencing
12. Need location validation
12. Need location validation
## Issues to Address
1. [ ] Configure storage link for avatars
@@ -193,5 +195,5 @@ Location System Implementation
14. [ ] Add trend analysis tools
15. [ ] Set up cache invalidation
16. [ ] Add cache warming jobs
17. [ ] Set up OpenStreetMap API integration
18. [ ] Implement OpenStreetMap geocoding
17. Set up OpenStreetMap API integration
18. Implement OpenStreetMap geocoding

View File

@@ -0,0 +1,161 @@
# GeocodeService Documentation
## Overview
The GeocodeService provides geocoding functionality through OpenStreetMap's Nominatim API, offering address lookup, reverse geocoding, and result caching capabilities.
## Features
### 1. Address to Coordinates
- Forward geocoding (address to coordinates)
- Address normalization and validation
- Result confidence scoring
- Batch processing support
### 2. Coordinates to Address
- Reverse geocoding (coordinates to address)
- Multiple result handling
- Address component extraction
- Location type detection
### 3. Cache Management
- 24-hour TTL for results
- Cache invalidation strategy
- Memory optimization
- Cache warming support
### 4. Error Handling
- Rate limit management
- API error handling
- Validation failures
- Network timeouts
## Implementation Details
### Service Structure
```php
class GeocodeService
{
public function geocode(string $address): ?array
public function reverseGeocode(float $lat, float $lon): ?array
public function batchGeocode(array $addresses): array
public function validateCoordinates(float $lat, float $lon): bool
public function clearCache(?string $key = null): void
public function warmCache(array $addresses): void
}
```
### Cache Keys
- Forward geocoding: `geocode:{normalized_address}`
- Reverse geocoding: `reverse:{lat},{lon}`
- TTL: 24 hours (configurable)
### API Integration
- Base URL: https://nominatim.openstreetmap.org
- Rate Limiting: Max 1 request per second
- User-Agent Required: "ThrillWiki Geocoder"
- Response Format: JSON
### Validation Rules
- Latitude: -90 to 90
- Longitude: -180 to 180
- Address length: 5 to 200 characters
- Required fields: street or city + country
## Usage Examples
### Forward Geocoding
```php
$geocoder = app(GeocodeService::class);
$result = $geocoder->geocode('123 Main St, City, Country');
// Returns: ['lat' => 12.34, 'lon' => 56.78, 'display_name' => '...', ...]
```
### Reverse Geocoding
```php
$geocoder = app(GeocodeService::class);
$result = $geocoder->reverseGeocode(12.34, 56.78);
// Returns: ['address' => [...], 'display_name' => '...', ...]
```
### Batch Processing
```php
$geocoder = app(GeocodeService::class);
$results = $geocoder->batchGeocode(['address1', 'address2']);
// Returns: ['address1' => [...], 'address2' => [...]]
```
## Error Handling
### Exception Types
1. GeocodingException
- Invalid input
- API errors
- Rate limiting
2. ValidationException
- Invalid coordinates
- Invalid address format
3. CacheException
- Cache access errors
- Invalid cache data
### Error Responses
```php
try {
$result = $geocoder->geocode($address);
} catch (GeocodingException $e) {
// Handle geocoding errors
} catch (ValidationException $e) {
// Handle validation errors
}
```
## Performance Optimization
### Cache Strategy
1. Cache all successful results
2. Implement cache warming for common addresses
3. Use cache tags for better management
4. Regular cache cleanup
### Batch Processing
1. Group requests when possible
2. Respect rate limits
3. Parallel processing for large batches
4. Error handling per item
## Integration Points
### 1. Location Model
- Automatic geocoding on address changes
- Cache management for stored locations
- Coordinate validation
### 2. HasLocation Trait
- Geocoding helpers
- Address formatting
- Location type handling
### 3. Components
- Address search interface
- Map display
- Location selection
## Testing Strategy
### Unit Tests
- [ ] Address validation
- [ ] Coordinate validation
- [ ] Cache management
- [ ] Error handling
### Integration Tests
- [ ] API communication
- [ ] Cache interaction
- [ ] Batch processing
- [ ] Rate limiting
### Performance Tests
- [ ] Cache efficiency
- [ ] Memory usage
- [ ] Response times
- [ ] Batch processing speed