Skip to content
Open
6 changes: 5 additions & 1 deletion apps/react-storybook/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ const config: StorybookConfig = {

docs: {},

staticDirs: ['../stories/assets', '../node_modules/devextreme/dist'],
staticDirs: [
'../stories/assets',
'../node_modules/devextreme/dist',
{ from: '../../demos/images/Maps', to: '/images/maps' },
],

typescript: {
reactDocgen: "react-docgen-typescript"
Expand Down
119 changes: 102 additions & 17 deletions apps/react-storybook/stories/map/OSMMap.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { useArgs } from 'storybook/preview-api';
import { fn } from 'storybook/test';

import Attribution from 'ol/control/Attribution.js';
import type OpenLayersMap from 'ol/Map.js';
import 'ol/ol.css';
import { fromLonLat, transformExtent } from 'ol/proj.js';
import View from 'ol/View.js';
import React from 'react';
import Button from 'devextreme-react/button';
import Map, { type MapRef } from 'devextreme-react/map';
import type {
MapLocation,
Expand All @@ -17,15 +19,53 @@ import 'devextreme/ui/map/openlayers';

const CENTER = { lat: 40.7484, lng: -73.9857 };
const CENTRAL_PARK_CENTER = { lat: 40.7829, lng: -73.9654 };
const DEFAULT_MARKER_LOCATION = 'Empire State Building';
const CUSTOM_MARKER_LOCATION = 'Bryant Park';
const ADDED_MARKER_LOCATION = 'Times Square';
const EXTENT: [number, number, number, number] = [-74.08, 40.67, -73.85, 40.88];
const TILE_SERVER = {
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
};
const PROVIDER_CONFIG = { tileServer: () => TILE_SERVER };
const MARKER_LOCATIONS: Record<string, MapLocation> = {
[DEFAULT_MARKER_LOCATION]: CENTER,
[CUSTOM_MARKER_LOCATION]: { lat: 40.7536, lng: -73.9832 },
[ADDED_MARKER_LOCATION]: { lat: 40.758, lng: -73.9855 },
};
const PROVIDER_CONFIG = {
calculateLocation: (query: string): Promise<MapLocation | undefined> => (
Promise.resolve(MARKER_LOCATIONS[query])
),
tileServer: () => TILE_SERVER,
};
const handleMarkerClick = fn();
const DEFAULT_MARKER = {
location: DEFAULT_MARKER_LOCATION,
onClick: handleMarkerClick,
};
const CUSTOM_MARKER = {
location: CUSTOM_MARKER_LOCATION,
iconSrc: 'images/maps/map-marker.png',
onClick: handleMarkerClick,
};
const ADDED_MARKER = {
location: ADDED_MARKER_LOCATION,
onClick: handleMarkerClick,
};
const STORY_STYLE: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: 8,
};
const TOOLBAR_STYLE: React.CSSProperties = {
display: 'flex',
gap: 8,
};

interface OsmStoryArgs {
autoAdjust: boolean;
centerOnCentralPark: boolean;
controls: boolean;
disabled: boolean;
Expand Down Expand Up @@ -61,6 +101,7 @@ const configureOpenLayersMap = (
};

const OsmMapStory = ({
autoAdjust,
centerOnCentralPark,
controls,
disabled,
Expand All @@ -71,29 +112,68 @@ const OsmMapStory = ({
zoom,
}: OsmMapStoryProps): React.ReactElement => {
const mapRef = React.useRef<MapRef>(null);
const [markerAdded, setMarkerAdded] = React.useState(false);
const markers = React.useMemo(() => [DEFAULT_MARKER, CUSTOM_MARKER], []);
const center = centerOnCentralPark ? CENTRAL_PARK_CENTER : CENTER;

React.useEffect(() => {
mapRef.current?.instance()?.option('center', center);
}, [center]);

const addMarker = (): void => {
const map = mapRef.current?.instance();
if (!map || markerAdded) {
return;
}

setMarkerAdded(true);
void map.addMarker(ADDED_MARKER).then(undefined, () => setMarkerAdded(false));
};

const removeMarker = (): void => {
const map = mapRef.current?.instance();
if (!map || !markerAdded) {
return;
}

setMarkerAdded(false);
void map.removeMarker(ADDED_MARKER).then(undefined, () => setMarkerAdded(true));
};

return (
<Map
ref={mapRef}
provider="osm"
providerConfig={PROVIDER_CONFIG}
defaultCenter={CENTER}
controls={controls}
disabled={disabled}
focusStateEnabled={focusStateEnabled}
rtlEnabled={rtlEnabled}
type={type}
zoom={zoom}
height={520}
width="100%"
onReady={(event) => configureOpenLayersMap(event, center, zoom)}
onZoomChange={(value) => updateArgs({ zoom: value })}
/>
<div style={STORY_STYLE}>
<div style={TOOLBAR_STYLE}>
<Button
text="Add Marker"
type="default"
disabled={markerAdded}
onClick={addMarker}
/>
<Button
text="Remove Marker"
disabled={!markerAdded}
onClick={removeMarker}
/>
</div>
<Map
ref={mapRef}
provider="osm"
providerConfig={PROVIDER_CONFIG}
autoAdjust={autoAdjust}
defaultCenter={CENTER}
controls={controls}
disabled={disabled}
focusStateEnabled={focusStateEnabled}
markers={markers}
rtlEnabled={rtlEnabled}
type={type}
zoom={zoom}
height={520}
width="100%"
onReady={(event) => configureOpenLayersMap(event, center, zoom)}
onZoomChange={(value) => updateArgs({ zoom: value })}
/>
</div>
);
};

Expand All @@ -109,6 +189,10 @@ const meta: Meta<OsmStoryArgs> = {
layout: 'fullscreen',
},
argTypes: {
autoAdjust: {
control: 'boolean',
description: 'Automatically adjusts the map viewport when markers change.',
},
centerOnCentralPark: {
control: 'boolean',
description: 'Switches the map center between the default New York location and Central Park.',
Expand Down Expand Up @@ -148,6 +232,7 @@ type Story = StoryObj<OsmStoryArgs>;

export const Default: Story = {
args: {
autoAdjust: false,
centerOnCentralPark: false,
controls: true,
disabled: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$map-marker-color: #0f6cbd !default;
$map-marker-accent-color: #fafafa !default;
10 changes: 10 additions & 0 deletions packages/devextreme-scss/scss/widgets/fluent/map/_index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
@use "colors" as *;
@use "../../base/map";

.dx-map-marker-default-body {
fill: $map-marker-color;
stroke: $map-marker-accent-color;
}

.dx-map-marker-default-center {
fill: $map-marker-accent-color;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$map-marker-color: #337ab7 !default;
$map-marker-accent-color: #fff !default;
10 changes: 10 additions & 0 deletions packages/devextreme-scss/scss/widgets/generic/map/_index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
@use "colors" as *;
@use "../../base/map";

.dx-map-marker-default-body {
fill: $map-marker-color;
stroke: $map-marker-accent-color;
}

.dx-map-marker-default-center {
fill: $map-marker-accent-color;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$map-marker-color: #1564c0 !default;
$map-marker-accent-color: #fff !default;
10 changes: 10 additions & 0 deletions packages/devextreme-scss/scss/widgets/material/map/_index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
@use "colors" as *;
@use "../../base/map";

.dx-map-marker-default-body {
fill: $map-marker-color;
stroke: $map-marker-accent-color;
}

.dx-map-marker-default-center {
fill: $map-marker-accent-color;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface MapEngineBounds {
southWest: MapLocation;
}

export interface MapEngineFitBoundsOptions {
includeMarkerPadding?: boolean;
}

export interface MapEngineViewState extends MapEngineSetViewOptions {
bounds?: MapEngineBounds;
}
Expand All @@ -28,22 +32,38 @@ export interface MapEngineClickEvent {
location: MapLocation;
}

export interface MapEngineMarkerOptions {
html?: string;
htmlOffset?: { top: number; left: number };
iconSrc?: string;
location: MapLocation;
onClick?: () => void;
}

export interface MapEngineMarker {
readonly originalMarker: unknown;
dispose: () => void;
}

export interface MapEngineEventHandlers {
click: (event: MapEngineClickEvent) => void;
markerSizeChange: () => void;
viewChange: (view: MapEngineViewState) => void;
}

export interface MapEngineMap {
readonly originalMap: unknown;
addMarker: (options: MapEngineMarkerOptions) => MapEngineMarker;
attachHandlers: (handlers: MapEngineEventHandlers) => void;
dispose: () => void;
fitBounds: (bounds: MapEngineBounds) => void;
fitBounds: (bounds: MapEngineBounds, options?: MapEngineFitBoundsOptions) => void;
getZoom: () => number | undefined;
replaceTileLayer: (options: MapEngineTileLayerOptions) => void;
setControls: (visible: boolean) => void;
setDisabled: (disabled: boolean) => void;
setFocus: (enabled: boolean, tabIndex: number) => void;
setView: (options: MapEngineSetViewOptions) => void;
updateDimensions: () => void;
updateDimensions: () => boolean;
}

export interface MapEngine {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { MapEngineMarkerOptions } from './provider.dynamic.osm.engine';

export const MARKER_FALLBACK_WIDTH = 25;
export const MARKER_FALLBACK_HEIGHT = 41;
export const DEFAULT_MARKER_SIZE = 44;

const MARKER_CLASS = 'dx-map-marker';
export const DEFAULT_MARKER_CLASS = `${MARKER_CLASS}-default`;
const DEFAULT_MARKER_ICON_CLASS = `${DEFAULT_MARKER_CLASS}-icon`;
const DEFAULT_MARKER_BODY_CLASS = `${DEFAULT_MARKER_CLASS}-body`;
const DEFAULT_MARKER_CENTER_CLASS = `${DEFAULT_MARKER_CLASS}-center`;
const IMAGE_MARKER_CLASS = `${MARKER_CLASS}-image`;
const DEFAULT_MARKER_WIDTH = 24.5;
const DEFAULT_MARKER_HEIGHT = 36.5;
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const DEFAULT_MARKER_VIEW_BOX = '5 2 14 20';
const DEFAULT_MARKER_BODY_PATH = [
'M12 2c-3.85 0-7 3.176-7 7.059',
' 0 5.294 7 12.941 7 12.941',
's7-7.765 7-12.941',
'c0-3.882-3.15-7.059-7-7.059z',
].join('');
const DEFAULT_MARKER_CENTER_PATH = [
'M12 6.706c-1.283 0-2.333 1.059-2.333 2.353',
's1.05 2.353 2.333 2.353',
' 2.333-1.059 2.333-2.353',
'c0-1.294-1.05-2.353-2.333-2.353z',
].join('');

const createDefaultMarkerElement = (
ownerDocument: Document,
): HTMLElement => {
const element = ownerDocument.createElement('div');
const svg = ownerDocument.createElementNS(SVG_NAMESPACE, 'svg');
const body = ownerDocument.createElementNS(SVG_NAMESPACE, 'path');
const center = ownerDocument.createElementNS(SVG_NAMESPACE, 'path');

element.className = `${MARKER_CLASS} ${DEFAULT_MARKER_CLASS}`;
element.style.alignItems = 'flex-end';
element.style.display = 'flex';
element.style.height = `${DEFAULT_MARKER_SIZE}px`;
element.style.justifyContent = 'center';
element.style.width = `${DEFAULT_MARKER_SIZE}px`;
svg.setAttribute('class', DEFAULT_MARKER_ICON_CLASS);
svg.setAttribute('viewBox', DEFAULT_MARKER_VIEW_BOX);
svg.setAttribute('width', `${DEFAULT_MARKER_WIDTH}`);
svg.setAttribute('height', `${DEFAULT_MARKER_HEIGHT}`);
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
svg.style.display = 'block';
svg.style.filter = [
'drop-shadow(0 0 0.5px rgba(255, 255, 255, 0.85))',
'drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.35))',
].join(' ');
svg.style.overflow = 'visible';
body.setAttribute('class', DEFAULT_MARKER_BODY_CLASS);
body.setAttribute('d', DEFAULT_MARKER_BODY_PATH);
body.setAttribute('stroke-width', '0.5');
center.setAttribute('class', DEFAULT_MARKER_CENTER_CLASS);
center.setAttribute('d', DEFAULT_MARKER_CENTER_PATH);
svg.appendChild(body);
svg.appendChild(center);
element.appendChild(svg);

return element;
};

export const createMarkerElement = (
ownerDocument: Document,
options: MapEngineMarkerOptions,
): { element: HTMLElement; offset: number[]; positioning: string } => {
if (options.html) {
const element = ownerDocument.createElement('div');
element.innerHTML = options.html;

return {
element,
offset: [options.htmlOffset?.left ?? 0, options.htmlOffset?.top ?? 0],
positioning: 'top-left',
};
}

if (options.iconSrc) {
const element = ownerDocument.createElement('img');
element.className = `${MARKER_CLASS} ${IMAGE_MARKER_CLASS}`;
element.src = options.iconSrc;
element.alt = '';
element.draggable = false;

return { element, offset: [0, 0], positioning: 'bottom-center' };
}

return {
element: createDefaultMarkerElement(ownerDocument),
offset: [0, 0],
positioning: 'bottom-center',
};
};
Loading
Loading