# 🌍 GeolocPlugin - address search/validation and route calculation CAP Plugin

**Geolocation plugin for SAP CAP.**

Add annotations → The plugin allows, address search, address auto correct and route calculation (duration, distance)

## Features

- **Address Search**: Automatic selection popup
- **Address Auto-Correction**: Automatic geocoding and normalization
- **Route Calculation**: Distances and durations in one click
- **Optimized Cache**: Drastic reduction of API calls
- **Multi-transport**: Car, bike, pedestrian (depends on chosen provider)

---

## Installation

```bash
npm install cap-js-geoloc-plugin
```

In your `package.json`:

```json
{
 "cds": {
    "requires": {
      "cap-js-geoloc-plugin": {}
    }
  }
}
```

Create .env file based on .env.example define PROVIDERS and API keys

---

## Usage

### Annotate your fields

```cds
// db/schema.cds
entity Routes {
  key ID : UUID;

  // Automatic address search with @AddressSearch
  @AddressSearch: true
  startingPoint : String(500);
  startingPointCoordinates : String(50);

  @AddressSearch: true
  endingPoint : String(500);
  endingPointCoordinates : String(50);

  // Automatic calculation with @RouteCalculation
  @RouteCalculation: {
    from: 'startingPointCoordinates',
    to: 'endingPointCoordinates',
    mode: 'transportMode',
    distanceField: 'distance',
    durationField: 'duration',
    autoCalculate: true  // Automatic calculation on save when coordinates change
  }
  distance : Decimal(10, 2);
  duration : String;
  transportMode : String default 'car';
}
```

### Declare your service (as usual)

```cds
// srv/routes-service.cds
using { myapp as db } from '../db/schema';

service RoutesService {
  @odata.draft.enabled
  entity Routes as projection on db.Routes;
}
```

### No JavaScript code needed!** The plugin works automatically with annotations only

The plugin automatically detects and creates:

- `@AddressSearch` fields → Search popups
- `@RouteCalculation` fields → Calculate Route button
- All actions (`searchAddress_*`, `calculateRoute`, `selectAddress`) → Auto-generated
- All handlers → Auto-registered
- `@AddressAutoCorrect` automatic correction adress according to country format and can be parsed to additional fields (country, street...)
- `@AddressSearch` and `@AddressAutoCorrect` can be combined

Optional: If you need custom validation, create a service handler:

```javascript
// srv/routes-service.js (OPTIONAL)
module.exports = function() {
  this.before('CREATE', 'Routes', async (req) => {
    // Your custom validation here
  });
};
```

---

## User Workflow

### Address Search

1. **Type** "Paris" in the `startingPoint` field
2. **Click** on "🔍 Search Starting Point"
3. **Choose** an address from the popup
4. **Done**: `startingPoint` and `startingPointCoordinates` are automatically filled

### Route Calculation

1. **Validate** both addresses (starting point + destination)
2. **Click** on "🚗 Calculate Route"
3. **Done**: `distance` and `duration` are automatically calculated

---

## Advanced Features

### Automatic Route Calculation on Save

Enable automatic route calculation when coordinates change by setting `autoCalculate: true`:

```cds
entity Routes {
  @RouteCalculation: {
    from: 'startingPointCoordinates',
    to: 'endingPointCoordinates',
    mode: 'transportMode',
    distanceField: 'distance',
    durationField: 'duration',
    autoCalculate: true  // Enables auto-calculation AND auto-refresh in Fiori UI
  }
  distance : Decimal(10, 2);
  duration : String;
  calculatedAt : DateTime;  // Optional: will auto-refresh if present
  mapImageURL : String;     // Optional: will auto-refresh if present
}
```

**How it works:**

- When you **CREATE** a route with both coordinates filled, distance and duration are automatically calculated
- When you **UPDATE** the `from` or `to` coordinates, the route is automatically recalculated
- **Fiori UI auto-refreshes**: SideEffects are automatically injected - no manual annotations needed!
- If the calculation fails, the save operation continues (non-blocking)
- The "Calculate Route" button is still available for manual recalculation

### Auto-Correction with @AddressAutoCorrect

Automatically geocode and normalize addresses when users type them:

```cds
entity Routes {
  @AddressAutoCorrect: {
    correctedField: 'correctedStartingPoint',
    coordinatesField: 'startingPointCoordinates',
    cityField: 'startingPointCity',
    streetField: 'startingPointStreet'
  }
  startingPoint : String(500);          // User input
  correctedStartingPoint : String(500);  // Auto-filled with formatted address
  startingPointCoordinates : String(50); // Auto-filled with coordinates
  startingPointCity : String(100);       // Auto-filled with city
  startingPointStreet : String(100);     // Auto-filled with street
}
```

