Skip to content

feat: Provider-agnostic map architecture with Google Maps support#223

Open
roncodes wants to merge 4 commits intomainfrom
feature/provider-agnostic-map-architecture
Open

feat: Provider-agnostic map architecture with Google Maps support#223
roncodes wants to merge 4 commits intomainfrom
feature/provider-agnostic-map-architecture

Conversation

@roncodes
Copy link
Copy Markdown
Member

@roncodes roncodes commented Apr 8, 2026

Summary

This PR introduces a provider-agnostic map architecture for FleetOps, decoupling the codebase from Leaflet-specific APIs and enabling multiple map providers to coexist. Google Maps is implemented as the first alternative provider with 1-to-1 feature parity against the existing Leaflet implementation.

The architecture is designed so that any developer can add a new map provider (Mapbox, MapLibre, HERE, etc.) by creating a single adapter service — with zero changes to business logic, components, or other services.


Architecture Overview

MapManagerService (provider-agnostic hub)
├── resolves adapter from config/environment or runtime settings
├── delegates all map calls to the active adapter
│
├── MapAdapterInterface (abstract base class, ~30 methods)
│   ├── map-adapter/leaflet.js  ← wraps existing Leaflet implementation
│   └── map-adapter/google.js   ← new Google Maps implementation
│
└── Components / Services call mapManager.* (never Leaflet directly)

New Files (14 files)

File Description
addon/services/map-adapter-interface.js Abstract base class defining the full adapter contract
addon/services/map-manager.js Central provider-agnostic service replacing leaflet-map-manager
addon/services/map-adapter/leaflet.js Leaflet adapter wrapping existing implementation
addon/services/map-adapter/google.js Full Google Maps JS API adapter
addon/components/map/google-live-map.{js,hbs} Google Maps surface component (mirrors leaflet-live-map)
addon/components/map-settings.{js,hbs} Admin settings panel for map provider selection
app/services/map-manager.js Re-export
app/services/map-adapter-interface.js Re-export
app/services/map-adapter/leaflet.js Re-export
app/services/map-adapter/google.js Re-export
app/components/map-settings.js Re-export
app/components/map/google-live-map.js Re-export

Modified Files (10 files)

File Change
addon/components/map/leaflet-live-map.{js,hbs} Injects mapManager; template conditionally renders Google or Leaflet branch
addon/services/movement-tracker.js Uses mapManager.updateMarkerPosition() / setMarkerRotation()
addon/services/position-playback.js Uses mapManager.updateMarkerPosition() / setMarkerRotation()
addon/services/geofence.js Uses mapManager.showDrawControl() / editPolygon()
config/environment.js Adds mapProvider, googleMapsApiKey, googleMapsLibraries
index.js Adds contentFor('head') to inject Google Maps script tag
server/src/Http/Controllers/Internal/v1/SettingController.php Adds getMapSettings() and saveMapSettings()
server/src/routes.php Registers GET/POST fleet-ops/settings/map
translations/en-us.yaml Adds map-settings.* translation keys

Google Maps Feature Parity

Feature Leaflet Google Maps
Live driver/vehicle markers ✅ L.Marker ✅ google.maps.Marker
Smooth marker animation ✅ slideTo() ✅ rAF interpolation loop
Marker rotation (heading) ✅ leaflet-marker-rotate ✅ CSS transform on icon
Service area polygons ✅ L.Polygon ✅ google.maps.Polygon
Zone polygons ✅ L.Polygon ✅ google.maps.Polygon
Route polylines ✅ L.Polyline ✅ google.maps.Polyline
Geofence drawing ✅ leaflet-draw ✅ DrawingManager
Geofence editing ✅ leaflet-draw edit ✅ Polygon setEditable()
Context menus ✅ leaflet-contextmenu ✅ InfoWindow-based menus
Position playback ✅ marker.slideTo() ✅ rAF interpolation
Fit bounds ✅ map.fitBounds() ✅ map.fitBounds()
Fly to / pan to ✅ map.flyTo() ✅ map.panTo()
GeoJSON overlay ✅ L.geoJSON() ✅ map.data.addGeoJson()
Traffic layer ❌ (tile-based) ✅ TrafficLayer
Transit layer ❌ (tile-based) ✅ TransitLayer
Map type switching ❌ tile swap ✅ setMapTypeId()

