chore: remove open-mapping files (should be on feature branch)

This commit is contained in:
Jeff Emmett 2025-12-04 06:45:27 -08:00
parent 48818816c4
commit 7ef0533a8f
7 changed files with 0 additions and 1715 deletions

View File

@ -1,336 +0,0 @@
# Open Mapping Project
## Overview
**Open Mapping** is a collaborative route planning module for canvas-website that provides advanced mapping functionality beyond traditional tools like Google Maps. Built on open-source foundations (OpenStreetMap, OSRM, Valhalla, MapLibre), it integrates seamlessly with the tldraw canvas environment.
## Vision
Create a "living map" that exists as a layer within the collaborative canvas, enabling teams to:
- Plan multi-destination trips with optimized routing
- Compare alternative routes visually
- Share and collaborate on itineraries in real-time
- Track budgets and schedules alongside geographic planning
- Work offline with cached map data
## Core Features
### 1. Map Canvas Integration
- MapLibre GL JS as the rendering engine
- Seamless embedding within tldraw canvas
- Pan/zoom synchronized with canvas viewport
- Map shapes that can be annotated like any canvas object
### 2. Multi-Path Routing
- Support for multiple routing profiles (car, bike, foot, transit)
- Side-by-side route comparison
- Alternative route suggestions
- Turn-by-turn directions with elevation profiles
### 3. Collaborative Editing
- Real-time waypoint sharing via Y.js/CRDT
- Cursor presence on map (see where collaborators are looking)
- Concurrent route editing without conflicts
- Share links for view-only or edit access
### 4. Layer Management
- Multiple basemap options (OSM, satellite, terrain)
- Custom overlay layers (GeoJSON import)
- Route-specific layers (cycling, hiking trails)
- POI layers with filtering
### 5. Calendar Integration
- Attach time windows to waypoints
- Visualize itinerary timeline
- Sync with external calendars (iCal export)
- Travel time estimation between events
### 6. Budget Tracking
- Cost estimates per route (fuel, tolls)
- Per-waypoint expense tracking
- Trip budget aggregation
- Currency conversion
### 7. Offline Capability
- Tile caching for offline use
- Route pre-computation and storage
- PWA support for mobile
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Canvas Website │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ tldraw Canvas │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Open Mapping Layer │ │ │
│ │ │ ┌─────────────┐ ┌─────────────────────────┐ │ │ │
│ │ │ │ MapLibre GL │ │ Route Visualization │ │ │ │
│ │ │ │ (basemap) │ │ (polylines/markers) │ │ │ │
│ │ │ └─────────────┘ └─────────────────────────┘ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────────────────┐ │ │ │
│ │ │ │ Layers │ │ Collaboration │ │ │ │
│ │ │ │ Panel │ │ Cursors/Presence │ │ │ │
│ │ │ └─────────────┘ └─────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌───────────┐ ┌─────────────────┐
│ Routing API │ │ Y.js │ │ Tile Server │
│ (OSRM/Valhalla)│ │ (collab) │ │ (MapLibre) │
└─────────────────┘ └───────────┘ └─────────────────┘
┌─────────────────┐
│ VROOM │
│ (optimization) │
└─────────────────┘
```
## Technology Stack
| Component | Technology | License | Notes |
|-----------|------------|---------|-------|
| Map Renderer | MapLibre GL JS | BSD-3 | Open-source Mapbox fork |
| Base Maps | OpenStreetMap | ODbL | Free, community-maintained |
| Routing Engine | OSRM / Valhalla | BSD-2 / MIT | Self-hosted, fast |
| Multi-Route | GraphHopper | Apache 2.0 | Custom profiles |
| Optimization | VROOM | BSD | TSP/VRP solver |
| Collaboration | Y.js | MIT | CRDT-based sync |
| State Management | Jotai | MIT | Already in use |
| Tile Caching | Service Worker | - | PWA standard |
## Routing Provider Comparison
| Feature | OSRM | Valhalla | GraphHopper | ORS |
|---------|------|----------|-------------|-----|
| Speed | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Profiles | 3 | 6+ | 10+ | 8+ |
| Alternatives | ✅ | ✅ | ✅ | ✅ |
| Isochrones | ❌ | ✅ | ✅ | ✅ |
| Transit | ❌ | ✅ | ⚠️ | ❌ |
| License | BSD-2 | MIT | Apache | GPL |
| Docker Ready | ✅ | ✅ | ✅ | ✅ |
**Recommendation**: Start with OSRM for simplicity and speed, add Valhalla for transit/isochrones.
## Implementation Phases
### Phase 1: Foundation (MVP)
- [ ] MapLibre GL JS integration with tldraw
- [ ] Basic waypoint placement and rendering
- [ ] Single-route calculation via OSRM
- [ ] Route polyline display
- [ ] Simple UI for profile selection (car/bike/foot)
### Phase 2: Multi-Route & Comparison
- [ ] Alternative routes visualization
- [ ] Route comparison panel (distance, time, cost)
- [ ] Profile-based coloring
- [ ] Elevation profile display
- [ ] Drag-to-reroute functionality
### Phase 3: Collaboration
- [ ] Y.js integration for real-time sync
- [ ] Cursor presence on map
- [ ] Concurrent waypoint editing
- [ ] Share link generation
- [ ] Permission management (view/edit)
### Phase 4: Layers & Customization
- [ ] Layer panel UI
- [ ] Multiple basemap options
- [ ] Overlay layer support (GeoJSON)
- [ ] Custom marker icons
- [ ] Style customization
### Phase 5: Calendar & Budget
- [ ] Time window attachment to waypoints
- [ ] Itinerary timeline view
- [ ] Budget tracking per waypoint
- [ ] Cost estimation for routes
- [ ] iCal export
### Phase 6: Optimization & Offline
- [ ] VROOM integration for TSP/VRP
- [ ] Multi-stop optimization
- [ ] Tile caching via Service Worker
- [ ] Offline route storage
- [ ] PWA manifest
## File Structure
```
src/open-mapping/
├── index.ts # Public exports
├── types/
│ └── index.ts # TypeScript definitions
├── components/
│ ├── index.ts
│ ├── MapCanvas.tsx # Main map component
│ ├── RouteLayer.tsx # Route polyline rendering
│ ├── WaypointMarker.tsx # Interactive markers
│ └── LayerPanel.tsx # Layer management UI
├── hooks/
│ ├── index.ts
│ ├── useMapInstance.ts # MapLibre instance management
│ ├── useRouting.ts # Route calculation
│ ├── useCollaboration.ts # Y.js sync
│ └── useLayers.ts # Layer state
├── services/
│ ├── index.ts
│ ├── RoutingService.ts # Multi-provider routing
│ ├── TileService.ts # Tile management/caching
│ └── OptimizationService.ts # VROOM integration
└── utils/
└── index.ts # Helper functions
```
## Docker Deployment
The open-mapping backend services will be deployed to `/opt/apps/open-mapping/` on Netcup RS 8000.
### Services
1. **OSRM** - Primary routing engine
- Pre-processed OSM data for region (Europe/Germany)
- HTTP API on internal port
2. **Valhalla** (optional) - Extended routing
- Transit integration via GTFS
- Isochrone calculations
3. **Tile Server** - Vector tiles
- OpenMapTiles-based
- Serves tiles for offline caching
4. **VROOM** - Route optimization
- Solves complex multi-stop problems
- REST API
### Docker Compose Preview
```yaml
version: '3.8'
services:
osrm:
image: osrm/osrm-backend:latest
volumes:
- ./data/osrm:/data
command: osrm-routed --algorithm mld /data/region.osrm
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.osrm.rule=Host(`routing.jeffemmett.com`)"
tileserver:
image: maptiler/tileserver-gl:latest
volumes:
- ./data/tiles:/data
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.tiles.rule=Host(`tiles.jeffemmett.com`)"
networks:
traefik-public:
external: true
```
## Data Requirements
### OSM Data
- Download PBF files from Geofabrik
- For Europe: ~30GB (full), ~5GB (Germany only)
- Pre-process with `osrm-extract`, `osrm-partition`, `osrm-customize`
### Vector Tiles
- Generate from OSM data using OpenMapTiles
- Or download pre-built from MapTiler
- Storage: ~50GB for detailed regional tiles
## API Endpoints
### Routing API (`/api/route`)
```typescript
POST /api/route
{
waypoints: [{ lat: number, lng: number }],
profile: 'car' | 'bike' | 'foot',
alternatives: number,
}
Response: Route[]
```
### Optimization API (`/api/optimize`)
```typescript
POST /api/optimize
{
waypoints: Waypoint[],
constraints: OptimizationConstraints,
}
Response: OptimizationResult
```
### Isochrone API (`/api/isochrone`)
```typescript
POST /api/isochrone
{
center: { lat: number, lng: number },
minutes: number[],
profile: string,
}
Response: GeoJSON.FeatureCollection
```
## Dependencies to Add
```json
{
"dependencies": {
"maplibre-gl": "^4.x",
"@maplibre/maplibre-gl-geocoder": "^1.x",
"geojson": "^0.5.x"
}
}
```
## Related Projects & Inspiration
- **Mapus** - Real-time collaborative mapping
- **uMap** - OpenStreetMap-based map maker
- **Organic Maps** - Offline-first navigation
- **Komoot** - Outdoor route planning
- **Rome2Rio** - Multi-modal journey planner
- **Wandrer.earth** - Exploration tracking
## Success Metrics
1. **Route Calculation** < 500ms for typical queries
2. **Collaboration Sync** < 100ms latency
3. **Offline Coverage** Entire planned region cached
4. **Budget Accuracy** ±15% for fuel estimates
5. **User Satisfaction** Preferred over Google Maps for trip planning
## Open Questions
1. Should we integrate transit data (GTFS feeds)?
2. What regions should we pre-process initially?
3. How to handle very long routes (cross-country)?
4. Should routes be persisted separately from canvas?
5. Integration with existing canvas tools (markdown notes on waypoints)?
## References
- [OSRM Documentation](https://project-osrm.org/docs/v5.24.0/api/)
- [Valhalla API](https://valhalla.github.io/valhalla/api/)
- [MapLibre GL JS](https://maplibre.org/maplibre-gl-js-docs/api/)
- [VROOM Project](http://vroom-project.org/)
- [Y.js Documentation](https://docs.yjs.dev/)

View File

@ -1,160 +0,0 @@
# Open Mapping Backend Services
# Deploy to: /opt/apps/open-mapping/ on Netcup RS 8000
#
# Prerequisites:
# 1. Download OSM data: wget https://download.geofabrik.de/europe/germany-latest.osm.pbf
# 2. Pre-process OSRM: docker run -t -v "${PWD}:/data" osrm/osrm-backend osrm-extract -p /opt/car.lua /data/germany-latest.osm.pbf
# Then: osrm-partition, osrm-customize
# 3. Download vector tiles or generate with OpenMapTiles
version: '3.8'
services:
# =========================================================================
# OSRM - Open Source Routing Machine
# Primary routing engine for fast route calculations
# =========================================================================
osrm:
image: osrm/osrm-backend:v5.27.1
container_name: open-mapping-osrm
restart: unless-stopped
volumes:
- ./data/osrm:/data:ro
command: osrm-routed --algorithm mld /data/germany-latest.osrm --max-table-size 10000
ports:
- "5000:5000" # Internal only, accessed via Traefik
networks:
- traefik-public
- open-mapping-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.osrm.rule=Host(`routing.jeffemmett.com`) && PathPrefix(`/osrm`)"
- "traefik.http.routers.osrm.middlewares=osrm-stripprefix"
- "traefik.http.middlewares.osrm-stripprefix.stripprefix.prefixes=/osrm"
- "traefik.http.services.osrm.loadbalancer.server.port=5000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
# =========================================================================
# Valhalla - Extended Routing Engine
# For isochrones, transit, and advanced features
# =========================================================================
valhalla:
image: ghcr.io/gis-ops/docker-valhalla/valhalla:latest
container_name: open-mapping-valhalla
restart: unless-stopped
volumes:
- ./data/valhalla:/custom_files
environment:
- tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf
- use_tiles_ignore_pbf=True
- build_elevation=True
- build_admins=True
- build_time_zones=True
- force_rebuild=False
ports:
- "8002:8002"
networks:
- traefik-public
- open-mapping-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.valhalla.rule=Host(`routing.jeffemmett.com`) && PathPrefix(`/valhalla`)"
- "traefik.http.routers.valhalla.middlewares=valhalla-stripprefix"
- "traefik.http.middlewares.valhalla-stripprefix.stripprefix.prefixes=/valhalla"
- "traefik.http.services.valhalla.loadbalancer.server.port=8002"
deploy:
resources:
limits:
memory: 8G
# =========================================================================
# TileServer GL - Vector Tile Server
# Serves map tiles for MapLibre GL JS
# =========================================================================
tileserver:
image: maptiler/tileserver-gl:v4.6.5
container_name: open-mapping-tiles
restart: unless-stopped
volumes:
- ./data/tiles:/data:ro
ports:
- "8080:8080"
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.tiles.rule=Host(`tiles.jeffemmett.com`)"
- "traefik.http.services.tiles.loadbalancer.server.port=8080"
# CORS headers for cross-origin tile requests
- "traefik.http.middlewares.tiles-cors.headers.accesscontrolallowmethods=GET,OPTIONS"
- "traefik.http.middlewares.tiles-cors.headers.accesscontrolalloworiginlist=*"
- "traefik.http.middlewares.tiles-cors.headers.accesscontrolmaxage=100"
- "traefik.http.routers.tiles.middlewares=tiles-cors"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
# =========================================================================
# VROOM - Vehicle Routing Optimization
# Solves TSP and VRP for multi-stop route optimization
# =========================================================================
vroom:
image: vroomvrp/vroom-docker:v1.14.0
container_name: open-mapping-vroom
restart: unless-stopped
environment:
- VROOM_ROUTER=osrm
- OSRM_URL=http://osrm:5000
ports:
- "3000:3000"
networks:
- traefik-public
- open-mapping-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.vroom.rule=Host(`routing.jeffemmett.com`) && PathPrefix(`/optimize`)"
- "traefik.http.routers.vroom.middlewares=vroom-stripprefix"
- "traefik.http.middlewares.vroom-stripprefix.stripprefix.prefixes=/optimize"
- "traefik.http.services.vroom.loadbalancer.server.port=3000"
depends_on:
- osrm
# =========================================================================
# API Gateway (optional)
# Unified routing API that abstracts backend services
# =========================================================================
# api:
# build: ./api
# container_name: open-mapping-api
# restart: unless-stopped
# environment:
# - OSRM_URL=http://osrm:5000
# - VALHALLA_URL=http://valhalla:8002
# - VROOM_URL=http://vroom:3000
# ports:
# - "4000:4000"
# networks:
# - traefik-public
# - open-mapping-internal
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.mapping-api.rule=Host(`mapping.jeffemmett.com`)"
# - "traefik.http.services.mapping-api.loadbalancer.server.port=4000"
networks:
traefik-public:
external: true
open-mapping-internal:
driver: bridge
# Persistent storage for processed routing data
volumes:
osrm-data:
valhalla-data:
tiles-data:

View File

@ -1,138 +0,0 @@
#!/bin/bash
# Open Mapping Backend Setup Script
# Run on Netcup RS 8000 to prepare routing data
#
# Usage: ./open-mapping.setup.sh [region]
# Example: ./open-mapping.setup.sh germany
# ./open-mapping.setup.sh europe
set -e
REGION=${1:-germany}
DATA_DIR="/opt/apps/open-mapping/data"
echo "=== Open Mapping Setup ==="
echo "Region: $REGION"
echo "Data directory: $DATA_DIR"
echo ""
# Create directories
mkdir -p "$DATA_DIR/osrm"
mkdir -p "$DATA_DIR/valhalla"
mkdir -p "$DATA_DIR/tiles"
cd "$DATA_DIR"
# =========================================================================
# Download OSM Data
# =========================================================================
echo "=== Downloading OSM data ==="
case $REGION in
germany)
OSM_URL="https://download.geofabrik.de/europe/germany-latest.osm.pbf"
OSM_FILE="germany-latest.osm.pbf"
;;
europe)
OSM_URL="https://download.geofabrik.de/europe-latest.osm.pbf"
OSM_FILE="europe-latest.osm.pbf"
;;
*)
echo "Unknown region: $REGION"
echo "Supported: germany, europe"
exit 1
;;
esac
if [ ! -f "osrm/$OSM_FILE" ]; then
echo "Downloading $OSM_URL..."
wget -O "osrm/$OSM_FILE" "$OSM_URL"
else
echo "OSM file already exists, skipping download"
fi
# =========================================================================
# Process OSRM Data
# =========================================================================
echo "=== Processing OSRM routing data ==="
echo "This may take several hours for large regions..."
cd osrm
# Extract
if [ ! -f "${OSM_FILE%.osm.pbf}.osrm" ]; then
echo "Running osrm-extract..."
docker run -t -v "${PWD}:/data" osrm/osrm-backend:v5.27.1 \
osrm-extract -p /opt/car.lua /data/$OSM_FILE
else
echo "OSRM extract already done, skipping"
fi
# Partition (for MLD algorithm)
if [ ! -f "${OSM_FILE%.osm.pbf}.osrm.partition" ]; then
echo "Running osrm-partition..."
docker run -t -v "${PWD}:/data" osrm/osrm-backend:v5.27.1 \
osrm-partition /data/${OSM_FILE%.osm.pbf}.osrm
else
echo "OSRM partition already done, skipping"
fi
# Customize
if [ ! -f "${OSM_FILE%.osm.pbf}.osrm.mldgr" ]; then
echo "Running osrm-customize..."
docker run -t -v "${PWD}:/data" osrm/osrm-backend:v5.27.1 \
osrm-customize /data/${OSM_FILE%.osm.pbf}.osrm
else
echo "OSRM customize already done, skipping"
fi
cd ..
# =========================================================================
# Download Vector Tiles (optional, can use Valhalla built-in)
# =========================================================================
echo "=== Setting up vector tiles ==="
# Option 1: Use OpenMapTiles pre-built (requires license for commercial)
# Option 2: Generate from OSM data (time consuming)
# Option 3: Use free tile providers with attribution
# For now, create a config to use external tile providers
cat > tiles/config.json << 'EOF'
{
"options": {
"paths": {
"fonts": "fonts",
"sprites": "sprites",
"styles": "styles",
"mbtiles": ""
}
},
"styles": {
"osm-bright": {
"style": "osm-bright/style.json"
}
}
}
EOF
echo "Tile server configured to use styles from ./tiles/"
echo "Download MBTiles from OpenMapTiles or generate from OSM for offline use"
# =========================================================================
# Verify Setup
# =========================================================================
echo ""
echo "=== Setup Complete ==="
echo ""
echo "Directory structure:"
ls -la "$DATA_DIR"
echo ""
echo "OSRM files:"
ls -la "$DATA_DIR/osrm/"
echo ""
echo "Next steps:"
echo "1. Copy docker-compose file to /opt/apps/open-mapping/"
echo "2. Run: docker compose up -d"
echo "3. Test OSRM: curl 'http://localhost:5000/route/v1/driving/13.388860,52.517037;13.397634,52.529407?overview=false'"
echo "4. Add to Cloudflare tunnel if needed"

View File

@ -1,325 +0,0 @@
/**
* OptimizationService - Route and trip optimization
*
* Uses VROOM or similar for:
* - Vehicle Routing Problems (VRP)
* - Traveling Salesman Problem (TSP)
* - Time window constraints
* - Capacity constraints
* - Multi-vehicle optimization
* - Cost tracking and budgeting
*/
import type {
Waypoint,
Route,
Coordinate,
TripItinerary,
TripBudget,
OptimizationServiceConfig,
} from '../types';
export interface OptimizationJob {
id: string;
waypoints: Waypoint[];
constraints: OptimizationConstraints;
result?: OptimizationResult;
status: 'pending' | 'running' | 'completed' | 'failed';
error?: string;
}
export interface OptimizationConstraints {
startLocation?: Coordinate;
endLocation?: Coordinate;
returnToStart?: boolean;
maxDuration?: number; // seconds
maxDistance?: number; // meters
timeWindows?: TimeWindow[];
vehicleCapacity?: number;
priorities?: number[]; // waypoint priorities
}
export interface TimeWindow {
waypointIndex: number;
start: Date;
end: Date;
}
export interface OptimizationResult {
orderedWaypoints: Waypoint[];
totalDistance: number;
totalDuration: number;
estimatedCost: OptimizationCost;
unassigned?: number[]; // indices of waypoints that couldn't be visited
violations?: string[];
}
export interface OptimizationCost {
fuel: number;
time: number; // value of time
total: number;
currency: string;
}
export interface CostParameters {
fuelPricePerLiter: number;
fuelConsumptionPer100km: number; // liters
valueOfTimePerHour: number;
currency: string;
}
const DEFAULT_COST_PARAMS: CostParameters = {
fuelPricePerLiter: 1.5, // EUR
fuelConsumptionPer100km: 8, // liters
valueOfTimePerHour: 20, // EUR
currency: 'EUR',
};
export class OptimizationService {
private config: OptimizationServiceConfig;
private costParams: CostParameters;
constructor(
config: OptimizationServiceConfig,
costParams: CostParameters = DEFAULT_COST_PARAMS
) {
this.config = config;
this.costParams = costParams;
}
/**
* Optimize waypoint order for minimum travel time/distance
*/
async optimizeRoute(
waypoints: Waypoint[],
constraints?: OptimizationConstraints
): Promise<OptimizationResult> {
if (waypoints.length <= 2) {
return {
orderedWaypoints: waypoints,
totalDistance: 0,
totalDuration: 0,
estimatedCost: { fuel: 0, time: 0, total: 0, currency: this.costParams.currency },
};
}
if (this.config.provider === 'vroom') {
return this.optimizeWithVROOM(waypoints, constraints);
}
// Fallback: simple nearest-neighbor heuristic
return this.nearestNeighborOptimization(waypoints, constraints);
}
/**
* Optimize a full trip itinerary with time constraints
*/
async optimizeItinerary(itinerary: TripItinerary): Promise<TripItinerary> {
// Extract all waypoints from all routes
const allWaypoints = itinerary.routes.flatMap((r) => r.waypoints);
// Build time windows from events
const timeWindows: TimeWindow[] = itinerary.events
.filter((e) => e.waypointId)
.map((e) => {
const waypointIndex = allWaypoints.findIndex((w) => w.id === e.waypointId);
return {
waypointIndex,
start: e.startTime,
end: e.endTime,
};
})
.filter((tw) => tw.waypointIndex >= 0);
const result = await this.optimizeRoute(allWaypoints, { timeWindows });
// Rebuild itinerary with optimized order
return {
...itinerary,
// Would need more sophisticated logic to rebuild routes
};
}
/**
* Estimate trip costs
*/
estimateCosts(
distance: number, // meters
duration: number, // seconds
additionalCosts?: Partial<TripBudget>
): OptimizationCost {
const distanceKm = distance / 1000;
const durationHours = duration / 3600;
const fuelLiters = (distanceKm / 100) * this.costParams.fuelConsumptionPer100km;
const fuelCost = fuelLiters * this.costParams.fuelPricePerLiter;
const timeCost = durationHours * this.costParams.valueOfTimePerHour;
return {
fuel: Math.round(fuelCost * 100) / 100,
time: Math.round(timeCost * 100) / 100,
total: Math.round((fuelCost + timeCost) * 100) / 100,
currency: this.costParams.currency,
};
}
/**
* Update cost calculation parameters
*/
setCostParameters(params: Partial<CostParameters>): void {
this.costParams = { ...this.costParams, ...params };
}
// =========================================================================
// Private Methods
// =========================================================================
private async optimizeWithVROOM(
waypoints: Waypoint[],
constraints?: OptimizationConstraints
): Promise<OptimizationResult> {
// Build VROOM request
const jobs = waypoints.map((wp, index) => ({
id: index,
location: [wp.coordinate.lng, wp.coordinate.lat],
service: wp.stayDuration ? wp.stayDuration * 60 : 0, // seconds
priority: constraints?.priorities?.[index] ?? 0,
time_windows: this.getTimeWindowForWaypoint(index, constraints?.timeWindows),
}));
const vehicles = [{
id: 0,
start: constraints?.startLocation
? [constraints.startLocation.lng, constraints.startLocation.lat]
: [waypoints[0].coordinate.lng, waypoints[0].coordinate.lat],
end: constraints?.endLocation
? [constraints.endLocation.lng, constraints.endLocation.lat]
: constraints?.returnToStart
? [waypoints[0].coordinate.lng, waypoints[0].coordinate.lat]
: undefined,
capacity: constraints?.vehicleCapacity ? [constraints.vehicleCapacity] : undefined,
}];
const body = { jobs, vehicles };
try {
const response = await fetch(this.config.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await response.json();
if (data.code !== 0) {
throw new Error(`VROOM error: ${data.error}`);
}
return this.parseVROOMResponse(data, waypoints);
} catch (error) {
console.error('VROOM optimization failed:', error);
return this.nearestNeighborOptimization(waypoints, constraints);
}
}
private parseVROOMResponse(
data: any,
originalWaypoints: Waypoint[]
): OptimizationResult {
const route = data.routes[0];
const orderedIndices = route.steps
.filter((s: any) => s.type === 'job')
.map((s: any) => s.job);
const orderedWaypoints = orderedIndices.map((i: number) => originalWaypoints[i]);
return {
orderedWaypoints,
totalDistance: data.summary.distance,
totalDuration: data.summary.duration,
estimatedCost: this.estimateCosts(data.summary.distance, data.summary.duration),
unassigned: data.unassigned?.map((u: any) => u.id),
};
}
private nearestNeighborOptimization(
waypoints: Waypoint[],
constraints?: OptimizationConstraints
): OptimizationResult {
const remaining = [...waypoints];
const ordered: Waypoint[] = [];
// Start from first waypoint or specified start
let current = remaining.shift()!;
ordered.push(current);
while (remaining.length > 0) {
// Find nearest unvisited waypoint
let nearestIndex = 0;
let nearestDist = Infinity;
for (let i = 0; i < remaining.length; i++) {
const dist = this.haversineDistance(
current.coordinate,
remaining[i].coordinate
);
if (dist < nearestDist) {
nearestDist = dist;
nearestIndex = i;
}
}
current = remaining.splice(nearestIndex, 1)[0];
ordered.push(current);
}
// Estimate total distance/duration
let totalDistance = 0;
for (let i = 0; i < ordered.length - 1; i++) {
totalDistance += this.haversineDistance(
ordered[i].coordinate,
ordered[i + 1].coordinate
);
}
// Rough duration estimate: 50 km/h average
const totalDuration = (totalDistance / 50000) * 3600;
return {
orderedWaypoints: ordered,
totalDistance,
totalDuration,
estimatedCost: this.estimateCosts(totalDistance, totalDuration),
};
}
private getTimeWindowForWaypoint(
index: number,
timeWindows?: TimeWindow[]
): number[][] | undefined {
const tw = timeWindows?.find((t) => t.waypointIndex === index);
if (!tw) return undefined;
return [[
Math.floor(tw.start.getTime() / 1000),
Math.floor(tw.end.getTime() / 1000),
]];
}
private haversineDistance(coord1: Coordinate, coord2: Coordinate): number {
const R = 6371000; // Earth's radius in meters
const lat1 = (coord1.lat * Math.PI) / 180;
const lat2 = (coord2.lat * Math.PI) / 180;
const deltaLat = ((coord2.lat - coord1.lat) * Math.PI) / 180;
const deltaLng = ((coord2.lng - coord1.lng) * Math.PI) / 180;
const a =
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}
export default OptimizationService;

View File

@ -1,517 +0,0 @@
/**
* RoutingService - Abstraction layer for routing providers
*
* Supports multiple backends:
* - OSRM (Open Source Routing Machine)
* - Valhalla
* - GraphHopper
* - OpenRouteService
*
* All providers expose a unified API for route calculation.
*/
import type {
Waypoint,
Route,
RoutingOptions,
RoutingServiceConfig,
RouteSummary,
RouteLeg,
Coordinate,
RoutingProfile,
} from '../types';
export class RoutingService {
private config: RoutingServiceConfig;
constructor(config: RoutingServiceConfig) {
this.config = config;
}
/**
* Calculate a route between waypoints
*/
async calculateRoute(
waypoints: Waypoint[],
options?: Partial<RoutingOptions>
): Promise<Route> {
const profile = options?.profile ?? 'car';
const coordinates = waypoints.map((w) => w.coordinate);
switch (this.config.provider) {
case 'osrm':
return this.calculateOSRMRoute(coordinates, profile, options);
case 'valhalla':
return this.calculateValhallaRoute(coordinates, profile, options);
case 'graphhopper':
return this.calculateGraphHopperRoute(coordinates, profile, options);
case 'openrouteservice':
return this.calculateORSRoute(coordinates, profile, options);
default:
throw new Error(`Unsupported routing provider: ${this.config.provider}`);
}
}
/**
* Calculate multiple alternative routes
*/
async calculateAlternatives(
waypoints: Waypoint[],
count: number = 3
): Promise<Route[]> {
// TODO: Implement alternatives calculation
// OSRM supports alternatives=true parameter
// Valhalla supports alternates parameter
const mainRoute = await this.calculateRoute(waypoints, { alternatives: count });
return mainRoute.alternatives
? [mainRoute, ...mainRoute.alternatives]
: [mainRoute];
}
/**
* Optimize waypoint ordering (traveling salesman)
*/
async optimizeWaypointOrder(waypoints: Waypoint[]): Promise<Waypoint[]> {
if (waypoints.length <= 2) return waypoints;
// Use OSRM trip endpoint or VROOM for optimization
const coordinates = waypoints.map((w) => `${w.coordinate.lng},${w.coordinate.lat}`).join(';');
const url = `${this.config.baseUrl}/trip/v1/driving/${coordinates}?roundtrip=false&source=first&destination=last`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.code !== 'Ok' || !data.trips?.[0]?.legs) {
return waypoints;
}
// Reorder waypoints based on optimization result
const optimizedIndices = data.waypoints.map((wp: { waypoint_index: number }) => wp.waypoint_index);
return optimizedIndices.map((index: number) => waypoints[index]);
} catch (error) {
console.error('Waypoint optimization failed:', error);
return waypoints;
}
}
/**
* Calculate isochrone (reachable area in given time)
*/
async calculateIsochrone(
center: Coordinate,
minutes: number[]
): Promise<GeoJSON.FeatureCollection> {
// Valhalla and ORS support isochrones natively
if (this.config.provider === 'valhalla') {
return this.calculateValhallaIsochrone(center, minutes);
}
if (this.config.provider === 'openrouteservice') {
return this.calculateORSIsochrone(center, minutes);
}
// For OSRM/GraphHopper, would need to approximate with sampling
console.warn('Isochrone not supported for provider:', this.config.provider);
return { type: 'FeatureCollection', features: [] };
}
// =========================================================================
// Private Provider-Specific Methods
// =========================================================================
private async calculateOSRMRoute(
coordinates: Coordinate[],
profile: RoutingProfile,
options?: Partial<RoutingOptions>
): Promise<Route> {
const coordString = coordinates
.map((c) => `${c.lng},${c.lat}`)
.join(';');
const osrmProfile = this.mapProfileToOSRM(profile);
const url = new URL(`${this.config.baseUrl}/route/v1/${osrmProfile}/${coordString}`);
url.searchParams.set('overview', 'full');
url.searchParams.set('geometries', 'geojson');
url.searchParams.set('steps', 'true');
if (options?.alternatives) {
url.searchParams.set('alternatives', 'true');
}
const response = await fetch(url.toString());
const data = await response.json();
if (data.code !== 'Ok') {
throw new Error(`OSRM error: ${data.message || data.code}`);
}
return this.parseOSRMResponse(data, profile);
}
private async calculateValhallaRoute(
coordinates: Coordinate[],
profile: RoutingProfile,
options?: Partial<RoutingOptions>
): Promise<Route> {
const locations = coordinates.map((c) => ({
lat: c.lat,
lon: c.lng,
}));
const costing = this.mapProfileToValhalla(profile);
const body = {
locations,
costing,
directions_options: {
units: 'kilometers',
},
alternates: options?.alternatives ?? 0,
};
const response = await fetch(`${this.config.baseUrl}/route`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await response.json();
if (data.error) {
throw new Error(`Valhalla error: ${data.error}`);
}
return this.parseValhallaResponse(data, profile);
}
private async calculateGraphHopperRoute(
coordinates: Coordinate[],
profile: RoutingProfile,
options?: Partial<RoutingOptions>
): Promise<Route> {
const url = new URL(`${this.config.baseUrl}/route`);
coordinates.forEach((c) => {
url.searchParams.append('point', `${c.lat},${c.lng}`);
});
url.searchParams.set('profile', this.mapProfileToGraphHopper(profile));
url.searchParams.set('points_encoded', 'false');
url.searchParams.set('instructions', 'true');
if (options?.alternatives) {
url.searchParams.set('algorithm', 'alternative_route');
url.searchParams.set('alternative_route.max_paths', String(options.alternatives));
}
if (this.config.apiKey) {
url.searchParams.set('key', this.config.apiKey);
}
const response = await fetch(url.toString());
const data = await response.json();
if (data.message) {
throw new Error(`GraphHopper error: ${data.message}`);
}
return this.parseGraphHopperResponse(data, profile);
}
private async calculateORSRoute(
coordinates: Coordinate[],
profile: RoutingProfile,
options?: Partial<RoutingOptions>
): Promise<Route> {
const orsProfile = this.mapProfileToORS(profile);
const body = {
coordinates: coordinates.map((c) => [c.lng, c.lat]),
alternative_routes: options?.alternatives
? { target_count: options.alternatives }
: undefined,
};
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (this.config.apiKey) {
headers['Authorization'] = this.config.apiKey;
}
const response = await fetch(
`${this.config.baseUrl}/v2/directions/${orsProfile}/geojson`,
{
method: 'POST',
headers,
body: JSON.stringify(body),
}
);
const data = await response.json();
if (data.error) {
throw new Error(`ORS error: ${data.error.message}`);
}
return this.parseORSResponse(data, profile);
}
private async calculateValhallaIsochrone(
center: Coordinate,
minutes: number[]
): Promise<GeoJSON.FeatureCollection> {
const body = {
locations: [{ lat: center.lat, lon: center.lng }],
costing: 'auto',
contours: minutes.map((m) => ({ time: m })),
polygons: true,
};
const response = await fetch(`${this.config.baseUrl}/isochrone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return response.json();
}
private async calculateORSIsochrone(
center: Coordinate,
minutes: number[]
): Promise<GeoJSON.FeatureCollection> {
const body = {
locations: [[center.lng, center.lat]],
range: minutes.map((m) => m * 60), // ORS uses seconds
range_type: 'time',
};
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (this.config.apiKey) {
headers['Authorization'] = this.config.apiKey;
}
const response = await fetch(`${this.config.baseUrl}/v2/isochrones/driving-car`, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
return response.json();
}
// =========================================================================
// Profile Mapping
// =========================================================================
private mapProfileToOSRM(profile: RoutingProfile): string {
const mapping: Partial<Record<RoutingProfile, string>> = {
car: 'driving',
bicycle: 'cycling',
foot: 'walking',
hiking: 'walking',
};
return mapping[profile] ?? 'driving';
}
private mapProfileToValhalla(profile: RoutingProfile): string {
const mapping: Partial<Record<RoutingProfile, string>> = {
car: 'auto',
truck: 'truck',
motorcycle: 'motorcycle',
bicycle: 'bicycle',
mountain_bike: 'bicycle',
road_bike: 'bicycle',
foot: 'pedestrian',
hiking: 'pedestrian',
transit: 'multimodal',
};
return mapping[profile] ?? 'auto';
}
private mapProfileToGraphHopper(profile: RoutingProfile): string {
const mapping: Partial<Record<RoutingProfile, string>> = {
car: 'car',
truck: 'truck',
motorcycle: 'motorcycle',
bicycle: 'bike',
mountain_bike: 'mtb',
road_bike: 'racingbike',
foot: 'foot',
hiking: 'hike',
};
return mapping[profile] ?? 'car';
}
private mapProfileToORS(profile: RoutingProfile): string {
const mapping: Partial<Record<RoutingProfile, string>> = {
car: 'driving-car',
truck: 'driving-hgv',
bicycle: 'cycling-regular',
mountain_bike: 'cycling-mountain',
road_bike: 'cycling-road',
foot: 'foot-walking',
hiking: 'foot-hiking',
wheelchair: 'wheelchair',
};
return mapping[profile] ?? 'driving-car';
}
// =========================================================================
// Response Parsing
// =========================================================================
private parseOSRMResponse(data: any, profile: RoutingProfile): Route {
const route = data.routes[0];
const id = `route-${Date.now()}`;
return {
id,
waypoints: [], // Populated by caller
geometry: route.geometry,
profile,
summary: {
distance: route.distance,
duration: route.duration,
},
legs: route.legs.map((leg: any, index: number) => ({
startWaypoint: `waypoint-${index}`,
endWaypoint: `waypoint-${index + 1}`,
distance: leg.distance,
duration: leg.duration,
geometry: { type: 'LineString', coordinates: [] }, // Would need to extract from steps
steps: leg.steps?.map((step: any) => ({
instruction: step.maneuver?.instruction ?? '',
distance: step.distance,
duration: step.duration,
geometry: step.geometry,
maneuver: {
type: step.maneuver?.type ?? 'continue',
modifier: step.maneuver?.modifier,
bearingBefore: step.maneuver?.bearing_before ?? 0,
bearingAfter: step.maneuver?.bearing_after ?? 0,
location: {
lat: step.maneuver?.location?.[1] ?? 0,
lng: step.maneuver?.location?.[0] ?? 0,
},
},
})),
})),
alternatives: data.routes.slice(1).map((alt: any) =>
this.parseOSRMResponse({ routes: [alt] }, profile)
),
};
}
private parseValhallaResponse(data: any, profile: RoutingProfile): Route {
const trip = data.trip;
const id = `route-${Date.now()}`;
return {
id,
waypoints: [],
geometry: {
type: 'LineString',
coordinates: this.decodeValhallaPolyline(trip.legs[0]?.shape ?? ''),
},
profile,
summary: {
distance: trip.summary.length * 1000, // km to m
duration: trip.summary.time,
},
legs: trip.legs.map((leg: any, index: number) => ({
startWaypoint: `waypoint-${index}`,
endWaypoint: `waypoint-${index + 1}`,
distance: leg.summary.length * 1000,
duration: leg.summary.time,
geometry: {
type: 'LineString',
coordinates: this.decodeValhallaPolyline(leg.shape ?? ''),
},
})),
};
}
private parseGraphHopperResponse(data: any, profile: RoutingProfile): Route {
const path = data.paths[0];
const id = `route-${Date.now()}`;
return {
id,
waypoints: [],
geometry: path.points,
profile,
summary: {
distance: path.distance,
duration: path.time / 1000, // ms to s
ascent: path.ascend,
descent: path.descend,
},
legs: [], // GraphHopper doesn't split by waypoint in the same way
alternatives: data.paths.slice(1).map((alt: any) =>
this.parseGraphHopperResponse({ paths: [alt] }, profile)
),
};
}
private parseORSResponse(data: any, profile: RoutingProfile): Route {
const feature = data.features[0];
const id = `route-${Date.now()}`;
return {
id,
waypoints: [],
geometry: feature.geometry,
profile,
summary: {
distance: feature.properties.summary.distance,
duration: feature.properties.summary.duration,
},
legs: feature.properties.segments.map((seg: any, index: number) => ({
startWaypoint: `waypoint-${index}`,
endWaypoint: `waypoint-${index + 1}`,
distance: seg.distance,
duration: seg.duration,
geometry: { type: 'LineString', coordinates: [] },
steps: seg.steps,
})),
};
}
private decodeValhallaPolyline(encoded: string): [number, number][] {
// Valhalla uses Google's polyline encoding
const coordinates: [number, number][] = [];
let index = 0;
let lat = 0;
let lng = 0;
while (index < encoded.length) {
let shift = 0;
let result = 0;
let byte: number;
do {
byte = encoded.charCodeAt(index++) - 63;
result |= (byte & 0x1f) << shift;
shift += 5;
} while (byte >= 0x20);
const dlat = result & 1 ? ~(result >> 1) : result >> 1;
lat += dlat;
shift = 0;
result = 0;
do {
byte = encoded.charCodeAt(index++) - 63;
result |= (byte & 0x1f) << shift;
shift += 5;
} while (byte >= 0x20);
const dlng = result & 1 ? ~(result >> 1) : result >> 1;
lng += dlng;
coordinates.push([lng / 1e6, lat / 1e6]);
}
return coordinates;
}
}
export default RoutingService;