**How it works:**

1. User types: "21 rue liberte dijon" (with typos)
2. On CREATE/UPDATE, the plugin automatically:
   - Geocodes the address
   - Fills `correctedStartingPoint`: "21 Rue de la Liberté, 21000 Dijon, France" (formatted by country)
   - Fills `startingPointCoordinates`: "47.3220, 5.0415"
   - Fills `startingPointCity`: "Dijon"
   - Fills `startingPointStreet`: "Rue de la Liberté"

**Country-specific formatting:**

The plugin automatically formats addresses according to each country's conventions:

- **France**: "23 rue des Pigeons, 67000 Strasbourg"
- **Germany**: "Musterstraße 23, 10115 Berlin"
- **UK**: "10 Downing Street, London SW1A 2AA"
- **USA**: "123 Main Street, New York, NY 10001"
- **Spain**: "Calle Mayor 23, 28013 Madrid"
- **Italy**: "Via Roma 123, 00100 Roma"
- And 20+ more countries...

The plugin detects the country from geocoding results (works with country names in multiple languages: English, French, German, Spanish, Italian, Dutch, and native names).

**Manual override for non-existent addresses:**

For new streets or addresses not yet in geocoding databases, you can bypass automatic geocoding:

```cds
entity Routes {
  @AddressAutoCorrect: {
    correctedField: 'correctedStartingPoint',
    coordinatesField: 'startingPointCoordinates',
    manualOverrideField: 'isManualAddress'  // Flag to skip geocoding
  }
  startingPoint : String(500);
  correctedStartingPoint : String(500);
  startingPointCoordinates : String(50);
  isManualAddress : Boolean default false;  // Set to true to skip geocoding
}
```

**How it works:**

- If `isManualAddress = true` → geocoding is skipped, you can enter any address and coordinates manually
- If `isManualAddress = false` → normal geocoding and validation
- Useful for construction sites, new developments, or addresses not yet in mapping databases

**Simple notation** (only corrected field):

```cds
@AddressAutoCorrect: 'correctedStartingPoint'
startingPoint : String(500);
correctedStartingPoint : String(500);
```

---

## Fiori Interface (Optional)

If you want to customize the Fiori UI, add annotations:

```cds
// app/routes/annotations.cds
using { RoutesService } from '../../srv/routes-service';

// Buttons in the header
annotate RoutesService.Routes with @(
  UI.Identification : [
    {
      $Type : 'UI.DataFieldForAction',
      Action : 'RoutesService.calculateRoute',
      Label : '🚗 Calculate Route',
      ![@UI.Hidden] : {$edmJson: {$Path: 'IsActiveEntity'}}
    }
  ]
);

```

---

## Configuration (Optional)

Create a `.env` file to customize:

