## React Native Yandex Maps (Яндекс Карты)

Форк библиотеки [react-native-yamap](https://github.com/volga-volga/react-native-yamap), разработанной компанией [Волга-Волга](https://vvdev.ru/)

Библиотека для интеграции MapKit SDK в React Native

| Version | React Native New Arch support |
|---------|-------------------------------|
| 6       | New Arch                      |
| 5       | Legacy + New Arch             |
| 4       | Legacy Arch                   |


## Миграция `4` → `5` или `4` → `6`

- Компоненты `Circle`, `Marker`, `Polygon` и `Polyline`:
  - дефолтное значение `handled` изменено с `true` на `false`


- Изменены названия и дефолтные значения props компонентов `Yamap` и `ClusteredYamap`:
  - `interactive`(`true`) → `interactiveDisabled`(`false`)
  - `scrollGesturesEnabled`(`true`) → `scrollGesturesDisabled`(`false`)
  - `zoomGesturesEnabled`(`true`) → `zoomGesturesDisabled`(`false`)
  - `tiltGesturesEnabled`(`true`) → `tiltGesturesDisabled`(`false`)
  - `rotateGesturesEnabled`(`true`) → `rotateGesturesDisabled`(`false`)
  - `fastTapEnabled`(`true`) → `fastTapDisabled`(`false`)

## Установка

```
yarn add react-native-yamap-plus
```

### Использование Lite версии Yandex MapKit SDK (без модулей Suggests, Search, Transport)

По умолчанию используется Full

Для изменения на Lite необходимо:

- android: в файле `android/build.gradle` добавить строку
```diff
buildscript {
    ext {
        ...
+       useYandexMapsLite = true
    }
```

- ios: в начало `Podfile` добавить строку
```diff
+ ENV['USE_YANDEX_MAPS_LITE'] = "1"
...
```

### Инициализировать карты

Для этого лучше всего зайти в корневой файл приложения, например `App.js`, и добавить инициализацию:

```js
import {YamapInstance} from 'react-native-yamap-plus';

YamapInstance.init(API_KEY);
```

### Изменение языка карт

```js
import {YamapInstance} from 'react-native-yamap-plus';

const currentLocale = await YamapInstance.getLocale();
YamapInstance.setLocale('en_US');  // 'ru_RU' или другие
YamapInstance.resetLocale();
```

-  **getLocale(): Promise\<string\>** - возвращает используемый язык карт;

-  **setLocale(locale: string): Promise\<void\>** - установить язык карт;

-  **resetLocale(): Promise\<void\>** - использовать для карт язык системы.

**ВАЖНО!**

1. Для **Android** изменение языка карт вступит в силу только после **перезапуска** приложения.
2. Для **iOS** методы изменения языка можно вызывать только до первого рендера карты. Также нельзя повторно вызывать метод, если язык уже изменялся (можно только после перезапуска приложения), иначе изменения приняты не будут, а в терминал будет выведено сообщение с предупреждением. В коде при этом не будет информации об ошибке.

### Использование компонента

```jsx
import React from 'react';
import {Yamap} from 'react-native-yamap-plus';

const Map = () => {
  return (
    <Yamap
      userLocationIcon={{ uri: 'https://www.clipartmax.com/png/middle/180-1801760_pin-png.png' }}
      initialRegion={{
        lat: 50,
        lon: 50,
        zoom: 10,
        azimuth: 80,
        tilt: 100
      }}
      style={{ flex: 1 }}
    />
  );
};
```

#### Доступные `props` для компонента **Yamap**:

| Название | Тип | Стандартное значение | Описание |
|--|--|--|--|
| showUserPosition | boolean | false | Отслеживание геоданных и отображение позиции пользователя |
| followUser | boolean | false | слежение камеры за пользователем |
| userLocationIcon | ImageSource | false | Иконка для позиции пользователя. Доступны те же значения что и у компонента Image из React Native |
| userLocationIconScale | number | 1 | Масштабирование иконки пользователя |
| initialRegion | InitialRegion | | Изначальное местоположение карты при загрузке |
| interactiveDisabled | boolean | false | Интерактивная ли карта (перемещение по карте, отслеживание нажатий) |
| nightMode | boolean | false | Использование ночного режима |
| onMapLoaded | function | | Колбек на загрузку карты |
| onCameraPositionChange | function | | Колбек на изменение положения камеры |
| onCameraPositionChangeEnd | function | | Колбек при завершении изменения положения камеры |
| onMapPress | function | | Событие нажития на карту. Возвращает координаты точки на которую нажали |
| onMapLongPress | function | | Событие долгого нажития на карту. Возвращает координаты точки на которую нажали |
| userLocationAccuracyFillColor | string |  | Цвет фона зоны точности определения позиции пользователя |
| userLocationAccuracyStrokeColor | string |  | Цвет границы зоны точности определения позиции пользователя |
| userLocationAccuracyStrokeWidth | number | | Толщина зоны точности определения позиции пользователя |
| scrollGesturesDisabled | boolean | false | Включены ли жесты скролла |
| zoomGesturesDisabled | boolean | false | Включены ли жесты зума |
| tiltGesturesDisabled | boolean | false | Включены ли жесты наклона камеры двумя пальцами |
| rotateGesturesDisabled | boolean | false | Включены ли жесты поворота камеры |
| fastTapDisabled | boolean | false | Убрана ли задержка в 300мс при клике/тапе |
| logoPosition | YandexLogoPosition | {} | Позиция логотипа Яндекса на карте |
| logoPadding | YandexLogoPadding | {} | Отступ логотипа Яндекса на карте |
| mapType | string | 'vector' | Тип карты |
| mapStyle | string | {} | Стили карты согласно [документации](https://yandex.ru/dev/maps/mapkit/doc/dg/concepts/style.html) |

#### Доступные методы для компонента **Yamap**:

-  `fitMarkers(points: Point[], duration?: number, animation?: Animation): void` - подобрать положение камеры, чтобы вместить указанные маркеры (если возможно);
-  `fitAllMarkers(duration?: number, animation?: Animation): void` - подобрать положение камеры, чтобы вместить все маркеры (если возможно);
-  `setCenter(center: { lon: number, lat: number }, zoom: number = 10, azimuth: number = 0, tilt: number = 0, duration: number = 0, animation: Animation = Animation.SMOOTH)` - устанавливает камеру в точку с заданным zoom, поворотом по азимуту и наклоном карты (`tilt`). Можно параметризовать анимацию: длительность и тип. Если длительность установить 0, то переход будет без анимации. Возможные типы анимаций `Animation.SMOOTH` и `Animation.LINEAR`;
-  `setZoom(zoom: number, duration: number, animation: Animation)` - изменить текущий zoom карты. Параметры `duration` и `animation` работают по аналогии с `setCenter`;
-  `getCameraPosition(callback: (position: CameraPosition) => void)` - запрашивает положение камеры и вызывает переданный колбек с текущим значением;
-  `getVisibleRegion(callback: (region: VisibleRegion) => void)` - запрашивает видимый регион и вызывает переданный колбек с текущим значением;
-  `setTrafficVisible(isVisible: boolean): void` - включить/отключить отображение слоя с пробками на картах;
-  `getScreenPoints(point: Point[], callback: (result: {screenPoints: ScreenPoint[]}) => void)` - получить кооординаты на экране (x и y) по координатам маркеров;
-  `getWorldPoints(screenPoint: ScreenPoint[], callback: (result: {worldPoints: Point[]}) => void)` - получить координаты точек (lat и lon) по координатам на экране.

#### Дополнительные `props` компонента **ClusteredYamap**: для стилизации кластеризованного маркера

| Название           | Тип                              | Стандартное значение    | Описание                                         |
|--------------------|----------------------------------|-------------------------|--------------------------------------------------|
| clusteredMarkers   | Array<{point: Point, data: any}> | undefined               | Массив точек на карте                            |
| сlusterIcon        | ImageSource                      | undefined               | Кастомная иконка для маркера (по умолчанию круг) |
| clusterColor       | string                           | 'red'                   | Цвет фона метки-кластера                         |
| сlusterSize        | YandexClusterSizes               | {width: 32, height: 32} | Размер маркера                                   |
| сlusterTextColor   | string                           | 'black'                 | Цвет текста                                      |
| сlusterTextSize    | number                           | 45                      | Размер текста                                    |
| сlusterTextXOffset | number                           | 0                       | Отступ для текста                                |
| сlusterTextYOffset | number                           | 0                       | Отступ для текста                                |

**ВАЖНО**

- Компонент карт стилизуется, как и `View` из React Native. Если карта не отображается, после инициализации с валидным ключем API, вероятно необходимо прописать стиль, который опишет размеры компонента (`height + width` или `flex`);
- При использовании изображений из JS (через `require('./img.png')`) в дебаге и релизе на Android могут быть разные размеры маркера. Рекомендуется проверять рендер в релизной сборке.

## Отображение примитивов

### Marker

```jsx
import {Yamap, Marker} from 'react-native-yamap-plus';

<Yamap>
  <Marker point={{ lat: 50, lon: 50 }} />
</Yamap>
```

#### Доступные `props` для примитива **Marker**:

| Название | Тип | Описание                                                                                        |
|--|--|-------------------------------------------------------------------------------------------------|
| point | Point | Координаты точки для отображения маркера                                                        |
| scale | number | Масштабирование иконки маркера. Не работает если использовать children у маркера                |
| source | ImageSource | Данные для изображения маркера.                                                                 |
| children | ReactElement | Рендер маркера как компонента. **DEPRECATED**. Используйте `source`.                                           |
| onPress | function | Действие при нажатии/клике                                                                      |
| anchor | {  x:  number,  y:  number  } | Якорь иконки маркера. Координаты принимают значения от 0 до 1                                   |
| zIndex | number | zIndex для объекта на карте                                                                     |
| visible | boolean | Отображение маркера на карте                                                                    |
| handled | boolean | Включение(**false**)/отключение(**true**) всплытия события нажатия для родителя `default:false` |

#### Доступные методы для примитива **Marker**:

-  `animatedMoveTo(point: Point, duration: number)` - плавное изменение позиции маркера;
-  `animatedRotateTo(angle: number, duration: number)` - плавное вращение маркера.

#### Использование компонентов в качестве маркеров

Существует два способа отобразить произвольный компонент в маркере:
1. Передать компонент в children
2. Выполнить snapshot компонента и полученное изображение передать в source

##### Children
Простой, но нежелательный способ. Помечен как deprecated.

Поддержка `children` была добавлена для совместимости с API `react-native-maps`. [Комментарий автора оригинальной библиотеки.](https://github.com/volga-volga/react-native-yamap/issues/289#issuecomment-2396565966)

При использовании `children` библиотека автоматически создаёт snapshot view-компонента на нативном уровне и использует его для отображения маркера. Особенности:

1. **Некорректный внешний вид:** Из-за особенностей нативного snapshot вёрстка компонента внутри маркера может отличаться от его обычного рендера вне карты.

2. **Нестабильное обновление:** При обновлении состояния компонента не гарантируется перерисовка содержимого маркера, особенно при изменении состояния вложенных элементов. Поведение на iOS и Android может отличаться.

   **Workarounds для принудительного обновления:**
  - Изменение `key` у *первого дочернего элемента* внутри `children` вызовет пересоздание снимка.
  - Изменение `key` у самого `Marker` пересоздаст снимок, но вызовет заметное мерцание: маркер исчезнет и появится заново.

3. **Производительность:** Для каждого маркера создаётся уникальный снимок, даже если маркеры визуально одинаковы. При большом количестве маркеров это приводит к просадкам производительности и расходу памяти.

4. **Неконтролируемый момент снимка:** Снимок создаётся библиотекой автоматически, и асинхронный контент (например, изображения по URL) может не успеть загрузиться.

##### Snapshot to source

Надёжнее самостоятельно создать снимок компонента и передать его в `source`. Рекомендуется использовать [react-native-view-shot](https://github.com/gre/react-native-view-shot). Это работает стабильно на обеих платформах и позволяет переиспользовать один снимок для множества маркеров.

Используйте `collapsable={false}` для `View`, с которого делается snapshot - это предотвратит возможные проблемы на Android.

<details>
  <summary>Примеры кода</summary>

Простой компонент, доступный для рендера сразу (синхронный):

<pre><code>const MyMarker = ({ point }) => {
  const viewRef = useRef&lt;View&gt;(null);
  const [source, setSource] = useState();
  const handleLayout = useCallback(async () => {
    if (!viewRef.current) return;
    
    const base64 = await captureRef(viewRef, {
      format: 'png',
      quality: 1,
      result: 'base64',
    });
    setSource({ uri: `data:image/png;base64,${base64}` });
  }, []);
  
  return (
    &lt;&gt;
      {!source && (
        &lt;View style={{ position: 'absolute' }} ref={viewRef} collapsable={false} onLayout={handleLayout}&gt;
          &lt;AnySyncLoadedComponent /&gt;
        &lt;/View&gt;
      )}
      
      {source && &lt;Marker point={point} source={source} /&gt;}
    &lt;/&gt;
  );
};
</code></pre>

Компонент с ожиданием контента (асинхронная загрузка):

<pre><code>const MyMarkerWithImage = ({ point, imageUrl }) => {
  const viewRef = useRef&lt;View&gt;(null);
  const [source, setSource] = useState();
  const handleCapture = useCallback(async () => {
    if (!viewRef.current) return;
    
    const base64 = await captureRef(viewRef, {
      format: 'png',
      quality: 1,
      result: 'base64',
    });
    setSource({ uri: `data:image/png;base64,${base64}` });
  }, []);
  
  return (
    &lt;&gt;
      {!source && (
        &lt;View style={{ position: 'absolute' }} collapsable={false}&gt;
          &lt;Image 
            source={{ uri: imageUrl }}
            onLoad={handleCapture} 
          /&gt;
        &lt;/View&gt;
      )}
      
      {source && &lt;Marker point={point} source={source} /&gt;}
    &lt;/&gt;
  );
};
</code></pre>

</details>

### Circle

```jsx
import {Yamap, Circle} from 'react-native-yamap-plus';

<Yamap>
  <Circle center={{ lat: 50, lon: 50 }} radius={300} />
</Yamap>
```

#### Доступные `props` для примитива **Circle**:

| Название | Тип | Описание |
|--|--|--|
| center | Point | Координаты центра круга |
| radius | number | Радиус круга в метрах |
| fillColor | string | Цвет заливки |
| strokeColor | string | Цвет границы |
| strokeWidth | number | Толщина границы |
| onPress | function | Действие при нажатии/клике |
| zIndex | number | zIndex для объекта на карте |
| handled | boolean | Включение(**false**)/отключение(**true**) всплытия события нажатия для родителя `default:false` |

### Polyline

```jsx
import {Yamap, Polyline} from 'react-native-yamap-plus';

<Yamap>
  <Polyline
    points={[
      { lat: 50, lon: 50 },
      { lat: 50, lon: 20 },
      { lat: 20, lon: 20 },
    ]}
  />
</Yamap>
```

#### Доступные `props` для примитива **Polyline**:

| Название | Тип | Описание |
|--|--|--|
| points | Point[] | Массив точек линии |
| strokeColor | string | Цвет линии |
| strokeWidth | number | Толщина линии |
| outlineColor | string | Цвет обводки |
| outlineWidth | number | Толщина обводки |
| dashLength | number | Длина штриха |
| dashOffset | number | Отступ первого штриха от начала полилинии |
| gapLength | number | Длина разрыва между штрихами |
| onPress | function | Действие при нажатии/клике |
| zIndex | number | zIndex для объекта на карте |
| handled | boolean | Включение(**false**)/отключение(**true**) всплытия события нажатия для родителя `default:false` |

### Polygon

```jsx
import {Yamap, Polygon} from 'react-native-yamap-plus';

<Yamap>
  <Polygon
    points={[
      { lat: 50, lon: 50 },
      { lat: 50, lon: 20 },
      { lat: 20, lon: 20 },
    ]}
  />
</Yamap>
```

#### Доступные `props` для примитива **Polygon**:

| Название | Тип | Описание |
|--|--|--|
| points | Point[] | Массив точек линии |
| fillColor | string | Цвет заливки |
| strokeColor | string | Цвет границы |
| strokeWidth | number | Толщина границы |
| innerRings | (Point[])[] | Массив полилиний, которые образуют отверстия в полигоне |
| onPress | function | Действие при нажатии/клике |
| zIndex | number | zIndex для объекта на карте |
| handled | boolean | Включение(**false**)/отключение(**true**) всплытия события нажатия для родителя `default:false` |

## Запрос маршрутов (модуль Transport) [Full]

```typescript
import {Transport} from 'react-native-yamap-plus';

const routes = await Transport.findRoutes(points, vehicles) // универсальный поиск маршрутов
const masstransitRoutes = await findMasstransitRoutes(points) // маршрутов на общественном транспорте
const pedestrianRoutes = await findPedestrianRoutes(points) // пешеходные маршруты
const drivingRoutes = await findDrivingRoutes(points) // маршруты на автомобиле
```

Тип роутера зависит от переданного в функцию массива `vehicles`:

- Если передан пустой массив (`findRoutes(points, [])`), то будет использован `PedestrianRouter`;
- Если передан массив с одним элементом `'car'` (`findRoutes(points, ['car'])`), то будет использован `DrivingRouter`;
- Во всех остальных случаях используется `MasstransitRouter`.

## Поиск по гео с подсказсками (GeoSuggestions) [Full]

Для поиска с геоподсказками нужно воспользоваться модулем Suggest:

```typescript

import {Suggest} from 'react-native-yamap-plus';

const find = async (query: string, options?: SuggestOptions) => {
  const suggestions = await Suggest.suggest(query, options);

  // suggestions = [{
  //   subtitle: "Москва, Россия"
  //   title: "улица Льва Толстого, 16"
  //   uri: "ymapsbm1://geo?data=xxxx"
  //   center: {lat: 55.755863189697266, lon: 37.617698669433594}
  // }, ...]

  // After searh session is finished
  Suggest.reset();
}
```
## Поиск по гео (GeoSearch) [Full]

Для поиска нужно воспользоваться модулем Suggest:

```typescript

import {Search} from 'react-native-yamap-plus';

const find = async (query: string, options?: SuggestOptions) => {
  // можно использовать Point, BoundingBox, Polyline и Polygon (4 точки, без innerRings)
  const search = await Search.searchText(
    'Москва',
    { point: { lat: 54, lon: 53 } },
    { disableSpellingCorrection: true, geometry: true },
  );

  // второй параметр это зум, определяющий на сколько малые объекты искать
  const searchByPoint = await Search.searchPoint({ lat: 54, lon: 53 }, 10, {
    disableSpellingCorrection: true,
    geometry: true,
  });

  const resolveURI = await Search.resolveURI("ymapsbm1://geo?data=IgoNAQBYQhUBAFhC", {
    disableSpellingCorrection: true,
    geometry: true,
  });

  const searchByURI = await Search.searchByURI("ymapsbm1://geo?data=IgoNAQBYQhUBAFhC", {
    disableSpellingCorrection: true,
    geometry: true,
  });
  
//   {"Components": [{"kind": "4", "name": "Малиновский сельсовет"}, {"kind": "4", "name": "Белебеевский район"}, {"kind": "3", "name": "Республика Башкортостан"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "3", "name": "Приволжский федеральный округ"}, {"kind": "1", "name": "Россия"}], "country_code": "RU", "formatted": "Россия, Республика Башкортостан, Белебеевский район, Малиновский сельсовет", "uri": "ymapsbm1://geo?data=IgoNAQBYQhUBAFhC"}
  
}
```

Также теперь можно воспользоваться геокодированием из поиска

```typescript

import {Search} from 'react-native-yamap-plus';

const address = Search.geocodePoint({lat: 54, lon: 53});

// {"Components": [{"kind": "4", "name": "Малиновский сельсовет"}, {"kind": "4", "name": "Белебеевский район"}, {"kind": "3", "name": "Республика Башкортостан"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "19", "name": "Понтийско-Каспийская степь"}, {"kind": "3", "name": "Приволжский федеральный округ"}, {"kind": "1", "name": "Россия"}], "country_code": "RU", "formatted": "Россия, Республика Башкортостан, Белебеевский район, Малиновский сельсовет", "uri": "ymapsbm1://geo?data=IgoNAQBYQhUBAFhC"}

const point = Search.geocodeAddress(address.formatted);

// возвращает координаты по адресу {"lat": 53.999187242158015, "lon": 54.089440735780194}

```


### Использование компонента ClusteredYamap

```jsx
import React from 'react';
import {ClusteredYamap} from 'react-native-yamap-plus';

const Map = () => {
  return (
    <ClusteredYamap
      clusterColor="red"
      clusteredMarkers={[
        {
          point: {
            lat: 56.754215,
            lon: 38.622504,
          },
          data: {},
        },
        {
          point: {
            lat: 56.754215,
            lon: 38.222504,
          },
          data: {},
        },
      ]}
      renderMarker={(info, index) => (
        <Marker
          key={index}
          point={info.point}
        />
      )}
      style={{flex: 1}}
    />
  );
};
```

# Использование с Expo
Для подключения нативного модуля в приложение с expo используйте expo prebuild.
Он выполнит eject и сгенерирует привычные папки android и ios с нативным кодом. Это позволит использовать любую библиотеку так же, как и приложение с react native cli.

# Проблемы

1. В никоторых случаях в симуляторе ios может крашиться приложение с ошибкой
```failed assertion `The following Metal object is being destroyed while still required to be alive by the command buffer```

Решение: In Xcode, in the "Product" Menu, go to "Scheme", then "Edit Scheme...". In the left panel select "Run (Debug)" and in Diagnostic tab uncheck "API Validation" under Metal settings.
