mahal_map 2.2.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install mahal_map maplibre-gl @grammaps/maps3d-web
13
13
  Обе зависимости — `peerDependencies`, в бандл `mahal_map` они не входят. Библиотека их не импортирует: MapLibre и `Maps3D` приходят снаружи, аргументами `create()` либо через `window`.
14
14
 
15
15
  - `maplibre-gl` (`^6.10.0`) — обязателен всегда.
16
- - `@grammaps/maps3d-web` (>=0.5.0; рекомендуется 0.8.0 — в ней наведение на здание и починенный пикинг клика) — даёт стили, тайлы, объём, пробки, рельеф, планы этажей, клик по объектам. Помечен `optional`: без него карта поднимется на запасном векторном стиле, но 3D и слоёв платформы на ней не будет.
16
+ - `@grammaps/maps3d-web` (>=0.11.0 — в ней отписки у подписок, `selectedBuildingKind()`, этажи в состоянии слоя и починенный клик по зданию сверху; на сборках постарше карта работает через запасные пути, но отписка и `selectedBuildingKind` не гарантированы) — даёт стили, тайлы, объём, пробки, рельеф, планы этажей, клик по объектам. Помечен `optional`: без него карта поднимется на запасном векторном стиле, но 3D и слоёв платформы на ней не будет.
17
17
 
18
18
  MapLibre можно не класть в свою сборку вовсе — платформа отдаёт согласованную версию вместе с веб-воркером:
19
19
 
@@ -51,6 +51,8 @@ const map = MahalMap.create({ container: "map" }, maplibregl, Maps3D);
51
51
 
52
52
  ```ts
53
53
  // vite.config.ts — чтобы адрес воркера совпал с файлом во время разработки
54
+ import { defineConfig } from "vite";
55
+
54
56
  export default defineConfig({ optimizeDeps: { exclude: ["maplibre-gl"] } });
55
57
  ```
56
58
 
@@ -427,6 +429,15 @@ map.setIndoorLevel(1); // нумерация OSM: 0 — первый назем
427
429
  map.refreshIndoor(); // перечитать после правки картографом
428
430
  ```
429
431
 
432
+ С Maps3D 0.11.0 список этажей приходит в состоянии слоя — опрашивать `getIndoorLevels()` на `idle`/`moveend` не нужно:
433
+
434
+ ```ts
435
+ map.onLayers((state) => {
436
+ if (state.id !== "indoor") return;
437
+ renderLevelPicker(state.params.levels ?? [], state.params.level);
438
+ });
439
+ ```
440
+
430
441
  После поиска показать найденный объект вместе с его этажом:
431
442
 
432
443
  ```ts
@@ -492,13 +503,13 @@ await maps3d.ready; // enhance() ждёт нативную карту MapLibre,
492
503
  ```ts
493
504
  map.setSelectionStyle({ color: "#e23b2f", opacity: 0.55, durationMs: 300 });
494
505
 
495
- map.onBuildingClick((building) => {
506
+ const stopBuildingClick = map.onBuildingClick((building) => {
496
507
  if (!building) return;
497
- // SDK уже подсветил его
498
- console.log(building.props?.osm_id, building.height);
508
+ // SDK уже подсветил его. index >= 0 — объёмный дом сцены, -1 — дом из слоя стиля
509
+ console.log(building.props?.osm_id, building.height, building.index);
499
510
  });
500
511
 
501
- map.onRoadClick((road) => {
512
+ const stopRoadClick = map.onRoadClick((road) => {
502
513
  if (!road) return;
503
514
  console.log(road.nameRu ?? road.name, road.class);
504
515
  });
@@ -506,8 +517,13 @@ map.onRoadClick((road) => {
506
517
  // Выделить здание по osm_id — например, после поиска
507
518
  map.selectBuilding(123456789); // false, если здания нет в загруженных данных
508
519
  map.selectedBuilding(); // текущий id или null
520
+ map.selectedBuildingKind(); // 'scene' — объём, 'flat' — слой стиля, null — выделения нет
509
521
  map.clearSelection();
510
522
 
523
+ // При размонтировании
524
+ stopBuildingClick();
525
+ stopRoadClick();
526
+
511
527
  // Дорога под точкой холста, допуск по умолчанию 12 px
512
528
  map.roadAt({ x: 320, y: 240 });
513
529
  map.roadsNamed; // есть ли в текущем стиле названия дорог
@@ -524,6 +540,22 @@ map.setSelectionStyle({ color: "#e23b2f" });
524
540
  map.toggle3DBuildings(true); // подписка и стиль выделения уже применены к новому слою
525
541
  ```