View File

@ -1,228 +0,0 @@
/**
* TileService - Manages map tile sources and caching
*
* Features:
* - Multiple tile providers (OSM, Mapbox, custom)
* - Offline tile caching via Service Worker
* - Vector tile support
* - Custom style management
*/
import type { TileServiceConfig, BoundingBox } from '../types';
export interface TileSource {
id: string;
name: string;
type: 'raster' | 'vector';
url: string;
attribution: string;
minZoom?: number;
maxZoom?: number;
tileSize?: number;
}
export const DEFAULT_TILE_SOURCES: TileSource[] = [
{
id: 'osm-standard',
name: 'OpenStreetMap',
type: 'raster',
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
},
{
id: 'osm-humanitarian',
name: 'Humanitarian',
type: 'raster',
url: 'https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
attribution: '&copy; OpenStreetMap contributors, Tiles: HOT',
maxZoom: 19,
},
{
id: 'carto-light',
name: 'Carto Light',
type: 'raster',
url: 'https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
attribution: '&copy; OpenStreetMap contributors, &copy; CARTO',
maxZoom: 19,
},
{
id: 'carto-dark',
name: 'Carto Dark',
type: 'raster',
url: 'https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; OpenStreetMap contributors, &copy; CARTO',
maxZoom: 19,
},
{
id: 'stamen-terrain',
name: 'Terrain',
type: 'raster',
url: 'https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png',
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>',
maxZoom: 18,
},
{
id: 'cycling',
name: 'Cycling Routes',
type: 'raster',
url: 'https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png',
attribution: '<a href="https://waymarkedtrails.org">Waymarked Trails</a>',
maxZoom: 18,
},
{
id: 'hiking',
name: 'Hiking Trails',
type: 'raster',
url: 'https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png',
attribution: '<a href="https://waymarkedtrails.org">Waymarked Trails</a>',
maxZoom: 18,
},
];
export class TileService {
private config: TileServiceConfig;
private cache: Cache | null = null;
private cacheName = 'open-mapping-tiles-v1';
constructor(config: TileServiceConfig) {
this.config = config;
this.initCache();
}
private async initCache(): Promise<void> {
if ('caches' in window) {
try {
this.cache = await caches.open(this.cacheName);
} catch (error) {
console.warn('TileService: Cache API not available', error);
}
}
}
/**
* Get all available tile sources
*/
getSources(): TileSource[] {
return DEFAULT_TILE_SOURCES;
}
/**
* Get a specific tile source by ID
*/
getSource(id: string): TileSource | undefined {
return DEFAULT_TILE_SOURCES.find((s) => s.id === id);
}
/**
* Generate tile URL for a specific coordinate
*/
getTileUrl(source: TileSource, z: number, x: number, y: number): string {
return source.url
.replace('{z}', String(z))
.replace('{x}', String(x))
.replace('{y}', String(y));
}
/**
* Pre-cache tiles for offline use
*/
async cacheTilesForArea(
sourceId: string,
bounds: BoundingBox,
minZoom: number,
maxZoom: number,
onProgress?: (progress: number) => void
): Promise<void> {
const source = this.getSource(sourceId);
if (!source || !this.cache) {
throw new Error('Cannot cache tiles: source not found or cache unavailable');
}
const tiles = this.getTilesInBounds(bounds, minZoom, maxZoom);
const total = tiles.length;
let completed = 0;
for (const { z, x, y } of tiles) {
const url = this.getTileUrl(source, z, x, y);
try {
const response = await fetch(url);
if (response.ok) {
await this.cache.put(url, response);
}
} catch (error) {
console.warn(`Failed to cache tile ${z}/${x}/${y}:`, error);
}
completed++;
onProgress?.(completed / total);
}
}
/**
* Clear cached tiles
*/
async clearCache(): Promise<void> {
if ('caches' in window) {
await caches.delete(this.cacheName);
this.cache = await caches.open(this.cacheName);
}
}
/**
* Get cache size estimate
*/
async getCacheSize(): Promise<number> {
if (!this.cache) return 0;
const keys = await this.cache.keys();
let totalSize = 0;
for (const request of keys) {
const response = await this.cache.match(request);
if (response) {
const blob = await response.blob();
totalSize += blob.size;
}
}
return totalSize;
}
/**
* Calculate tiles within a bounding box
*/
private getTilesInBounds(
bounds: BoundingBox,
minZoom: number,
maxZoom: number
): Array<{ z: number; x: number; y: number }> {
const tiles: Array<{ z: number; x: number; y: number }> = [];
for (let z = minZoom; z <= maxZoom; z++) {
const minTile = this.latLngToTile(bounds.south, bounds.west, z);
const maxTile = this.latLngToTile(bounds.north, bounds.east, z);
for (let x = minTile.x; x <= maxTile.x; x++) {
for (let y = maxTile.y; y <= minTile.y; y++) {
tiles.push({ z, x, y });
}
}
}
return tiles;
}
/**
* Convert lat/lng to tile coordinates
*/
private latLngToTile(lat: number, lng: number, zoom: number): { x: number; y: number } {
const n = Math.pow(2, zoom);
const x = Math.floor(((lng + 180) / 360) * n);
const y = Math.floor(
((1 - Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) / Math.PI) / 2) * n
);
return { x, y };
}
}
export default TileService;

View File

@ -1,11 +0,0 @@
export { RoutingService } from './RoutingService';
export { TileService, DEFAULT_TILE_SOURCES } from './TileService';
export type { TileSource } from './TileService';
export { OptimizationService } from './OptimizationService';
export type {
OptimizationJob,
OptimizationConstraints,
OptimizationResult,
OptimizationCost,
CostParameters,
} from './OptimizationService';