Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/devextreme-angular/src/ui/map/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, OsmTileServer, RouteMode, MapType } from 'devextreme/ui/map';

import DxMap from 'devextreme/ui/map';

Expand Down Expand Up @@ -302,10 +302,10 @@ export class DxMapComponent extends DxComponent implements OnDestroy, OnChanges,

*/
@Input()
get providerConfig(): { mapId?: string, useAdvancedMarkers?: boolean } {
get providerConfig(): { mapId?: string, tileServer?: OsmTileServer | undefined, useAdvancedMarkers?: boolean } {
return this._getOption('providerConfig');
}
set providerConfig(value: { mapId?: string, useAdvancedMarkers?: boolean }) {
set providerConfig(value: { mapId?: string, tileServer?: OsmTileServer | undefined, useAdvancedMarkers?: boolean }) {
this._setOption('providerConfig', value);
}

Expand Down Expand Up @@ -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<{ mapId?: string, tileServer?: OsmTileServer | undefined, useAdvancedMarkers?: boolean }>;

/**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {



import type { OsmTileServer } from 'devextreme/ui/map';

import {
DxIntegrationModule,
Expand All @@ -38,6 +39,14 @@ export class DxoMapProviderConfigComponent extends NestedOption implements OnDes
this._setOption('mapId', value);
}

@Input()
get tileServer(): OsmTileServer | undefined {
return this._getOption('tileServer');
}
set tileServer(value: OsmTileServer | undefined) {
this._setOption('tileServer', value);
}

@Input()
get useAdvancedMarkers(): boolean {
return this._getOption('useAdvancedMarkers');
Expand Down
9 changes: 9 additions & 0 deletions packages/devextreme-angular/src/ui/nested/provider-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {



import type { OsmTileServer } from 'devextreme/ui/map';

import {
DxIntegrationModule,
Expand All @@ -38,6 +39,14 @@ export class DxoProviderConfigComponent extends NestedOption implements OnDestro
this._setOption('mapId', value);
}

@Input()
get tileServer(): OsmTileServer | undefined {
return this._getOption('tileServer');
}
set tileServer(value: OsmTileServer | undefined) {
this._setOption('tileServer', value);
}

@Input()
get useAdvancedMarkers(): boolean {
return this._getOption('useAdvancedMarkers');
Expand Down
2 changes: 1 addition & 1 deletion packages/devextreme-metadata/aspnet/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const enums = {
Options: ['GaugeIndicator.type'],
},
GeoMapProvider: {
Items: ['bing', 'google', 'googleStatic', 'azure'],
Items: ['bing', 'google', 'googleStatic', 'azure', 'osm'],
},
SchedulerViewType: {
Items: [
Expand Down
3 changes: 2 additions & 1 deletion packages/devextreme-react/src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, OsmTileServer, RouteMode } from "devextreme/ui/map";

type ReplaceFieldTypes<TSource, TReplacement> = {
[P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P];
Expand Down Expand Up @@ -183,6 +183,7 @@ const Marker = Object.assign<typeof _componentMarker, NestedComponentMeta>(_comp
// Map
type IProviderConfigProps = React.PropsWithChildren<{
mapId?: string;
tileServer?: OsmTileServer | undefined;
useAdvancedMarkers?: boolean;
}>
const _componentProviderConfig = (props: IProviderConfigProps) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme-vue/src/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
RouteRemovedEvent,
MapProvider,
MapType,
OsmTileServer,
OsmTileServerConfig,
RouteMode,
} from "devextreme/ui/map";
import { prepareConfigurationComponentConfig } from "./core/index";
Expand Down Expand Up @@ -245,10 +247,12 @@ const DxProviderConfigConfig = {
"update:isActive": null,
"update:hoveredElement": null,
"update:mapId": null,
"update:tileServer": null,
"update:useAdvancedMarkers": null,
},
props: {
mapId: String,
tileServer: [Object, Function, String] as PropType<OsmTileServer | (((type: MapType) => string | OsmTileServerConfig | null | undefined)) | OsmTileServerConfig | string>,
useAdvancedMarkers: Boolean
}
};
Expand Down
6 changes: 6 additions & 0 deletions packages/devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export default [
],
},
...spellCheckConfig,
{
files: ['js/ui/map_types.d.ts'],
rules: {
'spellcheck/spell-checker': 'off',
},
},
{
plugins: {
'no-only-tests': noOnlyTests,
Expand Down
4 changes: 4 additions & 0 deletions packages/devextreme/js/__internal/ui/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export default errorUtils(errors.ERROR_MESSAGES, {

E1068: '\'aiIntegration\' is not configured for the AI Assistant.',

E1069: 'Could not find OpenLayers. Load the library before Map initialization.',

W1001: 'The "key" option cannot be modified after initialization',

W1002: 'An item with the key \'{0}\' does not exist',
Expand Down Expand Up @@ -160,4 +162,6 @@ export default errorUtils(errors.ERROR_MESSAGES, {
W1027: 'A prompt should be specified for a custom command.',
W1028: 'Nested/banded columns do not support the following properties: {0}.',
W1029: '\'hiddenWeekDays\' must leave at least one weekday visible.',
W1030: '\'tileServer\' is not configured for the OSM map provider.',
W1032: '\'attribution\' is not configured for the OSM tile server.',
});
7 changes: 6 additions & 1 deletion packages/devextreme/js/__internal/ui/map/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type { LocationOption } from './provider.dynamic';
import azure from './provider.dynamic.azure';
import bing from './provider.dynamic.bing';
import google from './provider.dynamic.google';
// eslint-disable-next-line spellcheck/spell-checker -- OpenStreetMap provider identifier
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';
Expand All @@ -30,6 +32,8 @@ const PROVIDERS = {
googleStatic,
google,
bing,
// eslint-disable-next-line spellcheck/spell-checker -- OpenStreetMap provider identifier
osm,
};

const MAP_CLASS = 'dx-map';
Expand All @@ -54,7 +58,8 @@ class Map extends Widget<MapProperties> {

_lastAsyncAction!: Promise<void>;

_provider!: azure | googleStatic | google | bing;
// eslint-disable-next-line spellcheck/spell-checker -- OpenStreetMap provider identifier
_provider!: azure | googleStatic | google | bing | osm;

_asyncActionSuppressed?: boolean;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { MapLocation } from '@js/ui/map';

export interface MapEngineTileLayerOptions {
attribution: string;
maxZoom: number;
// eslint-disable-next-line spellcheck/spell-checker -- tile server option name
subdomains?: string | string[];
url: string;
}

export interface MapEngineSetViewOptions {
center?: MapLocation;
zoom?: number;
}

export interface MapEngineMap {
readonly originalMap: unknown;
dispose: () => void;
replaceTileLayer: (options: MapEngineTileLayerOptions) => void;
setView: (options: MapEngineSetViewOptions) => void;
updateDimensions: () => void;
}

export interface MapEngine {
createMap: (container: Element, view?: MapEngineSetViewOptions) => MapEngineMap;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type { MapLocation } from '@js/ui/map';

import type {
MapEngine,
MapEngineMap,
MapEngineSetViewOptions,
MapEngineTileLayerOptions,
} from './provider.dynamic.osm.engine';

type Options = Record<string, unknown>;
type Coordinate = [number, number];

interface ViewLike {
setCenter: (center: Coordinate) => void;
setZoom: (zoom: number) => void;
}

interface TileLayerLike {
setSource: (source: unknown) => void;
}

interface MapLike {
addLayer: (layer: unknown) => void;
getView: () => ViewLike;
removeLayer: (layer: unknown) => void;
setTarget: (target?: Element) => void;
updateSize: () => void;
}

interface OpenLayersApi {
Map: new (options: Options) => MapLike;
View: new (options: Options) => ViewLike;
control: {
defaults: {
defaults: (options?: Options) => unknown;
};
};
interaction: {
defaults: {
defaults: (options?: Options) => unknown;
};
};
layer: {
Tile: new (options: Options) => TileLayerLike;
};
proj: {
fromLonLat: (coordinate: Coordinate) => Coordinate;
};
source: {
ImageTile: new (options: Options) => unknown;
};
}

const isRecord = (value: unknown): value is Record<string, unknown> => (
Boolean(value) && (typeof value === 'object' || typeof value === 'function')
);

const hasFunction = (value: unknown, property: string): boolean => (
isRecord(value) && typeof value[property] === 'function'
);

const isOpenLayersApi = (api: unknown): api is OpenLayersApi => {
if (!isRecord(api)) {
return false;
}

return typeof api.Map === 'function'
&& typeof api.View === 'function'
&& isRecord(api.control)
&& isRecord(api.control.defaults)
&& hasFunction(api.control.defaults, 'defaults')
&& isRecord(api.interaction)
&& isRecord(api.interaction.defaults)
&& hasFunction(api.interaction.defaults, 'defaults')
&& hasFunction(api.layer, 'Tile')
&& hasFunction(api.proj, 'fromLonLat')
&& hasFunction(api.source, 'ImageTile');
};

const toCoordinate = (api: OpenLayersApi, location: MapLocation): Coordinate => (
api.proj.fromLonLat([location.lng, location.lat])
);

const createTileUrlList = (
url: string,
// eslint-disable-next-line spellcheck/spell-checker -- tile server option name
subdomains: MapEngineTileLayerOptions['subdomains'],
): string[] => {
// eslint-disable-next-line spellcheck/spell-checker -- tile server option name
const values = Array.isArray(subdomains) ? subdomains : [...(subdomains ?? '')];

return values.map((value) => url.replace('{s}', value));
};

class OpenLayersMap implements MapEngineMap {
readonly originalMap: MapLike;

private readonly _keyboardEventTarget: Element;

private readonly _ownsKeyboardTabIndex: boolean;

private _tileLayer?: TileLayerLike;

private _disposed = false;

constructor(
private readonly _api: OpenLayersApi,
container: Element,
view: MapEngineSetViewOptions = {},
) {
const rootNode = container.getRootNode();
const ShadowRootConstructor = container.ownerDocument.defaultView?.ShadowRoot;
const shadowRoot = ShadowRootConstructor && rootNode instanceof ShadowRootConstructor
? rootNode
: undefined;

this._keyboardEventTarget = shadowRoot?.host ?? container;
this._ownsKeyboardTabIndex = !this._keyboardEventTarget.hasAttribute('tabindex');
if (this._ownsKeyboardTabIndex) {
this._keyboardEventTarget.setAttribute('tabindex', '0');
}

this.originalMap = new _api.Map({
controls: _api.control.defaults.defaults({ attribution: true, rotate: false, zoom: false }),
interactions: _api.interaction.defaults.defaults({ onFocusOnly: false }),
target: container,
view: new _api.View({
center: toCoordinate(_api, view.center ?? { lat: 0, lng: 0 }),
zoom: view.zoom ?? 1,
}),
});
}

dispose(): void {
if (this._disposed) {
return;
}

this._disposed = true;
if (this._tileLayer) {
this.originalMap.removeLayer(this._tileLayer);
this._tileLayer = undefined;
}
this.originalMap.setTarget(undefined);
if (this._ownsKeyboardTabIndex && this._keyboardEventTarget.getAttribute('tabindex') === '0') {
this._keyboardEventTarget.removeAttribute('tabindex');
}
}

replaceTileLayer(options: MapEngineTileLayerOptions): void {
const source = new this._api.source.ImageTile({
attributions: options.attribution,
maxZoom: options.maxZoom,
url: options.url.includes('{s}')
// eslint-disable-next-line spellcheck/spell-checker -- tile server option name
? createTileUrlList(options.url, options.subdomains)
: options.url,
});

if (this._tileLayer) {
this._tileLayer.setSource(source);
return;
}

this._tileLayer = new this._api.layer.Tile({ source });
this.originalMap.addLayer(this._tileLayer);
}

setView(options: MapEngineSetViewOptions): void {
const view = this.originalMap.getView();

if (options.center) {
view.setCenter(toCoordinate(this._api, options.center));
}
if (options.zoom !== undefined) {
view.setZoom(options.zoom);
}
}

updateDimensions(): void {
this.originalMap.updateSize();
}
}

export const createOpenLayersEngine = (api: unknown): MapEngine | undefined => (
isOpenLayersApi(api)
? { createMap: (container, view) => new OpenLayersMap(api, container, view) }
: undefined
);
Loading
Loading