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
110 changes: 93 additions & 17 deletions apps/react-storybook/stories/map/OSMMap.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 +18,45 @@ 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 DEFAULT_MARKER = { location: DEFAULT_MARKER_LOCATION };
const CUSTOM_MARKER = {
location: CUSTOM_MARKER_LOCATION,
iconSrc: 'images/maps/map-marker.png',
};
const ADDED_MARKER = { location: ADDED_MARKER_LOCATION };
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 +92,7 @@ const configureOpenLayersMap = (
};

const OsmMapStory = ({
autoAdjust,
centerOnCentralPark,
controls,
disabled,
Expand All @@ -71,29 +103,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 +180,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 +223,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
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;
export const DEFAULT_MARKER_CLASS = 'dx-map-marker-default';

const DEFAULT_MARKER_WIDTH = 24.5;
const DEFAULT_MARKER_HEIGHT = 36.5;
const DEFAULT_MARKER_COLOR = '#2d7fbd';
const DEFAULT_MARKER_ACCENT_COLOR = '#fff';
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 = `dx-map-marker ${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', 'dx-map-marker-default-icon');
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', 'dx-map-marker-default-body');
body.setAttribute('d', DEFAULT_MARKER_BODY_PATH);
body.setAttribute('fill', DEFAULT_MARKER_COLOR);
body.setAttribute('stroke', DEFAULT_MARKER_ACCENT_COLOR);
body.setAttribute('stroke-width', '0.5');
center.setAttribute('class', 'dx-map-marker-default-center');
center.setAttribute('d', DEFAULT_MARKER_CENTER_PATH);
center.setAttribute('fill', DEFAULT_MARKER_ACCENT_COLOR);
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 = 'dx-map-marker dx-map-marker-image';
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