feat: Provider-agnostic map architecture with Google Maps support#223
Open
feat: Provider-agnostic map architecture with Google Maps support#223
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
New Files (14 files)
addon/services/map-adapter-interface.jsaddon/services/map-manager.jsleaflet-map-manageraddon/services/map-adapter/leaflet.jsaddon/services/map-adapter/google.jsaddon/components/map/google-live-map.{js,hbs}addon/components/map-settings.{js,hbs}app/services/map-manager.jsapp/services/map-adapter-interface.jsapp/services/map-adapter/leaflet.jsapp/services/map-adapter/google.jsapp/components/map-settings.jsapp/components/map/google-live-map.jsModified Files (10 files)
addon/components/map/leaflet-live-map.{js,hbs}mapManager; template conditionally renders Google or Leaflet branchaddon/services/movement-tracker.jsmapManager.updateMarkerPosition()/setMarkerRotation()addon/services/position-playback.jsmapManager.updateMarkerPosition()/setMarkerRotation()addon/services/geofence.jsmapManager.showDrawControl()/editPolygon()config/environment.jsmapProvider,googleMapsApiKey,googleMapsLibrariesindex.jscontentFor('head')to inject Google Maps script tagserver/src/Http/Controllers/Internal/v1/SettingController.phpgetMapSettings()andsaveMapSettings()server/src/routes.phpGET/POST fleet-ops/settings/maptranslations/en-us.yamlmap-settings.*translation keysGoogle Maps Feature Parity
How to Enable Google Maps
Via environment variables (build-time):
Via admin settings panel (runtime, no rebuild):
Settings → Map Settings → Select "Google Maps" → Enter API key → Save
Required Google Cloud APIs:
Adding a New Map Provider (for developers)
addon/services/map-adapter/<name>.jsextendingMapAdapterInterfaceapp/services/map-adapter/<name>.jsre-exportmapProvider: '<name>'inconfig/environment.jsBackward Compatibility
leaflet— zero behaviour change for existing deploymentsleaflet-map-manager,leaflet-routing-control, etc.) are preserved unchangedleaflet-live-mapcomponent continues to work exactly as before when Leaflet is the active providerMapManagerServicefalls back to Leaflet if an unknown provider is configuredTesting Checklist