mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 05:11:10 -05:00
Add custom exceptions for geocoding and validation errors; document GeocodeService functionality
This commit is contained in:
10
app/Exceptions/GeocodingException.php
Normal file
10
app/Exceptions/GeocodingException.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class GeocodingException extends Exception
|
||||||
|
{
|
||||||
|
// Custom exception for geocoding-related errors
|
||||||
|
}
|
||||||
10
app/Exceptions/ValidationException.php
Normal file
10
app/Exceptions/ValidationException.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ValidationException extends Exception
|
||||||
|
{
|
||||||
|
// Custom exception for validation-related errors
|
||||||
|
}
|
||||||
247
app/Services/GeocodeService.php
Normal file
247
app/Services/GeocodeService.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,33 +80,32 @@ Location System Implementation
|
|||||||
- Added activity logging
|
- Added activity logging
|
||||||
- Created coordinate sync
|
- Created coordinate sync
|
||||||
- Matched Django GeoDjango features
|
- 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
|
### In Progress
|
||||||
1. [ ] Geocoding Service Implementation
|
1. Location Components 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
|
|
||||||
- [ ] Create map component
|
- [ ] Create map component
|
||||||
- [ ] Add location selection
|
- [ ] Add location selection
|
||||||
- [ ] Implement search interface
|
- [ ] Implement search interface
|
||||||
- [ ] Add clustering support
|
- [ ] Add clustering support
|
||||||
- [ ] Create location display
|
- [ ] 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
|
- [ ] Implement query caching
|
||||||
- [ ] Add index optimization
|
- [ ] Add index optimization
|
||||||
- [ ] Create monitoring tools
|
- [ ] Create monitoring tools
|
||||||
@@ -127,14 +126,17 @@ Location System Implementation
|
|||||||
- Geography type for accurate calculations
|
- Geography type for accurate calculations
|
||||||
- Spatial indexing with GiST
|
- Spatial indexing with GiST
|
||||||
|
|
||||||
2. Technical Decisions
|
2. GeocodeService Implementation
|
||||||
- Maintain backward compatibility with lat/lon fields
|
- OpenStreetMap's Nominatim API integration for cost-effective geocoding
|
||||||
- Use activity logging for change tracking
|
- 24-hour cache TTL to reduce API calls
|
||||||
- Implement coordinate normalization
|
- Rate limiting (1 request/second) to comply with API terms
|
||||||
- Support both geography and geometry types
|
- Custom exceptions for better error handling
|
||||||
- Add name and type fields for better organization
|
- Batch processing support for multiple addresses
|
||||||
- Use PostGIS functions matching Django's implementation
|
- Address normalization and validation
|
||||||
- Implement string representation for consistency
|
- Comprehensive error logging
|
||||||
|
- Cache key strategy using MD5 hashes
|
||||||
|
- Memory-efficient response handling
|
||||||
|
- User-Agent compliance with OpenStreetMap requirements
|
||||||
|
|
||||||
3. Cache Management
|
3. Cache Management
|
||||||
- 24-hour TTL
|
- 24-hour TTL
|
||||||
@@ -163,18 +165,18 @@ Location System Implementation
|
|||||||
- Statistics rollup
|
- Statistics rollup
|
||||||
|
|
||||||
## Notes and Considerations
|
## Notes and Considerations
|
||||||
1. Configure OpenStreetMap integration
|
1. ✅ Configure OpenStreetMap integration
|
||||||
2. Consider caching geocoding results
|
2. ✅ Consider caching geocoding results
|
||||||
3. May need clustering for large datasets
|
3. May need clustering for large datasets
|
||||||
4. Should implement distance-based search
|
4. Should implement distance-based search
|
||||||
5. Consider adding location history
|
5. Consider adding location history
|
||||||
6. Plan for offline maps
|
6. Plan for offline maps
|
||||||
7. Consider adding route planning
|
7. Consider adding route planning
|
||||||
8. Need to handle OpenStreetMap API errors
|
8. ✅ Need to handle OpenStreetMap API errors
|
||||||
9. Consider adding location sharing
|
9. Consider adding location sharing
|
||||||
10. Plan for mobile optimization
|
10. Plan for mobile optimization
|
||||||
11. Consider adding geofencing
|
11. Consider adding geofencing
|
||||||
12. Need location validation
|
12. ✅ Need location validation
|
||||||
|
|
||||||
## Issues to Address
|
## Issues to Address
|
||||||
1. [ ] Configure storage link for avatars
|
1. [ ] Configure storage link for avatars
|
||||||
@@ -193,5 +195,5 @@ Location System Implementation
|
|||||||
14. [ ] Add trend analysis tools
|
14. [ ] Add trend analysis tools
|
||||||
15. [ ] Set up cache invalidation
|
15. [ ] Set up cache invalidation
|
||||||
16. [ ] Add cache warming jobs
|
16. [ ] Add cache warming jobs
|
||||||
17. [ ] Set up OpenStreetMap API integration
|
17. ✅ Set up OpenStreetMap API integration
|
||||||
18. [ ] Implement OpenStreetMap geocoding
|
18. ✅ Implement OpenStreetMap geocoding
|
||||||
161
memory-bank/services/GeocodeService.md
Normal file
161
memory-bank/services/GeocodeService.md
Normal 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
|
||||||
Reference in New Issue
Block a user