From 966e1855c19b17d91aad554008ee8d508cbc913d Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 4 Dec 2025 06:45:27 -0800 Subject: [PATCH] chore: remove open-mapping files (should be on feature branch) --- OPEN_MAPPING_PROJECT.md | 336 ------------ open-mapping.docker-compose.yml | 160 ------ open-mapping.setup.sh | 138 ----- .../services/OptimizationService.ts | 325 ----------- src/open-mapping/services/RoutingService.ts | 517 ------------------ src/open-mapping/services/TileService.ts | 228 -------- src/open-mapping/services/index.ts | 11 - 7 files changed, 1715 deletions(-) delete mode 100644 OPEN_MAPPING_PROJECT.md delete mode 100644 open-mapping.docker-compose.yml delete mode 100644 open-mapping.setup.sh delete mode 100644 src/open-mapping/services/OptimizationService.ts delete mode 100644 src/open-mapping/services/RoutingService.ts delete mode 100644 src/open-mapping/services/TileService.ts delete mode 100644 src/open-mapping/services/index.ts diff --git a/OPEN_MAPPING_PROJECT.md b/OPEN_MAPPING_PROJECT.md deleted file mode 100644 index dbce209..0000000 --- a/OPEN_MAPPING_PROJECT.md +++ /dev/null @@ -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/) diff --git a/open-mapping.docker-compose.yml b/open-mapping.docker-compose.yml deleted file mode 100644 index 9f2bcd1..0000000 --- a/open-mapping.docker-compose.yml +++ /dev/null @@ -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: diff --git a/open-mapping.setup.sh b/open-mapping.setup.sh deleted file mode 100644 index eb87ed1..0000000 --- a/open-mapping.setup.sh +++ /dev/null @@ -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" diff --git a/src/open-mapping/services/OptimizationService.ts b/src/open-mapping/services/OptimizationService.ts deleted file mode 100644 index 0c67419..0000000 --- a/src/open-mapping/services/OptimizationService.ts +++ /dev/null @@ -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 { - 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 { - // 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 - ): 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): void { - this.costParams = { ...this.costParams, ...params }; - } - - // ========================================================================= - // Private Methods - // ========================================================================= - - private async optimizeWithVROOM( - waypoints: Waypoint[], - constraints?: OptimizationConstraints - ): Promise { - // 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; diff --git a/src/open-mapping/services/RoutingService.ts b/src/open-mapping/services/RoutingService.ts deleted file mode 100644 index 1d80f13..0000000 --- a/src/open-mapping/services/RoutingService.ts +++ /dev/null @@ -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 - ): Promise { - 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 { - // 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 { - 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 { - // 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 - ): Promise { - 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 - ): Promise { - 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 - ): Promise { - 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 - ): Promise { - 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 = { - '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 { - 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 { - const body = { - locations: [[center.lng, center.lat]], - range: minutes.map((m) => m * 60), // ORS uses seconds - range_type: 'time', - }; - - const headers: Record = { - '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> = { - car: 'driving', - bicycle: 'cycling', - foot: 'walking', - hiking: 'walking', - }; - return mapping[profile] ?? 'driving'; - } - - private mapProfileToValhalla(profile: RoutingProfile): string { - const mapping: Partial> = { - 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> = { - 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> = { - 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; diff --git a/src/open-mapping/services/TileService.ts b/src/open-mapping/services/TileService.ts deleted file mode 100644 index dbf15f1..0000000 --- a/src/open-mapping/services/TileService.ts +++ /dev/null @@ -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: '© OpenStreetMap contributors', - maxZoom: 19, - }, - { - id: 'osm-humanitarian', - name: 'Humanitarian', - type: 'raster', - url: 'https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', - attribution: '© 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: '© OpenStreetMap contributors, © 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: '© OpenStreetMap contributors, © 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 Stamen Design', - maxZoom: 18, - }, - { - id: 'cycling', - name: 'Cycling Routes', - type: 'raster', - url: 'https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png', - attribution: 'Waymarked Trails', - maxZoom: 18, - }, - { - id: 'hiking', - name: 'Hiking Trails', - type: 'raster', - url: 'https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', - attribution: 'Waymarked Trails', - 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 { - 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 { - 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 { - if ('caches' in window) { - await caches.delete(this.cacheName); - this.cache = await caches.open(this.cacheName); - } - } - - /** - * Get cache size estimate - */ - async getCacheSize(): Promise { - 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; diff --git a/src/open-mapping/services/index.ts b/src/open-mapping/services/index.ts deleted file mode 100644 index b866ac0..0000000 --- a/src/open-mapping/services/index.ts +++ /dev/null @@ -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';