diff --git a/apps/react-storybook/stories/map/OSMMap.stories.tsx b/apps/react-storybook/stories/map/OSMMap.stories.tsx
new file mode 100644
index 000000000000..3d21286ffdd4
--- /dev/null
+++ b/apps/react-storybook/stories/map/OSMMap.stories.tsx
@@ -0,0 +1,250 @@
+import type { Meta, StoryObj } from '@storybook/react-webpack5';
+
+import React, { useMemo } from 'react';
+import Map from 'devextreme-react/map';
+
+// OpenStreetMap (OSM) provider for the DevExtreme Map — powered by Leaflet.
+// The provider needs no API key of its own, but it does not bundle a tile/geocoding/routing
+// service: you supply them via `providerConfig` (tileServer / geocodeLocation / getRoute).
+//
+// This story lets you switch between several commercial OSM-based tile providers and paste your
+// own key for each (the "Tile provider" controls). The public OpenStreetMap tile server
+// (tile.openstreetmap.org) MUST NOT be used in production per the OSM Tile Usage Policy, so it is
+// intentionally not offered here. Routing uses the public OSRM demo server (evaluation only).
+//
+// NOTE: there is deliberately no "self-hosted" option in this published Storybook — a localhost
+// URL would point at the viewer's own machine, not a shared server. For the fully free, no-key,
+// self-hosted setup (tiles + routing + geocoding), run the OSM_SelfHosted_Server Docker stack
+// locally (see the devextreme-how-to-use-openstreetmap example repo).
+const OSM_ATTR = '© OpenStreetMap contributors';
+const markerUrl = 'https://js.devexpress.com/Demos/WidgetsGallery/JSDemos/images/maps/map-marker.png';
+
+type TileProvider = 'MapTiler' | 'Thunderforest' | 'Stadia Maps';
+type MapType = 'roadmap' | 'satellite' | 'hybrid';
+
+interface ProviderKeys {
+ maptiler: string;
+ thunderforest: string;
+ stadia: string;
+}
+
+// Resolve a Leaflet tile-layer config for the selected provider and map type. Each provider is a
+// function of the type so switching the "Map type" control re-resolves the tiles.
+const buildTileServer = (provider: TileProvider, type: string, keys: ProviderKeys) => {
+ switch (provider) {
+ case 'Thunderforest': {
+ // Thunderforest has no satellite/aerial imagery, so the type slots map to distinct
+ // cartographic styles to demonstrate type switching.
+ const style = { roadmap: 'atlas', satellite: 'landscape', hybrid: 'outdoors' }[type] ?? 'atlas';
+ return {
+ url: `https://{s}.tile.thunderforest.com/${style}/{z}/{x}/{y}.png?apikey=${keys.thunderforest}`,
+ attribution: `Maps © Thunderforest, ${OSM_ATTR}`,
+ subdomains: 'abc',
+ maxZoom: 22,
+ };
+ }
+ case 'Stadia Maps': {
+ const style = { roadmap: 'alidade_smooth', satellite: 'alidade_satellite', hybrid: 'alidade_satellite' }[type] ?? 'alidade_smooth';
+ return {
+ url: `https://tiles.stadiamaps.com/tiles/${style}/{z}/{x}/{y}.png?api_key=${keys.stadia}`,
+ attribution: `© Stadia Maps ${OSM_ATTR}`,
+ maxZoom: 20,
+ };
+ }
+ case 'MapTiler':
+ default: {
+ const style = { roadmap: 'streets-v2', satellite: 'satellite', hybrid: 'hybrid' }[type] ?? 'streets-v2';
+ return {
+ url: `https://api.maptiler.com/maps/${style}/{z}/{x}/{y}.png?key=${keys.maptiler}`,
+ attribution: `© MapTiler ${OSM_ATTR}`,
+ maxZoom: 20,
+ };
+ }
+ }
+};
+
+// Real road routing via the public OSRM demo server (evaluation only; host your own in production).
+const getRoute = ({ locations }: { locations: { lat: number; lng: number }[] }): Promise<[number, number][]> => {
+ const coords = locations.map((l) => `${l.lng},${l.lat}`).join(';');
+ return fetch(`https://router.project-osrm.org/route/v1/driving/${coords}?overview=full&geometries=geojson`)
+ .then((r) => r.json())
+ .then((res) => (res.routes[0].geometry.coordinates as [number, number][])
+ .map(([lng, lat]) => [lat, lng] as [number, number]));
+};
+
+const markersData = [
+ { location: { lat: 40.755833, lng: -73.986389 }, tooltip: { text: 'Times Square' } },
+ { location: { lat: 40.7825, lng: -73.966111 }, tooltip: { text: 'Central Park' } },
+ { location: { lat: 40.753889, lng: -73.981389 }, tooltip: { text: 'Fifth Avenue' } },
+ { location: { lat: 40.705748, lng: -73.996299 }, tooltip: { text: 'Brooklyn Bridge' } },
+];
+
+const routeWaypoints: [number, number][] = [
+ [40.7825, -73.966111],
+ [40.755833, -73.986389],
+ [40.753889, -73.981389],
+ [40.705748, -73.996299],
+];
+
+const centers: Record = {
+ 'New York': { lat: 40.74, lng: -73.985 },
+ London: { lat: 51.5074, lng: -0.1278 },
+ Tokyo: { lat: 35.6762, lng: 139.7649 },
+};
+
+// Custom args (not native Map props) used to drive the story controls.
+interface OsmStoryArgs {
+ tileProvider: TileProvider;
+ maptilerKey: string;
+ thunderforestKey: string;
+ stadiaKey: string;
+ type: MapType;
+ center: keyof typeof centers;
+ zoom: number;
+ controls: boolean;
+ disabled: boolean;
+ autoAdjust: boolean;
+ customMarkerIcons: boolean;
+ showMarkers: boolean;
+ showRoutes: boolean;
+ routeColor: string;
+ height: number;
+ width: string;
+}
+
+const meta: Meta = {
+ title: 'Map/OSM Provider',
+ component: Map,
+ parameters: { layout: 'fullscreen' },
+ argTypes: {
+ // --- Tile provider ---
+ tileProvider: {
+ control: 'select',
+ options: ['MapTiler', 'Thunderforest', 'Stadia Maps'],
+ table: { category: 'Tile provider' },
+ description: 'Which commercial OSM tile provider to render.',
+ },
+ maptilerKey: {
+ control: 'text',
+ table: { category: 'Tile provider' },
+ description: 'Your MapTiler API key (https://cloud.maptiler.com). Required to see MapTiler tiles.',
+ },
+ thunderforestKey: {
+ control: 'text',
+ table: { category: 'Tile provider' },
+ description: 'Your Thunderforest API key (https://www.thunderforest.com).',
+ },
+ stadiaKey: {
+ control: 'text',
+ table: { category: 'Tile provider' },
+ description: 'Your Stadia Maps API key (https://stadiamaps.com).',
+ },
+ // --- Map ---
+ type: { control: 'select', options: ['roadmap', 'satellite', 'hybrid'], table: { category: 'Map' } },
+ center: {
+ control: 'select', options: Object.keys(centers), table: { category: 'Map' },
+ description: 'Initial center — the map can be freely panned afterwards.',
+ },
+ zoom: {
+ control: { type: 'number', min: 1, max: 19 }, table: { category: 'Map' },
+ description: 'Initial zoom — the map can be freely zoomed afterwards.',
+ },
+ controls: { control: 'boolean', table: { category: 'Map' } },
+ disabled: { control: 'boolean', table: { category: 'Map' } },
+ autoAdjust: {
+ control: 'boolean',
+ table: { category: 'Map' },
+ description: 'Auto-fit the viewport to the markers/routes.',
+ },
+ // --- Markers ---
+ showMarkers: { control: 'boolean', table: { category: 'Markers' } },
+ customMarkerIcons: {
+ control: 'boolean',
+ table: { category: 'Markers' },
+ description: 'Use a custom pushpin image instead of the default Leaflet marker.',
+ },
+ // --- Routes ---
+ showRoutes: { control: 'boolean', table: { category: 'Routes' } },
+ routeColor: {
+ control: 'select',
+ options: ['blue', 'green', 'red', 'purple', 'orange'],
+ table: { category: 'Routes' },
+ },
+ // --- Layout ---
+ height: { control: 'number', table: { category: 'Layout' } },
+ width: { control: 'text', table: { category: 'Layout' } },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const render: Story['render'] = (args) => {
+ const {
+ tileProvider, maptilerKey, thunderforestKey, stadiaKey,
+ type, center, zoom, controls, disabled, autoAdjust,
+ customMarkerIcons, showMarkers, showRoutes, routeColor, height, width,
+ } = args;
+
+ // providerConfig identity changes only when the provider or a key changes, so the map rebuilds
+ // its tile layer then — not on every unrelated control change.
+ const providerConfig = useMemo(() => ({
+ tileServer: (t: string) => buildTileServer(tileProvider, t, {
+ maptiler: maptilerKey, thunderforest: thunderforestKey, stadia: stadiaKey,
+ }),
+ getRoute,
+ }), [tileProvider, maptilerKey, thunderforestKey, stadiaKey]);
+
+ const markers = useMemo(() => (showMarkers ? markersData : []), [showMarkers]);
+ const routes = useMemo(() => (showRoutes
+ ? [{ weight: 6, opacity: 0.6, color: routeColor, locations: routeWaypoints }]
+ : []), [showRoutes, routeColor]);
+
+ return (
+
+ );
+};
+
+export const Default: Story = {
+ args: {
+ tileProvider: 'MapTiler',
+ // Paste your own keys here in the controls panel to see tiles render.
+ maptilerKey: 'YOUR_MAPTILER_KEY',
+ thunderforestKey: 'YOUR_THUNDERFOREST_KEY',
+ stadiaKey: 'YOUR_STADIA_KEY',
+ type: 'roadmap',
+ center: 'New York',
+ zoom: 12,
+ controls: true,
+ disabled: false,
+ autoAdjust: false,
+ showMarkers: true,
+ customMarkerIcons: true,
+ showRoutes: true,
+ routeColor: 'blue',
+ height: 520,
+ width: '100%',
+ },
+ render,
+};
diff --git a/packages/devextreme-angular/src/ui/map/index.ts b/packages/devextreme-angular/src/ui/map/index.ts
index b27d9beaf4b7..456ad37ef587 100644
--- a/packages/devextreme-angular/src/ui/map/index.ts
+++ b/packages/devextreme-angular/src/ui/map/index.ts
@@ -22,7 +22,7 @@ import {
} from '@angular/core';
-import type { ClickEvent, DisposingEvent, InitializedEvent, MarkerAddedEvent, MarkerRemovedEvent, OptionChangedEvent, ReadyEvent, RouteAddedEvent, RouteRemovedEvent, MapProvider, RouteMode, MapType } from 'devextreme/ui/map';
+import type { ClickEvent, DisposingEvent, InitializedEvent, MarkerAddedEvent, MarkerRemovedEvent, OptionChangedEvent, ReadyEvent, RouteAddedEvent, RouteRemovedEvent, MapProvider, OsmGeocodeFunction, OsmGetRouteFunction, OsmTileServer, RouteMode, MapType } from 'devextreme/ui/map';
import DxMap from 'devextreme/ui/map';
@@ -302,10 +302,10 @@ export class DxMapComponent extends DxComponent implements OnDestroy, OnChanges,
*/
@Input()
- get providerConfig(): { mapId?: string, useAdvancedMarkers?: boolean } {
+ get providerConfig(): { geocodeLocation?: OsmGeocodeFunction, getRoute?: OsmGetRouteFunction, mapId?: string, tileServer?: OsmTileServer, useAdvancedMarkers?: boolean } {
return this._getOption('providerConfig');
}
- set providerConfig(value: { mapId?: string, useAdvancedMarkers?: boolean }) {
+ set providerConfig(value: { geocodeLocation?: OsmGeocodeFunction, getRoute?: OsmGetRouteFunction, mapId?: string, tileServer?: OsmTileServer, useAdvancedMarkers?: boolean }) {
this._setOption('providerConfig', value);
}
@@ -582,7 +582,7 @@ export class DxMapComponent extends DxComponent implements OnDestroy, OnChanges,
* This member supports the internal infrastructure and is not intended to be used directly from your code.
*/
- @Output() providerConfigChange: EventEmitter<{ mapId?: string, useAdvancedMarkers?: boolean }>;
+ @Output() providerConfigChange: EventEmitter<{ geocodeLocation?: OsmGeocodeFunction, getRoute?: OsmGetRouteFunction, mapId?: string, tileServer?: OsmTileServer, useAdvancedMarkers?: boolean }>;
/**
diff --git a/packages/devextreme-angular/src/ui/map/nested/provider-config.ts b/packages/devextreme-angular/src/ui/map/nested/provider-config.ts
index 0b75435ab9a5..6d0ad0edae10 100644
--- a/packages/devextreme-angular/src/ui/map/nested/provider-config.ts
+++ b/packages/devextreme-angular/src/ui/map/nested/provider-config.ts
@@ -14,6 +14,7 @@ import {
+import type { OsmGeocodeFunction, OsmGetRouteFunction, OsmTileServer } from 'devextreme/ui/map';
import {
DxIntegrationModule,
@@ -30,6 +31,22 @@ import { NestedOption } from 'devextreme-angular/core';
providers: [NestedOptionHost]
})
export class DxoMapProviderConfigComponent extends NestedOption implements OnDestroy, OnInit {
+ @Input()
+ get geocodeLocation(): OsmGeocodeFunction {
+ return this._getOption('geocodeLocation');
+ }
+ set geocodeLocation(value: OsmGeocodeFunction) {
+ this._setOption('geocodeLocation', value);
+ }
+
+ @Input()
+ get getRoute(): OsmGetRouteFunction {
+ return this._getOption('getRoute');
+ }
+ set getRoute(value: OsmGetRouteFunction) {
+ this._setOption('getRoute', value);
+ }
+
@Input()
get mapId(): string {
return this._getOption('mapId');
@@ -38,6 +55,14 @@ export class DxoMapProviderConfigComponent extends NestedOption implements OnDes
this._setOption('mapId', value);
}
+ @Input()
+ get tileServer(): OsmTileServer {
+ return this._getOption('tileServer');
+ }
+ set tileServer(value: OsmTileServer) {
+ this._setOption('tileServer', value);
+ }
+
@Input()
get useAdvancedMarkers(): boolean {
return this._getOption('useAdvancedMarkers');
diff --git a/packages/devextreme-angular/src/ui/nested/provider-config.ts b/packages/devextreme-angular/src/ui/nested/provider-config.ts
index c4ef29aa4db1..38792fdc46a5 100644
--- a/packages/devextreme-angular/src/ui/nested/provider-config.ts
+++ b/packages/devextreme-angular/src/ui/nested/provider-config.ts
@@ -14,6 +14,7 @@ import {
+import type { OsmGeocodeFunction, OsmGetRouteFunction, OsmTileServer } from 'devextreme/ui/map';
import {
DxIntegrationModule,
@@ -30,6 +31,22 @@ import { NestedOption } from 'devextreme-angular/core';
providers: [NestedOptionHost]
})
export class DxoProviderConfigComponent extends NestedOption implements OnDestroy, OnInit {
+ @Input()
+ get geocodeLocation(): OsmGeocodeFunction {
+ return this._getOption('geocodeLocation');
+ }
+ set geocodeLocation(value: OsmGeocodeFunction) {
+ this._setOption('geocodeLocation', value);
+ }
+
+ @Input()
+ get getRoute(): OsmGetRouteFunction {
+ return this._getOption('getRoute');
+ }
+ set getRoute(value: OsmGetRouteFunction) {
+ this._setOption('getRoute', value);
+ }
+
@Input()
get mapId(): string {
return this._getOption('mapId');
@@ -38,6 +55,14 @@ export class DxoProviderConfigComponent extends NestedOption implements OnDestro
this._setOption('mapId', value);
}
+ @Input()
+ get tileServer(): OsmTileServer {
+ return this._getOption('tileServer');
+ }
+ set tileServer(value: OsmTileServer) {
+ this._setOption('tileServer', value);
+ }
+
@Input()
get useAdvancedMarkers(): boolean {
return this._getOption('useAdvancedMarkers');
diff --git a/packages/devextreme-metadata/aspnet/enums.ts b/packages/devextreme-metadata/aspnet/enums.ts
index 06c672dca88b..54680970fd01 100644
--- a/packages/devextreme-metadata/aspnet/enums.ts
+++ b/packages/devextreme-metadata/aspnet/enums.ts
@@ -44,7 +44,7 @@ export const enums = {
Options: ['GaugeIndicator.type'],
},
GeoMapProvider: {
- Items: ['bing', 'google', 'googleStatic', 'azure'],
+ Items: ['bing', 'google', 'googleStatic', 'azure', 'osm'],
},
SchedulerViewType: {
Items: [
diff --git a/packages/devextreme-react/src/map.ts b/packages/devextreme-react/src/map.ts
index dfaee1be9225..62b7059bb811 100644
--- a/packages/devextreme-react/src/map.ts
+++ b/packages/devextreme-react/src/map.ts
@@ -8,7 +8,7 @@ import dxMap, {
import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component";
import NestedOption from "./core/nested-option";
-import type { ClickEvent, DisposingEvent, InitializedEvent, MarkerAddedEvent, MarkerRemovedEvent, ReadyEvent, RouteAddedEvent, RouteRemovedEvent, RouteMode } from "devextreme/ui/map";
+import type { ClickEvent, DisposingEvent, InitializedEvent, MarkerAddedEvent, MarkerRemovedEvent, ReadyEvent, RouteAddedEvent, RouteRemovedEvent, OsmGeocodeFunction, OsmGetRouteFunction, OsmTileServer, RouteMode } from "devextreme/ui/map";
type ReplaceFieldTypes = {
[P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P];
@@ -182,7 +182,10 @@ const Marker = Object.assign(_comp
// owners:
// Map
type IProviderConfigProps = React.PropsWithChildren<{
+ geocodeLocation?: OsmGeocodeFunction;
+ getRoute?: OsmGetRouteFunction;
mapId?: string;
+ tileServer?: OsmTileServer;
useAdvancedMarkers?: boolean;
}>
const _componentProviderConfig = (props: IProviderConfigProps) => {
diff --git a/packages/devextreme-vue/src/map.ts b/packages/devextreme-vue/src/map.ts
index 7e1c8fecdc49..2c1159e67499 100644
--- a/packages/devextreme-vue/src/map.ts
+++ b/packages/devextreme-vue/src/map.ts
@@ -14,6 +14,11 @@ import {
RouteRemovedEvent,
MapProvider,
MapType,
+ OsmGeocodeFunction,
+ OsmGetRouteFunction,
+ OsmGetRouteParams,
+ OsmTileServer,
+ OsmTileServerConfig,
RouteMode,
} from "devextreme/ui/map";
import { prepareConfigurationComponentConfig } from "./core/index";
@@ -244,11 +249,17 @@ const DxProviderConfigConfig = {
emits: {
"update:isActive": null,
"update:hoveredElement": null,
+ "update:geocodeLocation": null,
+ "update:getRoute": null,
"update:mapId": null,
+ "update:tileServer": null,
"update:useAdvancedMarkers": null,
},
props: {
+ geocodeLocation: [Object, Function] as PropType any))>,
+ getRoute: [Object, Function] as PropType any))>,
mapId: String,
+ tileServer: [Object, Function, String] as PropType string | OsmTileServerConfig | null | undefined)) | OsmTileServerConfig | string>,
useAdvancedMarkers: Boolean
}
};
diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs
index 9338d6973791..f3fb5da344dc 100644
--- a/packages/devextreme/eslint.config.mjs
+++ b/packages/devextreme/eslint.config.mjs
@@ -27,6 +27,16 @@ const compat = new FlatCompat({
allConfig: js.configs.all
});
+// Allow OSM/Leaflet domain identifiers used by the Map's OpenStreetMap provider
+// (feature/library names that cannot be renamed: the `osm` provider, Leaflet's `latlng`
+// event field, and the `subdomains` tile-layer option).
+const spellCheckerRule = spellCheckConfig
+ .map((config) => config?.rules?.['spellcheck/spell-checker'])
+ .find((rule) => Array.isArray(rule) && Array.isArray(rule[1]?.skipWords));
+if (spellCheckerRule) {
+ spellCheckerRule[1].skipWords.push('osm', 'latlng', 'subdomains');
+}
+
export default [
{
ignores: [
diff --git a/packages/devextreme/js/__internal/ui/map/map.ts b/packages/devextreme/js/__internal/ui/map/map.ts
index 8b987349f994..613b8a214490 100644
--- a/packages/devextreme/js/__internal/ui/map/map.ts
+++ b/packages/devextreme/js/__internal/ui/map/map.ts
@@ -21,6 +21,7 @@ import type { LocationOption } from './provider.dynamic';
import azure from './provider.dynamic.azure';
import bing from './provider.dynamic.bing';
import google from './provider.dynamic.google';
+import osm from './provider.dynamic.osm';
// NOTE external urls must have protocol explicitly specified
// (because inside Cordova package the protocol is "file:")
import googleStatic from './provider.google_static';
@@ -30,6 +31,7 @@ const PROVIDERS = {
googleStatic,
google,
bing,
+ osm,
};
const MAP_CLASS = 'dx-map';
@@ -54,7 +56,7 @@ class Map extends Widget {
_lastAsyncAction!: Promise;
- _provider!: azure | googleStatic | google | bing;
+ _provider!: azure | googleStatic | google | bing | osm;
_asyncActionSuppressed?: boolean;
@@ -262,7 +264,7 @@ class Map extends Widget {
}
_optionChanged(args: OptionChanged): void {
- const { name, value } = args;
+ const { name, fullName, value } = args;
const changeBag = this._optionChangeBag;
this._optionChangeBag = null;
@@ -336,8 +338,15 @@ class Map extends Widget {
this._queueAsyncAction('updateMarkers', this._rendered.markers, this._rendered.markers);
break;
case 'providerConfig':
- this._suppressAsyncAction = true;
- this._invalidate();
+ // The OSM tile server can be swapped at runtime without recreating the map.
+ // Any other providerConfig change requires a full provider re-initialization.
+ if (fullName === 'providerConfig.tileServer') {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this._queueAsyncAction('updateTileServer');
+ } else {
+ this._suppressAsyncAction = true;
+ this._invalidate();
+ }
break;
case 'onReady':
case 'onUpdated':
diff --git a/packages/devextreme/js/__internal/ui/map/provider.dynamic.osm.ts b/packages/devextreme/js/__internal/ui/map/provider.dynamic.osm.ts
new file mode 100644
index 000000000000..f5348714dcc9
--- /dev/null
+++ b/packages/devextreme/js/__internal/ui/map/provider.dynamic.osm.ts
@@ -0,0 +1,621 @@
+/* eslint-disable @typescript-eslint/no-misused-promises */
+import Color from '@js/color';
+import $ from '@js/core/renderer';
+import ajax from '@js/core/utils/ajax';
+import { noop } from '@js/core/utils/common';
+import { isDefined } from '@js/core/utils/type';
+import { getWindow } from '@js/core/utils/window';
+import type { RouteMode } from '@js/ui/map';
+import errors from '@js/ui/widget/ui.errors';
+
+import type {
+ LocationOption,
+ MarkerObject, MarkerOptions, PlainLocation, RouteObject, RouteOptions,
+} from './provider.dynamic';
+import DynamicProvider from './provider.dynamic';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+declare let L: any;
+
+const window = getWindow();
+
+let LEAFLET_JS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
+let LEAFLET_CSS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
+
+const DEFAULT_MAX_ZOOM = 19;
+const DEFAULT_SUBDOMAINS = 'abc';
+
+interface OsmTileServerConfig {
+ url: string;
+ attribution?: string;
+ subdomains?: string | string[];
+ maxZoom?: number;
+}
+
+type OsmTileServerObject = OsmTileServerConfig
+ | ((type: string) => string | OsmTileServerConfig | null | undefined);
+type OsmTileServerOption = string | OsmTileServerObject;
+
+export type OsmLocation = PlainLocation;
+
+// @ts-expect-error ts-error
+const osmMapsLoaded = (): boolean => Boolean(window.L?.map);
+
+// eslint-disable-next-line @typescript-eslint/init-declarations
+let osmMapsLoader: Promise | undefined;
+
+class OsmProvider extends DynamicProvider {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ _tileLayer?: any;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ _zoomControl?: any;
+
+ _currentTileType?: string;
+
+ _clickHandler?: (e: { latlng: OsmLocation; originalEvent: Event }) => void;
+
+ _viewChangeHandler?: () => void;
+
+ _preventZoomChangeEvent?: boolean;
+
+ _movementMode(type: RouteMode | string = ''): string {
+ const modes: Record = {
+ driving: 'driving',
+ walking: 'walking',
+ };
+
+ return modes[type] ?? 'driving';
+ }
+
+ _resolveLocation(location?: LocationOption | null): Promise {
+ return new Promise((resolve) => {
+ const latLng = this._getLatLng(location);
+ if (latLng) {
+ resolve(L.latLng(latLng.lat, latLng.lng));
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this._geocodeLocation(location as string).then((geocodedLocation) => {
+ resolve(geocodedLocation as unknown as OsmLocation);
+ });
+ }
+ });
+ }
+
+ _geocodeLocationImpl(location: string): Promise {
+ return new Promise((resolve) => {
+ if (!isDefined(location)) {
+ resolve(L.latLng(0, 0));
+ return;
+ }
+
+ const geocodeFn = this._option('providerConfig')?.geocodeLocation as ((query: string) => Promise<{ lat: number; lng: number } | null | undefined>) | undefined;
+
+ if (!geocodeFn) {
+ errors.log('W1031', location);
+ resolve(L.latLng(0, 0));
+ return;
+ }
+
+ geocodeFn(location).then((result) => {
+ if (result?.lat != null && result?.lng != null) {
+ resolve(L.latLng(result.lat, result.lng));
+ } else {
+ resolve(L.latLng(0, 0));
+ }
+ }).catch(() => {
+ resolve(L.latLng(0, 0));
+ });
+ });
+ }
+
+ _normalizeLocation(location: OsmLocation): { lat: number; lng: number } {
+ return {
+ lat: location.lat,
+ lng: location.lng,
+ };
+ }
+
+ _loadImpl(): Promise {
+ return new Promise((resolve, reject) => {
+ if (osmMapsLoaded()) {
+ resolve();
+ return;
+ }
+
+ osmMapsLoader ??= this._loadMapResources();
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ osmMapsLoader.then(() => {
+ if (osmMapsLoaded()) {
+ resolve();
+ return;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this._loadMapResources().then(() => {
+ if (osmMapsLoaded()) {
+ resolve();
+ } else {
+ reject(new Error('Leaflet (window.L) is not available after loading its resources.'));
+ }
+ }, reject);
+ }, reject);
+ });
+ }
+
+ _loadMapResources(): Promise {
+ return Promise.all([
+ this._loadMapScript(),
+ this._loadMapStyles(),
+ ]).then(() => {});
+ }
+
+ _loadMapScript(): Promise {
+ return new Promise((resolve, reject) => {
+ ajax.sendRequest({
+ url: LEAFLET_JS_URL,
+ dataType: 'script',
+ }).then(() => {
+ resolve();
+ }, reject);
+ });
+ }
+
+ _loadMapStyles(): Promise {
+ return new Promise((resolve, reject) => {
+ ajax.sendRequest({
+ url: LEAFLET_CSS_URL,
+ dataType: 'text',
+ }).then((css) => {
+ $('