How to Enable Google Maps

Via environment variables (build-time):

MAP_PROVIDER=google
GOOGLE_MAPS_API_KEY=AIza...

Via admin settings panel (runtime, no rebuild):
Settings → Map Settings → Select "Google Maps" → Enter API key → Save

Required Google Cloud APIs:

  • Maps JavaScript API
  • Drawing Library
  • Geometry Library
  • Geocoding API

Adding a New Map Provider (for developers)

  1. Create addon/services/map-adapter/<name>.js extending MapAdapterInterface
  2. Create app/services/map-adapter/<name>.js re-export
  3. Set mapProvider: '<name>' in config/environment.js
  4. No changes to any component or service are required

Backward Compatibility

  • Default provider remains leaflet — zero behaviour change for existing deployments
  • All existing Leaflet services (leaflet-map-manager, leaflet-routing-control, etc.) are preserved unchanged
  • The leaflet-live-map component continues to work exactly as before when Leaflet is the active provider
  • The MapManagerService falls back to Leaflet if an unknown provider is configured

Testing Checklist

  • Leaflet provider: all existing map features work unchanged
  • Google Maps provider: markers render and animate correctly
  • Google Maps provider: geofence drawing and editing works
  • Google Maps provider: service area polygons render correctly
  • Google Maps provider: context menus appear on right-click
  • Google Maps provider: position playback animates markers
  • Settings panel: provider toggle persists across page reloads
  • Settings panel: Google Maps API key is stored securely (not in GET response)
  • Runtime provider switch triggers map re-render

roncodes added 4 commits April 7, 2026 23:17
Introduces a full abstraction layer that decouples FleetOps from
Leaflet-specific APIs, enabling multiple map providers to coexist
and be selected at runtime. Google Maps is implemented as the first
alternative provider with 1-to-1 feature parity.

## New files

### Core abstraction
- addon/services/map-adapter-interface.js
  Abstract base class defining the ~30-method contract all adapters
  must implement (viewport, markers, overlays, drawing, popups,
  context menus, events, utilities).

- addon/services/map-manager.js
  Provider-agnostic central service that replaces leaflet-map-manager
  as the single point of contact for all map operations. Resolves the
  correct adapter from config/environment or runtime settings, then
  delegates every call. Exposes setActiveProvider() for runtime
  switching and waitForMap() for async initialisation.

### Adapters
- addon/services/map-adapter/leaflet.js
  Wraps the existing Leaflet/ember-leaflet implementation. Preserves
  all existing behaviour: L.Marker with leaflet-marker-rotate,
  L.Polyline routing, leaflet-draw for geofences, leaflet-contextmenu,
  slideTo() smooth animation, and layer visibility panes.