526
542
 
543
+ #### Отписка
544
+
545
+ `onBuildingClick`, `onBuildingHover`, `onRoadClick`, `onParkingHover` и `onParkingClick` возвращают функцию отписки и принимают `null`. Повторный вызов заменяет обработчик. Отписка снимает **только свою** подписку: если обработчик уже заменён следующим вызовом, она ничего не делает — отложенный вызов прежнего владельца (например, cleanup размонтированного компонента) не погасит обработчик нового. Работает и для подписки, сделанной до подъёма слоя: снятая до подъёма, она на слой не попадёт.
546
+
547
+ ```ts
548
+ useEffect(() => map.onBuildingClick(openCard), [map]); // отписка уходит в cleanup
549
+ ```
550
+
551
+ #### Порог зума объёма
552
+
553
+ `getBuildingsMinZoom()` — зум, с которого поднимается объём: `maps3d.minZoom`, который `mahal_map` передал в Maps3D, либо умолчание Maps3D (`16`). В отличие от `rendered` в состоянии слоя, ответ не зависит от текущего зума.
554
+
555
+ ```ts
556
+ const willBe3D = targetZoom >= map.getBuildingsMinZoom();
557
+ ```
558
+
527
559
  ### Наведение на здание
528
560
 
529
561
  Требует `@grammaps/maps3d-web` **0.8.0** и старше. Подсветку и курсор рисует сам SDK — приложение задаёт облик и получает уведомления.
@@ -539,11 +571,11 @@ map.onBuildingHover((building) => {
539
571
 
540
572
  Колбэк приходит **только в момент смены** дома под курсором, а не на каждое движение мыши.
541
573
 
542
- Слежение включается самой подпиской: пока `onBuildingHover` не вызван, SDK курсор не трогает и сцену не разбирает. `onBuildingHover(null)` отписывает и гасит подсветку.
574
+ Слежение включается самой подпиской: пока `onBuildingHover` не вызван, SDK курсор не трогает и сцену не разбирает. `onBuildingHover(null)` или возвращённая отписка снимает подписку и гасит подсветку.
543
575
 
544
576
  | Метод | Описание |
545
577
  | --- | --- |
546
- | `onBuildingHover(cb \| null)` | Подписка на наведение; `null` отписывает и гасит подсветку. |
578
+ | `onBuildingHover(cb \| null)` | Подписка на наведение; возвращает отписку, `null` отписывает и гасит подсветку. |
547
579
  | `setHoverStyle(s)` | Облик: `color`, `opacity`, `glow`, `durationMs`, `easing`, `cursor`. `cursor: null` — не трогать курсор. |
548
580
  | `setHoverEnabled(on)` | Вкл/выкл наведение, не трогая подписку. |
549
581
  | `hoverEnabled` | Включено ли наведение сейчас. |
@@ -571,11 +603,13 @@ row.addEventListener("mouseleave", () => map.hoverBuilding(null));
571
603
  map.setHoverEnabled(false); // подписка сохраняется
572
604
  ```
573
605
 
574
- Цену ограничивает сам SDK: разбор не чаще раза в кадр, пропускается во время движения карты и не трогает сцену, пока курсор на том же доме. Но разбор — это чтение пикселя обратно из GPU, поэтому на слабых устройствах наведение лучше не включать.
606
+ Цену ограничивает сам SDK: разбор не чаще раза в кадр, пропускается во время движения камеры — и от жестов, и от `jumpTo`/`easeTo` из приложения (с 0.11.0) — и не трогает сцену, пока курсор на том же доме. С 0.11.0 наведение читает пиксель из GPU асинхронно и главный поток не держит; гасить наведение на время движения карты снаружи больше не нужно.
575
607
 
576
608
  ### Парковки: наведение, клик, справка
577
609
 
578
610
  > Нужен `@grammaps/maps3d-web` **0.10.0+**. На более старой сборке методы ничего не делают и возвращают пустые значения (`false`, `null`).
611
+ >
612
+ > Подписки `onParkingHover`/`onParkingClick` можно делать до подъёма слоя — они запоминаются и переигрываются, как у зданий. Обе возвращают отписку.
579
613
 
580
614
  Устроено как у зданий: приложение задаёт облик и получает уведомления, подсветку и курсор рисует SDK.
581
615
 
@@ -604,13 +638,13 @@ map.getMap().on("contextmenu", (e) => console.log(map.parkingAt(e.point, 4)));
604
638
 
605
639
  | Метод | Описание |
606
640
  | ---------------------------------- | ------------------------------------------------------------------------------------------------- |
607
- | `onParkingHover(callback \| null)` | Участок под курсором либо `null`. Зовётся **только при смене участка**. `null` — отписаться и погасить. |
641
+ | `onParkingHover(callback \| null)` | Участок под курсором либо `null`. Зовётся **только при смене участка**. Возвращает отписку; `null` — отписаться и погасить. |
608
642
  | `setParkingHoverStyle(style)` | Облик подсветки: `color`, `opacity`, `outlineColor`, `outlineWidth`, `cursor`. |
609
643
  | `setParkingHoverEnabled(enabled)` | Выключить/включить наведение, не теряя подписку. |
610
644
  | `parkingHoverEnabled` | Геттер: включено ли наведение. |
611
645
  | `hoverParking(id \| null)` | Подсветить участок программно по `osm_id`; `null` — погасить. `false` — участка нет в данных. |
612
646
  | `hoveredParking()` | `osm_id` подсвеченного участка или `null`. |
613
- | `onParkingClick(callback \| null)` | Клик по участку: справка либо `null`. `null` вместо колбэка — отписаться. |
647
+ | `onParkingClick(callback \| null)` | Клик по участку: справка либо `null`. Возвращает отписку; `null` вместо колбэка — отписаться. |
614
648
  | `parkingAt(point, tolPx?)` | Справка по точке холста. Допуск по умолчанию `0` — строго внутрь участка. |
615
649
 
616
650
  Облик подсветки (`IMaps3DParkingHoverStyle`):
@@ -664,6 +698,8 @@ MahalMap.toggle3DBuildings(map, false);
664
698
 
665
699
  Для переключения пользуйтесь именно `toggle3DBuildings()`: он поднимет слой, если его ещё не было (`enable3D: false` при создании), а на сборках Maps3D без реестра слоёв откатится на `setBuildingsEnabled()`.
666
700
 
701
+ Поднять слой можно, только если `Maps3D` был при `create()` — тогда карта стоит на стиле платформы. Карта, созданная без него, работает на запасном стиле без источников платформы и без ключа в запросах: `toggle3DBuildings(true)` на ней ничего не делает и пишет предупреждение. Подключить `Maps3D` позже — значит пересоздать карту.
702
+
667
703
  ```ts
668
704
  map.toggle3DBuildings(false); // совместимо со всеми сборками
669
705
  ```
@@ -841,7 +877,7 @@ onBeforeUnmount(() => {
841
877
  | `antialias: true` не забыть | ставится сам при включённом 3D |
842
878
  | `Maps3D.enhance(map, opts); await maps3d.ready` | `enable3D: true` + `maps3d: {...}`, `await map.whenMaps3DReady()` |
843
879
  | `map.setStyle(Maps3D.styleUrl("dark", base))` | `map.setStyle("dark")` — внутри выбранного семейства |
844
- | `maps3d.refreshLayers()` после смены стиля | вызывается сам на `style.load` |
880
+ | `maps3d.refreshLayers()` после смены стиля | вызывается сам на `styledata`, когда новый стиль загружен |
845
881
  | `maps3d.destroy(); map.remove()` | `map.destroy()` |
846
882
  | ключ руками в каждый вызов | один `keyUtils.saveKey()` на всё |
847
883
 
@@ -945,7 +981,7 @@ const map = MahalMap.create(
945
981
  );
946
982
  ```
947
983
 
948
- В NPM-версии второй аргумент `maplibreObject` рекомендуется передавать явно. В browser SDK он берется из `window.maplibregl`.
984
+ В NPM-версии второй аргумент `maplibreObject` рекомендуется передавать явно. Без него SDK ищет `window.maplibregl` — он есть, только если MapLibre подключён скриптом (`<script src=".../maplibre-gl.js">`). ESM-импорт глобальную переменную не создаёт: тогда передайте библиотеку вторым аргументом (`MahalMap.create({ container: "map" }, maplibregl)`) либо назначьте `window.maplibregl = maplibregl` до вызова `create()`.
949
985
 
950
986
  ### `MahalMap.createAsync(options, maplibreObject?, maps3dCtor?)`
951
987
 
@@ -994,7 +1030,7 @@ try {
994
1030
  </div>
995
1031
  <template #fallback>
996
1032
  <div class="flex h-[360px] items-center justify-center">
997
- {{ loadingLabel }}
1033
+ Загрузка карты…
998
1034
  </div>
999
1035
  </template>
1000
1036
  </ClientOnly>
@@ -1019,10 +1055,17 @@ onMounted(async () => {
1019
1055
 
1020
1056
  keyUtils.saveKey(import.meta.env.VITE_MAHAL_API_KEY_TILE);
1021
1057
 
1058
+ // Без элемента SDK взял бы контейнер по умолчанию — id "map", то есть чужой элемент.
1059
+ const container = mapElement.value;
1060
+
1061
+ if (!container || disposed) {
1062
+ return;
1063
+ }
1064
+
1022
1065
  try {
1023
1066
  const instance = await MahalMap.createAsync(
1024
1067
  {
1025
- container: mapElement.value,
1068
+ container,
1026
1069
  center: DUSHANBE_CENTER,
1027
1070
  zoom: 11,
1028
1071
  },
@@ -1258,6 +1301,8 @@ MahalMap.showPlace(map, 68.787, 38.573, 2, 18);
1258
1301
  MahalMap.onBuildingClick(map, (building) => console.log(building?.id));
1259
1302
  MahalMap.selectBuilding(map, 123456789);
1260
1303
  MahalMap.selectedBuilding(map);
1304
+ MahalMap.selectedBuildingKind(map);
1305
+ MahalMap.getBuildingsMinZoom(map);
1261
1306
  MahalMap.setSelectionStyle(map, { color: "#e23b2f" });
1262
1307
  MahalMap.clearSelection(map);
1263
1308
  MahalMap.onRoadClick(map, (road) => console.log(road?.name));
@@ -1313,7 +1358,7 @@ MahalMap.setZoom(map, 14);
1313
1358
  | `getMap(instance)` | Возвращает нативный MapLibre Map. |
1314
1359
  | `getCamera(instance)` | Возвращает CameraController. |
1315
1360
  | `setStyle(instance, theme)` | Переключает тему стандартного стиля. |
1316
- | `setLanguage(instance, lang)` | Переключает язык стандартного стиля. |
1361
+ | `setLanguage(instance, lang)` | Запоминает язык для поиска и роутинга; стиль и подписи не меняет. |
1317
1362
  | `setCenter(instance, center)` | Меняет центр карты. |
1318
1363
  | `setZoom(instance, zoom)` | Меняет zoom карты. |
1319
1364
  | `addMarker(instance, marker)` | Добавляет маркер. |
@@ -1325,16 +1370,18 @@ MahalMap.setZoom(map, 14);
1325
1370
  | `getLayers(instance)` | Снимок всех слоёв. |
1326
1371
  | `onLayers(instance, callback)` | Подписка на изменения слоёв; возвращает отписку. |
1327
1372
  | `refreshLayers(instance)` | Пере-применить волю клиента ко всем слоям. |
1328
- | `getIndoorLevels(instance)` | Этажи, найденные в текущем виде карты. |
1373
+ | `getIndoorLevels(instance)` | Этажи в текущем виде карты (с 0.11.0 — из `params.levels` слоя). |
1329
1374
  | `setIndoorLevel(instance, level)` | Переключить этаж (0 — первый наземный). |
1330
1375
  | `refreshIndoor(instance)` | Перечитать планы этажей. |
1331
1376
  | `showPlace(instance, lon, lat, level?, zoom?)` | Показать объект и открыть его этаж. |
1332
- | `onBuildingClick(instance, callback)` | Клик по зданию. |
1377
+ | `onBuildingClick(instance, callback \| null)` | Клик по зданию; возвращает отписку. |
1333
1378
  | `selectBuilding(instance, id, style?)` | Выделить здание по `osm_id`. |
1334
1379
  | `clearSelection(instance)` | Снять выделение. |
1335
1380
  | `selectedBuilding(instance)` | Идентификатор выделенного здания или `null`. |
1381
+ | `selectedBuildingKind(instance)` | `'scene'` / `'flat'` / `null` — чем нарисовано выделение. |
1382
+ | `getBuildingsMinZoom(instance)` | Зум появления объёма, переданный в Maps3D (по умолчанию `16`). |
1336
1383
  | `setSelectionStyle(instance, style)` | Облик выделения. |
1337
- | `onRoadClick(instance, callback)` | Клик по дороге. |
1384
+ | `onRoadClick(instance, callback \| null)` | Клик по дороге; возвращает отписку. |
1338
1385
  | `roadAt(instance, point, tolPx?)` | Дорога под точкой холста. |
1339
1386
  | `onParkingHover(instance, callback \| null)` | Парковка под курсором (только при смене участка). |
1340
1387
  | `setParkingHoverStyle(instance, style)` | Облик подсветки парковки. |
@@ -1649,6 +1696,8 @@ interface MeasureShape {
1649
1696
  import {
1650
1697
  Search,
1651
1698
  SearchPoi,
1699
+ createSearch,
1700
+ createSearchPoi,
1652
1701
  SearchByLocation,
1653
1702
  CheckJSApi,
1654
1703
  Router,
@@ -1657,7 +1706,7 @@ import {
1657
1706
 
1658
1707
  ### `Search(text, token, additionalParam?)`
1659
1708
 
1660
- Поиск адресов (геокодер). Вызовы дебаунсятся на 500 мс: при вводе по символу уходит один запрос.
1709
+ Поиск адресов (геокодер). Каждый вызов — отдельный запрос, без дебаунса: статический метод зовут разные поля ввода, и общий дебаунс отменял бы запрос одного поля вызовом из другого. Для ввода по символу — `createSearch()`.
1661
1710
 
1662
1711
  ```ts
1663
1712
  const results = await Search("Рудаки 33", token, {
@@ -1679,7 +1728,7 @@ const results = await Search("Рудаки 33", token, {
1679
1728
 
1680
1729
  ### `SearchPoi(text, token, additionalParam?)`
1681
1730
 
1682
- Поиск POI (организации, объекты). Сигнатура и дебаунс те же, что у `Search`, таймер отдельный — параллельный ввод в двух полях не перебивает запросы друг друга.
1731
+ Поиск POI (организации, объекты). Сигнатура та же, что у `Search`, и тоже без дебаунса; для ввода по символу — `createSearchPoi()`.
1683
1732
 
1684
1733
  ```ts
1685
1734
  const places = await SearchPoi("кафе", token, {
@@ -1691,6 +1740,30 @@ const places = await SearchPoi("кафе", token, {
1691
1740
 
1692
1741
  Возвращает `ISearchResponse[]`.
1693
1742
 
1743
+ ### `createSearch(delay?)` / `createSearchPoi(delay?)`
1744
+
1745
+ Поиск с дебаунсом для **одного** поля ввода. Возвращают функцию с сигнатурой `Search`/`SearchPoi`; пауза по умолчанию — 500 мс. Создавайте экземпляр на поле (компонент) — очередь у каждого своя, и чужие поля его запросы не отменяют.
1746
+
1747
+ Вызов, перебитый следующим вызовом **этого же** экземпляра до ухода запроса, отклоняется с `DebounceCancelledError`. Это не сбой: ответ получит последний вызов, а отменённый пропускайте молча.
1748
+
1749
+ ```ts
1750
+ import { createSearch } from "mahal_map";
1751
+
1752
+ const searchAddress = createSearch(); // одна на поле ввода
1753
+
1754
+ input.addEventListener("input", async () => {
1755
+ try {
1756
+ suggestions = await searchAddress(input.value, token);
1757
+ } catch (error) {
1758
+ // Перебит следующим вводом в этом же поле
1759
+ if ((error as Error).name === "DebounceCancelledError") return;
1760
+ showError(error);
1761
+ }
1762
+ });
1763
+ ```
1764
+
1765
+ Сверяйте по `name`, а не через `instanceof`: на странице может оказаться две сборки SDK. Класс тоже экспортируется — `import { DebounceCancelledError } from "mahal_map"`.
1766
+
1694
1767
  ### `SearchByLocation(params)`
1695
1768
 
1696
1769
  Обратный геокодинг: адреса и POI по координатам. Без дебаунса.