From a5a4f1cdefcfcd70a196fa005acfc9b2b4bbd74f Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 27 Aug 2026 15:20:24 +0800 Subject: [PATCH 1/2] feat(map): configurable Leaflet tile provider + keyless OpenStreetMap default CARTO's basemaps.cartocdn.com raster tiles now require an API key (https://carto.com/basemaps/apikey/) and render 'API key required' watermarks on every Fleetbase instance using the Leaflet map. - Default all Leaflet maps to the keyless OpenStreetMap tile server - Add Leaflet tile provider URL settings (light + dark mode) to Fleet-Ops map settings, persisted per-company via the existing fleet-ops/settings/map endpoint - Resolve tile URLs through the map-settings service / a new leaflet-tile-url helper so a custom provider (including keyed CARTO URLs) applies across all console Leaflet maps - Sanitize custom tile URLs server-side (http/https only) --- .template-lintrc.js | 2 ++ addon/components/ai/route-preview-map.js | 6 +--- addon/components/customer/orders.js | 3 +- addon/components/map/leaflet-live-map.js | 23 +++----------- addon/components/modals/place-details.hbs | 2 +- addon/components/modals/point-map.hbs | 2 +- addon/components/orchestrator-workbench.js | 3 +- addon/components/order-tracking-lookup.js | 3 +- addon/components/place/details.hbs | 2 +- addon/components/positions-replay.js | 5 ++- addon/components/service-area/details.hbs | 2 +- addon/components/widget/live-fleet.js | 14 ++++----- addon/components/zone/details.hbs | 2 +- addon/controllers/settings/map.js | 16 ++++++++++ addon/helpers/leaflet-tile-url.js | 19 ++++++++++++ addon/services/map-settings.js | 22 +++++++++++++ addon/templates/settings/map.hbs | 10 ++++++ addon/utils/leaflet-tile-url.js | 31 +++++++++++++++++++ app/helpers/leaflet-tile-url.js | 1 + app/utils/leaflet-tile-url.js | 1 + .../Internal/v1/SettingController.php | 22 +++++++++++++ translations/en-us.yaml | 4 +++ 22 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 addon/helpers/leaflet-tile-url.js create mode 100644 addon/utils/leaflet-tile-url.js create mode 100644 app/helpers/leaflet-tile-url.js create mode 100644 app/utils/leaflet-tile-url.js diff --git a/.template-lintrc.js b/.template-lintrc.js index 46748176d..09a3d8350 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -10,5 +10,7 @@ module.exports = { 'link-href-attributes': 'off', 'require-input-label': 'off', 'no-array-prototype-extensions': 'off', + // `leaflet-tile-url` is a zero-argument helper resolving the configured tile provider + 'no-implicit-this': { allow: ['leaflet-tile-url'] }, }, }; diff --git a/addon/components/ai/route-preview-map.js b/addon/components/ai/route-preview-map.js index be7e145f0..c5f3ee3dd 100644 --- a/addon/components/ai/route-preview-map.js +++ b/addon/components/ai/route-preview-map.js @@ -168,11 +168,7 @@ export default class AiRoutePreviewMapComponent extends Component { get tileUrl() { const theme = document.body?.dataset?.theme; - if (theme === 'dark') { - return 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'; - } - - return 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; + return this.mapSettings.getLeafletTileUrl(theme === 'dark' ? 'dark' : 'light'); } get emptyText() { diff --git a/addon/components/customer/orders.js b/addon/components/customer/orders.js index c20df8d33..1de736c0b 100644 --- a/addon/components/customer/orders.js +++ b/addon/components/customer/orders.js @@ -9,6 +9,7 @@ import { debug } from '@ember/debug'; import { task, timeout } from 'ember-concurrency'; import { Control as RoutingControl } from '@fleetbase/leaflet-routing-machine'; import engineService from '@fleetbase/ember-core/decorators/engine-service'; +import { DEFAULT_LEAFLET_TILE_URL } from '../../utils/leaflet-tile-url'; import registerComponent from '@fleetbase/ember-core/utils/register-component'; import OrderProgressCardComponent from '../order-progress-card'; import DisplayPlaceComponent from '../display-place'; @@ -38,7 +39,7 @@ export default class CustomerOrdersComponent extends Component { @tracked longitude; @tracked route; @tracked query; - @tracked tileSourceUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; + @tracked tileSourceUrl = DEFAULT_LEAFLET_TILE_URL; @tracked scheduledAt; @tracked deliveryInstructions = {}; diff --git a/addon/components/map/leaflet-live-map.js b/addon/components/map/leaflet-live-map.js index fd4b44d86..ae7727f8a 100644 --- a/addon/components/map/leaflet-live-map.js +++ b/addon/components/map/leaflet-live-map.js @@ -48,7 +48,6 @@ export default class MapLeafletLiveMapComponent extends Component { @tracked latitude = this.location.getLatitude(); @tracked longitude = this.location.getLongitude(); @tracked contextmenuItems = []; - @tracked tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; @tracked theme = 'light'; @tracked routes = []; @tracked drivers = []; @@ -57,6 +56,10 @@ export default class MapLeafletLiveMapComponent extends Component { @tracked leafletPluginsReady = hasLeafletPluginsReady(); _viewportReloadLocks = new Set(); + get tileUrl() { + return this.mapSettings.getLeafletTileUrl(this.theme); + } + constructor() { super(...arguments); @@ -888,22 +891,4 @@ export default class MapLeafletLiveMapComponent extends Component { // Fallback to default Singapore longitude return 103.8864; } - - #changeTileSource(source) { - switch (source) { - case 'dark': - this.theme = 'dark'; - this.tileUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'; - break; - case 'custom': - this.theme = 'custom'; - this.tileUrl = source.startsWith('https://') ? source : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; - break; - case 'light': - default: - this.theme = 'light'; - this.tileUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; - break; - } - } } diff --git a/addon/components/modals/place-details.hbs b/addon/components/modals/place-details.hbs index 4b49a002b..6be51d8a1 100644 --- a/addon/components/modals/place-details.hbs +++ b/addon/components/modals/place-details.hbs @@ -3,7 +3,7 @@
- + diff --git a/addon/components/modals/point-map.hbs b/addon/components/modals/point-map.hbs index 0b78d5a35..308f5e2b7 100644 --- a/addon/components/modals/point-map.hbs +++ b/addon/components/modals/point-map.hbs @@ -1,7 +1,7 @@
- + diff --git a/addon/components/orchestrator-workbench.js b/addon/components/orchestrator-workbench.js index d748789c0..2c63fedd8 100644 --- a/addon/components/orchestrator-workbench.js +++ b/addon/components/orchestrator-workbench.js @@ -36,6 +36,7 @@ export default class OrchestratorWorkbenchComponent extends Component { @service modalsManager; @service location; @service mapManager; + @service mapSettings; @service routeEngine; @service('order-allocation') allocationService; @@ -661,7 +662,7 @@ export default class OrchestratorWorkbenchComponent extends Component { get tileSourceUrl() { const isDark = document.documentElement.classList.contains('dark'); - return isDark ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; + return this.mapSettings.getLeafletTileUrl(isDark ? 'dark' : 'light'); } // ── Computed helpers ────────────────────────────────────────────────────── diff --git a/addon/components/order-tracking-lookup.js b/addon/components/order-tracking-lookup.js index f341833e1..8067095f1 100644 --- a/addon/components/order-tracking-lookup.js +++ b/addon/components/order-tracking-lookup.js @@ -10,6 +10,7 @@ import { task } from 'ember-concurrency'; import { OSRMv1, Control as RoutingControl } from '@fleetbase/leaflet-routing-machine'; import getRoutingHost from '@fleetbase/ember-core/utils/get-routing-host'; import engineService from '@fleetbase/ember-core/decorators/engine-service'; +import { DEFAULT_LEAFLET_TILE_URL } from '../utils/leaflet-tile-url'; export default class OrderTrackingLookupComponent extends Component { @service urlSearchParams; @@ -28,7 +29,7 @@ export default class OrderTrackingLookupComponent extends Component { @tracked latitude; @tracked longitude; @tracked route; - @tracked tileSourceUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'; + @tracked tileSourceUrl = DEFAULT_LEAFLET_TILE_URL; constructor() { super(...arguments); diff --git a/addon/components/place/details.hbs b/addon/components/place/details.hbs index bb057a8f8..cb2d9e7e4 100644 --- a/addon/components/place/details.hbs +++ b/addon/components/place/details.hbs @@ -75,7 +75,7 @@ @zoomControl={{false}} as |layers| > - + - + this.load.perform(), RECONCILE_INTERVAL_MS); diff --git a/addon/components/zone/details.hbs b/addon/components/zone/details.hbs index d7af87681..284582073 100644 --- a/addon/components/zone/details.hbs +++ b/addon/components/zone/details.hbs @@ -22,7 +22,7 @@ @zoomControl={{false}} as |layers| > - + + * + * + * Recomputes automatically when map settings load or change. + */ +export default class LeafletTileUrlHelper extends Helper { + @service mapSettings; + + compute(_params, { theme = 'light' } = {}) { + return this.mapSettings.getLeafletTileUrl(theme); + } +} diff --git a/addon/services/map-settings.js b/addon/services/map-settings.js index 4267c0b67..fae705bbd 100644 --- a/addon/services/map-settings.js +++ b/addon/services/map-settings.js @@ -1,9 +1,12 @@ import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; +import { getLeafletTileUrl } from '../utils/leaflet-tile-url'; const DEFAULT_SETTINGS = { mapProvider: 'leaflet', + leafletTileUrl: '', + leafletDarkTileUrl: '', googleMapsApiKey: '', googleMapsMapId: '', googleMapsMapType: 'roadmap', @@ -45,6 +48,25 @@ export default class MapSettingsService extends Service { return this.mapProvider === 'google'; } + get leafletTileUrl() { + return this.getLeafletTileUrl('light'); + } + + get leafletDarkTileUrl() { + return this.getLeafletTileUrl('dark'); + } + + /** + * Resolve the Leaflet tile URL for a theme, preferring the company's + * configured custom tile provider and falling back to the keyless default. + * + * @param {String} theme 'light' or 'dark' + * @return {String} + */ + getLeafletTileUrl(theme = 'light') { + return getLeafletTileUrl(this.settings, theme); + } + async load({ force = false } = {}) { if (!force && this.isLoaded) { return this.settings; diff --git a/addon/templates/settings/map.hbs b/addon/templates/settings/map.hbs index 065d99393..fe65273b5 100644 --- a/addon/templates/settings/map.hbs +++ b/addon/templates/settings/map.hbs @@ -31,6 +31,16 @@ /> + {{#if this.isLeafletSelected}} + + + + + + + + {{/if}} + {{#if this.isGoogleMapsSelected}}