- addon/services/map-adapter/google.js
  Full Google Maps JavaScript API adapter. Implements every interface
  method using the Maps JS API v3:
  - Smooth marker animation via requestAnimationFrame interpolation
    (equivalent to Leaflet's slideTo)
  - Marker rotation via CSS transform on the icon element
  - google.maps.drawing.DrawingManager for geofence creation/editing
  - google.maps.InfoWindow-based context menus
  - google.maps.Polyline for route overlays
  - google.maps.Polygon / Circle for service area overlays
  - Traffic and Transit layer support
  - Map type switching (roadmap/satellite/hybrid/terrain)

### Components
- addon/components/map/google-live-map.{js,hbs}
  Google Maps surface component. Initialised imperatively via
  did-insert. Mirrors all features of leaflet-live-map: driver/vehicle
  markers, service area polygons, route polylines, position playback,
  context menus, and drawing tools.

- addon/components/map-settings.{js,hbs}
  Admin settings panel for selecting the map provider and configuring
  Google Maps options (API key, map type, traffic/transit layers).
  Settings are persisted via the new fleet-ops/settings/map API.

## Modified files

### Components
- addon/components/map/leaflet-live-map.{js,hbs}
  Injects mapManager service alongside existing services. Template
  conditionally renders Map::GoogleLiveMap or the existing Leaflet
  branch based on mapManager.isGoogleMaps. All marker/polygon
  registration calls now go through mapManager so both adapters
  receive the same data.

### Services
- addon/services/movement-tracker.js
  Replaces direct leafletLayer access with mapManager.updateMarkerPosition()
  and mapManager.setMarkerRotation(). Falls back gracefully when no
  marker is registered for a given model.

- addon/services/position-playback.js
  Replaces marker.slideTo() / marker.setRotationAngle() with
  mapManager.updateMarkerPosition() / mapManager.setMarkerRotation().
  The playback task is now provider-agnostic.

- addon/services/geofence.js
  Replaces leafletMapManager.showDrawControl() / hideDrawControl() /
  editPolygon() with mapManager equivalents.

### Configuration
- config/environment.js
  Adds mapProvider (default: 'leaflet'), googleMapsApiKey, and
  googleMapsLibraries config keys. All values are read from env vars
  (MAP_PROVIDER, GOOGLE_MAPS_API_KEY, GOOGLE_MAPS_LIBRARIES).

- index.js
  Adds contentFor('head') hook that injects the Google Maps JS API
  script tag when mapProvider === 'google' and a key is configured.

### Server
- server/src/Http/Controllers/Internal/v1/SettingController.php
  Adds getMapSettings() and saveMapSettings() methods. The Google Maps
  API key is stored in a separate protected setting key and is never
  returned in GET responses.

- server/src/routes.php
  Registers GET/POST fleet-ops/settings/map routes.

### i18n
- translations/en-us.yaml
  Adds map-settings.* translation keys for the new settings panel.

## App re-exports
- app/services/map-manager.js
- app/services/map-adapter-interface.js
- app/services/map-adapter/leaflet.js
- app/services/map-adapter/google.js
- app/components/map-settings.js
- app/components/map/google-live-map.js

## How to enable Google Maps

1. Set MAP_PROVIDER=google in your .env file
2. Set GOOGLE_MAPS_API_KEY=<your-key> in your .env file
3. Ensure the key has these APIs enabled in Google Cloud Console:
   - Maps JavaScript API
   - Drawing Library
   - Geometry Library
   - Geocoding API
4. Rebuild the frontend (pnpm build)

Or configure at runtime via Settings > Map Settings in the FleetOps
admin panel (no rebuild required for provider toggle).

## Adding a new map provider

1. Create addon/services/map-adapter/<name>.js extending MapAdapterInterface
2. Create app/services/map-adapter/<name>.js re-export
3. Set mapProvider: '<name>' in config/environment.js
4. No changes to any component or service are required
Wires the previously-created MapSettings component into the FleetOps
settings section as a first-class route, following the exact same
patterns used by the existing Routing and Notifications settings pages.

## New files

### Route layer
- addon/routes/settings/map.js
  Minimal Ember route class (matches settings/routing.js pattern).

- addon/controllers/settings/map.js
  Full controller with @Tracked state for mapProvider, googleMapsApiKey,
  googleMapsMapType, googleMapsTrafficLayer, googleMapsTransitLayer.
  Implements getSettings and saveSettings ember-concurrency tasks that
  call the fleet-ops/settings/map API endpoints added in the previous
  commit. Applies the new provider to the live mapManager on save so
  the map switches without a page reload.

- addon/templates/settings/map.hbs
  Full settings page template following the Layout::Section::Header +
  Layout::Section::Body + ContentPanel pattern used by all other
  settings routes. Includes:
  - Save button in the header (disabled while tasks run)
  - Map provider Select (Leaflet / Google Maps)
  - Conditional Google Maps options panel:
    - API key password input
    - Map type Select (roadmap / satellite / hybrid / terrain)
    - Traffic layer Toggle
    - Transit layer Toggle
    - Required Google Cloud APIs info box
  - Loading spinner while getSettings is running
  - RegistryYield extension point for third-party panels

### App re-exports (3 files)
- app/routes/settings/map.js
- app/controllers/settings/map.js
- app/templates/settings/map.js

## Modified files

### Router
- addon/routes.js
  Adds this.route('map') inside the settings route group, between
  routing and payments (alphabetical / logical order).

### Sidebar
- addon/components/layout/fleet-ops-sidebar.js
  Adds a Map Settings item to the settingsItems array:
  - intl key: menu.map
  - icon: map
  - route: settings.map
  - permission: fleet-ops view map-settings
  Inserted between Routing and Custom Fields to maintain logical order.

### Translations
- translations/en-us.yaml
  Adds two new translation blocks:
  1. menu.map: "Map" — sidebar label
  2. settings.map.* — 20 keys covering all labels, help texts,
     placeholders, and status messages used by the new route template.
     Keys mirror the settings.routing.* structure for consistency.
…m core-api

The Google Maps API key is already managed at the system admin level
through the core-api Settings → Services panel, which stores it at
`config('services.google_maps.api_key')` via `Setting::configureSystem`.
FleetOps should not duplicate this responsibility.

## Backend (server/src/Http/Controllers/Internal/v1/SettingController.php)

`getMapSettings`
- Removed the separate `fleet-ops.map-settings.google-api-key` lookup.
- Now reads `config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''))`
  — the same key managed by core-api — and includes it in the response
  so the frontend Google Maps adapter can initialise correctly.
- Single source of truth: the key lives only in the system-level services
  config; FleetOps reads but never stores it.

`saveMapSettings`
- Removed all API key acceptance and storage logic.
- Any `googleMapsApiKey` field sent by a client is silently stripped
  before the settings blob is persisted, preventing accidental storage.
- Comment updated to make the delegation to core-api explicit.

## Frontend (addon/controllers/settings/map.js)

- Removed `@tracked googleMapsApiKey` property.
- Removed `onApiKeyChange` action.
- Removed the conditional API key inclusion from the `saveSettings` task
  payload — the key is no longer sent to the server from this page.
- Removed the stale comment about the key not being returned by the server
  (it now is returned, sourced from the system config).
- Fixed the `notifications.success` call to use the correct `settings.map.*`
  translation key namespace (was incorrectly referencing `map-settings.*`).

## Frontend (addon/templates/settings/map.hbs)

- Removed the API key `<InputGroup>` block (password input + help text).
- Removed the "Required Google Cloud APIs" info box — this information
  belongs in the core-api admin panel where the key is configured.
- The Google Maps conditional section now shows only the map type selector
  and the traffic/transit layer toggles.

## Translations (translations/en-us.yaml)

Removed 8 translation keys from the `settings.map` block that were
exclusively used by the now-deleted API key input and info box:
- `google-maps-api-key`
- `google-maps-api-key-help-text`
- `google-maps-api-key-placeholder`
- `google-api-key-requirements-title`
- `google-api-key-requirement-maps-js`
- `google-api-key-requirement-drawing`
- `google-api-key-requirement-geometry`
- `google-api-key-requirement-geocoding`
…translation keys

The standalone MapSettings component (addon/components/map-settings.js,
addon/components/map-settings.hbs, app/components/map-settings.js) was
never invoked in any template or imported by any other file. Its
functionality is fully covered by the proper route-based implementation
at addon/templates/settings/map.hbs + addon/controllers/settings/map.js.

Also removes the entire top-level map-settings: block from
translations/en-us.yaml (22 lines). All active translation keys for the
map settings page now live under the settings.map.* namespace, which is
consistent with every other settings section in FleetOps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant