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
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
@@ -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