```bash
# Geocoding (Free Nominatim by default)
GEOLOC_GEOCODING_PROVIDER=nominatim
GEOLOC_GEOCODING_DELAY=1000  # Rate limit: ms between requests (1000 = 1 req/sec)

# Routing (Free OSRM by default)
GEOLOC_ROUTING_PROVIDER=osrm
GEOLOC_ROUTING_PROFILE=car  # car | bike | foot

# Cache
GEOLOC_CACHE_ENABLED=true
GEOLOC_CACHE_TTL=2592000  # 30 days

.env.example:
# =============================================================================
# GeolocPlugin Configuration
# =============================================================================

# -----------------------------------------------------------------------------
# ROUTING - Real distance calculation by route
# -----------------------------------------------------------------------------

# Routing provider: 'osrm' | 'openrouteservice' | 'locationiq' | 'googlemaps' | 'mapbox' | 'here'
# - osrm: FREE, unlimited, no API key required (recommended)
# - openrouteservice: Free up to 2,500 req/day (API key required)
# - locationiq: Free up to 5,000 req/day (API key required)
# - googlemaps: Pay-as-you-go (API key required, $5 per 1000 requests)
# - mapbox: Free up to 100,000 req/month (API key required)
# - here: Free up to 250,000 req/month (API key required)
GEOLOC_ROUTING_PROVIDER=osrm

# Default transport profile: 'car' | 'bike' | 'foot'
GEOLOC_ROUTING_PROFILE=car

# Fallback to OSRM on error: 'true' | 'false'
GEOLOC_ROUTING_FALLBACK=true

# -----------------------------------------------------------------------------
# API KEYS (optional - only if you're not using OSRM)
# -----------------------------------------------------------------------------

# OpenRouteService API Key (free: https://openrouteservice.org/dev/#/signup)
OPENROUTESERVICE_API_KEY=

# LocationIQ API Key (free: https://locationiq.com/register)
LOCATIONIQ_API_KEY=

# Google Maps API Key (pay-as-you-go: https://console.cloud.google.com/google/maps-apis)
# Enable "Directions API" in your Google Cloud Console
GOOGLE_MAPS_API_KEY=

# Mapbox API Key (free: https://account.mapbox.com/auth/signup/)
# Free tier: 100,000 requests/month
MAPBOX_API_KEY=

# HERE API Key (free: https://developer.here.com/sign-up)
# Free tier: 250,000 requests/month
HERE_API_KEY=

# -----------------------------------------------------------------------------
# GEOCODING - Address validation and normalization
# -----------------------------------------------------------------------------

# Geocoding provider: 'nominatim' | 'openrouteservice' | 'locationiq' | 'googlemaps' | 'here' | 'mapbox'
# - nominatim: FREE, limited to 1 req/sec (no API key required, recommended for development)
# - openrouteservice: Uses your existing quota (API key required, shared with routing)
#   does not allow full address search in free mode
# - locationiq: Uses your existing quota (API key required, shared with routing)
# - googlemaps: Pay-as-you-go, excellent quality (API key required, enable "Geocoding API")
# - here: Free up to 250,000 req/month, excellent quality (API key required)
# - mapbox: Free up to 100,000 req/month, excellent quality (API key required)
# Note: API keys for googlemaps, here, and mapbox are shared with routing if configured
GEOLOC_GEOCODING_PROVIDER=nominatim

# Rate limiting: delay in milliseconds between geocoding requests (applies to ALL providers)
# Default: 1000ms (1 second) - respects Nominatim usage policy
# Set to 0 to disable rate limiting (useful for paid APIs with higher limits)
# Examples: 1000 = 1 req/sec | 500 = 2 req/sec | 200 = 5 req/sec | 0 = no limit
GEOLOC_GEOCODING_DELAY=1000

# -----------------------------------------------------------------------------
# CACHE - API call optimization
# -----------------------------------------------------------------------------

# Enable cache: 'true' | 'false'
GEOLOC_CACHE_ENABLED=true

# Cache time-to-live in seconds (30 days = 2592000)
GEOLOC_CACHE_TTL=2592000

# Maximum number of cache entries
GEOLOC_CACHE_MAX_SIZE=1000

# =============================================================================
# CONFIGURATION EXAMPLES
# =============================================================================

# -----------------------------------------------------------------------------
# Example 1: FREE configuration (OSRM + Nominatim)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=osrm
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_GEOCODING_PROVIDER=nominatim
# GEOLOC_CACHE_ENABLED=true
#
# → 100% free, unlimited (OSRM)
# → No API key required

# -----------------------------------------------------------------------------
# Example 2: Production with OpenRouteService (2,500 req/day)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=openrouteservice
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_ROUTING_FALLBACK=true
# GEOLOC_GEOCODING_PROVIDER=openrouteservice
# OPENROUTESERVICE_API_KEY=your-api-key-here
# GEOLOC_CACHE_ENABLED=true
#
# → Routing AND Geocoding via OpenRouteService (shared quota)
# → Fallback to OSRM if routing quota exceeded

# -----------------------------------------------------------------------------
# Example 3: Production with LocationIQ (5,000 req/day)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=locationiq
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_ROUTING_FALLBACK=true
# GEOLOC_GEOCODING_PROVIDER=locationiq
# LOCATIONIQ_API_KEY=your-api-key-here
# GEOLOC_CACHE_ENABLED=true
#
# → Routing AND Geocoding via LocationIQ (shared quota)
# → Fallback to OSRM if routing quota exceeded

# -----------------------------------------------------------------------------
# Example 4: Bike application
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=osrm
# GEOLOC_ROUTING_PROFILE=bike
# GEOLOC_CACHE_ENABLED=true
#
# → Uses bike paths for routing

# -----------------------------------------------------------------------------
# Example 5: Mix OSRM (routing) + LocationIQ (geocoding)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=osrm
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_GEOCODING_PROVIDER=locationiq
# LOCATIONIQ_API_KEY=your-api-key-here
# GEOLOC_CACHE_ENABLED=true
#
# → Free unlimited routing (OSRM)
# → Geocoding via LocationIQ (5,000 req/day)
# → Optimal: LocationIQ quota saved for geocoding only

# -----------------------------------------------------------------------------
# Example 6: Disable cache (development/debug)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=osrm
# GEOLOC_GEOCODING_PROVIDER=nominatim
# GEOLOC_CACHE_ENABLED=false
#
# → All requests call the API (useful for testing)

# -----------------------------------------------------------------------------
# Example 7: Production with Google Maps Directions API
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=googlemaps
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_ROUTING_FALLBACK=true
# GOOGLE_MAPS_API_KEY=your-api-key-here
# GEOLOC_GEOCODING_PROVIDER=nominatim
# GEOLOC_CACHE_ENABLED=true
#
# → High accuracy routing via Google Maps (pay-as-you-go)
# → Fallback to OSRM if quota exceeded
# → Free geocoding via Nominatim

# -----------------------------------------------------------------------------
# Example 8: Production with Mapbox Directions API (100k req/month free)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=mapbox
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_ROUTING_FALLBACK=true
# MAPBOX_API_KEY=your-api-key-here
# GEOLOC_GEOCODING_PROVIDER=nominatim
# GEOLOC_CACHE_ENABLED=true
#
# → Routing via Mapbox (100,000 free requests/month)
# → Fallback to OSRM if quota exceeded
# → Free geocoding via Nominatim

# -----------------------------------------------------------------------------
# Example 9: Production with HERE Maps Routing API (250k req/month free)
# -----------------------------------------------------------------------------
# GEOLOC_ROUTING_PROVIDER=here
# GEOLOC_ROUTING_PROFILE=car
# GEOLOC_ROUTING_FALLBACK=true
# HERE_API_KEY=your-api-key-here
# GEOLOC_GEOCODING_PROVIDER=nominatim
# GEOLOC_CACHE_ENABLED=true
#
# → Routing via HERE Maps (250,000 free requests/month)
# → Fallback to OSRM if quota exceeded
# → Free geocoding via Nominatim
# → Best free tier option (highest quota)




```

