Skip to content
Merged
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
2 changes: 2 additions & 0 deletions memory-bank/usage/map.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ graph TD
- `id`: Unique identifier for the map instance (required)
- `zoom`: Optional zoom level (inherited from MapBaseProps)
- `className`: Optional CSS class name (inherited from MapBaseProps)
- `forceAspectRatio`: Optional boolean to force aspect ratio (16:9 for Desktop, 4:3 for Mobile), `true` by default (inherited from MapBaseProps)
- `disableControls`: Optional boolean to hide map controls (Yandex Maps only), `false` by default
- `disableBalloons`: Optional boolean to disable info balloons (Yandex Maps only), `false` by default
- `areaMargin:` - Optional offset (in pixels) for the marked area of the map relative to the map's container (`30` by default)

#### MapBaseProps (Common Props)

Expand Down
2 changes: 2 additions & 0 deletions src/components/Map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Map

`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker. Only for `Yandex maps`. `false` by default

`areaMargin?: number | [number, number] | [number, number, number]` - Offset (in pixels) for the marked area of the map relative to the map's container. Only for `Yandex maps`. `30` by default

`markers?: object[]` - Description for placemarkers. You need to use it for `Yandex maps`. Specify the parameters given below.

- `address?: string` — Place name, address
Expand Down
73 changes: 53 additions & 20 deletions src/components/Map/YMap/YMap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {YMapMarkerLabelPrivate, YMapMarkerPrivate, YMapProps} from '../../../models';
import {Coordinate} from '../../../models/constructor-items/common';

import {ParsedMargin, calculateMapParamsWithMarginAndZoom, parseMargin} from './utils';

enum GeoObjectTypes {
Properties = 'properties',
Options = 'options',
Expand All @@ -26,7 +28,7 @@ const geoObjectPropsAndOptions: Record<keyof YMapMarkerLabelPrivate, GeoObjectTy
preset: GeoObjectTypes.Options,
};

type PlacemarksProps = Pick<YMapProps, 'zoom' | 'markers'>;
type PlacemarksProps = Pick<YMapProps, 'zoom' | 'markers' | 'areaMargin'>;

export class YMap {
private ymap: Ymaps.Map;
Expand Down Expand Up @@ -97,45 +99,76 @@ export class YMap {
});
}

// eslint-disable-next-line complexity
private recalcZoomAndCenter(props: PlacemarksProps) {
const coordsLength = this.coords.length;
const {zoom = 0} = props;
const {zoom = 0, areaMargin} = props;

if (!coordsLength) {
return;
}

let leftBottom = [Infinity, Infinity],
rightTop = [-Infinity, -Infinity];
const utils = window.ymaps.util.bounds;

this.coords.forEach((point) => {
leftBottom = [Math.min(leftBottom[0], point[0]), Math.min(leftBottom[1], point[1])];
rightTop = [Math.max(rightTop[0], point[0]), Math.max(rightTop[1], point[1])];
});
const [leftTop, rightBottom] = utils.fromPoints(this.coords);

let newMapParams = {
zoom,
center: [],
};

if (zoom) {
// compute only the center
newMapParams.center = window.ymaps.util.bounds.getCenter([leftBottom, rightTop]);
} else {
newMapParams = window.ymaps.util.bounds.getCenterAndZoom(
[leftBottom, rightTop],
[this.mapRef?.clientWidth, this.mapRef?.clientHeight],
undefined,
{margin: DEFAULT_MAP_CONTROL_BUTTON_HEIGHT},
);
const parsedAreaMargin = areaMargin
? parseMargin(areaMargin)
: ([0, 0, 0, 0] as ParsedMargin);

const hasZoom = Boolean(zoom);
const hasAreaMargin = parsedAreaMargin.some(Boolean);
const containerSize = [
this.mapRef?.clientWidth ?? 0,
this.mapRef?.clientHeight ?? 0,
] as Coordinate;

switch (true) {
case hasAreaMargin && hasZoom:
// calculate center and zoom in accordace with current zoom and margin
newMapParams = calculateMapParamsWithMarginAndZoom(
[leftTop, rightBottom],
zoom,
parsedAreaMargin,
containerSize,
);
break;
case hasAreaMargin:
// calculate center and zoom with custom margin
newMapParams = utils.getCenterAndZoom(
[leftTop, rightBottom],
containerSize,
undefined,
{margin: areaMargin, preciseZoom: true},
);
break;
case hasZoom:
// calculate only center
newMapParams.center = utils.getCenter([leftTop, rightBottom]);
break;
default:
// calculate center and zoom with default margin
newMapParams = utils.getCenterAndZoom(
[leftTop, rightBottom],
containerSize,
undefined,
{margin: DEFAULT_MAP_CONTROL_BUTTON_HEIGHT},
);
}

this.ymap.setCenter(newMapParams.center);

// Use default zoom for one placemark
if (coordsLength > 1 && !zoom) {
this.ymap.setZoom(newMapParams.zoom);
if (coordsLength <= 1 && !hasAreaMargin && hasZoom) {
return;
}

this.ymap.setZoom(newMapParams.zoom);
}

private clearOldPlacemarks() {
Expand Down
5 changes: 3 additions & 2 deletions src/components/Map/YMap/YandexMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const YandexMap = (props: YMapProps) => {
id,
disableControls = false,
disableBalloons = false,
areaMargin,
className,
forceAspectRatio = true,
} = props;
Expand Down Expand Up @@ -127,14 +128,14 @@ const YandexMap = (props: YMapProps) => {
}))
: markers;

await ymap.showPlacemarks({markers: privateMarkers, zoom});
await ymap.showPlacemarks({markers: privateMarkers, zoom, areaMargin});

setReady(true);
};

showPlacemarks();
}
}, [ymap, markers, zoom, disableBalloons]);
}, [ymap, markers, zoom, disableBalloons, areaMargin]);

if (!markers) return null;

Expand Down
130 changes: 130 additions & 0 deletions src/components/Map/YMap/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {Coordinate, YMapMargin} from '../../../models';

export type ParsedMargin = [top: number, right: number, bottom: number, left: number];

export const parseMargin = (margin: YMapMargin): ParsedMargin => {
if (!Array.isArray(margin)) {
return [margin, margin, margin, margin];
}

if (margin.length === 2) {
return [margin[0], margin[1], margin[0], margin[1]];
}

return margin;
};

export const calcPixelBounds = (
[leftTop, rightBottom]: [Coordinate, Coordinate],
zoom: number,
containerSize: Coordinate,
) => {
const utils = window.ymaps.util.bounds;

let [[leftPx, topPx], [rightPx, bottomPx]] = utils.toGlobalPixelBounds(
[leftTop, rightBottom],
zoom,
) as [Coordinate, Coordinate];

// fall back to container size in case there is only one marker and area is 0
if (rightPx - leftPx <= 0) {
const halfX = containerSize[0] / 2;
leftPx -= halfX;
rightPx += halfX;
}

if (bottomPx - topPx <= 0) {
const halfY = containerSize[1] / 2;
topPx -= halfY;
bottomPx += halfY;
}

return [
[leftPx, topPx],
[rightPx, bottomPx],
];
};

const calcNewZoom = (l: number, zoom: number, marginSum: number) => {
return Math.log2((Math.pow(2, zoom) * (l - marginSum)) / l);
};

export const calculateMapParamsWithMarginAndZoom = (
[leftTop, rightBottom]: [Coordinate, Coordinate],
zoom: number,
areaMargin: ParsedMargin,
containerSize: Coordinate,
) => {
const utils = window.ymaps.util.bounds;

// calculate pixel bounds with current zoom
let [[leftPx, topPx], [rightPx, bottomPx]] = calcPixelBounds(
[leftTop, rightBottom],
zoom,
containerSize,
);

const [topMargin, rightMargin, bottomMargin, leftMargin] = areaMargin;

let zoomV: number;
let zoomH: number;

// calculate new zoom value after margins are applied
if (leftMargin && rightMargin) {
zoomH = calcNewZoom(rightPx - leftPx, zoom, leftMargin + rightMargin);
} else {
zoomH = zoom;
}

if (topMargin && bottomMargin) {
zoomV = calcNewZoom(bottomPx - topPx, zoom, topMargin + bottomMargin);
} else {
zoomV = zoom;
}

const newZoom = Math.min(zoomV, zoomH);

// calculate pixel bounds with new zoom
[[leftPx, topPx], [rightPx, bottomPx]] = calcPixelBounds(
[leftTop, rightBottom],
newZoom,
containerSize,
);

// calculate new bounds (scale if both size are present, otherwise shift the map)
if (leftMargin && rightMargin) {
leftPx -= leftMargin;
rightPx += rightMargin;
} else if (leftMargin) {
leftPx -= leftMargin;
rightPx -= leftMargin;
} else if (rightMargin) {
leftPx += rightMargin;
rightPx += rightMargin;
}

if (topMargin && bottomMargin) {
topPx -= topMargin;
bottomPx += bottomMargin;
} else if (topMargin) {
topPx -= topMargin;
bottomPx -= topMargin;
} else if (bottomMargin) {
topPx += bottomMargin;
bottomPx += bottomMargin;
}

// transform new bounds into coordinates
const [newLeftTop, newRightBottom] = utils.fromGlobalPixelBounds(
[
[leftPx, topPx],
[rightPx, bottomPx],
],
newZoom,
);

return {
center: utils.getCenter([newLeftTop, newRightBottom]),
zoom: newZoom,
};
};
2 changes: 2 additions & 0 deletions src/components/Map/__stories__/Map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ For a detailed usage guide of the Map component, see [Map Usage](https://github.

`disableBalloons?: boolean` - If `true`, disables info ballon opening when clicking on a marker (`false` by default)

`areaMargin?: number | [number, number] | [number, number, number]` - Offset (in pixels) for the marked area of the map relative to the map's container (`30` by default)

#### YMapMarker Interface

`address?: string` — Optional string address for the marker
Expand Down
10 changes: 0 additions & 10 deletions src/components/Map/__stories__/Map.stories.scss

This file was deleted.

15 changes: 10 additions & 5 deletions src/components/Map/__stories__/Map.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {ApiKeyInput} from './ApiKeyInput';

import data from './data.json';

import './Map.stories.scss';

const maxMapWidth = 500;

export default {
Expand Down Expand Up @@ -47,22 +45,29 @@ export const YMap = YMapTemplate.bind({});
export const YMapHiddenControls = YMapTemplate.bind({});
export const YMapHiddenBalloons = YMapTemplate.bind({});
export const YMapCustomMarkers = YMapTemplate.bind({});
export const YMapAreaOffset = YMapTemplate.bind({});

YMapHiddenControls.storyName = 'Y Map (Hidden Controls)';
YMapHiddenBalloons.storyName = 'Y Map (Hidden Balloons)';
YMapCustomMarkers.storyName = 'Y Map (Custom Markers)';
YMapAreaOffset.storyName = 'Y Map (Area Margin)';

GoogleMap.args = data.gmap;
YMap.args = data.ymap;
YMap.args = data.ymap as MapProps;

YMapHiddenControls.args = {
...data.ymap,
disableControls: true,
};
} as MapProps;

YMapHiddenBalloons.args = {
...data.ymap,
disableBalloons: true,
};
} as MapProps;

YMapCustomMarkers.args = data.ymapCustomMarkers as MapProps;

YMapAreaOffset.args = {
...data.ymap,
areaMargin: [0, 0, 0, 200],
} as MapProps;
2 changes: 1 addition & 1 deletion src/models/constructor-items/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export interface MediaBlockProps extends MediaBaseBlockProps, WithBorder {
}

export interface MapBlockProps extends MediaBaseBlockProps, WithBorder {
map: Omit<MapProps, 'forceAspectRatio'>;
map: Omit<MapProps, 'forceAspectRatio' | 'areaOffset'>;
}

export interface InfoBlockProps {
Expand Down
8 changes: 7 additions & 1 deletion src/models/constructor-items/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export interface BackgroundMediaProps extends MediaProps, Animatable, QAProps {
mediaClassName?: string;
}

export type Coordinate = number[];
export type Coordinate = [number, number];

export interface MapBaseProps {
zoom?: number;
Expand All @@ -335,10 +335,16 @@ export interface GMapProps extends MapBaseProps {
address: string;
}

export type YMapMargin =
| number
| [vertical: number, horizontal: number]
| [top: number, right: number, bottom: number, left: number];

export interface YMapProps extends MapBaseProps {
markers: YMapMarker[];
disableControls?: boolean;
disableBalloons?: boolean;
areaMargin?: YMapMargin;
id: string;
}

Expand Down
3 changes: 0 additions & 3 deletions src/schema/validators/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,6 @@ export const MapProps = {
type: 'array',
items: YMapMarker,
},
forceAspectRatio: {
type: 'boolean',
},
disableControls: {
type: 'boolean',
},
Expand Down