### Available Providers

#### Routing Providers

| Provider | Free | Limit | API Key | Use Case |
|----------|------|-------|---------|----------|
| **OSRM** | Yes | Unlimited* | No | Development |
| **OpenRouteService** | 2500/day | 2500 req/day | Yes | Production |
| **LocationIQ** | 5000/day | 5000 req/day | Yes | Production |
| **Google Maps** | Pay-as-you-go | $5/1000 req | Yes | Enterprise |
| **HERE** | 250000/month | 250k req/month | Yes | Production |
| **Mapbox** | 100000/month | 100k req/month | Yes | Production |

#### Geocoding Providers (Address Search)

| Provider | Free | Limit | API Key | Quality |
|----------|------|-------|---------|---------|
| **Nominatim** | Yes | 1 req/sec* | No | Good (OSM data) |
| **LocationIQ** | 5000/day | 5,000 req/day | Yes | Good (OSM data) |
| **OpenRouteService** | 2500/day | 2,500 req/day | Yes | Limited features |
| **Google Maps** | Pay-as-you-go | $5/1,000 req | Yes | Excellent |
| **HERE** | 250000/month | 250k req/month | Yes | Excellent |
| **Mapbox** | 100,000/month | 100k req/month | Yes | Excellent |

*Fair use policy applies (no SLA)

---

## Programmatic API (Optional)

If you want to use the plugin directly in your code:

### Geocoding

```javascript
const GeolocPlugin = require('geoloc-plugin');

// Validate an address
const result = await GeolocPlugin.geocoding.validateAddress('Paris, France');
// → { formatted: "Paris, Île-de-France, France", coordinates: "48.8566,2.3522", ... }

// Search for multiple suggestions
const results = await GeolocPlugin.geocoding.searchAddress('Lyon', 10);
// → { found: true, count: 10, results: [...] }
```

### Routing

```javascript
// Calculate a route
const route = await GeolocPlugin.routing.calculateRoute(
  '48.8566,2.3522',  // Paris
  '45.7640,4.8357',  // Lyon
  'car'
);
// → { distanceKm: 465.87, durationMinutes: 297, formattedDuration: "4h 57min" }
```

---

## 🎓 Complete Documentation

- [README.md](README.md) - This file
- [EXAMPLES.md](EXAMPLES.md) - Detailed examples
- [CHANGELOG.md](CHANGELOG.md) - Version history

---

## Development

---

## Contributing

Contributions are welcome! Create an issue or pull request.

---
